From ac032e0e0961b162e0a31c7774aab31d7b3330cd Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 24 May 2016 10:51:14 +0800 Subject: [PATCH] MDL-41922 mod_quiz: Don't report quiz due when an attempt was finished --- .../tests/behat/quiz_overview.feature | 94 ++++++++ mod/quiz/lib.php | 81 ++++--- mod/quiz/tests/lib_test.php | 222 ++++++++++++++++++ 3 files changed, 370 insertions(+), 27 deletions(-) create mode 100644 blocks/course_overview/tests/behat/quiz_overview.feature diff --git a/blocks/course_overview/tests/behat/quiz_overview.feature b/blocks/course_overview/tests/behat/quiz_overview.feature new file mode 100644 index 0000000000000..238591fdf35f0 --- /dev/null +++ b/blocks/course_overview/tests/behat/quiz_overview.feature @@ -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" + diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php index cd021e3f95c19..f4b9175fcc90c 100644 --- a/mod/quiz/lib.php +++ b/mod/quiz/lib.php @@ -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 = '
' . - ''; - $str .= '
' . get_string('quizcloseson', 'quiz', - userdate($quiz->timeclose)) . '
'; + $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 .= '
' . - quiz_num_attempt_summary($quiz, $quiz, true) . '
'; - } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), - $context)) { // Student + $str .= '
' . quiz_num_attempt_summary($quiz, $quiz, true) . '
'; + + } 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 .= '
' . - get_string('numattemptsmade', 'quiz', $numattempts) . '
'; + + if (isset($USER->id)) { + if ($attemptsinfo[$quiz->id]['hasfinished']) { + // The student's last attempt is finished. + continue; + } + + if ($attemptsinfo[$quiz->id]['count'] > 0) { + $str .= '
' . + get_string('numattemptsmade', 'quiz', $attemptsinfo[$quiz->id]['count']) . '
'; + } else { + $str .= '
' . $strnoattempts . '
'; + } + } else { $str .= '
' . $strnoattempts . '
'; } + } 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 .= '
'; + // Give a link to the quiz, and the deadline. + $html = '
' . + ''; + $html .= '
' . get_string('quizcloseson', 'quiz', + userdate($quiz->timeclose)) . '
'; + $html .= $str; + $html .= '
'; 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; } } } diff --git a/mod/quiz/tests/lib_test.php b/mod/quiz/tests/lib_test.php index 75edda7fd7304..22353ff0ac33a 100644 --- a/mod/quiz/tests/lib_test.php +++ b/mod/quiz/tests/lib_test.php @@ -227,4 +227,226 @@ public function test_quiz_get_completion_state() { $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); + } + }