Merge branch 'MDL-54805-master' of git://github.com/junpataleta/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 6 Jun 2016 16:06:36 +0000 (18:06 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 6 Jun 2016 16:06:36 +0000 (18:06 +0200)
12 files changed:
blocks/course_overview/tests/behat/quiz_overview.feature [new file with mode: 0644]
course/externallib.php
course/format/lib.php
message/externallib.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/tests/base_test.php
mod/assign/tests/locallib_test.php
mod/lti/locallib.php
mod/quiz/lib.php
mod/quiz/tests/lib_test.php

diff --git a/blocks/course_overview/tests/behat/quiz_overview.feature b/blocks/course_overview/tests/behat/quiz_overview.feature
new file mode 100644 (file)
index 0000000..238591f
--- /dev/null
@@ -0,0 +1,94 @@
+@block @block_course_overview @mod_quiz
+Feature: View the quiz being due
+  In order to know what quizzes are due
+  As a student
+  I can visit my dashboard
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+      | Course 2 | C2        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | C1     | student        |
+      | student2 | C2     | student        |
+      | teacher1 | C1     | editingteacher |
+      | teacher1 | C2     | editingteacher |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                    | timeclose  |
+      | quiz     | C1     | Q1A      | Quiz 1A No deadline     | 0          |
+      | quiz     | C1     | Q1B      | Quiz 1B Past deadline   | 1337       |
+      | quiz     | C1     | Q1C      | Quiz 1C Future deadline | 9000000000 |
+      | quiz     | C1     | Q1D      | Quiz 1D Future deadline | 9000000000 |
+      | quiz     | C1     | Q1E      | Quiz 1E Future deadline | 9000000000 |
+      | quiz     | C2     | Q2A      | Quiz 2A Future deadline | 9000000000 |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | qtype     | name           | questiontext              | questioncategory |
+      | truefalse | First question | Answer the first question | Test questions   |
+    And quiz "Quiz 1A No deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1B Past deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1C Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1D Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1E Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 2A Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+
+  Scenario: View my quizzes that are due
+    Given I log in as "student1"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+    And I log out
+    And I log in as "student2"
+    And I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should not see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should see "Quiz 2A Future deadline" in the "Course overview" "block"
+
+  Scenario: View my quizzes that are due and never finished
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Quiz 1D Future deadline"
+    And I press "Attempt quiz now"
+    And I follow "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I follow "Course 1"
+    And I follow "Quiz 1E Future deadline"
+    And I press "Attempt quiz now"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+
index ea687c6..b348aa1 100644 (file)
@@ -2140,7 +2140,7 @@ class core_course_external extends external_api {
                 'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
                 'requiredcapabilities' => new external_multiple_structure(
                     new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
-                    VALUE_OPTIONAL
+                    'Optional list of required capabilities (used to filter the list)', VALUE_DEFAULT, array()
                 ),
                 'limittoenrolled' => new external_value(PARAM_BOOL, 'limit to enrolled courses', VALUE_DEFAULT, 0),
             )
index 208c188..20f39a4 100644 (file)
@@ -984,14 +984,14 @@ abstract class format_base {
         }
         if (!is_object($section)) {
             $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
-                'id,section,sequence');
+                'id,section,sequence,summary');
         }
         if (!$section || !$section->section) {
             // Not possible to delete 0-section.
             return false;
         }
 
-        if (!$forcedeleteifnotempty && !empty($section->sequence)) {
+        if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
             return false;
         }
 
