MDL-52206 core: Update new activity_custom_completion

Update the custom_completion classes to accept the new completion
criteria
This commit is contained in:
Peter Dias 2021-06-24 14:02:56 +08:00
parent d975251813
commit 2c7893cf9f
12 changed files with 178 additions and 90 deletions

View File

@ -40,15 +40,20 @@ abstract class activity_custom_completion {
/** @var int The user's ID. */
protected $userid;
/** @var array The current state of core completion */
protected $completionstate;
/**
* activity_custom_completion constructor.
*
* @param cm_info $cm
* @param int $userid
* @param array|null $completionstate The current state of the core completion criteria
*/
public function __construct(cm_info $cm, int $userid) {
public function __construct(cm_info $cm, int $userid, ?array $completionstate = null) {
$this->cm = $cm;
$this->userid = $userid;
$this->completionstate = $completionstate;
}
/**

View File

@ -73,7 +73,11 @@ class cm_completion_details {
$this->returndetails = $returndetails;
$cmcompletionclass = activity_custom_completion::get_cm_completion_class($this->cminfo->modname);
if ($cmcompletionclass) {
$this->cmcompletion = new $cmcompletionclass($this->cminfo, $this->userid);
$this->cmcompletion = new $cmcompletionclass(
$this->cminfo,
$this->userid,
$completioninfo->get_core_completion_state($cminfo, $userid)
);
}
}
@ -128,6 +132,13 @@ class cm_completion_details {
'status' => $status,
'description' => get_string('detail_desc:receivegrade', 'completion'),
];
if (!is_null($this->cminfo->completionpassgrade) && $this->cminfo->completionpassgrade) {
$details['completionpassgrade'] = (object)[
'status' => $completiondata->passgrade ?? COMPLETION_INCOMPLETE,
'description' => get_string('detail_desc:receivepassgrade', 'completion'),
];
}
}
if ($this->cmcompletion) {

View File

@ -93,6 +93,7 @@ class cm_completion_details_test extends advanced_testcase {
['modname', $modname],
['completionview', $completionoptions['completionview'] ?? COMPLETION_VIEW_NOT_REQUIRED],
['completiongradeitemnumber', $completionoptions['completionusegrade'] ?? null],
['completionpassgrade', $completionoptions['completionpassgrade'] ?? null],
]));
return new cm_completion_details($this->completioninfo, $mockcminfo, 2);
@ -192,13 +193,13 @@ class cm_completion_details_test extends advanced_testcase {
public function get_details_provider() {
return [
'No completion tracking' => [
COMPLETION_TRACKING_NONE, null, null, []
COMPLETION_TRACKING_NONE, null, null, null, []
],
'Manual completion tracking' => [
COMPLETION_TRACKING_MANUAL, null, null, []
COMPLETION_TRACKING_MANUAL, null, null, null, []
],
'Automatic, require view, not viewed' => [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_INCOMPLETE, null, [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_INCOMPLETE, null, null, [
'completionview' => (object)[
'status' => COMPLETION_INCOMPLETE,
'description' => get_string('detail_desc:view', 'completion'),
@ -206,7 +207,7 @@ class cm_completion_details_test extends advanced_testcase {
]
],
'Automatic, require view, viewed' => [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_COMPLETE, null, [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_COMPLETE, null, null, [
'completionview' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:view', 'completion'),
@ -214,7 +215,7 @@ class cm_completion_details_test extends advanced_testcase {
]
],
'Automatic, require grade, incomplete' => [
COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_INCOMPLETE, [
COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_INCOMPLETE, null, [
'completionusegrade' => (object)[
'status' => COMPLETION_INCOMPLETE,
'description' => get_string('detail_desc:receivegrade', 'completion'),
@ -222,7 +223,7 @@ class cm_completion_details_test extends advanced_testcase {
]
],
'Automatic, require grade, complete' => [
COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_COMPLETE, [
COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_COMPLETE, null, [
'completionusegrade' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:receivegrade', 'completion'),
@ -230,7 +231,7 @@ class cm_completion_details_test extends advanced_testcase {
]
],
'Automatic, require view (complete) and grade (incomplete)' => [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_COMPLETE, COMPLETION_INCOMPLETE, [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_COMPLETE, COMPLETION_INCOMPLETE, null, [
'completionview' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:view', 'completion'),
@ -242,7 +243,7 @@ class cm_completion_details_test extends advanced_testcase {
]
],
'Automatic, require view (incomplete) and grade (complete)' => [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_INCOMPLETE, COMPLETION_COMPLETE, [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_INCOMPLETE, COMPLETION_COMPLETE, null, [
'completionview' => (object)[
'status' => COMPLETION_INCOMPLETE,
'description' => get_string('detail_desc:view', 'completion'),
@ -253,6 +254,62 @@ class cm_completion_details_test extends advanced_testcase {
]
]
],
'Automatic, require grade, require pass grade, complete' => [
COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_COMPLETE, COMPLETION_COMPLETE, [
'completionusegrade' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:receivegrade', 'completion'),
],
'completionpassgrade' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:receivepassgrade', 'completion'),
],
]
],
'Automatic, require grade, require pass grade, incomplete' => [
COMPLETION_TRACKING_AUTOMATIC, null, COMPLETION_COMPLETE, COMPLETION_INCOMPLETE, [
'completionusegrade' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:receivegrade', 'completion'),
],
'completionpassgrade' => (object)[
'status' => COMPLETION_INCOMPLETE,
'description' => get_string('detail_desc:receivepassgrade', 'completion'),
],
]
],
'Automatic, require view (complete), require grade(complete), require pass grade(complete)' => [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_COMPLETE, COMPLETION_COMPLETE, COMPLETION_COMPLETE, [
'completionview' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:view', 'completion'),
],
'completionusegrade' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:receivegrade', 'completion'),
],
'completionpassgrade' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:receivepassgrade', 'completion'),
],
]
],
'Automatic, require view (incomplete), require grade(complete), require pass grade(complete)' => [
COMPLETION_TRACKING_AUTOMATIC, COMPLETION_INCOMPLETE, COMPLETION_COMPLETE, COMPLETION_COMPLETE, [
'completionview' => (object)[
'status' => COMPLETION_INCOMPLETE,
'description' => get_string('detail_desc:view', 'completion'),
],
'completionusegrade' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:receivegrade', 'completion'),
],
'completionpassgrade' => (object)[
'status' => COMPLETION_COMPLETE,
'description' => get_string('detail_desc:receivepassgrade', 'completion'),
],
]
],
];
}
@ -263,13 +320,16 @@ class cm_completion_details_test extends advanced_testcase {
* @param int $completion The completion tracking mode.
* @param int|null $completionview Completion status of the "view" completion condition.
* @param int|null $completiongrade Completion status of the "must receive grade" completion condition.
* @param int|null $completionpassgrade Completion status of the "must receive passing grade" completion condition.
* @param array $expecteddetails Expected completion details returned by get_details().
*/
public function test_get_details(int $completion, ?int $completionview, ?int $completiongrade, array $expecteddetails) {
public function test_get_details(int $completion, ?int $completionview,
?int $completiongrade, ?int $completionpassgrade, array $expecteddetails) {
$options = [];
$getdatareturn = (object)[
'viewed' => $completionview,
'completiongrade' => $completiongrade,
'passgrade' => $completionpassgrade,
];
if (!is_null($completionview)) {
@ -278,13 +338,16 @@ class cm_completion_details_test extends advanced_testcase {
if (!is_null($completiongrade)) {
$options['completionusegrade'] = true;
}
if (!is_null($completionpassgrade)) {
$options['completionpassgrade'] = true;
}
$cmcompletion = $this->setup_data($completion, $options, $getdatareturn);
$this->assertEquals($expecteddetails, $cmcompletion->get_details());
}
/**
* Data provider for test_get_details().
* Data provider for test_get_details_custom_order().
* @return array[]
*/
public function get_details_custom_order_provider() {

View File

@ -697,14 +697,6 @@ class completion_info {
}
$newstate = COMPLETION_COMPLETE;
$completionstate = [];
if ($cm->completionview == COMPLETION_VIEW_REQUIRED) {
$newstate = ($current->viewed == COMPLETION_VIEWED ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE);
$completionstate = [
'viewed' => $newstate
];
}
if ($cm instanceof stdClass) {
// Modname hopefully is provided in $cm but just in case it isn't, let's grab it.
if (!isset($cm->modname)) {
@ -717,35 +709,14 @@ class completion_info {
}
// Make sure we're using a cm_info object.
$cminfo = cm_info::create($cm, $userid);
// Check grade
if (!is_null($cminfo->completiongradeitemnumber)) {
$newstate = $this->get_grade_completion($cminfo, $userid);
$completionstate['usegrade'] = $newstate;
if ($cm->completionpassgrade) {
// If we are asking to use pass grade completion but haven't set it properly,
// then default to COMPLETION_COMPLETE_PASS.
if ($newstate == COMPLETION_COMPLETE) {
$newstate = COMPLETION_COMPLETE_PASS;
}
// The criteria is to mark an activity as complete if a passing grade has been achieved.
// COMPLETION_COMPLETE_FAIL still marks the activity as completed which is incorrect.
// Rectify this.
if ($newstate == COMPLETION_COMPLETE_FAIL) {
$newstate = COMPLETION_INCOMPLETE;
}
$completionstate['passgrade'] = $newstate;
}
}
$completionstate = $this->get_core_completion_state($cminfo, $userid);
if (plugin_supports('mod', $cminfo->modname, FEATURE_COMPLETION_HAS_RULES)) {
$response = true;
$cmcompletionclass = activity_custom_completion::get_cm_completion_class($cminfo->modname);
if ($cmcompletionclass) {
/** @var activity_custom_completion $cmcompletion */
$cmcompletion = new $cmcompletionclass($cminfo, $userid);
$cmcompletion = new $cmcompletionclass($cminfo, $userid, $completionstate);
$response = $cmcompletion->get_overall_completion_state() != COMPLETION_INCOMPLETE;
} else {
// Fallback to the get_completion_state callback.
@ -1167,6 +1138,51 @@ class completion_info {
return (object)$cacheddata[$cminfo->id];
}
/**
* Get the latest completion state for each criteria used in the module
*
* @param cm_info $cm The corresponding module's information
* @param int $userid The id for the user we are calculating core completion state
* @return array $data The individualised core completion state used in the module.
* Consists of the following keys completiongrade, passgrade, viewed
*/
public function get_core_completion_state(cm_info $cm, int $userid): array {
global $DB;
$data = [];
// Include in the completion info the grade completion, if necessary.
if (!is_null($cm->completiongradeitemnumber)) {
$newstate = $this->get_grade_completion($cm, $userid);
$data['completiongrade'] = $newstate;
if ($cm->completionpassgrade) {
// If we are asking to use pass grade completion but haven't set it properly,
// then default to COMPLETION_COMPLETE_PASS.
if ($newstate == COMPLETION_COMPLETE) {
$newstate = COMPLETION_COMPLETE_PASS;
}
// The activity is using 'passing grade' criteria therefore fail indication should be on this criteria.
// The user has received a (failing) grade so 'completiongrade' should properly indicate this.
if ($newstate == COMPLETION_COMPLETE_FAIL) {
$data['completiongrade'] = COMPLETION_COMPLETE;
}
$data['passgrade'] = $newstate;
}
}
// If view is required, try and fetch from the db. In some cases, cache can be invalid.
if ($cm->completionview == COMPLETION_VIEW_REQUIRED) {
$data['viewed'] = COMPLETION_INCOMPLETE;
$record = $DB->get_record('course_modules_completion', array('coursemoduleid' => $cm->id, 'userid' => $userid));
if ($record) {
$data['viewed'] = ($record->viewed == COMPLETION_VIEWED ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE);
}
}
return $data;
}
/**
* Adds the user's custom completion data on the given course module.
*
@ -1175,12 +1191,7 @@ class completion_info {
* @return array The additional completion data.
*/
protected function get_other_cm_completion_data(cm_info $cm, int $userid): array {
$data = [];
// Include in the completion info the grade completion, if necessary.
if (!is_null($cm->completiongradeitemnumber)) {
$data['completiongrade'] = $this->get_grade_completion($cm, $userid);
}
$data = $this->get_core_completion_state($cm, $userid);
// Custom activity module completion data.
@ -1200,7 +1211,7 @@ class completion_info {
}
/** @var activity_custom_completion $customcmcompletion */
$customcmcompletion = new $cmcompletionclass($cm, $userid);
$customcmcompletion = new $cmcompletionclass($cm, $userid, $data);
foreach ($customdata['customcompletionrules'] as $rule => $enabled) {
if (!$enabled) {
// Skip inactive completion rules.

View File

@ -89,6 +89,7 @@ class custom_completion extends activity_custom_completion {
'completionview',
'completionsubmit',
'completionusegrade',
'completionpassgrade',
];
}
}

View File

@ -80,6 +80,7 @@ class custom_completion extends activity_custom_completion {
'completionview',
'completionentries',
'completionusegrade',
'completionpassgrade',
];
}
}

View File

@ -112,6 +112,7 @@ class custom_completion extends activity_custom_completion {
'completionreplies',
'completionposts',
'completionusegrade',
'completionpassgrade',
];
}
}

View File

@ -84,6 +84,7 @@ class custom_completion extends activity_custom_completion {
'completionview',
'completionentries',
'completionusegrade',
'completionpassgrade',
];
}
}

View File

@ -103,6 +103,7 @@ class custom_completion extends activity_custom_completion {
'completiontimespent',
'completionendreached',
'completionusegrade',
'completionpassgrade',
];
}
}

View File

@ -48,23 +48,14 @@ class custom_completion extends activity_custom_completion {
$completionpassorattempts = $this->cm->customdata['customcompletionrules']['completionpassorattemptsexhausted'];
if (empty($completionpassorattempts['completionpass'])) {
if (empty($completionpassorattempts['completionpassgrade'])) {
return true;
}
// Check for passing grade.
$item = grade_item::fetch([
'courseid' => $this->cm->get_course()->id,
'itemtype' => 'mod',
'itemmodule' => 'quiz',
'iteminstance' => $this->cm->instance,
'outcomeid' => null
]);
if ($item) {
$grades = grade_grade::fetch_users_grades($item, [$this->userid], false);
if (!empty($grades[$this->userid]) && $grades[$this->userid]->is_passed($item)) {
return true;
}
if ($this->completionstate &&
isset($this->completionstate['passgrade']) &&
$this->completionstate['passgrade'] == COMPLETION_COMPLETE_PASS) {
return true;
}
// If a passing grade is required and exhausting all available attempts is not accepted for completion,
@ -146,18 +137,15 @@ class custom_completion extends activity_custom_completion {
*/
public function get_custom_rule_descriptions(): array {
$minattempts = $this->cm->customdata['customcompletionrules']['completionminattempts'] ?? 0;
$description['completionminattempts'] = get_string('completiondetail:minattempts', 'mod_quiz', $minattempts);
// Completion pass grade is now part of core. Only show the following if it's combined with min attempts.
$completionpassorattempts = $this->cm->customdata['customcompletionrules']['completionpassorattemptsexhausted'] ?? [];
if (!empty($completionpassorattempts['completionattemptsexhausted'])) {
$passorallattemptslabel = get_string('completiondetail:passorexhaust', 'mod_quiz');
} else {
$passorallattemptslabel = get_string('completiondetail:passgrade', 'mod_quiz');
$description['completionpassorattemptsexhausted'] = get_string('completiondetail:passorexhaust', 'mod_quiz');
}
return [
'completionpassorattemptsexhausted' => $passorallattemptslabel,
'completionminattempts' => get_string('completiondetail:minattempts', 'mod_quiz', $minattempts),
];
return $description;
}
/**
@ -170,6 +158,7 @@ class custom_completion extends activity_custom_completion {
'completionview',
'completionminattempts',
'completionusegrade',
'completionpassgrade',
'completionpassorattemptsexhausted',
];
}

View File

@ -20,6 +20,7 @@ namespace mod_quiz;
use advanced_testcase;
use cm_info;
use core_completion\cm_completion_details;
use grade_item;
use mod_quiz\completion\custom_completion;
use question_engine;
@ -127,11 +128,8 @@ class custom_completion_test extends advanced_testcase {
}
/**
* Test checking the completion state of a quiz.
* Test checking the completion state of a quiz base on core's completionpassgrade criteria.
* The quiz requires a passing grade to be completed.
*
* @covers ::get_state
* @covers ::get_custom_rule_descriptions
*/
public function test_completionpass() {
list($students, $quiz, $cm) = $this->setup_quiz_for_testing_completion([
@ -139,7 +137,7 @@ class custom_completion_test extends advanced_testcase {
'qtype' => 'numerical',
'quizoptions' => [
'completionusegrade' => 1,
'completionpass' => 1
'completionpassgrade' => 1
]
]);
@ -153,13 +151,14 @@ class custom_completion_test extends advanced_testcase {
'tosubmit' => [1 => ['answer' => '3.14']]
]);
$completioninfo = new \completion_info($cm->get_course());
$completiondetails = new cm_completion_details($completioninfo, $cm, (int) $passstudent->id);
// Check the results.
$customcompletion = new custom_completion($cm, (int) $passstudent->id);
$this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
$this->assertEquals(COMPLETION_COMPLETE_PASS, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a pass grade',
$customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
// Do a failing attempt.
@ -170,13 +169,13 @@ class custom_completion_test extends advanced_testcase {
'tosubmit' => [1 => ['answer' => '0']]
]);
$completiondetails = new cm_completion_details($completioninfo, $cm, (int) $failstudent->id);
// Check the results.
$customcompletion = new custom_completion($cm, (int) $failstudent->id);
$this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
$this->assertEquals(COMPLETION_COMPLETE_FAIL, $completiondetails->get_details()['completionpassgrade']->status);
$this->assertEquals(
'Receive a pass grade',
$customcompletion->get_custom_rule_descriptions()['completionpassorattemptsexhausted']
'Receive a passing grade',
$completiondetails->get_details()['completionpassgrade']->description
);
}
@ -194,7 +193,7 @@ class custom_completion_test extends advanced_testcase {
'quizoptions' => [
'attempts' => 2,
'completionusegrade' => 1,
'completionpass' => 1,
'completionpassgrade' => 1,
'completionattemptsexhausted' => 1
]
]);
@ -209,8 +208,11 @@ class custom_completion_test extends advanced_testcase {
'tosubmit' => [1 => ['answer' => '3.14']]
]);
$completioninfo = new \completion_info($cm->get_course());
// Check the results. Quiz is completed by $passstudent because of passing grade.
$customcompletion = new custom_completion($cm, (int) $passstudent->id);
$studentid = (int) $passstudent->id;
$customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
$this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
$this->assertEquals(
@ -227,7 +229,8 @@ class custom_completion_test extends advanced_testcase {
]);
// Check the results. Quiz is not completed by $exhauststudent yet because of failing grade and of remaining attempts.
$customcompletion = new custom_completion($cm, (int) $exhauststudent->id);
$studentid = (int) $exhauststudent->id;
$customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
$this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_INCOMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
$this->assertEquals(
@ -244,7 +247,7 @@ class custom_completion_test extends advanced_testcase {
]);
// Check the results. Quiz is completed by $exhauststudent because there are no remaining attempts.
$customcompletion = new custom_completion($cm, (int) $exhauststudent->id);
$customcompletion = new custom_completion($cm, $studentid, $completioninfo->get_core_completion_state($cm, $studentid));
$this->assertArrayHasKey('completionpassorattemptsexhausted', $cm->customdata['customcompletionrules']);
$this->assertEquals(COMPLETION_COMPLETE, $customcompletion->get_state('completionpassorattemptsexhausted'));
$this->assertEquals(

View File

@ -210,6 +210,7 @@ class custom_completion extends activity_custom_completion {
'completionstatusallscos',
'completionstatusrequired',
'completionusegrade',
'completionpassgrade',
'completionscorerequired',
];
}