diff --git a/course/modlib.php b/course/modlib.php index 26170a83785..2008c0a4be6 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -678,6 +678,7 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) { if (core_tag_tag::is_enabled('core', 'course_modules') && isset($moduleinfo->tags)) { core_tag_tag::set_item_tags('core', 'course_modules', $moduleinfo->coursemodule, $modcontext, $moduleinfo->tags); } + $moduleinfo = edit_module_post_actions($moduleinfo, $course); // Now that module is fully updated, also update completion data if required. // (this will wipe all user completion data and recalculate it) @@ -691,8 +692,6 @@ function update_moduleinfo($cm, $moduleinfo, $course, $mform = null) { $cm->name = $moduleinfo->name; \core\event\course_module_updated::create_from_cm($cm, $modcontext)->trigger(); - $moduleinfo = edit_module_post_actions($moduleinfo, $course); - return array($cm, $moduleinfo); } diff --git a/mod/quiz/tests/behat/quiz_activity_completion_unlocked.feature b/mod/quiz/tests/behat/quiz_activity_completion_unlocked.feature new file mode 100644 index 00000000000..7a421563115 --- /dev/null +++ b/mod/quiz/tests/behat/quiz_activity_completion_unlocked.feature @@ -0,0 +1,67 @@ +@mod @mod_quiz @core_completion +Feature: Activity completion in the quiz activity with unlocked and re-grading. + In order to have visibility of quiz completion requirements + As a student + I need to be able to view my quiz completion progress even teacher have re-grading the grade pass. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student1 | Student | 1 | student1@example.com | + | teacher1 | Teacher | 1 | teacher1@example.com | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | + | Course 1 | C1 | 0 | 1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following config values are set as admin: + | grade_item_advanced | hiddenuntil | + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | defaultmark | + | Test questions | truefalse | First question | Answer the first question | 8 | + | Test questions | truefalse | Second question | Answer the second question | 2 | + And the following "activity" exists: + | activity | quiz | + | course | C1 | + | idnumber | quiz1 | + | name | Test quiz name | + | section | 1 | + | gradepass | 10.00 | + | grade | 10 | + | grademethod | 1 | + | completion | 2 | + | completionusegrade | 1 | + | completionpassgrade | 1 | + And quiz "Test quiz name" contains the following questions: + | question | page | + | First question | 1 | + | Second question | 2 | + + @javascript + Scenario: Student will receive correct completion even when teacher unlocked completion and re-grading. + Given I am on the "Test quiz name" "quiz activity" page logged in as student1 + And the "Receive a grade" completion condition of "Test quiz name" is displayed as "todo" + And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "todo" + And user "student1" has attempted "Test quiz name" with responses: + | slot | response | + | 1 | True | + | 2 | False | + And I am on "Course 1" course homepage + And I follow "Test quiz name" + And the "Receive a grade" completion condition of "Test quiz name" is displayed as "done" + And the "Receive a passing grade" completion condition of "Test quiz name" is displayed as "failed" + And I log out + When I am on the "Test quiz name" "quiz activity" page logged in as teacher1 + And I navigate to "Settings" in current page administration + And I expand all fieldsets + And I press "Unlock completion options" + And I set the following fields to these values: + | gradepass | 8 | + And I press "Save and return to course" + And I navigate to "Reports > Activity completion" in current page administration + Then "Completed (achieved pass grade)" "icon" should exist in the "Student 1" "table_row" diff --git a/mod/quiz/tests/custom_completion_test.php b/mod/quiz/tests/custom_completion_test.php index 7407e492677..0420ffad606 100644 --- a/mod/quiz/tests/custom_completion_test.php +++ b/mod/quiz/tests/custom_completion_test.php @@ -46,7 +46,7 @@ class custom_completion_test extends advanced_testcase { * Setup function for all tests. * * @param array $completionoptions ['nbstudents'] => int, ['qtype'] => string, ['quizoptions'] => array - * @return array [$students, $quiz, $cm] + * @return array [$students, $quiz, $cm, $litecm] */ private function setup_quiz_for_testing_completion(array $completionoptions): array { global $CFG, $DB; @@ -60,6 +60,8 @@ class custom_completion_test extends advanced_testcase { $studentrole = $DB->get_record('role', ['shortname' => 'student']); $course = $this->getDataGenerator()->create_course(['enablecompletion' => true]); $students = []; + $sumgrades = $completionoptions['sumgrades'] ?? 1; + $nbquestions = $completionoptions['nbquestions'] ?? 1; for ($i = 0; $i < $completionoptions['nbstudents']; $i++) { $students[$i] = $this->getDataGenerator()->create_user(); $this->assertTrue($this->getDataGenerator()->enrol_user($students[$i]->id, $course->id, $studentrole->id)); @@ -71,30 +73,36 @@ class custom_completion_test extends advanced_testcase { 'course' => $course->id, 'grade' => 100.0, 'questionsperpage' => 0, - 'sumgrades' => 1, + 'sumgrades' => $sumgrades, 'completion' => COMPLETION_TRACKING_AUTOMATIC ], $completionoptions['quizoptions']); $quiz = $quizgenerator->create_instance($data); - $cm = get_coursemodule_from_id('quiz', $quiz->cmid); - $cm = cm_info::create($cm); + $litecm = get_coursemodule_from_id('quiz', $quiz->cmid); + $cm = cm_info::create($litecm); // Create a question. $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); $cat = $questiongenerator->create_question_category(); - $question = $questiongenerator->create_question($completionoptions['qtype'], null, ['category' => $cat->id]); - quiz_add_quiz_question($question->id, $quiz); + for ($i = 0; $i < $nbquestions; $i++) { + $overrideparams = ['category' => $cat->id]; + if (isset($completionoptions['questiondefaultmarks'][$i])) { + $overrideparams['defaultmark'] = $completionoptions['questiondefaultmarks'][$i]; + } + $question = $questiongenerator->create_question($completionoptions['qtype'], null, $overrideparams); + quiz_add_quiz_question($question->id, $quiz); + } // Set grade to pass. $item = grade_item::fetch(['courseid' => $course->id, 'itemtype' => 'mod', 'itemmodule' => 'quiz', 'iteminstance' => $quiz->id, 'outcomeid' => null]); $item->gradepass = 80; $item->update(); - return [ $students, $quiz, - $cm + $cm, + $litecm ]; } @@ -324,4 +332,173 @@ class custom_completion_test extends advanced_testcase { $rules ); } + + /** + * Test update moduleinfo. + * + * @covers \update_moduleinfo + */ + public function test_update_moduleinfo() { + $this->setAdminUser(); + // We need lite cm object not a full cm because update_moduleinfo is not allow some properties to be updated. + list($students, $quiz, $cm, $litecm) = $this->setup_quiz_for_testing_completion([ + 'nbstudents' => 1, + 'qtype' => 'numerical', + 'nbquestions' => 2, + 'sumgrades' => 100, + 'questiondefaultmarks' => [20, 80], + 'quizoptions' => [ + 'completionusegrade' => 1, + 'completionpassgrade' => 1, + 'completionview' => 0, + ] + ]); + $course = $cm->get_course(); + + list($student) = $students; + // Do a first attempt with a pass marks = 20. + $this->do_attempt_quiz([ + 'quiz' => $quiz, + 'student' => $student, + 'attemptnumber' => 1, + 'tosubmit' => [1 => ['answer' => '3.14']] + ]); + $completioninfo = new \completion_info($course); + $cminfo = \cm_info::create($cm); + $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id); + + // Check the results. Completion is fail because gradepass = 80. + $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status); + $this->assertEquals( + 'Receive a passing grade', + $completiondetails->get_details()['completionpassgrade']->description + ); + + // Update quiz with passgrade = 20 and use highest grade to calculate. + $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 20, QUIZ_GRADEHIGHEST); + update_moduleinfo($litecm, $moduleinfo, $course, null); + + $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id); + + // Check the results. Completion is pass. + $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status); + $this->assertEquals( + 'Receive a passing grade', + $completiondetails->get_details()['completionpassgrade']->description + ); + + // Do a second attempt with pass marks = 80. + $this->do_attempt_quiz([ + 'quiz' => $quiz, + 'student' => $student, + 'attemptnumber' => 2, + 'tosubmit' => [2 => ['answer' => '3.14']] + ]); + + // Update quiz with gradepass = 80 and use highest grade to calculate completion. + $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 80, QUIZ_GRADEHIGHEST); + update_moduleinfo($litecm, $moduleinfo, $course, null); + + $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id); + + // Check the results. Completion is pass. + $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status); + $this->assertEquals( + 'Receive a passing grade', + $completiondetails->get_details()['completionpassgrade']->description + ); + + // Update quiz with gradepass = 80 and use average grade to calculate completion. + $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 80, QUIZ_GRADEAVERAGE); + update_moduleinfo($litecm, $moduleinfo, $course, null); + + $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id); + + // Check the results. Completion is fail because student grade = 50. + $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status); + $this->assertEquals( + 'Receive a passing grade', + $completiondetails->get_details()['completionpassgrade']->description + ); + + // Update quiz with gradepass = 50 and use average grade to calculate completion. + $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_GRADEAVERAGE); + update_moduleinfo($litecm, $moduleinfo, $course, null); + + $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id); + + // Check the results. Completion is pass. + $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status); + $this->assertEquals( + 'Receive a passing grade', + $completiondetails->get_details()['completionpassgrade']->description + ); + + // Update quiz with gradepass = 50 and use first attempt grade to calculate completion. + $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_ATTEMPTFIRST); + update_moduleinfo($litecm, $moduleinfo, $course, null); + + $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id); + + // Check the results. Completion is fail. + $this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status); + $this->assertEquals( + 'Receive a passing grade', + $completiondetails->get_details()['completionpassgrade']->description + ); + // Update quiz with gradepass = 50 and use last attempt grade to calculate completion. + $moduleinfo = $this->prepare_module_info($cm, $quiz, $course, 50, QUIZ_ATTEMPTLAST); + update_moduleinfo($litecm, $moduleinfo, $course, null); + + $completiondetails = new cm_completion_details($completioninfo, $cminfo, (int) $student->id); + + // Check the results. Completion is fail. + $this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status); + $this->assertEquals( + 'Receive a passing grade', + $completiondetails->get_details()['completionpassgrade']->description + ); + } + + /** + * Set up moduleinfo object sample data for quiz instance. + * + * @param object $cm course-module instance + * @param object $quiz quiz instance data. + * @param object $course Course related data. + * @param int $gradepass Grade to pass and completed completion. + * @param string $grademethod grade attempt method. + * @return \stdClass + */ + private function prepare_module_info(object $cm, object $quiz, object $course, int $gradepass, string $grademethod): \stdClass { + $grouping = $this->getDataGenerator()->create_grouping(['courseid' => $course->id]); + // Module test values. + $moduleinfo = new \stdClass(); + $moduleinfo->coursemodule = $cm->id; + $moduleinfo->section = 1; + $moduleinfo->course = $course->id; + $moduleinfo->groupingid = $grouping->id; + $draftideditor = 0; + file_prepare_draft_area($draftideditor, null, null, null, null); + $moduleinfo->introeditor = ['text' => 'This is a module', 'format' => FORMAT_HTML, 'itemid' => $draftideditor]; + $moduleinfo->modulename = 'quiz'; + $moduleinfo->quizpassword = ''; + $moduleinfo->cmidnumber = ''; + $moduleinfo->marksopen = 1; + $moduleinfo->visible = 1; + $moduleinfo->visibleoncoursepage = 1; + $moduleinfo->completion = COMPLETION_TRACKING_AUTOMATIC; + $moduleinfo->completionview = COMPLETION_VIEW_NOT_REQUIRED; + $moduleinfo->name = $quiz->name; + $moduleinfo->timeopen = $quiz->timeopen; + $moduleinfo->timeclose = $quiz->timeclose; + $moduleinfo->timelimit = $quiz->timelimit; + $moduleinfo->graceperiod = $quiz->graceperiod; + $moduleinfo->decimalpoints = $quiz->decimalpoints; + $moduleinfo->questiondecimalpoints = $quiz->questiondecimalpoints; + $moduleinfo->gradepass = $gradepass; + $moduleinfo->grademethod = $grademethod; + + return $moduleinfo; + } }