index 88d0a5f..6702c3b 100644 (file)
@@ -137,7 +137,8 @@ class core_message_external extends external_api {
             if ($success && empty($contactlist[$message['touserid']]) && !empty($blocknoncontacts)) {
                 // The user isn't a contact and they have selected to block non contacts so this message won't be sent.
                 $success = false;
-                $errormessage = get_string('userisblockingyounoncontact', 'message');
+                $errormessage = get_string('userisblockingyounoncontact', 'message',
+                        fullname(core_user::get_user($message['touserid'])));
             }
 
             //now we can send the message (at least try)
index c4ce567..cd23c8e 100644 (file)
@@ -1411,7 +1411,11 @@ function assign_get_completion_state($course, $cm, $userid, $type) {
 
     // If completion option is enabled, evaluate it and return true/false.
     if ($assign->get_instance()->completionsubmit) {
-        $submission = $assign->get_user_submission($userid, false);
+        if ($assign->get_instance()->teamsubmission) {
+            $submission = $assign->get_group_submission($userid, 0, false);
+        } else {
+            $submission = $assign->get_user_submission($userid, false);
+        }
         return $submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED;
     } else {
         // Completion option is not enabled so just return $type.
index c5a2b1e..b506521 100644 (file)
@@ -5633,7 +5633,12 @@ class assign {
             $this->update_submission($submission, $userid, true, $instance->teamsubmission);
             $completion = new completion_info($this->get_course());
             if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-                $completion->update_state($this->get_course_module(), COMPLETION_COMPLETE, $userid);
+                $this->update_activity_completion_records($instance->teamsubmission,
+                                                          $instance->requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          COMPLETION_COMPLETE,
+                                                          $completion);
             }
 
             if (!empty($data->submissionstatement) && $USER->id == $userid) {
@@ -6325,7 +6330,12 @@ class assign {
         }
         $completion = new completion_info($this->get_course());
         if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-            $completion->update_state($this->get_course_module(), $complete, $USER->id);
+            $this->update_activity_completion_records($instance->teamsubmission,
+                                                      $instance->requireallteammemberssubmit,
+                                                      $submission,
+                                                      $USER->id,
+                                                      $complete,
+                                                      $completion);
         }
 
         if (!$instance->submissiondrafts) {
@@ -7991,6 +8001,42 @@ class assign {
         }
         return $this->get_course_module()->id . '_' . $id;
     }
+
+    /**
+     * Updates and creates the completion records in mdl_course_modules_completion.
+     *
+     * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
+     * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
+     * @param obj $submission the submission
+     * @param int $userid the user id
+     * @param int $complete
+     * @param obj $completion
+     *
+     * @return null
+     */
+    protected function update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+
+        if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
+            ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
+             $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
+
+            $members = groups_get_members($submission->groupid);
+
+            foreach ($members as $member) {
+                $completion->update_state($this->get_course_module(), $complete, $member->id);
+            }
+        } else {
+            $completion->update_state($this->get_course_module(), $complete, $userid);
+        }
+
+        return;
+    }
+
 }
 
 /**
index 412f72f..be6166d 100644 (file)
@@ -209,32 +209,6 @@ class mod_assign_mod_form extends moodleform_mod {
         $this->apply_admin_defaults();
 
         $this->add_action_buttons();
-
-        // Add warning popup/noscript tag, if grades are changed by user.
-        $hasgrade = false;
-        if (!empty($this->_instance)) {
-            $hasgrade = $DB->record_exists_select('assign_grades',
-                                                  'assignment = ? AND grade <> -1',
-                                                  array($this->_instance));
-        }
-
-        if ($mform->elementExists('grade') && $hasgrade) {
-            $module = array(
-                'name' => 'mod_assign',
-                'fullpath' => '/mod/assign/module.js',
-                'requires' => array('node', 'event'),
-                'strings' => array(array('changegradewarning', 'mod_assign'))
-                );
-            $PAGE->requires->js_init_call('M.mod_assign.init_grade_change', null, false, $module);
-
-            // Add noscript tag in case.
-            $noscriptwarning = $mform->createElement('static',
-                                                     'warning',
-                                                     null,
-                                                     html_writer::tag('noscript',
-                                                     get_string('changegradewarning', 'mod_assign')));
-            $mform->insertElementBefore($noscriptwarning, 'grade');
-        }
     }
 
     /**
index 57c234b..5c92b96 100644 (file)
@@ -90,7 +90,7 @@ class mod_assign_base_testcase extends advanced_testcase {
 
         $this->resetAfterTest(true);
 
-        $this->course = $this->getDataGenerator()->create_course();
+        $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
         $this->teachers = array();
         for ($i = 0; $i < self::DEFAULT_TEACHER_COUNT; $i++) {
             array_push($this->teachers, $this->getDataGenerator()->create_user());
@@ -350,4 +350,18 @@ class testable_assign extends assign {
 
         return $mform;
     }
+
+    public function testable_update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+        return parent::update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion);
+    }
 }
index 2062696..65670a5 100644 (file)
@@ -2631,4 +2631,94 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $grade = $assign->get_user_grade($this->students[0]->id, false);
         $this->assertEquals('30.0', $grade->grade);
     }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_solitary_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                'requireallteammemberssubmit' => 0));
+
+        $cm = $assign->get_course_module();
+
+        $student = $this->students[0];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        $this->setUser($student);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student->id, true);
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(0, 0, $submission,
+                $student->id, COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_team_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                 'teamsubmission' => 1));
+
+        $cm = $assign->get_course_module();
+
+        $student1 = $this->students[0];
+        $student2 = $this->students[1];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        // Put both users into a group.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student2->id));
+
+        $this->setUser($student1);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student1->id, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $submission->groupid = $group1->id;
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(1, 0, $submission, $student1->id,
+                COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
 }
index 75f7d8e..e8709c6 100644 (file)
@@ -2596,17 +2596,31 @@ function lti_load_type_from_cartridge($url, $type) {
         array(
             "title" => "lti_typename",
             "launch_url" => "lti_toolurl",
-            "description" => "lti_description"
+            "description" => "lti_description",
+            "icon" => "lti_icon",
+            "secure_icon" => "lti_secureicon"
         ),
         array(
-            "icon_url" => "lti_icon",
-            "secure_icon_url" => "lti_secureicon"
+            "icon_url" => "lti_extension_icon",
+            "secure_icon_url" => "lti_extension_secureicon"
         )
     );
     // If an activity name exists, unset the cartridge name so we don't override it.
     if (isset($type->lti_typename)) {
         unset($toolinfo['lti_typename']);
     }
+
+    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
+    if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
+        $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
+    }
+    unset($toolinfo['lti_extension_icon']);
+
+    if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
+        $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
+    }
+    unset($toolinfo['lti_extension_secureicon']);
+
     foreach ($toolinfo as $property => $value) {
         $type->$property = $value;
     }
@@ -2626,17 +2640,31 @@ function lti_load_tool_from_cartridge($url, $lti) {
             "title" => "name",
             "launch_url" => "toolurl",
             "secure_launch_url" => "securetoolurl",
-            "description" => "intro"
+            "description" => "intro",
+            "icon" => "icon",
+            "secure_icon" => "secureicon"
         ),
         array(
-            "icon_url" => "icon",
-            "secure_icon_url" => "secureicon"
+            "icon_url" => "extension_icon",
+            "secure_icon_url" => "extension_secureicon"
         )
     );
     // If an activity name exists, unset the cartridge name so we don't override it.
     if (isset($lti->name)) {
         unset($toolinfo['name']);
     }
+
+    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
+    if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
+        $toolinfo['icon'] = $toolinfo['extension_icon'];
+    }
+    unset($toolinfo['extension_icon']);
+
+    if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
+        $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
+    }
+    unset($toolinfo['extension_secureicon']);
+
     foreach ($toolinfo as $property => $value) {
         $lti->$property = $value;
     }
index cd021e3..f4b9175 100644 (file)
@@ -544,14 +544,14 @@ function quiz_cron() {
 }
 
 /**
- * @param int $quizid the quiz id.
+ * @param int|array $quizids A quiz ID, or an array of quiz IDs.
  * @param int $userid the userid.
  * @param string $status 'all', 'finished' or 'unfinished' to control
  * @param bool $includepreviews
  * @return an array of all the user's attempts at this quiz. Returns an empty
  *      array if there are none.
  */
