768 lines
38 KiB
PHP
768 lines
38 KiB
PHP
<?php
|
|
// This file is part of the Studyplan plugin for Moodle
|
|
//
|
|
// 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 <https://www.gnu.org/licenses/>.
|
|
/**
|
|
* Class to collect course completion info for a given course
|
|
* @package local_treestudyplan
|
|
* @copyright 2023 P.M. Kuipers
|
|
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
namespace local_treestudyplan;
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
require_once($CFG->libdir.'/externallib.php');
|
|
require_once($CFG->libdir.'/gradelib.php');
|
|
require_once($CFG->dirroot.'/course/lib.php');
|
|
|
|
use core\check\performance\debugging;
|
|
use core_course\local\repository\caching_content_item_readonly_repository;
|
|
use core_course\local\repository\content_item_readonly_repository;
|
|
use \grade_item;
|
|
use \grade_scale;
|
|
use \grade_outcome;
|
|
|
|
/**
|
|
* Class to collect course completion info for a given course
|
|
*/
|
|
class corecompletioninfo {
|
|
/** @var \stdClass */
|
|
private $course;
|
|
/** @var \completion_info */
|
|
private $completion;
|
|
/** @var \course_modinfo */
|
|
private $modinfo;
|
|
|
|
/**
|
|
* Cached dict of completion:: constants to equivalent webservice strings
|
|
* @var array */
|
|
private static $completionhandles = null;
|
|
/**
|
|
* Cached dict of all completion type constants to equivalent strings
|
|
* @var array */
|
|
private static $completiontypes = null;
|
|
|
|
/**
|
|
* Course id of relevant course
|
|
*/
|
|
public function id() {
|
|
return $this->course->id;
|
|
}
|
|
/**
|
|
* Construct new object for a given course
|
|
* @param \stdClass $course Course database record
|
|
*/
|
|
public function __construct($course) {
|
|
global $DB;
|
|
$this->course = $course;
|
|
$this->completion = new \completion_info($this->course);
|
|
$this->modinfo = get_fast_modinfo($this->course);
|
|
}
|
|
|
|
/**
|
|
* Get dictionary of all completion types as CONST => 'string'
|
|
* @return array
|
|
*/
|
|
public static function completiontypes() {
|
|
/* While it is tempting to use the global array COMPLETION_CRITERIA_TYPES,
|
|
so we don't have to manually add any completion types if moodle decides to add a few.
|
|
We can just as easily add the list here manually, since adding a completion type
|
|
requires adding code to this page anyway.
|
|
And this way we can keep the moodle code style checker happy.
|
|
(Moodle will probably refator that part of the code anyway in the future, without
|
|
taking effects of this plugin into account :)
|
|
|
|
Array declaration based completion/criteria/completion_criteria.php:85 where the global
|
|
COMPLETION_CRITERIA_TYPES iw/was defined.
|
|
*/
|
|
if (!isset(self::$completiontypes)) {
|
|
self::$completiontypes = [
|
|
COMPLETION_CRITERIA_TYPE_SELF => 'self',
|
|
COMPLETION_CRITERIA_TYPE_DATE => 'date',
|
|
COMPLETION_CRITERIA_TYPE_UNENROL => 'unenrol',
|
|
COMPLETION_CRITERIA_TYPE_ACTIVITY => 'activity',
|
|
COMPLETION_CRITERIA_TYPE_DURATION => 'duration',
|
|
COMPLETION_CRITERIA_TYPE_GRADE => 'grade',
|
|
COMPLETION_CRITERIA_TYPE_ROLE => 'role',
|
|
COMPLETION_CRITERIA_TYPE_COURSE => 'course',
|
|
];
|
|
}
|
|
return self::$completiontypes;
|
|
}
|
|
|
|
/**
|
|
* Translate a numeric completion constant to a text string
|
|
* @param int $completion The completion code as defined in completionlib.php to translate to a text handle
|
|
*/
|
|
public static function completion_handle($completion) {
|
|
if (empty(self::$completionhandles)) {
|
|
// Cache the translation table, to avoid overhead.
|
|
self::$completionhandles = [
|
|
COMPLETION_INCOMPLETE => "incomplete",
|
|
COMPLETION_COMPLETE => "complete",
|
|
COMPLETION_COMPLETE_PASS => "complete-pass",
|
|
COMPLETION_COMPLETE_FAIL => "complete-fail",
|
|
COMPLETION_COMPLETE_FAIL_HIDDEN => "complete-fail"]; // The front end won't differentiate between hidden or not.
|
|
}
|
|
return self::$completionhandles[$completion] ?? "undefined";
|
|
}
|
|
|
|
/**
|
|
* Webservice editor structure for completion_item
|
|
* @param int $value Webservice requirement constant
|
|
*/
|
|
public static function completion_item_editor_structure($value = VALUE_REQUIRED) : \external_description {
|
|
return new \external_single_structure([
|
|
"id" => new \external_value(PARAM_INT, 'criteria id', VALUE_OPTIONAL),
|
|
"title" => new \external_value(PARAM_RAW, 'name of subitem', VALUE_OPTIONAL),
|
|
"link" => new \external_value(PARAM_RAW, 'optional link to more details', VALUE_OPTIONAL),
|
|
"details" => new \external_single_structure([
|
|
"type" => new \external_value(PARAM_RAW, 'type', VALUE_OPTIONAL),
|
|
"criteria" => new \external_value(PARAM_RAW, 'criteria', VALUE_OPTIONAL),
|
|
"requirement" => new \external_value(PARAM_RAW, 'requirement', VALUE_OPTIONAL),
|
|
"status" => new \external_value(PARAM_RAW, 'status', VALUE_OPTIONAL),
|
|
]),
|
|
"progress" => completionscanner::structure(),
|
|
], 'completion type', $value);
|
|
}
|
|
|
|
/**
|
|
* Webservice editor structure for completion_type
|
|
* @param int $value Webservice requirement constant
|
|
*/
|
|
public static function completion_type_editor_structure($value = VALUE_REQUIRED) : \external_description {
|
|
return new \external_single_structure([
|
|
"items" => new \external_multiple_structure(self::completion_item_editor_structure(), 'subitems', VALUE_OPTIONAL),
|
|
"title" => new \external_value(PARAM_RAW, 'optional title', VALUE_OPTIONAL),
|
|
"desc" => new \external_value(PARAM_RAW, 'optional description', VALUE_OPTIONAL),
|
|
"type" => new \external_value(PARAM_TEXT, 'completion type name'),
|
|
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation for this type ["all", "any"]'),
|
|
], 'completion type', $value);
|
|
}
|
|
|
|
/**
|
|
* Webservice editor structure for course completion
|
|
* @param int $value Webservice requirement constant
|
|
*/
|
|
/**
|
|
* Webservice structure for editor info
|
|
* @param int $value Webservice requirement constant
|
|
*/
|
|
public static function editor_structure($value = VALUE_REQUIRED) : \external_description {
|
|
return new \external_single_structure([
|
|
"conditions" => new \external_multiple_structure(self::completion_type_editor_structure(), 'completion conditions'),
|
|
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation ["all", "any"]'),
|
|
"enabled" => new \external_value(PARAM_BOOL, "whether completion is enabled here"),
|
|
], 'course completion info', $value);
|
|
}
|
|
|
|
/**
|
|
* Webservice user view structure for completion_item
|
|
* @param int $value Webservice requirement constant
|
|
*/
|
|
public static function completion_item_user_structure($value = VALUE_REQUIRED) : \external_description {
|
|
return new \external_single_structure([
|
|
"id" => new \external_value(PARAM_INT, 'id of completion', VALUE_OPTIONAL),
|
|
"title" => new \external_value(PARAM_RAW, 'name of subitem', VALUE_OPTIONAL),
|
|
"details" => new \external_single_structure([
|
|
"type" => new \external_value(PARAM_RAW, 'type', VALUE_OPTIONAL),
|
|
"criteria" => new \external_value(PARAM_RAW, 'criteria', VALUE_OPTIONAL),
|
|
"requirement" => new \external_value(PARAM_RAW, 'requirement', VALUE_OPTIONAL),
|
|
"status" => new \external_value(PARAM_RAW, 'status', VALUE_OPTIONAL),
|
|
]),
|
|
"link" => new \external_value(PARAM_RAW, 'optional link to more details', VALUE_OPTIONAL),
|
|
"completed" => new \external_value(PARAM_BOOL, 'simple completed or not'),
|
|
"status" => new \external_value(PARAM_TEXT,
|
|
'extended completion status ["incomplete", "progress", "complete", "complete-pass", "complete-fail"]'),
|
|
"pending" => new \external_value(PARAM_BOOL,
|
|
'optional pending state, for submitted but not yet reviewed activities', VALUE_OPTIONAL),
|
|
"grade" => new \external_value(PARAM_TEXT, 'optional grade result for this subitem', VALUE_OPTIONAL),
|
|
"feedback" => new \external_value(PARAM_RAW, 'optional feedback for this subitem ', VALUE_OPTIONAL),
|
|
"warning" => new \external_value(PARAM_TEXT, 'optional warning text', VALUE_OPTIONAL),
|
|
], 'completion type', $value);
|
|
}
|
|
|
|
/**
|
|
* Webservice user view structure for completion_type
|
|
* @param int $value Webservice requirement constant
|
|
*/
|
|
public static function completion_type_user_structure($value = VALUE_REQUIRED) : \external_description {
|
|
return new \external_single_structure([
|
|
"items" => new \external_multiple_structure(self::completion_item_user_structure(), 'subitems', VALUE_OPTIONAL),
|
|
"title" => new \external_value(PARAM_RAW, 'optional title', VALUE_OPTIONAL),
|
|
"desc" => new \external_value(PARAM_RAW, 'optional description', VALUE_OPTIONAL),
|
|
"type" => new \external_value(PARAM_TEXT, 'completion type name'),
|
|
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation for this type ["all", "any"]'),
|
|
"completed" => new \external_value(PARAM_BOOL, 'current completion value for this type'),
|
|
"status" => new \external_value(PARAM_TEXT,
|
|
'extended completion status ["incomplete", "progress", "complete", "complete-pass", "complete-fail"]'),
|
|
"progress" => new \external_value(PARAM_INT, 'completed sub-conditions'),
|
|
"count" => new \external_value(PARAM_INT, 'total number of sub-conditions'),
|
|
], 'completion type', $value);
|
|
}
|
|
|
|
/**
|
|
* Webservice structure for userinfo
|
|
* @param int $value Webservice requirement constant
|
|
*/
|
|
public static function user_structure($value = VALUE_REQUIRED) : \external_description {
|
|
return new \external_single_structure([
|
|
"progress" => new \external_value(PARAM_INT, 'completed sub-conditions'),
|
|
"enabled" => new \external_value(PARAM_BOOL, "whether completion is enabled here"),
|
|
"tracked" => new \external_value(PARAM_BOOL, "whether completion is tracked for the user", VALUE_OPTIONAL),
|
|
"count" => new \external_value(PARAM_INT, 'total number of sub-conditions'),
|
|
"conditions" => new \external_multiple_structure(self::completion_type_user_structure(), 'completion conditions'),
|
|
"completed" => new \external_value(PARAM_BOOL, 'current completion value'),
|
|
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation ["all", "any"]'),
|
|
"pending" => new \external_value(PARAM_BOOL, "true if the user has any assignments pending grading", VALUE_OPTIONAL),
|
|
], 'course completion info', $value);
|
|
}
|
|
|
|
/**
|
|
* Convert agregation method constant to equivalent string for webservice
|
|
* @param int $method COMPLETION_AGGREGATION_ALL || COMPLETION_AGGREGATION_ANY
|
|
* @return string 'all' or 'any'
|
|
*/
|
|
private static function aggregation_handle($method) {
|
|
return ($method == COMPLETION_AGGREGATION_ALL) ? "all" : "any";
|
|
}
|
|
|
|
/**
|
|
* Webservice model for editor info
|
|
* @param int[] $studentlist List of user id's to use for checking issueing progress within a study plan
|
|
* @return array Webservice data model
|
|
*/
|
|
public function editor_model(array $studentlist = null) {
|
|
global $DB, $CFG;
|
|
|
|
$conditions = [];
|
|
$aggregation = "all"; // Default.
|
|
$info = [
|
|
"conditions" => $conditions,
|
|
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method()),
|
|
"enabled" => $this->completion->is_enabled()
|
|
];
|
|
|
|
// Check if completion tracking is enabled for this course - otherwise, revert to defaults .
|
|
if ($this->completion->is_enabled()) {
|
|
$aggregation = $this->completion->get_aggregation_method();
|
|
// Loop through all condition types to see if they are applicable.
|
|
foreach (self::completiontypes() as $type => $handle) {
|
|
$criterias = $this->completion->get_criteria($type); // Returns array of relevant criteria items.
|
|
if (count($criterias) > 0 ) {
|
|
// Only take it into account if the criteria count is > 0.
|
|
$cinfo = [
|
|
"type" => $handle,
|
|
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method($type)),
|
|
"title" => reset($criterias)->get_type_title(),
|
|
"items" => [],
|
|
];
|
|
|
|
foreach ($criterias as $criteria) {
|
|
/* Unfortunately, we cannot easily get the criteria details with get_details() without having a
|
|
user completion object involved, so'we'll have to retrieve the details per completion type.
|
|
See moodle/completion/criteria/completion_criteria_*.php::get_details() for the code that
|
|
the code below is based on.
|
|
*/
|
|
|
|
if ($type == COMPLETION_CRITERIA_TYPE_SELF) {
|
|
$details = [
|
|
"type" => $criteria->get_title(),
|
|
"criteria" => $criteria->get_title(),
|
|
"requirement" => get_string('markingyourselfcomplete', 'completion'),
|
|
"status" => "",
|
|
];
|
|
} else if ($type == COMPLETION_CRITERIA_TYPE_DATE) {
|
|
$details = [
|
|
"type" => get_string('datepassed', 'completion'),
|
|
"criteria" => get_string('remainingenroleduntildate', 'completion'),
|
|
"requirement" => date("Y-m-d", $criteria->timeend),
|
|
"status" => "",
|
|
];
|
|
} else if ($type == COMPLETION_CRITERIA_TYPE_UNENROL) {
|
|
$details = [
|
|
"type" => get_string('unenrolment', 'completion'),
|
|
"criteria" => get_string('unenrolment', 'completion'),
|
|
"requirement" => get_string('unenrolingfromcourse', 'completion'),
|
|
"status" => "",
|
|
];
|
|
} else if ($type == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
|
|
$cm = $this->modinfo->get_cm($criteria->moduleinstance);
|
|
/* Criteria and requirements will be built in a moment by code copied
|
|
from completion_criteria_activity.php.
|
|
*/
|
|
$details = [
|
|
"type" => $criteria->get_title(),
|
|
"criteria" => "",
|
|
"requirement" => "",
|
|
"status" => "",
|
|
];
|
|
if ($cm->has_view()) {
|
|
$details['criteria'] = \html_writer::link($cm->url, $cm->get_formatted_name());
|
|
} else {
|
|
$details['criteria'] = $cm->get_formatted_name();
|
|
}
|
|
// Build requirements.
|
|
$details['requirement'] = array();
|
|
|
|
if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
|
|
$details['requirement'][] = get_string('markingyourselfcomplete', 'completion');
|
|
} else if ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
|
|
if ($cm->completionview) {
|
|
$modulename = \core_text::strtolower(get_string('modulename', $criteria->module));
|
|
$details['requirement'][] = get_string('viewingactivity', 'completion', $modulename);
|
|
}
|
|
|
|
if (!is_null($cm->completiongradeitemnumber)) {
|
|
$details['requirement'][] = get_string('achievinggrade', 'completion');
|
|
}
|
|
|
|
if ($cm->completionpassgrade) {
|
|
$details['requirement'][] = get_string('achievingpassinggrade', 'completion');
|
|
}
|
|
}
|
|
|
|
$details['requirement'] = implode(', ', $details['requirement']);
|
|
|
|
} else if ($type == COMPLETION_CRITERIA_TYPE_DURATION) {
|
|
$details = [
|
|
"type" => get_string('periodpostenrolment', 'completion'),
|
|
"criteria" => get_string('remainingenroledfortime', 'completion'),
|
|
"requirement" => get_string('xdays', 'completion', ceil($criteria->enrolperiod / (60 * 60 * 24))),
|
|
"status" => "",
|
|
];
|
|
} else if ($type == COMPLETION_CRITERIA_TYPE_GRADE) {
|
|
$details = [
|
|
"type" => get_string('coursegrade', 'completion'),
|
|
"criteria" => get_string('graderequired', 'completion'),
|
|
// TODO: convert to selected representation (letter, percentage, etc).
|
|
"requirement" => get_string('graderequired', 'completion')
|
|
.": ".format_float($criteria->gradepass, 1),
|
|
"status" => "",
|
|
];
|
|
} else if ($type == COMPLETION_CRITERIA_TYPE_ROLE) {
|
|
$criteria = $criteria->get_title();
|
|
|
|
$details = [
|
|
"type" => get_string('manualcompletionby', 'completion'),
|
|
"criteria" => $criteria,
|
|
"requirement" => get_string('markedcompleteby', 'completion', $criteria),
|
|
"status" => "",
|
|
];
|
|
} else if ($type == COMPLETION_CRITERIA_TYPE_COURSE) {
|
|
$prereq = get_course($criteria->courseinstance);
|
|
$coursecontext = \context_course::instance($prereq->id, MUST_EXIST);
|
|
$fullname = format_string($prereq->fullname, true, array('context' => $coursecontext));
|
|
$details = [
|
|
"type" => $criteria->get_title(),
|
|
"criteria" => '<a href="'.$CFG->wwwroot.'/course/view.php?id='.
|
|
$criteria->courseinstance.'">'.s($fullname).'</a>',
|
|
"requirement" => get_string('coursecompleted', 'completion'),
|
|
"status" => "",
|
|
];
|
|
} else {
|
|
// Moodle added a criteria type.
|
|
$details = [
|
|
"type" => "",
|
|
"criteria" => "",
|
|
"requirement" => "",
|
|
"status" => "",
|
|
];
|
|
}
|
|
|
|
$scanner = new completionscanner($criteria, $this->course);
|
|
|
|
// Only add the items list if we actually have items...
|
|
$cinfo["items"][] = [
|
|
"id" => $criteria->id,
|
|
"title" => $criteria->get_title_detailed(),
|
|
"details" => $details,
|
|
"progress" => $scanner->model($studentlist),
|
|
];
|
|
|
|
}
|
|
|
|
$info['conditions'][] = $cinfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Determine overall completion for a given type
|
|
* @param int $typeaggregation COMPLETION_AGGREGATION_ALL or COMPLETION_AGGREGATION_ANY
|
|
* @param \completion_criteria_completion[] $completions List of completions to aggregate
|
|
* @return bool Completed or not
|
|
*/
|
|
private function aggregate_completions($typeaggregation, $completions) {
|
|
$completed = 0;
|
|
$count = count($completions);
|
|
foreach ($completions as $c) {
|
|
if ($c->is_complete()) {
|
|
$completed++;
|
|
}
|
|
}
|
|
if ($typeaggregation == COMPLETION_AGGREGATION_ALL) {
|
|
return $completed >= $count;
|
|
} else { // COMPLETION_AGGREGATION_ANY.
|
|
return $completed > 1;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Webservice model for user course completion info
|
|
* @param int $userid ID of user to check specific info for
|
|
* @return array Webservice data model
|
|
*/
|
|
public function user_model($userid) {
|
|
global $DB;
|
|
|
|
$progress = $this->get_advanced_progress_percentage($userid);
|
|
$info = [
|
|
'progress' => $progress->completed,
|
|
"count" => $progress->count,
|
|
"conditions" => [],
|
|
"completed" => $this->completion->is_course_complete($userid),
|
|
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method()),
|
|
"enabled" => $this->completion->is_enabled(),
|
|
"tracked" => $this->completion->is_tracked_user($userid),
|
|
];
|
|
|
|
// Check if completion tracking is enabled for this course - otherwise, revert to defaults .
|
|
if ($this->completion->is_enabled() && $this->completion->is_tracked_user($userid)) {
|
|
$anypending = false;
|
|
// Loop through all conditions to see if they are applicable.
|
|
foreach (self::completiontypes() as $type => $handle) {
|
|
// Get the main completion for this type.
|
|
$completions = $this->completion->get_completions($userid, $type);
|
|
if (count($completions) > 0) {
|
|
$typeaggregation = $this->completion->get_aggregation_method($type);
|
|
$completed = $this->aggregate_completions($typeaggregation, $completions);
|
|
$cinfo = [
|
|
"type" => $handle,
|
|
"aggregation" => self::aggregation_handle($typeaggregation),
|
|
"completed" => $completed,
|
|
"status" => $completed ? "complete" : "incomplete",
|
|
"title" => reset($completions)->get_criteria()->get_type_title(),
|
|
"items" => [],
|
|
];
|
|
|
|
$progress = 0;
|
|
foreach ($completions as $completion) {
|
|
$criteria = $completion->get_criteria();
|
|
|
|
$iinfo = [
|
|
"id" => $criteria->id,
|
|
"title" => $criteria->get_title_detailed(),
|
|
"details" => $criteria->get_details($completion),
|
|
"completed" => $completion->is_complete(), // Make sure to override for activi.
|
|
"status" => self::completion_handle(
|
|
$completion->is_complete() ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE),
|
|
];
|
|
|
|
if ($type == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
|
|
$cm = $this->modinfo->get_cm($criteria->moduleinstance);
|
|
|
|
// Retrieve data for this object.
|
|
$data = $this->completion->get_data($cm, false, $userid);
|
|
// If it's an activity completion, add all the relevant activities as sub-items.
|
|
$completionstatus = $data->completionstate;
|
|
$gradecompletion = $this->completion->get_grade_completion($cm, $userid);
|
|
|
|
/* To comply with the moodle completion report, only count COMPLETED_PASS as completed if
|
|
the completion is marked as complete by the system. Occasinally those don't match
|
|
and we want to show similar behaviour. This happens when completion data is reset
|
|
in a module
|
|
*/
|
|
if ( !$completion->is_complete()
|
|
&&
|
|
in_array($gradecompletion, [COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS])) {
|
|
/* If a passing grade was provided, but the activity was not completed,
|
|
* most likely the completion data was erased.
|
|
*/
|
|
|
|
if ( !is_null($cm->completiongradeitemnumber) || ($cm->completionpassgrade) ) {
|
|
// Show a warning if this activity has grade completions to help make sense of the completion.
|
|
$iinfo["warning"] = get_string("warning_incomplete_pass", "local_treestudyplan");
|
|
} else {
|
|
// Show a warning if this activity has no grade requirment for completion.
|
|
$iinfo["warning"] = get_string("warning_incomplete_nograderq", "local_treestudyplan");
|
|
}
|
|
}
|
|
|
|
$iinfo['status'] = self::completion_handle($data->completionstate);
|
|
// Re-evaluate the completed value, to make sure COMPLETE_FAIL doesn't creep in as completed.
|
|
if (($data->completionstate == COMPLETION_INCOMPLETE)
|
|
||
|
|
($data->completionstate == COMPLETION_COMPLETE_FAIL)) {
|
|
$iinfo['completed'] = false;
|
|
} else {
|
|
$iinfo['completed'] = true;
|
|
$progress += 1; // Add a point to the progress counter.
|
|
}
|
|
|
|
// Determine the grade (retrieve from grade item, not from completion).
|
|
$grade = $this->get_grade($cm, $userid);
|
|
$iinfo['grade'] = $grade->grade;
|
|
$iinfo['feedback'] = $grade->feedback;
|
|
$iinfo['pending'] = $grade->pending;
|
|
|
|
$anypending = $anypending || $grade->pending;
|
|
// Overwrite the status with progress if something has been graded, or is pending.
|
|
if ($completionstatus != COMPLETION_INCOMPLETE || $anypending) {
|
|
if ($cinfo["status"] == "incomplete") {
|
|
$cinfo["status"] = "progress";
|
|
}
|
|
}
|
|
|
|
} else if ($type == COMPLETION_CRITERIA_TYPE_GRADE) {
|
|
// Make sure we provide the current course grade.
|
|
$iinfo['grade'] = floatval($iinfo['details']['status']);
|
|
if ($iinfo["grade"] > 0) {
|
|
$iinfo["grade"] = format_float($iinfo["grade"], 1). "/".
|
|
format_float(floatval($iinfo['details']['requirement']));
|
|
$iinfo["status"] = $completion->is_complete() ? "complete-pass" : "complete-fail";
|
|
if ($cinfo["status"] == "incomplete") {
|
|
$cinfo["status"] = "progress";
|
|
}
|
|
}
|
|
|
|
if ($completion->is_complete()) {
|
|
$progress += 1; // Add a point to the progress counter.
|
|
}
|
|
} else {
|
|
if ($completion->is_complete()) {
|
|
$progress += 1; // Add a point to the progress counter.
|
|
}
|
|
}
|
|
// Finally add the item to the items list.
|
|
$cinfo["items"][] = $iinfo;
|
|
}
|
|
|
|
// Set the count and progress stats based on the Type's aggregation style.
|
|
if ($typeaggregation == COMPLETION_AGGREGATION_ALL) {
|
|
// Count and Progress amount to the sum of items.
|
|
$cinfo["count"] = count($cinfo["items"]);
|
|
$cinfo["progress"] = $progress;
|
|
} else { // Typeaggregation == COMPLETION_AGGREGATION_ANY.
|
|
// Count and progress are either 1 or 0, since any of the items.
|
|
// Complete's the type.
|
|
$cinfo["count"] = (count($cinfo["items"]) > 0) ? 1 : 0;
|
|
$cinfo["progress"] = ($progress > 0) ? 1 : 0;
|
|
}
|
|
|
|
$info['conditions'][] = $cinfo;
|
|
$info['pending'] = $anypending;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Get the grade for a certain course module
|
|
* @param \cm_info $cm Course module
|
|
* @param int $userid ID of user to retrieve grade for
|
|
* @return stdClass|null object containing 'grade' and optional 'feedback' attribute
|
|
*/
|
|
private function get_grade($cm, $userid) {
|
|
// TODO: Display grade in the way described in the course setup (with letters if needed).
|
|
|
|
$gi = grade_item::fetch(['itemtype' => 'mod',
|
|
'itemmodule' => $cm->modname,
|
|
'iteminstance' => $cm->instance,
|
|
'courseid' => $this->course->id]); // Make sure we only get results relevant to this course.
|
|
|
|
if ($gi) {
|
|
// Only the following types of grade yield a result.
|
|
if (($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE)) {
|
|
$scale = $gi->load_scale();
|
|
|
|
$grade = (object)$gi->get_final($userid); // Get the grade for the specified user.
|
|
$result = new \stdClass;
|
|
// Check if the final grade is available and numeric (safety check).
|
|
if (!empty($grade) && !empty($grade->finalgrade) && is_numeric($grade->finalgrade)) {
|
|
// Convert scale grades to corresponding scale name.
|
|
if (isset($scale)) {
|
|
// Get scale value.
|
|
$result->grade = $scale->get_nearest_item($grade->finalgrade);
|
|
} else {
|
|
// Round final grade to 1 decimal point.
|
|
$result->grade = round($grade->finalgrade, 1);
|
|
}
|
|
|
|
$result->feedback = trim($grade->feedback);
|
|
$result->pending = (new gradingscanner($gi))->pending($userid);
|
|
} else {
|
|
$result->grade = "-"; // Activity is gradable, but user did not receive a grade yet.
|
|
$result->feedback = null;
|
|
$result->pending = false;
|
|
}
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
return null; // Activity cannot be graded (Shouldn't be happening, but still....).
|
|
}
|
|
|
|
/**
|
|
* Get the overall grade for this course
|
|
* @param int $userid ID of user to retrieve grade for
|
|
* @return stdClass|null object containing 'grade' and optional 'feedback' attribute
|
|
*/
|
|
private function get_course_grade($userid) {
|
|
// TODO: Display grade in the way described in the course setup (with letters if needed).
|
|
$gi = grade_item::fetch(['itemtype' => 'course',
|
|
'iteminstance' => $this->course->id,
|
|
'courseid' => $this->course->id]);
|
|
|
|
if ($gi) {
|
|
// Only the following types of grade yield a result.
|
|
if (($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE)) {
|
|
$scale = $gi->load_scale();
|
|
$grade = $gi->get_final($userid); // Get the grade for the specified user.
|
|
// Check if the final grade is available and numeric (safety check).
|
|
if (!empty($grade) && !empty($grade->finalgrade) && is_numeric($grade->finalgrade)) {
|
|
// Convert scale grades to corresponding scale name.
|
|
if (isset($scale)) {
|
|
// Get scale value.
|
|
return $scale->get_nearest_item($grade->finalgrade);
|
|
} else {
|
|
// Round final grade to 1 decimal point.
|
|
return round($grade->finalgrade, 1);
|
|
}
|
|
} else {
|
|
return "-"; // User did not receive a grade yet for this course.
|
|
}
|
|
}
|
|
}
|
|
|
|
return null; // Course cannot be graded (Shouldn't be happening, but still....).
|
|
}
|
|
|
|
/**
|
|
* Returns the percentage completed by a certain user, returns null if no completion data is available.
|
|
*
|
|
* @param int $userid The id of the user, 0 for the current user
|
|
* @return \stdClass The percentage info, left all 0 if completion is not supported in the course,
|
|
* or if there are no activities that support completion.
|
|
*/
|
|
public function get_advanced_progress_percentage($userid): \stdClass {
|
|
|
|
// First, let's make sure completion is enabled.
|
|
if (!$this->completion->is_enabled()) {
|
|
debugging("Completion is not enabled for {$this->course->shortname}", DEBUG_NORMAL);
|
|
return (object)[
|
|
'count' => 0,
|
|
'completed' => 0,
|
|
'percentage' => 0,
|
|
];
|
|
}
|
|
|
|
if (!$this->completion->is_tracked_user($userid)) {
|
|
debugging("$userid is not tracked in {$this->course->shortname}");
|
|
return (object)[
|
|
'count' => 0,
|
|
'completed' => 0,
|
|
'percentage' => 0,
|
|
];
|
|
}
|
|
|
|
$completions = $this->completion->get_completions($userid);
|
|
|
|
$aggregation = $this->completion->get_aggregation_method();
|
|
$critcount = [];
|
|
|
|
// Before we check how many modules have been completed see if the course has completed. .
|
|
// Count all completions, but treat .
|
|
foreach ($completions as $completion) {
|
|
$crit = $completion->get_criteria();
|
|
|
|
// Make a new object for the type if it's not already there.
|
|
$type = $crit->criteriatype;
|
|
if (!array_key_exists($type, $critcount)) {
|
|
$critcount[$type] = new \stdClass;
|
|
$critcount[$type]->count = 0;
|
|
$critcount[$type]->completed = 0;
|
|
$critcount[$type]->aggregation = $this->completion->get_aggregation_method($type);
|
|
}
|
|
// Get a reference to the counter object for this type.
|
|
$typecount =& $critcount[$type];
|
|
|
|
$typecount->count += 1;
|
|
if ($crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
|
|
// Get the cm data object.
|
|
$cm = $this->modinfo->get_cm($crit->moduleinstance);
|
|
// Retrieve data for this object.
|
|
$data = $this->completion->get_data($cm, false, $userid);
|
|
// Count complete, but failed as incomplete too...
|
|
if (($data->completionstate == COMPLETION_INCOMPLETE) || ($data->completionstate == COMPLETION_COMPLETE_FAIL)) {
|
|
$typecount->completed += 0;
|
|
} else {
|
|
$typecount->completed += 1;
|
|
}
|
|
} else {
|
|
if ($completion->is_complete()) {
|
|
$typecount->completed += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now that we have all completions sorted by type, we can be smart about how to do the count.
|
|
$count = 0;
|
|
$completed = 0;
|
|
$completionpercentage = 0;
|
|
foreach ($critcount as $c) {
|
|
// Take only types that are actually present into account.
|
|
if ($c->count > 0) {
|
|
// If the aggregation for the type is ANY, reduce the count to 1 for this type.
|
|
// And adjust the progress accordingly (check if any have been completed or not).
|
|
if ($c->aggregation == COMPLETION_AGGREGATION_ALL) {
|
|
$ct = $c->count;
|
|
$cmpl = $c->completed;
|
|
} else {
|
|
$ct = 1;
|
|
$cmpl = ($c->completed > 0) ? 1 : 0;
|
|
}
|
|
// If ANY completion for the types, count only the criteria type with the highest completion percentage -.
|
|
// Overwrite data if current type is more complete.
|
|
if ($aggregation == COMPLETION_AGGREGATION_ANY) {
|
|
$pct = $cmpl / $ct;
|
|
if ($pct > $completionpercentage) {
|
|
$count = $ct;
|
|
$completed = $cmpl;
|
|
$completionpercentage = $pct;
|
|
}
|
|
} else {
|
|
// If ALL completion for the types, add the count for this type to that of the others.
|
|
$count += $ct;
|
|
$completed += $cmpl;
|
|
// Don't really care about recalculating completion percentage every round in this case.
|
|
}
|
|
}
|
|
}
|
|
|
|
$result = new \stdClass;
|
|
$result->count = $count;
|
|
$result->completed = $completed;
|
|
$result->percentage = ($count > 0) ? (($completed / $count) * 100) : 0;
|
|
return $result;
|
|
}
|
|
|
|
}
|