moodle_local_treestudyplan/classes/corecompletioninfo.php

744 lines
38 KiB
PHP
Raw Normal View History

<?php
2023-08-24 23:02:41 +02:00
// 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/>.
/**
2023-08-27 21:57:21 +02:00
* Class to collect course completion info for a given course
2023-08-24 23:02:41 +02:00
* @package local_treestudyplan
* @copyright 2023 P.M. Kuipers
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace local_treestudyplan;
2023-08-25 12:04:27 +02:00
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/externallib.php');
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->dirroot.'/course/lib.php');
2023-08-31 07:40:55 +02:00
use core\check\performance\debugging;
use core_course\local\repository\caching_content_item_readonly_repository;
use core_course\local\repository\content_item_readonly_repository;
2024-06-02 18:47:23 +02:00
use grade_item;
use grade_scale;
use grade_outcome;
2023-08-27 21:57:21 +02:00
/**
* Class to collect course completion info for a given course
*/
class corecompletioninfo {
2023-08-27 21:57:21 +02:00
/** @var \stdClass */
private $course;
2023-08-27 21:57:21 +02:00
/** @var \completion_info */
private $completion;
2023-08-27 21:57:21 +02:00
/** @var \course_modinfo */
private $modinfo;
2023-08-27 21:57:21 +02:00
2023-08-27 22:20:17 +02:00
/**
2023-08-27 21:57:21 +02:00
* Cached dict of completion:: constants to equivalent webservice strings
* @var array */
2023-08-25 13:34:31 +02:00
private static $completionhandles = null;
2023-08-27 21:57:21 +02:00
/**
* Cached dict of all completion type constants to equivalent strings
* @var array */
2023-08-25 13:34:31 +02:00
private static $completiontypes = null;
2023-08-27 21:57:21 +02:00
/**
* Course id of relevant course
*/
2023-08-24 23:02:41 +02:00
public function id() {
return $this->course->id;
}
2023-08-27 21:57:21 +02:00
/**
* Construct new object for a given course
* @param \stdClass $course Course database record
*/
2023-08-24 23:02:41 +02:00
public function __construct($course) {
global $DB;
$this->course = $course;
$this->completion = new \completion_info($this->course);
$this->modinfo = get_fast_modinfo($this->course);
}
2023-08-27 21:57:21 +02:00
/**
* Get dictionary of all completion types as CONST => 'string'
* @return array
*/
2023-08-25 13:34:31 +02:00
public static function completiontypes() {
2023-08-25 17:33:20 +02:00
/* While it is tempting to use the global array COMPLETION_CRITERIA_TYPES,
2023-08-25 13:34:31 +02:00
so we don't have to manually add any completion types if moodle decides to add a few.
2023-08-25 17:33:20 +02:00
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.
2023-08-25 13:34:31 +02:00
*/
if (!isset(self::$completiontypes)) {
2023-08-25 17:33:20 +02:00
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',
];
2023-08-25 13:34:31 +02:00
}
return self::$completiontypes;
}
/**
2023-08-24 23:02:41 +02:00
* Translate a numeric completion constant to a text string
2023-08-27 21:57:21 +02:00
* @param int $completion The completion code as defined in completionlib.php to translate to a text handle
*/
2023-08-25 13:34:31 +02:00
public static function completion_handle($completion) {
if (empty(self::$completionhandles)) {
2023-08-24 23:02:41 +02:00
// Cache the translation table, to avoid overhead.
2023-08-25 13:34:31 +02:00
self::$completionhandles = [
COMPLETION_INCOMPLETE => "incomplete",
2023-08-24 23:02:41 +02:00
COMPLETION_COMPLETE => "complete",
COMPLETION_COMPLETE_PASS => "complete-pass",
COMPLETION_COMPLETE_FAIL => "complete-fail",
2023-08-25 10:41:56 +02:00
COMPLETION_COMPLETE_FAIL_HIDDEN => "complete-fail"]; // The front end won't differentiate between hidden or not.
}
2023-08-25 13:34:31 +02:00
return self::$completionhandles[$completion] ?? "undefined";
}
2023-08-27 21:57:21 +02:00
/**
* Webservice editor structure for completion_item
* @param int $value Webservice requirement constant
*/
2024-06-02 19:23:40 +02:00
public static function completion_item_editor_structure($value = VALUE_REQUIRED): \external_description {
return new \external_single_structure([
2023-08-24 23:02:41 +02:00
"id" => new \external_value(PARAM_INT, 'criteria id', VALUE_OPTIONAL),
2023-10-07 23:02:57 +02:00
"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([
2023-08-24 23:02:41 +02:00
"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),
2023-06-03 00:01:43 +02:00
]),
"progress" => completionscanner::structure(),
2023-08-24 23:02:41 +02:00
], 'completion type', $value);
}
2023-08-27 21:57:21 +02:00
/**
* Webservice editor structure for completion_type
* @param int $value Webservice requirement constant
*/
2024-06-02 19:23:40 +02:00
public static function completion_type_editor_structure($value = VALUE_REQUIRED): \external_description {
return new \external_single_structure([
2023-08-24 23:02:41 +02:00
"items" => new \external_multiple_structure(self::completion_item_editor_structure(), 'subitems', VALUE_OPTIONAL),
2023-10-07 23:02:57 +02:00
"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'),
2023-08-24 23:02:41 +02:00
"aggregation" => new \external_value(PARAM_TEXT, 'completion aggregation for this type ["all", "any"]'),
], 'completion type', $value);
}
2023-08-27 21:57:21 +02:00
/**
* Webservice editor structure for course completion
* @param int $value Webservice requirement constant
*/
/**
* Webservice structure for editor info
* @param int $value Webservice requirement constant
*/
2024-06-02 19:23:40 +02:00
public static function editor_structure($value = VALUE_REQUIRED): \external_description {
return new \external_single_structure([
2023-08-24 23:02:41 +02:00
"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);
}
2023-08-27 21:57:21 +02:00
/**
* Webservice user view structure for completion_item
* @param int $value Webservice requirement constant
*/
2024-06-02 19:23:40 +02:00
public static function completion_item_user_structure($value = VALUE_REQUIRED): \external_description {
return new \external_single_structure([
2023-08-24 23:02:41 +02:00
"id" => new \external_value(PARAM_INT, 'id of completion', VALUE_OPTIONAL),
2023-10-07 23:02:57 +02:00
"title" => new \external_value(PARAM_RAW, 'name of subitem', VALUE_OPTIONAL),
"details" => new \external_single_structure([
2023-08-24 23:02:41 +02:00
"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),
]),
2023-10-07 23:02:57 +02:00
"link" => new \external_value(PARAM_RAW, 'optional link to more details', VALUE_OPTIONAL),
"completed" => new \external_value(PARAM_BOOL, 'simple completed or not'),
2023-08-25 11:52:05 +02:00
"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),
2023-08-24 23:02:41 +02:00
"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),
2023-08-24 23:02:41 +02:00
], 'completion type', $value);
}
2023-08-27 21:57:21 +02:00
/**
* Webservice user view structure for completion_type
* @param int $value Webservice requirement constant
*/
2024-06-02 19:23:40 +02:00
public static function completion_type_user_structure($value = VALUE_REQUIRED): \external_description {
return new \external_single_structure([
2023-08-24 23:02:41 +02:00
"items" => new \external_multiple_structure(self::completion_item_user_structure(), 'subitems', VALUE_OPTIONAL),
2023-10-07 23:02:57 +02:00
"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'),
2023-08-24 23:02:41 +02:00
"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'),
2023-08-25 11:52:05 +02:00
"status" => new \external_value(PARAM_TEXT,
'extended completion status ["incomplete", "progress", "complete", "complete-pass", "complete-fail"]'),
2023-05-20 18:25:22 +02:00
"progress" => new \external_value(PARAM_INT, 'completed sub-conditions'),
"count" => new \external_value(PARAM_INT, 'total number of sub-conditions'),
2023-08-24 23:02:41 +02:00
], 'completion type', $value);
}
2023-08-27 21:57:21 +02:00
/**
* Webservice structure for userinfo
* @param int $value Webservice requirement constant
*/
2024-06-02 19:23:40 +02:00
public static function user_structure($value = VALUE_REQUIRED): \external_description {
return new \external_single_structure([
"progress" => new \external_value(PARAM_INT, 'completed sub-conditions'),
2023-08-24 23:02:41 +02:00
"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'),
2023-08-24 23:02:41 +02:00
"conditions" => new \external_multiple_structure(self::completion_type_user_structure(), 'completion conditions'),
"completed" => new \external_value(PARAM_BOOL, 'current completion value'),
2023-08-24 23:02:41 +02:00
"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);
}
2023-08-27 21:57:21 +02:00
/**
* Convert agregation method constant to equivalent string for webservice
* @param int $method COMPLETION_AGGREGATION_ALL || COMPLETION_AGGREGATION_ANY
* @return string 'all' or 'any'
*/
2023-08-24 23:02:41 +02:00
private static function aggregation_handle($method) {
2023-08-25 13:34:31 +02:00
return ($method == COMPLETION_AGGREGATION_ALL) ? "all" : "any";
}
2023-08-27 21:57:21 +02:00
/**
* Webservice model for editor info
2023-08-31 07:40:55 +02:00
* @param int[] $studentlist List of user id's to use for checking issueing progress within a study plan
2023-08-27 21:57:21 +02:00
* @return array Webservice data model
*/
2023-08-31 07:40:55 +02:00
public function editor_model(array $studentlist = null) {
2023-08-25 13:34:31 +02:00
global $DB, $CFG;
2023-08-24 23:02:41 +02:00
$conditions = [];
2023-08-25 10:41:56 +02:00
$aggregation = "all"; // Default.
$info = [
"conditions" => $conditions,
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method()),
2024-06-02 23:23:32 +02:00
"enabled" => $this->completion->is_enabled(),
];
2023-08-24 23:02:41 +02:00
// Check if completion tracking is enabled for this course - otherwise, revert to defaults .
if ($this->completion->is_enabled()) {
$aggregation = $this->completion->get_aggregation_method();
2023-08-24 23:02:41 +02:00
// Loop through all condition types to see if they are applicable.
2023-08-31 07:40:55 +02:00
foreach (self::completiontypes() as $type => $handle) {
2023-08-24 23:02:41 +02:00
$criterias = $this->completion->get_criteria($type); // Returns array of relevant criteria items.
2024-06-02 23:23:32 +02:00
if (count($criterias) > 0) {
2023-08-25 11:52:05 +02:00
// Only take it into account if the criteria count is > 0.
$cinfo = [
2023-08-31 07:40:55 +02:00
"type" => $handle,
"aggregation" => self::aggregation_handle($this->completion->get_aggregation_method($type)),
"title" => reset($criterias)->get_type_title(),
"items" => [],
];
2023-08-24 23:02:41 +02:00
foreach ($criterias as $criteria) {
2023-08-25 17:33:20 +02:00
/* 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.
*/
2024-02-23 09:20:10 +01:00
unset($title); // Clear title from previous iteration if it was set.
2023-08-24 23:02:41 +02:00
if ($type == COMPLETION_CRITERIA_TYPE_SELF) {
$details = [
"type" => $criteria->get_title(),
"criteria" => $criteria->get_title(),
"requirement" => get_string('markingyourselfcomplete', 'completion'),
"status" => "",
];
2023-08-24 23:09:20 +02:00
} else if ($type == COMPLETION_CRITERIA_TYPE_DATE) {
$details = [
"type" => get_string('datepassed', 'completion'),
"criteria" => get_string('remainingenroleduntildate', 'completion'),
2023-08-24 23:02:41 +02:00
"requirement" => date("Y-m-d", $criteria->timeend),
"status" => "",
];
2023-08-24 23:09:20 +02:00
} else if ($type == COMPLETION_CRITERIA_TYPE_UNENROL) {
$details = [
"type" => get_string('unenrolment', 'completion'),
"criteria" => get_string('unenrolment', 'completion'),
"requirement" => get_string('unenrolingfromcourse', 'completion'),
"status" => "",
];
2023-08-24 23:09:20 +02:00
} else if ($type == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
$cm = $this->modinfo->get_cm($criteria->moduleinstance);
2023-08-25 11:52:05 +02:00
/* Criteria and requirements will be built in a moment by code copied
from completion_criteria_activity.php.
*/
$details = [
"type" => $criteria->get_title(),
2023-08-25 11:52:05 +02:00
"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();
}
2024-06-02 23:23:32 +02:00
// Set title based on cm formatted name.
2024-02-23 09:20:10 +01:00
$title = $cm->get_formatted_name();
2023-08-24 23:02:41 +02:00
// Build requirements.
2024-06-02 19:23:40 +02:00
$details['requirement'] = [];
if ($cm->completion == COMPLETION_TRACKING_MANUAL) {
$details['requirement'][] = get_string('markingyourselfcomplete', 'completion');
2023-08-24 23:02:41 +02:00
} else if ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
if ($cm->completionview) {
2023-07-04 23:38:08 +02:00
$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']);
2023-08-24 23:09:20 +02:00
} else if ($type == COMPLETION_CRITERIA_TYPE_DURATION) {
$details = [
"type" => get_string('periodpostenrolment', 'completion'),
"criteria" => get_string('remainingenroledfortime', 'completion'),
2023-08-25 13:34:31 +02:00
"requirement" => get_string('xdays', 'completion', ceil($criteria->enrolperiod / (60 * 60 * 24))),
"status" => "",
];
2023-08-24 23:09:20 +02:00
} else if ($type == COMPLETION_CRITERIA_TYPE_GRADE) {
2024-02-23 09:20:10 +01:00
$displaytype = \grade_get_setting($this->course->id, 'displaytype', $CFG->grade_displaytype);
$gradepass = $criteria->gradepass;
// Find grade item for course result.
2024-06-02 23:23:32 +02:00
$gi = new \grade_item(['courseid' => $this->course->id, 'itemtype' => 'course']);
2024-06-02 19:23:40 +02:00
$displaygrade = \grade_format_gradevalue($gradepass, $gi, true, $displaytype, 1);
$details = [
"type" => get_string('coursegrade', 'completion'),
"criteria" => get_string('graderequired', 'completion'),
2023-08-25 11:52:05 +02:00
"requirement" => get_string('graderequired', 'completion')
2024-02-23 09:20:10 +01:00
.": ".$displaygrade,
"status" => "",
];
2024-06-02 19:23:40 +02:00
$title = get_string('graderequired', 'completion').': '.$displaygrade;
2024-02-23 09:20:10 +01:00
2023-08-24 23:09:20 +02:00
} else if ($type == COMPLETION_CRITERIA_TYPE_ROLE) {
$details = [
"type" => get_string('manualcompletionby', 'completion'),
2023-12-13 12:22:34 +01:00
"criteria" => $criteria->get_title(),
2024-05-10 15:22:52 +02:00
"requirement" => get_string('markedcompleteby', 'completion', $criteria->get_title()),
"status" => "",
];
2023-08-24 23:09:20 +02:00
} else if ($type == COMPLETION_CRITERIA_TYPE_COURSE) {
$prereq = get_course($criteria->courseinstance);
$coursecontext = \context_course::instance($prereq->id, MUST_EXIST);
2024-06-02 19:23:40 +02:00
$fullname = format_string($prereq->fullname, true, ['context' => $coursecontext]);
$details = [
"type" => $criteria->get_title(),
2023-08-25 11:52:05 +02:00
"criteria" => '<a href="'.$CFG->wwwroot.'/course/view.php?id='.
$criteria->courseinstance.'">'.s($fullname).'</a>',
"requirement" => get_string('coursecompleted', 'completion'),
"status" => "",
];
} else {
2023-08-24 23:02:41 +02:00
// Moodle added a criteria type.
$details = [
"type" => "",
"criteria" => "",
"requirement" => "",
"status" => "",
];
}
2023-08-24 23:02:41 +02:00
$scanner = new completionscanner($criteria, $this->course);
2023-08-25 09:44:34 +02:00
// Only add the items list if we actually have items...
$cinfo["items"][] = [
"id" => $criteria->id,
2024-06-02 19:23:40 +02:00
"title" => isset($title) ? $title : $criteria->get_title_detailed(),
"details" => $details,
2023-08-31 07:40:55 +02:00
"progress" => $scanner->model($studentlist),
];
}
$info['conditions'][] = $cinfo;
}
}
}
return $info;
}
2023-08-27 21:57:21 +02:00
/**
* 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
*/
2023-08-24 23:02:41 +02:00
private function aggregate_completions($typeaggregation, $completions) {
$completed = 0;
$count = count($completions);
2023-08-24 23:02:41 +02:00
foreach ($completions as $c) {
if ($c->is_complete()) {
$completed++;
}
}
2023-08-24 23:02:41 +02:00
if ($typeaggregation == COMPLETION_AGGREGATION_ALL) {
return $completed >= $count;
2023-08-24 23:09:20 +02:00
} else { // COMPLETION_AGGREGATION_ANY.
return $completed > 1;
}
}
2023-08-27 21:57:21 +02:00
/**
* 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) {
2023-08-25 13:34:31 +02:00
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()),
2023-08-24 23:02:41 +02:00
"enabled" => $this->completion->is_enabled(),
"tracked" => $this->completion->is_tracked_user($userid),
];
2023-08-24 23:02:41 +02:00
// 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;
2023-08-24 23:02:41 +02:00
// Loop through all conditions to see if they are applicable.
2023-08-31 07:40:55 +02:00
foreach (self::completiontypes() as $type => $handle) {
2023-08-24 23:02:41 +02:00
// 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);
2023-08-24 23:02:41 +02:00
$completed = $this->aggregate_completions($typeaggregation, $completions);
$cinfo = [
2023-08-31 07:40:55 +02:00
"type" => $handle,
"aggregation" => self::aggregation_handle($typeaggregation),
"completed" => $completed,
2023-08-25 10:41:56 +02:00
"status" => $completed ? "complete" : "incomplete",
"title" => reset($completions)->get_criteria()->get_type_title(),
"items" => [],
];
2023-05-20 18:25:22 +02:00
$progress = 0;
2023-08-24 23:02:41 +02:00
foreach ($completions as $completion) {
$criteria = $completion->get_criteria();
$iinfo = [
"id" => $criteria->id,
"title" => $criteria->get_title_detailed(),
"details" => $criteria->get_details($completion),
2023-08-24 23:02:41 +02:00
"completed" => $completion->is_complete(), // Make sure to override for activi.
2023-08-25 11:52:05 +02:00
"status" => self::completion_handle(
2024-06-02 23:23:32 +02:00
$completion->is_complete() ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE),
];
2023-08-24 23:02:41 +02:00
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);
2023-08-24 23:02:41 +02:00
// If it's an activity completion, add all the relevant activities as sub-items.
2023-09-08 12:47:29 +02:00
$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
*/
2024-06-02 23:23:32 +02:00
if (!$completion->is_complete()
2023-09-08 12:47:29 +02:00
&&
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.
*/
2024-06-02 23:23:32 +02:00
if (!is_null($cm->completiongradeitemnumber) || ($cm->completionpassgrade)) {
// Show a warning if this activity has grade completions to help make sense of the completion.
2023-09-08 12:47:29 +02:00
$iinfo["warning"] = get_string("warning_incomplete_pass", "local_treestudyplan");
} else {
// Show a warning if this activity has no grade requirment for completion.
2023-09-08 12:47:29 +02:00
$iinfo["warning"] = get_string("warning_incomplete_nograderq", "local_treestudyplan");
}
}
2023-09-08 12:47:29 +02:00
$iinfo['status'] = self::completion_handle($data->completionstate);
2023-08-24 23:02:41 +02:00
// Re-evaluate the completed value, to make sure COMPLETE_FAIL doesn't creep in as completed.
2023-09-08 12:47:29 +02:00
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.
}
2023-09-08 12:47:29 +02:00
2023-08-24 23:02:41 +02:00
// 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;
2023-08-24 23:02:41 +02:00
// Overwrite the status with progress if something has been graded, or is pending.
2023-08-25 09:33:42 +02:00
if ($completionstatus != COMPLETION_INCOMPLETE || $anypending) {
2023-08-24 23:02:41 +02:00
if ($cinfo["status"] == "incomplete") {
$cinfo["status"] = "progress";
}
}
2023-08-24 23:09:20 +02:00
} else if ($type == COMPLETION_CRITERIA_TYPE_GRADE) {
2023-08-24 23:02:41 +02:00
// Make sure we provide the current course grade.
2024-02-23 09:20:10 +01:00
$rawgrade = floatval($iinfo['details']['status']);
$iinfo['grade'] = $this->format_course_grade($rawgrade);
$rq = floatval($iinfo['details']['requirement']);
$iinfo['details']['requirement'] = $this->format_course_grade($rq);
2024-06-02 23:23:32 +02:00
;
$iinfo["status"] = $completion->is_complete() ? "complete-pass" : "complete-fail";
2024-02-23 09:20:10 +01:00
if ($cinfo["status"] == "incomplete") {
$cinfo["status"] = "progress";
}
if ($completion->is_complete()) {
$progress += 1; // Add a point to the progress counter.
}
2023-09-08 12:47:29 +02:00
} else {
if ($completion->is_complete()) {
$progress += 1; // Add a point to the progress counter.
}
}
2023-08-25 09:44:34 +02:00
// Finally add the item to the items list.
$cinfo["items"][] = $iinfo;
}
2023-05-20 18:25:22 +02:00
2023-08-24 23:02:41 +02:00
// 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.
2023-05-20 18:25:22 +02:00
$cinfo["count"] = count($cinfo["items"]);
$cinfo["progress"] = $progress;
2023-08-25 09:44:34 +02:00
} else { // Typeaggregation == COMPLETION_AGGREGATION_ANY.
2023-08-24 23:02:41 +02:00
// Count and progress are either 1 or 0, since any of the items.
2023-08-25 09:44:34 +02:00
// Complete's the type.
2023-08-25 12:16:51 +02:00
$cinfo["count"] = (count($cinfo["items"]) > 0) ? 1 : 0;
2023-08-25 13:34:31 +02:00
$cinfo["progress"] = ($progress > 0) ? 1 : 0;
2023-05-20 18:25:22 +02:00
}
$info['conditions'][] = $cinfo;
$info['pending'] = $anypending;
}
}
}
return $info;
}
/**
* Get the grade for a certain course module
2023-08-27 21:57:21 +02:00
* @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
*/
2023-08-24 23:02:41 +02:00
private function get_grade($cm, $userid) {
2023-08-25 13:04:19 +02:00
$gi = grade_item::fetch(['itemtype' => 'mod',
2023-08-24 23:02:41 +02:00
'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)) {
$grade = (object)$gi->get_final($userid); // Get the grade for the specified user.
$result = new \stdClass;
2023-08-24 23:02:41 +02:00
// Check if the final grade is available and numeric (safety check).
if (!empty($grade) && !empty($grade->finalgrade) && is_numeric($grade->finalgrade)) {
2024-06-02 19:23:40 +02:00
$result->grade = \grade_format_gradevalue($grade->finalgrade, $gi, true, null, 1);
2024-02-23 09:20:10 +01:00
$result->feedback = \trim($grade->feedback);
$result->pending = (new gradingscanner($gi))->pending($userid);
2023-08-24 23:09:20 +02:00
} else {
2023-08-24 23:02:41 +02:00
$result->grade = "-"; // Activity is gradable, but user did not receive a grade yet.
$result->feedback = null;
$result->pending = false;
}
return $result;
}
}
2023-08-24 23:02:41 +02:00
return null; // Activity cannot be graded (Shouldn't be happening, but still....).
}
/**
2023-08-27 21:57:21 +02:00
* Get the overall grade for this course
2024-06-03 04:00:46 +02:00
* @param int $grade The grade object to format
* @return stdClass|null object containing 'grade' and optional 'feedback' attribute
*/
2024-02-23 09:20:10 +01:00
private function format_course_grade($grade) {
$gi = new \grade_item(['itemtype' => 'course',
'courseid' => $this->course->id]);
2023-08-24 23:02:41 +02:00
if ($gi) {
2024-06-02 19:23:40 +02:00
return \grade_format_gradevalue($grade, $gi, true, null, 1);
}
2024-02-23 09:20:10 +01:00
return "x"; // 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
2023-08-31 07:40:55 +02:00
* @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.
2023-08-24 23:02:41 +02:00
*/
2023-08-31 07:40:55 +02:00
public function get_advanced_progress_percentage($userid): \stdClass {
// First, let's make sure completion is enabled.
if (!$this->completion->is_enabled()) {
2023-09-08 12:47:29 +02:00
debugging("Completion is not enabled for {$this->course->shortname}", DEBUG_NORMAL);
2023-08-31 07:40:55 +02:00
return (object)[
'count' => 0,
'completed' => 0,
'percentage' => 0,
];
}
if (!$this->completion->is_tracked_user($userid)) {
2023-08-31 07:40:55 +02:00
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 = [];
2023-08-24 23:02:41 +02:00
// Before we check how many modules have been completed see if the course has completed. .
2023-08-25 09:44:34 +02:00
// Count all completions, but treat .
2023-08-24 23:02:41 +02:00
foreach ($completions as $completion) {
2023-07-04 23:38:08 +02:00
$crit = $completion->get_criteria();
2023-08-24 23:02:41 +02:00
// Make a new object for the type if it's not already there.
2023-07-04 23:38:08 +02:00
$type = $crit->criteriatype;
2023-08-24 23:02:41 +02:00
if (!array_key_exists($type, $critcount)) {
2023-07-04 23:38:08 +02:00
$critcount[$type] = new \stdClass;
$critcount[$type]->count = 0;
$critcount[$type]->completed = 0;
$critcount[$type]->aggregation = $this->completion->get_aggregation_method($type);
}
2023-08-24 23:02:41 +02:00
// Get a reference to the counter object for this type.
2023-07-04 23:38:08 +02:00
$typecount =& $critcount[$type];
$typecount->count += 1;
2023-08-24 23:02:41 +02:00
if ($crit->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) {
2023-08-25 09:44:34 +02:00
// Get the cm data object.
2023-07-04 23:38:08 +02:00
$cm = $this->modinfo->get_cm($crit->moduleinstance);
2023-08-25 09:44:34 +02:00
// Retrieve data for this object.
2023-07-04 23:38:08 +02:00
$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;
2023-08-24 23:02:41 +02:00
}
2023-08-24 23:09:20 +02:00
} else {
2023-08-24 23:02:41 +02:00
if ($completion->is_complete()) {
2023-07-04 23:38:08 +02:00
$typecount->completed += 1;
}
2023-08-24 23:02:41 +02:00
}
}
2023-08-24 23:02:41 +02:00
// Now that we have all completions sorted by type, we can be smart about how to do the count.
$count = 0;
$completed = 0;
2023-08-25 09:33:42 +02:00
$completionpercentage = 0;
2023-08-24 23:02:41 +02:00
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;
2023-08-24 23:09:20 +02:00
} else {
$ct = 1;
2023-08-25 12:16:51 +02:00
$cmpl = ($c->completed > 0) ? 1 : 0;
}
2023-08-25 09:44:34 +02:00
// If ANY completion for the types, count only the criteria type with the highest completion percentage -.
2023-08-24 23:02:41 +02:00
// Overwrite data if current type is more complete.
if ($aggregation == COMPLETION_AGGREGATION_ANY) {
2023-08-25 13:34:31 +02:00
$pct = $cmpl / $ct;
2023-08-25 09:33:42 +02:00
if ($pct > $completionpercentage) {
$count = $ct;
$completed = $cmpl;
2023-08-25 09:33:42 +02:00
$completionpercentage = $pct;
}
2023-08-25 13:34:31 +02:00
} else {
// If ALL completion for the types, add the count for this type to that of the others.
$count += $ct;
$completed += $cmpl;
2023-08-24 23:02:41 +02:00
// Don't really care about recalculating completion percentage every round in this case.
}
}
}
$result = new \stdClass;
$result->count = $count;
$result->completed = $completed;
2024-06-02 23:23:32 +02:00
$result->percentage = ($count > 0) ? (($completed / $count) * 100) : 0;
2023-08-24 23:02:41 +02:00
return $result;
}
}