-function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
+function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) {
     global $DB, $CFG;
     // TODO MDL-33071 it is very annoying to have to included all of locallib.php
     // just to get the quiz_attempt::FINISHED constants, but I will try to sort
@@ -578,15 +578,18 @@ function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $include
             break;
     }
 
+    $quizids = (array) $quizids;
+    list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED);
+    $params += $inparams;
+    $params['userid'] = $userid;
+
     $previewclause = '';
     if (!$includepreviews) {
         $previewclause = ' AND preview = 0';
     }
 
-    $params['quizid'] = $quizid;
-    $params['userid'] = $userid;
     return $DB->get_records_select('quiz_attempts',
-            'quiz = :quizid AND userid = :userid' . $previewclause . $statuscondition,
+            "quiz $insql AND userid = :userid" . $previewclause . $statuscondition,
             $params, 'attempt ASC');
 }
 
@@ -1465,6 +1468,20 @@ function quiz_print_overview($courses, &$htmlarray) {
         return;
     }
 
+    // Get the quizzes attempts.
+    $attemptsinfo = [];
+    $quizids = [];
+    foreach ($quizzes as $quiz) {
+        $quizids[] = $quiz->id;
+        $attemptsinfo[$quiz->id] = ['count' => 0, 'hasfinished' => false];
+    }
+    $attempts = quiz_get_user_attempts($quizids, $USER->id);
+    foreach ($attempts as $attempt) {
+        $attemptsinfo[$attempt->quiz]['count']++;
+        $attemptsinfo[$attempt->quiz]['hasfinished'] = true;
+    }
+    unset($attempts);
+
     // Fetch some language strings outside the main loop.
     $strquiz = get_string('modulename', 'quiz');
     $strnoattempts = get_string('noattempts', 'quiz');
