Merge branch 'MDL-70815-311-6' of git://github.com/junpataleta/moodle into MOODLE_311_STABLE
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace core_completion;
|
||||
|
||||
use cm_info;
|
||||
use coding_exception;
|
||||
use moodle_exception;
|
||||
|
||||
/**
|
||||
* Base class for defining an activity module's custom completion rules.
|
||||
*
|
||||
* Class for defining an activity module's custom completion rules and fetching the completion statuses
|
||||
* of the custom completion rules for a given module instance and a user.
|
||||
*
|
||||
* @package core_completion
|
||||
* @copyright 2021 Jun Pataleta <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
abstract class activity_custom_completion {
|
||||
|
||||
/** @var cm_info The course module information object. */
|
||||
protected $cm;
|
||||
|
||||
/** @var int The user's ID. */
|
||||
protected $userid;
|
||||
|
||||
/**
|
||||
* activity_custom_completion constructor.
|
||||
*
|
||||
* @param cm_info $cm
|
||||
* @param int $userid
|
||||
*/
|
||||
public function __construct(cm_info $cm, int $userid) {
|
||||
$this->cm = $cm;
|
||||
$this->userid = $userid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the custom rule is defined by this plugin and is enabled for this activity instance.
|
||||
*
|
||||
* @param string $rule The custom completion rule.
|
||||
*/
|
||||
public function validate_rule(string $rule): void {
|
||||
// Check that this custom completion rule is defined.
|
||||
if (!$this->is_defined($rule)) {
|
||||
throw new coding_exception("Undefined custom completion rule '$rule'");
|
||||
}
|
||||
|
||||
// Check that this custom rule is included in the course module's custom completion rules.
|
||||
if (!$this->is_available($rule)) {
|
||||
throw new moodle_exception("Custom completion rule '$rule' is not used by this activity.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this module defines this custom rule.
|
||||
*
|
||||
* @param string $rule The custom completion rule.
|
||||
* @return bool
|
||||
*/
|
||||
public function is_defined(string $rule): bool {
|
||||
return in_array($rule, static::get_defined_custom_rules());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the custom completion rule is being used by the activity module instance.
|
||||
*
|
||||
* @param string $rule The custom completion rule.
|
||||
* @return bool
|
||||
*/
|
||||
public function is_available(string $rule): bool {
|
||||
return in_array($rule, $this->get_available_custom_rules());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of custom completion rules that are being used by this activity module instance.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available_custom_rules(): array {
|
||||
$rules = static::get_defined_custom_rules();
|
||||
$availablerules = [];
|
||||
foreach ($rules as $rule) {
|
||||
$customrule = $this->cm->customdata['customcompletionrules'][$rule] ?? false;
|
||||
if (!empty($customrule)) {
|
||||
$availablerules[] = $rule;
|
||||
}
|
||||
}
|
||||
return $availablerules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the overall completion status of this activity instance for a user based on its available custom completion rules.
|
||||
*
|
||||
* @return int The completion state (e.g. COMPLETION_COMPLETE, COMPLETION_INCOMPLETE).
|
||||
*/
|
||||
public function get_overall_completion_state(): int {
|
||||
foreach ($this->get_available_custom_rules() as $rule) {
|
||||
$state = $this->get_state($rule);
|
||||
// Return early if one of the custom completion rules is not yet complete.
|
||||
if ($state == COMPLETION_INCOMPLETE) {
|
||||
return $state;
|
||||
}
|
||||
}
|
||||
// If this was reached, then all custom rules have been marked complete.
|
||||
return COMPLETION_COMPLETE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the description for a given custom completion rule.
|
||||
*
|
||||
* @param string $rule The custom completion rule.
|
||||
* @return string
|
||||
*/
|
||||
public function get_custom_rule_description(string $rule): string {
|
||||
$descriptions = $this->get_custom_rule_descriptions();
|
||||
if (!isset($descriptions[$rule])) {
|
||||
// Lang string not found for this custom completion rule. Just return it.
|
||||
return $rule;
|
||||
}
|
||||
return $descriptions[$rule];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the module's custom completion class implementation if it's available.
|
||||
*
|
||||
* @param string $modname The activity module name. Usually from cm_info::modname.
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_cm_completion_class(string $modname): ?string {
|
||||
$cmcompletionclass = "mod_{$modname}\\completion\\custom_completion";
|
||||
if (class_exists($cmcompletionclass) && is_subclass_of($cmcompletionclass, self::class)) {
|
||||
return $cmcompletionclass;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the completion state for a given completion rule.
|
||||
*
|
||||
* @param string $rule The completion rule.
|
||||
* @return int The completion state.
|
||||
*/
|
||||
public abstract function get_state(string $rule): int;
|
||||
|
||||
/**
|
||||
* Fetch the list of custom completion rules that this module defines.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public abstract static function get_defined_custom_rules(): array;
|
||||
|
||||
/**
|
||||
* Returns an associative array of the descriptions of custom completion rules.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public abstract function get_custom_rule_descriptions(): array;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace core_completion;
|
||||
|
||||
use advanced_testcase;
|
||||
use coding_exception;
|
||||
use moodle_exception;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
/**
|
||||
* Class for unit testing core_completion/activity_custom_completion.
|
||||
*
|
||||
* @package core_completion
|
||||
* @copyright 2021 Jun Pataleta <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class activity_custom_completion_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Fetches a mocked activity_custom_completion instance.
|
||||
*
|
||||
* @param string[] $methods List of methods to mock.
|
||||
* @return activity_custom_completion|MockObject
|
||||
*/
|
||||
protected function setup_mock(array $methods) {
|
||||
return $this->getMockBuilder(activity_custom_completion::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods($methods)
|
||||
->getMockForAbstractClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_overall_completion_state().
|
||||
*/
|
||||
public function overall_completion_state_provider(): array {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/completionlib.php');
|
||||
return [
|
||||
'First incomplete, second complete' => [
|
||||
['completionsubmit', 'completioncreate'],
|
||||
[COMPLETION_INCOMPLETE, COMPLETION_COMPLETE],
|
||||
1,
|
||||
COMPLETION_INCOMPLETE
|
||||
],
|
||||
'First complete, second incomplete' => [
|
||||
['completionsubmit', 'completioncreate'],
|
||||
[COMPLETION_COMPLETE, COMPLETION_INCOMPLETE],
|
||||
2,
|
||||
COMPLETION_INCOMPLETE
|
||||
],
|
||||
'All complete' => [
|
||||
['completionsubmit', 'completioncreate'],
|
||||
[COMPLETION_COMPLETE, COMPLETION_COMPLETE],
|
||||
2,
|
||||
COMPLETION_COMPLETE
|
||||
],
|
||||
'No rules' => [
|
||||
[],
|
||||
[],
|
||||
0,
|
||||
COMPLETION_COMPLETE
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \core_completion\activity_custom_completion::get_overall_completion_state().
|
||||
*
|
||||
* @dataProvider overall_completion_state_provider
|
||||
* @param string[] $rules The custom completion rules.
|
||||
* @param int[] $rulestates The completion states of these custom completion rules.
|
||||
* @param int $invokecount Expected invoke count of get_state().
|
||||
* @param int $state The expected overall completion state
|
||||
*/
|
||||
public function test_get_overall_completion_state(array $rules, array $rulestates, int $invokecount, int $state) {
|
||||
$stub = $this->setup_mock([
|
||||
'get_available_custom_rules',
|
||||
'get_state',
|
||||
]);
|
||||
|
||||
// Mock activity_custom_completion's get_available_custom_rules() method.
|
||||
$stub->expects($this->once())
|
||||
->method('get_available_custom_rules')
|
||||
->willReturn($rules);
|
||||
|
||||
// Mock activity_custom_completion's get_state() method.
|
||||
if ($invokecount > 0) {
|
||||
$stub->expects($this->exactly($invokecount))
|
||||
->method('get_state')
|
||||
->withConsecutive(
|
||||
[$rules[0]],
|
||||
[$rules[1]]
|
||||
)
|
||||
->willReturn($rulestates[0], $rulestates[1]);
|
||||
} else {
|
||||
$stub->expects($this->never())
|
||||
->method('get_state');
|
||||
}
|
||||
|
||||
$this->assertEquals($state, $stub->get_overall_completion_state());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_validate_rule().
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function validate_rule_provider() {
|
||||
return [
|
||||
'Not defined' => [
|
||||
false, true, coding_exception::class
|
||||
],
|
||||
'Not available' => [
|
||||
true, false, moodle_exception::class
|
||||
],
|
||||
'Defined and available' => [
|
||||
true, true, null
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for validate_rule()
|
||||
*
|
||||
* @dataProvider validate_rule_provider
|
||||
* @param bool $defined is_defined()'s mocked return value.
|
||||
* @param bool $available is_available()'s mocked return value.
|
||||
* @param string|null $expectedexception Expected expectation class name.
|
||||
*/
|
||||
public function test_validate_rule(bool $defined, bool $available, ?string $expectedexception) {
|
||||
$stub = $this->setup_mock([
|
||||
'is_defined',
|
||||
'is_available'
|
||||
]);
|
||||
|
||||
// Mock activity_custom_completion's is_defined() method.
|
||||
$stub->expects($this->any())
|
||||
->method('is_defined')
|
||||
->willReturn($defined);
|
||||
|
||||
// Mock activity_custom_completion's is_available() method.
|
||||
$stub->expects($this->any())
|
||||
->method('is_available')
|
||||
->willReturn($available);
|
||||
|
||||
if ($expectedexception) {
|
||||
$this->expectException($expectedexception);
|
||||
}
|
||||
$stub->validate_rule('customcompletionrule');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for is_available().
|
||||
*/
|
||||
public function test_is_available() {
|
||||
$stub = $this->setup_mock([
|
||||
'get_available_custom_rules',
|
||||
]);
|
||||
|
||||
// Mock activity_custom_completion's get_available_custom_rules() method.
|
||||
$stub->expects($this->any())
|
||||
->method('get_available_custom_rules')
|
||||
->willReturn(['rule1', 'rule2']);
|
||||
|
||||
// Rule is available.
|
||||
$this->assertTrue($stub->is_available('rule1'));
|
||||
|
||||
// Rule is not available.
|
||||
$this->assertFalse($stub->is_available('rule'));
|
||||
}
|
||||
}
|
||||
+167
-65
@@ -26,6 +26,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use core_completion\activity_custom_completion;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
@@ -643,7 +645,7 @@ class completion_info {
|
||||
* @return mixed
|
||||
*/
|
||||
public function internal_get_state($cm, $userid, $current) {
|
||||
global $USER, $DB, $CFG;
|
||||
global $USER, $DB;
|
||||
|
||||
// Get user ID
|
||||
if (!$userid) {
|
||||
@@ -657,50 +659,48 @@ class completion_info {
|
||||
return COMPLETION_INCOMPLETE;
|
||||
}
|
||||
|
||||
// Modname hopefully is provided in $cm but just in case it isn't, let's grab it
|
||||
if (!isset($cm->modname)) {
|
||||
$cm->modname = $DB->get_field('modules', 'name', array('id'=>$cm->module));
|
||||
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)) {
|
||||
$cm->modname = $DB->get_field('modules', 'name', array('id' => $cm->module));
|
||||
}
|
||||
// Some functions call this method and pass $cm as an object with ID only. Make sure course is set as well.
|
||||
if (!isset($cm->course)) {
|
||||
$cm->course = $this->course_id;
|
||||
}
|
||||
}
|
||||
// Make sure we're using a cm_info object.
|
||||
$cminfo = cm_info::create($cm, $userid);
|
||||
|
||||
$newstate = COMPLETION_COMPLETE;
|
||||
|
||||
// Check grade
|
||||
if (!is_null($cm->completiongradeitemnumber)) {
|
||||
require_once($CFG->libdir.'/gradelib.php');
|
||||
$item = grade_item::fetch(array('courseid'=>$cm->course, 'itemtype'=>'mod',
|
||||
'itemmodule'=>$cm->modname, 'iteminstance'=>$cm->instance,
|
||||
'itemnumber'=>$cm->completiongradeitemnumber));
|
||||
if ($item) {
|
||||
// Fetch 'grades' (will be one or none)
|
||||
$grades = grade_grade::fetch_users_grades($item, array($userid), false);
|
||||
if (empty($grades)) {
|
||||
// No grade for user
|
||||
return COMPLETION_INCOMPLETE;
|
||||
}
|
||||
if (count($grades) > 1) {
|
||||
$this->internal_systemerror("Unexpected result: multiple grades for
|
||||
item '{$item->id}', user '{$userid}'");
|
||||
}
|
||||
$newstate = self::internal_get_grade_state($item, reset($grades));
|
||||
if ($newstate == COMPLETION_INCOMPLETE) {
|
||||
return COMPLETION_INCOMPLETE;
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->internal_systemerror("Cannot find grade item for '{$cm->modname}'
|
||||
cm '{$cm->id}' matching number '{$cm->completiongradeitemnumber}'");
|
||||
if (!is_null($cminfo->completiongradeitemnumber)) {
|
||||
$newstate = $this->get_grade_completion($cminfo, $userid);
|
||||
if ($newstate == COMPLETION_INCOMPLETE) {
|
||||
return COMPLETION_INCOMPLETE;
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES)) {
|
||||
$function = $cm->modname.'_get_completion_state';
|
||||
if (!function_exists($function)) {
|
||||
$this->internal_systemerror("Module {$cm->modname} claims to support
|
||||
if (plugin_supports('mod', $cminfo->modname, FEATURE_COMPLETION_HAS_RULES)) {
|
||||
$cmcompletionclass = activity_custom_completion::get_cm_completion_class($cminfo->modname);
|
||||
if ($cmcompletionclass) {
|
||||
/** @var activity_custom_completion $cmcompletion */
|
||||
$cmcompletion = new $cmcompletionclass($cminfo, $userid);
|
||||
if ($cmcompletion->get_overall_completion_state() == COMPLETION_INCOMPLETE) {
|
||||
return COMPLETION_INCOMPLETE;
|
||||
}
|
||||
} else {
|
||||
// Fallback to the get_completion_state callback.
|
||||
$function = $cminfo->modname . '_get_completion_state';
|
||||
if (!function_exists($function)) {
|
||||
$this->internal_systemerror("Module {$cminfo->modname} claims to support
|
||||
FEATURE_COMPLETION_HAS_RULES but does not have required
|
||||
{$cm->modname}_get_completion_state function");
|
||||
}
|
||||
if (!$function($this->course, $cm, $userid, COMPLETION_AND)) {
|
||||
return COMPLETION_INCOMPLETE;
|
||||
{$cminfo->modname}_get_completion_state function");
|
||||
}
|
||||
if (!$function($this->course, $cminfo, $userid, COMPLETION_AND)) {
|
||||
return COMPLETION_INCOMPLETE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,44 @@ class completion_info {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the completion state for an activity completion's require grade completion requirement.
|
||||
*
|
||||
* @param cm_info $cm The course module information.
|
||||
* @param int $userid The user ID.
|
||||
* @return int The completion state.
|
||||
*/
|
||||
public function get_grade_completion(cm_info $cm, int $userid): int {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->libdir . '/gradelib.php');
|
||||
$item = grade_item::fetch([
|
||||
'courseid' => $cm->course,
|
||||
'itemtype' => 'mod',
|
||||
'itemmodule' => $cm->modname,
|
||||
'iteminstance' => $cm->instance,
|
||||
'itemnumber' => $cm->completiongradeitemnumber
|
||||
]);
|
||||
if ($item) {
|
||||
// Fetch 'grades' (will be one or none).
|
||||
$grades = grade_grade::fetch_users_grades($item, [$userid], false);
|
||||
if (empty($grades)) {
|
||||
// No grade for user.
|
||||
return COMPLETION_INCOMPLETE;
|
||||
}
|
||||
if (count($grades) > 1) {
|
||||
$this->internal_systemerror("Unexpected result: multiple grades for
|
||||
item '{$item->id}', user '{$userid}'");
|
||||
}
|
||||
return self::internal_get_grade_state($item, reset($grades));
|
||||
} else {
|
||||
$this->internal_systemerror("Cannot find grade item for '{$cm->modname}'
|
||||
cm '{$cm->id}' matching number '{$cm->completiongradeitemnumber}'");
|
||||
}
|
||||
|
||||
return COMPLETION_INCOMPLETE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a module as viewed.
|
||||
*
|
||||
@@ -927,7 +965,7 @@ class completion_info {
|
||||
* Obtains completion data for a particular activity and user (from the
|
||||
* completion cache if available, or by SQL query)
|
||||
*
|
||||
* @param stcClass|cm_info $cm Activity; only required field is ->id
|
||||
* @param stdClass|cm_info $cm Activity; only required field is ->id
|
||||
* @param bool $wholecourse If true (default false) then, when necessary to
|
||||
* fill the cache, retrieves information from the entire course not just for
|
||||
* this one activity
|
||||
@@ -936,10 +974,12 @@ class completion_info {
|
||||
* testing and so that it can be called recursively from within
|
||||
* get_fast_modinfo. (Needs only list of all CMs with IDs.)
|
||||
* Otherwise the method calls get_fast_modinfo itself.
|
||||
* @return object Completion data (record from course_modules_completion)
|
||||
* @return object Completion data. Record from course_modules_completion plus other completion statuses such as
|
||||
* - Completion status for 'must-receive-grade' completion rule.
|
||||
* - Custom completion statuses defined by the activity module plugin.
|
||||
*/
|
||||
public function get_data($cm, $wholecourse = false, $userid = 0, $modinfo = null) {
|
||||
global $USER, $CFG, $DB;
|
||||
global $USER, $DB;
|
||||
$completioncache = cache::make('core', 'completion');
|
||||
|
||||
// Get user ID
|
||||
@@ -965,7 +1005,27 @@ class completion_info {
|
||||
}
|
||||
}
|
||||
|
||||
// Not there, get via SQL
|
||||
// Some call completion_info::get_data and pass $cm as an object with ID only. Make sure course is set as well.
|
||||
if ($cm instanceof stdClass && !isset($cm->course)) {
|
||||
$cm->course = $this->course_id;
|
||||
}
|
||||
// Make sure we're working on a cm_info object.
|
||||
$cminfo = cm_info::create($cm, $userid);
|
||||
|
||||
// Default data to return when no completion data is found.
|
||||
$defaultdata = [
|
||||
'id' => 0,
|
||||
'coursemoduleid' => $cminfo->id,
|
||||
'userid' => $userid,
|
||||
'completionstate' => 0,
|
||||
'viewed' => 0,
|
||||
'overrideby' => null,
|
||||
'timemodified' => 0,
|
||||
];
|
||||
|
||||
// If cached completion data is not found, fetch via SQL.
|
||||
// Fetch completion data for all of the activities in the course ONLY if we're caching the fetched completion data.
|
||||
// If we're not caching the completion data, then just fetch the completion data for the user in this course module.
|
||||
if ($usecache && $wholecourse) {
|
||||
// Get whole course data for cache
|
||||
$alldatabycmc = $DB->get_records_sql("
|
||||
@@ -991,49 +1051,85 @@ class completion_info {
|
||||
if (isset($alldata[$othercm->id])) {
|
||||
$data = $alldata[$othercm->id];
|
||||
} else {
|
||||
// Row not present counts as 'not complete'
|
||||
$data = array();
|
||||
$data['id'] = 0;
|
||||
// Row not present counts as 'not complete'.
|
||||
$data = $defaultdata;
|
||||
$data['coursemoduleid'] = $othercm->id;
|
||||
$data['userid'] = $userid;
|
||||
$data['completionstate'] = 0;
|
||||
$data['viewed'] = 0;
|
||||
$data['overrideby'] = null;
|
||||
$data['timemodified'] = 0;
|
||||
}
|
||||
$cacheddata[$othercm->id] = $data;
|
||||
// Make sure we're working on a cm_info object.
|
||||
$othercminfo = cm_info::create($othercm, $userid);
|
||||
// Add the other completion data for this user in this module instance.
|
||||
$data += $this->get_other_cm_completion_data($othercminfo, $userid);
|
||||
$cacheddata[$othercminfo->id] = $data;
|
||||
}
|
||||
|
||||
if (!isset($cacheddata[$cm->id])) {
|
||||
$this->internal_systemerror("Unexpected error: course-module {$cm->id} could not be found on course {$this->course->id}");
|
||||
if (!isset($cacheddata[$cminfo->id])) {
|
||||
$errormessage = "Unexpected error: course-module {$cminfo->id} could not be found on course {$this->course->id}";
|
||||
$this->internal_systemerror($errormessage);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Get single record
|
||||
$data = $DB->get_record('course_modules_completion', array('coursemoduleid'=>$cm->id, 'userid'=>$userid));
|
||||
$data = $DB->get_record('course_modules_completion', array('coursemoduleid' => $cminfo->id, 'userid' => $userid));
|
||||
if ($data) {
|
||||
$data = (array)$data;
|
||||
} else {
|
||||
// Row not present counts as 'not complete'
|
||||
$data = array();
|
||||
$data['id'] = 0;
|
||||
$data['coursemoduleid'] = $cm->id;
|
||||
$data['userid'] = $userid;
|
||||
$data['completionstate'] = 0;
|
||||
$data['viewed'] = 0;
|
||||
$data['overrideby'] = null;
|
||||
$data['timemodified'] = 0;
|
||||
// Row not present counts as 'not complete'.
|
||||
$data = $defaultdata;
|
||||
}
|
||||
// Fill the other completion data for this user in this module instance.
|
||||
$data += $this->get_other_cm_completion_data($cminfo, $userid);
|
||||
|
||||
// Put in cache
|
||||
$cacheddata[$cm->id] = $data;
|
||||
$cacheddata[$cminfo->id] = $data;
|
||||
}
|
||||
|
||||
if ($usecache) {
|
||||
$cacheddata['cacherev'] = $this->course->cacherev;
|
||||
$completioncache->set($key, $cacheddata);
|
||||
}
|
||||
return (object)$cacheddata[$cm->id];
|
||||
return (object)$cacheddata[$cminfo->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the user's custom completion data on the given course module.
|
||||
*
|
||||
* @param cm_info $cm The course module information.
|
||||
* @param int $userid The user ID.
|
||||
* @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);
|
||||
}
|
||||
|
||||
// Custom activity module completion data.
|
||||
|
||||
// Return early if the plugin does not define custom completion rules.
|
||||
if (empty($cm->customdata['customcompletionrules'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Return early if the activity modules doe not implement the activity_custom_completion class.
|
||||
$cmcompletionclass = activity_custom_completion::get_cm_completion_class($cm->modname);
|
||||
if (!$cmcompletionclass) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var activity_custom_completion $customcmcompletion */
|
||||
$customcmcompletion = new $cmcompletionclass($cm, $userid);
|
||||
foreach ($cm->customdata['customcompletionrules'] as $rule => $enabled) {
|
||||
if (!$enabled) {
|
||||
// Skip inactive completion rules.
|
||||
continue;
|
||||
}
|
||||
// Get this custom completion rule's completion state.
|
||||
$data['customcompletion'][$rule] = $customcmcompletion->get_state($rule);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1063,11 +1159,17 @@ class completion_info {
|
||||
}
|
||||
$transaction->allow_commit();
|
||||
|
||||
$cmcontext = context_module::instance($data->coursemoduleid, MUST_EXIST);
|
||||
$coursecontext = $cmcontext->get_parent_context();
|
||||
$cmcontext = context_module::instance($data->coursemoduleid);
|
||||
|
||||
$completioncache = cache::make('core', 'completion');
|
||||
if ($data->userid == $USER->id) {
|
||||
// Fetch other completion data to cache (e.g. require grade completion status, custom completion rule statues).
|
||||
$cminfo = cm_info::create($cm, $data->userid); // Make sure we're working on a cm_info object.
|
||||
$otherdata = $this->get_other_cm_completion_data($cminfo, $data->userid);
|
||||
foreach ($otherdata as $key => $value) {
|
||||
$data->$key = $value;
|
||||
}
|
||||
|
||||
// Update module completion in user's cache.
|
||||
if (!($cachedata = $completioncache->get($data->userid . '_' . $cm->course))
|
||||
|| $cachedata['cacherev'] != $this->course->cacherev) {
|
||||
|
||||
@@ -325,42 +325,134 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$c->get_data($cm, false, 100);
|
||||
}
|
||||
|
||||
public function test_internal_get_state() {
|
||||
global $DB;
|
||||
$this->mock_setup();
|
||||
/**
|
||||
* Data provider for test_internal_get_state().
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function internal_get_state_provider() {
|
||||
return [
|
||||
'View required, but not viewed yet' => [
|
||||
COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
|
||||
],
|
||||
'View not required and not viewed yet' => [
|
||||
COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
|
||||
],
|
||||
'View not required, grade required but no grade yet, $cm->modname not set' => [
|
||||
COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
|
||||
],
|
||||
'View not required, grade required but no grade yet, $cm->course not set' => [
|
||||
COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
|
||||
],
|
||||
'View not required, grade not required' => [
|
||||
COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$mockbuilder = $this->getMockBuilder('completion_info');
|
||||
$mockbuilder->setMethods(array('internal_get_grade_state'));
|
||||
$mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
|
||||
$c = $mockbuilder->getMock();
|
||||
/**
|
||||
* Test for completion_info::get_state().
|
||||
*
|
||||
* @dataProvider internal_get_state_provider
|
||||
* @param int $completionview
|
||||
* @param int $completionusegrade
|
||||
* @param string $unsetfield
|
||||
* @param int $expectedstate
|
||||
*/
|
||||
public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
|
||||
$this->setup_data();
|
||||
|
||||
$cm = (object)array('id'=>13, 'course'=>42, 'completiongradeitemnumber'=>null);
|
||||
/** @var \mod_assign_generator $assigngenerator */
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$assign = $assigngenerator->create_instance([
|
||||
'course' => $this->course->id,
|
||||
'completion' => COMPLETION_ENABLED,
|
||||
'completionview' => $completionview,
|
||||
'completionusegrade' => $completionusegrade,
|
||||
]);
|
||||
|
||||
$userid = $this->user->id;
|
||||
$this->setUser($userid);
|
||||
|
||||
$cm = get_coursemodule_from_instance('assign', $assign->id);
|
||||
if ($unsetfield) {
|
||||
unset($cm->$unsetfield);
|
||||
}
|
||||
// If view is required, but they haven't viewed it yet.
|
||||
$cm->completionview = COMPLETION_VIEW_REQUIRED;
|
||||
$current = (object)array('viewed'=>COMPLETION_NOT_VIEWED);
|
||||
$this->assertEquals(COMPLETION_INCOMPLETE, $c->internal_get_state($cm, 123, $current));
|
||||
$current = (object)['viewed' => COMPLETION_NOT_VIEWED];
|
||||
|
||||
// OK set view not required.
|
||||
$cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
|
||||
$completioninfo = new completion_info($this->course);
|
||||
$this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
|
||||
}
|
||||
|
||||
// Test not getting module name.
|
||||
$cm->modname='label';
|
||||
$this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
|
||||
/**
|
||||
* Covers the case where internal_get_state() is being called for a user different from the logged in user.
|
||||
*/
|
||||
public function test_internal_get_state_with_different_user() {
|
||||
$this->setup_data();
|
||||
|
||||
// Test getting module name.
|
||||
$cm->module = 13;
|
||||
unset($cm->modname);
|
||||
/** @var $DB PHPUnit_Framework_MockObject_MockObject */
|
||||
$DB->expects($this->once())
|
||||
->method('get_field')
|
||||
->with('modules', 'name', array('id'=>13))
|
||||
->will($this->returnValue('lable'));
|
||||
$this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
|
||||
/** @var \mod_assign_generator $assigngenerator */
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$assign = $assigngenerator->create_instance([
|
||||
'course' => $this->course->id,
|
||||
'completion' => COMPLETION_ENABLED,
|
||||
'completionusegrade' => 1,
|
||||
]);
|
||||
|
||||
// Note: This function is not fully tested (including kind of the main part) because:
|
||||
// * the grade_item/grade_grade calls are static and can't be mocked,
|
||||
// * the plugin_supports call is static and can't be mocked.
|
||||
$userid = $this->user->id;
|
||||
|
||||
$cm = get_coursemodule_from_instance('assign', $assign->id);
|
||||
$usercm = cm_info::create($cm, $userid);
|
||||
|
||||
// Create a teacher account.
|
||||
$teacher = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
|
||||
// Log in as the teacher.
|
||||
$this->setUser($teacher);
|
||||
|
||||
// Grade the student for this assignment.
|
||||
$assign = new assign($usercm->context, $cm, $cm->course);
|
||||
$data = (object)[
|
||||
'sendstudentnotifications' => false,
|
||||
'attemptnumber' => 1,
|
||||
'grade' => 90,
|
||||
];
|
||||
$assign->save_grade($userid, $data);
|
||||
|
||||
// The target user already received a grade, so internal_get_state should be already complete.
|
||||
$completioninfo = new completion_info($this->course);
|
||||
$this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
|
||||
|
||||
// As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
|
||||
$this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for internal_get_state() for an activity that supports custom completion.
|
||||
*/
|
||||
public function test_internal_get_state_with_custom_completion() {
|
||||
$this->setup_data();
|
||||
|
||||
$choicerecord = [
|
||||
'course' => $this->course,
|
||||
'completion' => COMPLETION_TRACKING_AUTOMATIC,
|
||||
'completionsubmit' => COMPLETION_ENABLED,
|
||||
];
|
||||
$choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
|
||||
$cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
|
||||
|
||||
$completioninfo = new completion_info($this->course);
|
||||
|
||||
// Fetch completion for the user who hasn't made a choice yet.
|
||||
$completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
|
||||
$this->assertEquals(COMPLETION_INCOMPLETE, $completion);
|
||||
|
||||
// Have the user make a choice.
|
||||
$choicewithoptions = choice_get_choice($choice->id);
|
||||
$optionids = array_keys($choicewithoptions->option);
|
||||
choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
|
||||
$completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
|
||||
$this->assertEquals(COMPLETION_COMPLETE, $completion);
|
||||
}
|
||||
|
||||
public function test_set_module_viewed() {
|
||||
@@ -488,77 +580,114 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$c->reset_all_state($cm);
|
||||
}
|
||||
|
||||
public function test_get_data() {
|
||||
/**
|
||||
* Data provider for test_get_data().
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function get_data_provider() {
|
||||
return [
|
||||
'No completion record' => [
|
||||
false, false, false, COMPLETION_INCOMPLETE
|
||||
],
|
||||
'Not completed' => [
|
||||
false, false, true, COMPLETION_INCOMPLETE
|
||||
],
|
||||
'Completed' => [
|
||||
false, false, true, COMPLETION_COMPLETE
|
||||
],
|
||||
'Whole course, complete' => [
|
||||
true, false, true, COMPLETION_COMPLETE
|
||||
],
|
||||
'Get data for another user, result should be not cached' => [
|
||||
false, true, true, COMPLETION_INCOMPLETE
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for completion_info::get_data().
|
||||
*
|
||||
* @dataProvider get_data_provider
|
||||
* @param bool $wholecourse Whole course parameter for get_data().
|
||||
* @param bool $sameuser Whether the user calling get_data() is the user itself.
|
||||
* @param bool $hasrecord Whether to create a course_modules_completion record.
|
||||
* @param int $completion The completion state expected.
|
||||
*/
|
||||
public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
|
||||
global $DB;
|
||||
$this->mock_setup();
|
||||
|
||||
$this->setup_data();
|
||||
$user = $this->user;
|
||||
|
||||
/** @var \mod_choice_generator $choicegenerator */
|
||||
$choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
|
||||
$choice = $choicegenerator->create_instance([
|
||||
'course' => $this->course->id,
|
||||
'completion' => true,
|
||||
'completionview' => true,
|
||||
]);
|
||||
|
||||
$cm = get_coursemodule_from_instance('choice', $choice->id);
|
||||
|
||||
// Let's manually create a course completion record instead of going thru the hoops to complete an activity.
|
||||
if ($hasrecord) {
|
||||
$cmcompletionrecord = (object)[
|
||||
'coursemoduleid' => $cm->id,
|
||||
'userid' => $user->id,
|
||||
'completionstate' => $completion,
|
||||
'viewed' => 0,
|
||||
'overrideby' => null,
|
||||
'timemodified' => 0,
|
||||
];
|
||||
$DB->insert_record('course_modules_completion', $cmcompletionrecord);
|
||||
}
|
||||
|
||||
// Whether we expect for the returned completion data to be stored in the cache.
|
||||
$iscached = true;
|
||||
|
||||
if (!$sameuser) {
|
||||
$iscached = false;
|
||||
$this->setAdminUser();
|
||||
} else {
|
||||
$this->setUser($user);
|
||||
}
|
||||
|
||||
// Mock other completion data.
|
||||
$completioninfo = new completion_info($this->course);
|
||||
|
||||
$result = $completioninfo->get_data($cm, $wholecourse, $user->id);
|
||||
// Course module ID of the returned completion data must match this activity's course module ID.
|
||||
$this->assertEquals($cm->id, $result->coursemoduleid);
|
||||
// User ID of the returned completion data must match the user's ID.
|
||||
$this->assertEquals($user->id, $result->userid);
|
||||
// The completion state of the returned completion data must match the expected completion state.
|
||||
$this->assertEquals($completion, $result->completionstate);
|
||||
|
||||
// If the user has no completion record, then the default record should be returned.
|
||||
if (!$hasrecord) {
|
||||
$iscached = false;
|
||||
$this->assertEquals(0, $result->id);
|
||||
}
|
||||
|
||||
// Check caching.
|
||||
$key = "{$user->id}_{$this->course->id}";
|
||||
$cache = cache::make('core', 'completion');
|
||||
if ($iscached) {
|
||||
// If we expect this to be cached, then fetching the result must match the cached data.
|
||||
$this->assertEquals($result, (object)$cache->get($key)[$cm->id]);
|
||||
|
||||
$c = new completion_info((object)array('id'=>42, 'cacherev'=>1));
|
||||
$cm = (object)array('id'=>13, 'course'=>42);
|
||||
|
||||
// 1. Not current user, record exists.
|
||||
$sillyrecord = (object)array('frog'=>'kermit');
|
||||
|
||||
/** @var $DB PHPUnit_Framework_MockObject_MockObject */
|
||||
$DB->expects($this->at(0))
|
||||
->method('get_record')
|
||||
->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>123))
|
||||
->will($this->returnValue($sillyrecord));
|
||||
$result = $c->get_data($cm, false, 123);
|
||||
$this->assertEquals($sillyrecord, $result);
|
||||
$this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
|
||||
|
||||
// 2. Not current user, default record, whole course.
|
||||
$cache->purge();
|
||||
$DB->expects($this->at(0))
|
||||
->method('get_records_sql')
|
||||
->will($this->returnValue(array()));
|
||||
$modinfo = new stdClass();
|
||||
$modinfo->cms = array((object)array('id'=>13));
|
||||
$result=$c->get_data($cm, true, 123, $modinfo);
|
||||
$this->assertEquals((object)array(
|
||||
'id' => '0', 'coursemoduleid' => 13, 'userid' => 123, 'completionstate' => 0,
|
||||
'viewed' => 0, 'timemodified' => 0, 'overrideby' => 0), $result);
|
||||
$this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
|
||||
|
||||
// 3. Current user, single record, not from cache.
|
||||
$DB->expects($this->at(0))
|
||||
->method('get_record')
|
||||
->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>314159))
|
||||
->will($this->returnValue($sillyrecord));
|
||||
$result = $c->get_data($cm);
|
||||
$this->assertEquals($sillyrecord, $result);
|
||||
$cachevalue = $cache->get('314159_42');
|
||||
$this->assertEquals((array)$sillyrecord, $cachevalue[13]);
|
||||
|
||||
// 4. Current user, 'whole course', but from cache.
|
||||
$result = $c->get_data($cm, true);
|
||||
$this->assertEquals($sillyrecord, $result);
|
||||
|
||||
// 5. Current user, 'whole course' and record not in cache.
|
||||
$cache->purge();
|
||||
|
||||
// Scenario: Completion data exists for one CMid.
|
||||
$basicrecord = (object)array('coursemoduleid'=>13);
|
||||
$DB->expects($this->at(0))
|
||||
->method('get_records_sql')
|
||||
->will($this->returnValue(array('1'=>$basicrecord)));
|
||||
|
||||
// There are two CMids in total, the one we had data for and another one.
|
||||
$modinfo = new stdClass();
|
||||
$modinfo->cms = array((object)array('id'=>13), (object)array('id'=>14));
|
||||
$result = $c->get_data($cm, true, 0, $modinfo);
|
||||
|
||||
// Check result.
|
||||
$this->assertEquals($basicrecord, $result);
|
||||
|
||||
// Check the cache contents.
|
||||
$cachevalue = $cache->get('314159_42');
|
||||
$this->assertEquals($basicrecord, (object)$cachevalue[13]);
|
||||
$this->assertEquals(array('id' => '0', 'coursemoduleid' => 14,
|
||||
'userid' => 314159, 'completionstate' => 0, 'viewed' => 0, 'overrideby' => 0, 'timemodified' => 0),
|
||||
$cachevalue[14]);
|
||||
// Check cached data for other course modules in the course.
|
||||
// The sample module created in setup_data() should suffice to confirm this.
|
||||
if ($wholecourse) {
|
||||
$this->assertArrayHasKey($this->module1->id, $cache->get($key));
|
||||
} else {
|
||||
$this->assertArrayNotHasKey($this->module1->id, $cache->get($key));
|
||||
}
|
||||
} else {
|
||||
// Otherwise, this should not be cached.
|
||||
$this->assertFalse($cache->get($key));
|
||||
}
|
||||
}
|
||||
|
||||
public function test_internal_set_data() {
|
||||
@@ -1042,6 +1171,60 @@ class core_completionlib_testcase extends advanced_testcase {
|
||||
$this->assertTrue(completion_can_view_data($student->id, $this->course->id));
|
||||
$this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_grade_completion().
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function get_grade_completion_provider() {
|
||||
return [
|
||||
'Grade not required' => [false, false, null, moodle_exception::class, null],
|
||||
'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
|
||||
'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
|
||||
'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
|
||||
'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for \completion_info::get_grade_completion().
|
||||
*
|
||||
* @dataProvider get_grade_completion_provider
|
||||
* @param bool $completionusegrade Whether the test activity has grade completion requirement.
|
||||
* @param bool $hasgrade Whether to set grade for the user in this activity.
|
||||
* @param int|null $passinggrade Passing grade to set for the test activity.
|
||||
* @param string|null $expectedexception Expected exception.
|
||||
* @param int|null $expectedresult The expected completion status.
|
||||
*/
|
||||
public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade, ?string $expectedexception,
|
||||
?int $expectedresult) {
|
||||
$this->setup_data();
|
||||
|
||||
/** @var \mod_assign_generator $assigngenerator */
|
||||
$assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
|
||||
$assign = $assigngenerator->create_instance([
|
||||
'course' => $this->course->id,
|
||||
'completion' => COMPLETION_ENABLED,
|
||||
'completionusegrade' => $completionusegrade,
|
||||
'gradepass' => $passinggrade,
|
||||
]);
|
||||
|
||||
$cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
|
||||
if ($completionusegrade && $hasgrade) {
|
||||
$assigninstance = new assign($cm->context, $cm, $this->course);
|
||||
$grade = $assigninstance->get_user_grade($this->user->id, true);
|
||||
$grade->grade = 75;
|
||||
$assigninstance->update_grade($grade);
|
||||
}
|
||||
|
||||
$completioninfo = new completion_info($this->course);
|
||||
if ($expectedexception) {
|
||||
$this->expectException($expectedexception);
|
||||
}
|
||||
$gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
|
||||
$this->assertEquals($expectedresult, $gradecompletion);
|
||||
}
|
||||
}
|
||||
|
||||
class core_completionlib_fake_recordset implements Iterator {
|
||||
|
||||
@@ -33,6 +33,15 @@ information provided here is intended especially for developers.
|
||||
* New DML driver method `$DB->sql_group_concat` for performing group concatenation of a field within a SQL query
|
||||
* Added new class, AMD modules and WS that allow displaying forms in modal popups or load and submit in AJAX requests.
|
||||
See https://docs.moodle.org/dev/Modal_and_AJAX_forms for more details.
|
||||
* New base class for defining an activity's custom completion requirements: \core_completion\activity_custom_completion.
|
||||
Activity module plugins that define custom completion conditions should implement a mod_[modname]\completion\custom_completion
|
||||
subclass and the following methods:
|
||||
- get_state(): Provides the completion state for a given custom completion rule.
|
||||
- get_defined_custom_rules(): Returns an array of the activity module's custom completion rules.
|
||||
e.g. ['completionsubmit']
|
||||
- get_custom_rule_descriptions(): Returns an associative array with values containing the user-facing textual description
|
||||
of the custom completion rules (which serve as the keys to these values).
|
||||
e.g. ['completionsubmit' => 'Must submit']
|
||||
|
||||
=== 3.10 ===
|
||||
* PHPUnit has been upgraded to 8.5. That comes with a few changes:
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace mod_choice\completion;
|
||||
|
||||
use core_completion\activity_custom_completion;
|
||||
|
||||
/**
|
||||
* Activity custom completion subclass for the choice activity.
|
||||
*
|
||||
* Class for defining mod_choice's custom completion rules and fetching the completion statuses
|
||||
* of the custom completion rules for a given choice instance and a user.
|
||||
*
|
||||
* @package mod_choice
|
||||
* @copyright 2021 Jun Pataleta <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class custom_completion extends activity_custom_completion {
|
||||
|
||||
/**
|
||||
* Fetches the completion state for a given completion rule.
|
||||
*
|
||||
* @param string $rule The completion rule.
|
||||
* @return int The completion state.
|
||||
*/
|
||||
public function get_state(string $rule): int {
|
||||
global $DB;
|
||||
|
||||
$this->validate_rule($rule);
|
||||
|
||||
// Choice only supports completionsubmit as a custom rule.
|
||||
$status = $DB->record_exists('choice_answers', ['choiceid' => $this->cm->instance, 'userid' => $this->userid]);
|
||||
return $status ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list of custom completion rules that this module defines.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_defined_custom_rules(): array {
|
||||
return [
|
||||
'completionsubmit'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array of the descriptions of custom completion rules.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_custom_rule_descriptions(): array {
|
||||
return [
|
||||
'completionsubmit' => get_string('completiondetail:submit', 'choice')
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ $string['calendarend'] = '{$a} closes';
|
||||
$string['calendarstart'] = '{$a} opens';
|
||||
$string['cannotsubmit'] = 'Sorry, there was a problem submitting your choice. Please try again.';
|
||||
$string['closebeforeopen'] = 'You have specified a close date before the open date.';
|
||||
$string['completiondetail:submit'] = 'Make a choice';
|
||||
$string['completionsubmit'] = 'Show as complete when user makes a choice';
|
||||
$string['displayhorizontal'] = 'Display horizontally';
|
||||
$string['displaymode'] = 'Display mode for the options';
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace mod_choice;
|
||||
|
||||
use advanced_testcase;
|
||||
use cm_info;
|
||||
use coding_exception;
|
||||
use mod_choice\completion\custom_completion;
|
||||
use moodle_exception;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/completionlib.php');
|
||||
|
||||
/**
|
||||
* Class for unit testing mod_choice/custom_completion.
|
||||
*
|
||||
* @package mod_choice
|
||||
* @copyright 2021 Jun Pataleta <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class custom_completion_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Data provider for get_state().
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function get_state_provider(): array {
|
||||
return [
|
||||
'Undefined rule' => [
|
||||
'somenonexistentrule', COMPLETION_DISABLED, false, null, coding_exception::class
|
||||
],
|
||||
'Rule not available' => [
|
||||
'completionsubmit', COMPLETION_DISABLED, false, null, moodle_exception::class
|
||||
],
|
||||
'Rule available, user has not submitted' => [
|
||||
'completionsubmit', COMPLETION_ENABLED, false, COMPLETION_INCOMPLETE, null
|
||||
],
|
||||
'Rule available, user has submitted' => [
|
||||
'completionsubmit', COMPLETION_ENABLED, true, COMPLETION_COMPLETE, null
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for get_state().
|
||||
*
|
||||
* @dataProvider get_state_provider
|
||||
* @param string $rule The custom completion rule.
|
||||
* @param int $available Whether this rule is available.
|
||||
* @param bool $submitted Whether the user has made a choice.
|
||||
* @param int|null $status Expected status.
|
||||
* @param string|null $exception Expected exception.
|
||||
*/
|
||||
public function test_get_state(string $rule, int $available, ?bool $submitted, ?int $status, ?string $exception) {
|
||||
global $DB;
|
||||
|
||||
if (!is_null($exception)) {
|
||||
$this->expectException($exception);
|
||||
}
|
||||
|
||||
// Custom completion rule data for cm_info::customdata.
|
||||
$customdataval = [
|
||||
'customcompletionrules' => [
|
||||
$rule => $available
|
||||
]
|
||||
];
|
||||
|
||||
// Build a mock cm_info instance.
|
||||
$mockcminfo = $this->getMockBuilder(cm_info::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['__get'])
|
||||
->getMock();
|
||||
|
||||
// Mock the return of the magic getter method when fetching the cm_info object's customdata and instance values.
|
||||
$mockcminfo->expects($this->any())
|
||||
->method('__get')
|
||||
->will($this->returnValueMap([
|
||||
['customdata', $customdataval],
|
||||
['instance', 1],
|
||||
]));
|
||||
|
||||
// Mock the DB calls.
|
||||
$DB = $this->createMock(get_class($DB));
|
||||
$DB->expects($this->atMost(1))
|
||||
->method('record_exists')
|
||||
->willReturn($submitted);
|
||||
|
||||
$customcompletion = new custom_completion($mockcminfo, 2);
|
||||
$this->assertEquals($status, $customcompletion->get_state($rule));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for get_defined_custom_rules().
|
||||
*/
|
||||
public function test_get_defined_custom_rules() {
|
||||
$rules = custom_completion::get_defined_custom_rules();
|
||||
$this->assertCount(1, $rules);
|
||||
$this->assertEquals('completionsubmit', reset($rules));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for get_defined_custom_rule_descriptions().
|
||||
*/
|
||||
public function test_get_custom_rule_descriptions() {
|
||||
// Get defined custom rules.
|
||||
$rules = custom_completion::get_defined_custom_rules();
|
||||
|
||||
// Build a mock cm_info instance.
|
||||
$mockcminfo = $this->getMockBuilder(cm_info::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['__get'])
|
||||
->getMock();
|
||||
|
||||
// Instantiate a custom_completion object using the mocked cm_info.
|
||||
$customcompletion = new custom_completion($mockcminfo, 1);
|
||||
|
||||
// Get custom rule descriptions.
|
||||
$ruledescriptions = $customcompletion->get_custom_rule_descriptions();
|
||||
|
||||
// Confirm that defined rules and rule descriptions are consistent with each other.
|
||||
$this->assertEquals(count($rules), count($ruledescriptions));
|
||||
foreach ($rules as $rule) {
|
||||
$this->assertArrayHasKey($rule, $ruledescriptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for is_defined().
|
||||
*/
|
||||
public function test_is_defined() {
|
||||
// Build a mock cm_info instance.
|
||||
$mockcminfo = $this->getMockBuilder(cm_info::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$customcompletion = new custom_completion($mockcminfo, 1);
|
||||
|
||||
// Rule is defined.
|
||||
$this->assertTrue($customcompletion->is_defined('completionsubmit'));
|
||||
|
||||
// Undefined rule.
|
||||
$this->assertFalse($customcompletion->is_defined('somerandomrule'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_get_available_custom_rules().
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
public function get_available_custom_rules_provider(): array {
|
||||
return [
|
||||
'Completion submit available' => [
|
||||
COMPLETION_ENABLED, ['completionsubmit']
|
||||
],
|
||||
'Completion submit not available' => [
|
||||
COMPLETION_DISABLED, []
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for get_available_custom_rules().
|
||||
*
|
||||
* @dataProvider get_available_custom_rules_provider
|
||||
* @param int $status
|
||||
* @param array $expected
|
||||
*/
|
||||
public function test_get_available_custom_rules(int $status, array $expected) {
|
||||
$customdataval = [
|
||||
'customcompletionrules' => [
|
||||
'completionsubmit' => $status
|
||||
]
|
||||
];
|
||||
|
||||
// Build a mock cm_info instance.
|
||||
$mockcminfo = $this->getMockBuilder(cm_info::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['__get'])
|
||||
->getMock();
|
||||
|
||||
// Mock the return of magic getter for the customdata attribute.
|
||||
$mockcminfo->expects($this->any())
|
||||
->method('__get')
|
||||
->with('customdata')
|
||||
->willReturn($customdataval);
|
||||
|
||||
$customcompletion = new custom_completion($mockcminfo, 1);
|
||||
$this->assertEquals($expected, $customcompletion->get_available_custom_rules());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user