@@ -1474,15 +1491,7 @@ function quiz_print_overview($courses, &$htmlarray) {
     $now = time();
     foreach ($quizzes as $quiz) {
         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
-            // Give a link to the quiz, and the deadline.
-            $str = '<div class="quiz overview">' .
-                    '<div class="name">' . $strquiz . ': <a ' .
-                    ($quiz->visible ? '' : ' class="dimmed"') .
-                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
-                    $quiz->coursemodule . '">' .
-                    $quiz->name . '</a></div>';
-            $str .= '<div class="info">' . get_string('quizcloseson', 'quiz',
-                    userdate($quiz->timeclose)) . '</div>';
+            $str = '';
 
             // Now provide more information depending on the uers's role.
             $context = context_module::instance($quiz->coursemodule);
@@ -1490,30 +1499,48 @@ function quiz_print_overview($courses, &$htmlarray) {
                 // For teacher-like people, show a summary of the number of student attempts.
                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
                 // fields set to make the following call work.
-                $str .= '<div class="info">' .
-                        quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
-            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
-                    $context)) { // Student
+                $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
+
+            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
                 // For student-like people, tell them how many attempts they have made.
-                if (isset($USER->id) &&
-                        ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
-                    $numattempts = count($attempts);
-                    $str .= '<div class="info">' .
-                            get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
+
+                if (isset($USER->id)) {
+                    if ($attemptsinfo[$quiz->id]['hasfinished']) {
+                        // The student's last attempt is finished.
+                        continue;
+                    }
+
+                    if ($attemptsinfo[$quiz->id]['count'] > 0) {
+                        $str .= '<div class="info">' .
+                            get_string('numattemptsmade', 'quiz', $attemptsinfo[$quiz->id]['count']) . '</div>';
+                    } else {
+                        $str .= '<div class="info">' . $strnoattempts . '</div>';
+                    }
+
                 } else {
                     $str .= '<div class="info">' . $strnoattempts . '</div>';
                 }
+
             } else {
                 // For ayone else, there is no point listing this quiz, so stop processing.
                 continue;
             }
 
-            // Add the output for this quiz to the rest.
-            $str .= '</div>';
+            // Give a link to the quiz, and the deadline.
+            $html = '<div class="quiz overview">' .
+                    '<div class="name">' . $strquiz . ': <a ' .
+                    ($quiz->visible ? '' : ' class="dimmed"') .
+                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
+                    $quiz->coursemodule . '">' .
+                    $quiz->name . '</a></div>';
+            $html .= '<div class="info">' . get_string('quizcloseson', 'quiz',
+                    userdate($quiz->timeclose)) . '</div>';
+            $html .= $str;
+            $html .= '</div>';
             if (empty($htmlarray[$quiz->course]['quiz'])) {
-                $htmlarray[$quiz->course]['quiz'] = $str;
+                $htmlarray[$quiz->course]['quiz'] = $html;
             } else {
-                $htmlarray[$quiz->course]['quiz'] .= $str;
+                $htmlarray[$quiz->course]['quiz'] .= $html;
             }
         }
     }
index 75edda7..22353ff 100644 (file)
@@ -227,4 +227,226 @@ class mod_quiz_lib_testcase extends advanced_testcase {
         $this->assertTrue(quiz_get_completion_state($course, $cm, $passstudent->id, 'return'));
         $this->assertFalse(quiz_get_completion_state($course, $cm, $failstudent->id, 'return'));
     }
+
+    public function test_quiz_get_user_attempts() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $dg = $this->getDataGenerator();
+        $quizgen = $dg->get_plugin_generator('mod_quiz');
+        $course = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $role = $DB->get_record('role', ['shortname' => 'student']);
+
+        $dg->enrol_user($u1->id, $course->id, $role->id);
+        $dg->enrol_user($u2->id, $course->id, $role->id);
+        $dg->enrol_user($u3->id, $course->id, $role->id);
+        $dg->enrol_user($u4->id, $course->id, $role->id);
+
+        $quiz1 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
+        $quiz2 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
+
+        // Questions.
+        $questgen = $dg->get_plugin_generator('core_question');
+        $quizcat = $questgen->create_question_category();
+        $question = $questgen->create_question('numerical', null, ['category' => $quizcat->id]);
+        quiz_add_quiz_question($question->id, $quiz1);
+        quiz_add_quiz_question($question->id, $quiz2);
+
+        $quizobj1a = quiz::create($quiz1->id, $u1->id);
+        $quizobj1b = quiz::create($quiz1->id, $u2->id);
+        $quizobj1c = quiz::create($quiz1->id, $u3->id);
+        $quizobj1d = quiz::create($quiz1->id, $u4->id);
+        $quizobj2a = quiz::create($quiz2->id, $u1->id);
+
+        // Set attempts.
+        $quba1a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context());
+        $quba1a->set_preferred_behaviour($quizobj1a->get_quiz()->preferredbehaviour);
+        $quba1b = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1b->get_context());
+        $quba1b->set_preferred_behaviour($quizobj1b->get_quiz()->preferredbehaviour);
+        $quba1c = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1c->get_context());
+        $quba1c->set_preferred_behaviour($quizobj1c->get_quiz()->preferredbehaviour);
+        $quba1d = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1d->get_context());
+        $quba1d->set_preferred_behaviour($quizobj1d->get_quiz()->preferredbehaviour);
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+
+        // User 1 passes quiz 1.
+        $attempt = quiz_create_attempt($quizobj1a, 1, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj1a, $quba1a, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1a, $quba1a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
+        $attemptobj->process_finish($timenow, false);
+
+        // User 2 goes overdue in quiz 1.
+        $attempt = quiz_create_attempt($quizobj1b, 1, false, $timenow, false, $u2->id);
+        quiz_start_new_attempt($quizobj1b, $quba1b, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1b, $quba1b, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_going_overdue($timenow, true);
+
+        // User 3 does not finish quiz 1.
+        $attempt = quiz_create_attempt($quizobj1c, 1, false, $timenow, false, $u3->id);
+        quiz_start_new_attempt($quizobj1c, $quba1c, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1c, $quba1c, $attempt);
+
+        // User 4 abandons the quiz 1.
+        $attempt = quiz_create_attempt($quizobj1d, 1, false, $timenow, false, $u4->id);
+        quiz_start_new_attempt($quizobj1d, $quba1d, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1d, $quba1d, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_abandon($timenow, true);
+
+        // User 1 attempts the quiz three times (abandon, finish, in progress).
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 1, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_abandon($timenow, true);
+
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 2, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 2, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_finish($timenow, false);
+
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 3, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 3, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+
+        // Check for user 1.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'finished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'unfinished');
+        $this->assertCount(0, $attempts);
+
+        // Check for user 2.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
+        $this->assertEquals($u2->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'finished');
+        $this->assertCount(0, $attempts);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
+        $this->assertEquals($u2->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        // Check for user 3.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u3->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'finished');
+        $this->assertCount(0, $attempts);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u3->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        // Check for user 4.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u4->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'finished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u4->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'unfinished');
+        $this->assertCount(0, $attempts);
+
+        // Multiple attempts for user 1 in quiz 2.
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'all');
+        $this->assertCount(3, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'finished');
+        $this->assertCount(2, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+
+        // Multiple quiz attempts fetched at once.
+        $attempts = quiz_get_user_attempts([$quiz1->id, $quiz2->id], $u1->id, 'all');
+        $this->assertCount(4, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+    }
+
 }