. /** * * @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_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; use \core_plugin_manager; class gradeinfo { private $studyitem = null; private $id; private $gradeitem; private $icon; private $link; private $gradinglink; private $scale; private $outcome; private $hidden = false; private $name; private $typename; private $section; private $sectionorder; private $cmid; private $coursesort; private static $contentitems = null; private $gradingscanner; private static $sections = []; protected static function getSectionSequence($sectionid) { global $DB; if (!array_key_exists($sectionid, self::$sections)) { self::$sections[$sectionid] = explode(", ", $DB->get_field("course_sections", "sequence", ["id" => $sectionid])); } return self::$sections[$sectionid]; } public function getGradeitem() { return $this->gradeitem; } public function getGradingscanner() { return $this->gradingscanner; } public function getScale() { return $this->scale; } protected static function get_contentitems() { global $PAGE; if (empty(static::$contentitems)) { $PAGE->set_context(\context_system::instance()); static::$contentitems = (new content_item_readonly_repository())->find_all(); } return static::$contentitems; } public static function get_contentitem($name) { $contentitems = static::get_contentitems(); for ($i = 0; $i < count($contentitems); $i++) { if ($contentitems[$i]->get_name() == $name) { return $contentitems[$i]; } } return null; } public static function getCourseContextById($id) { $gi = grade_item::fetch(["id" => $id]); if (!$gi || course_module_instance_pending_deletion($gi->courseid, $gi->itemmodule, $gi->iteminstance)) { throw new \InvalidArgumentException ("Grade {$id} not found in database"); } return \context_course::instance($gi->courseid);; } public function __construct($id, studyitem $studyitem = null) { global $DB; $this->studyitem = $studyitem; $gi = grade_item::fetch(["id" => $id]); if (!$gi || course_module_instance_pending_deletion($gi->courseid, $gi->itemmodule, $gi->iteminstance)) { throw new \InvalidArgumentException ("Grade {$id} not found in database"); } $this->id = $id; $this->gradeitem = $gi; // Determine the icon for the associated activity. $contentitem = static::get_contentitem($gi->itemmodule); $this->icon = empty($contentitem) ? "" : $contentitem->get_icon(); // Determine a link to the associated activity. if ($gi->itemtype != "mod" || empty($gi->itemmodule) || empty($gi->iteminstance)) { $this->link = ""; $this->cmid = 0; $this->section = 0; $this->sectionorder = 0; } else { list($c, $cminfo) = get_course_and_cm_from_instance($gi->iteminstance, $gi->itemmodule); $this->cmid = $cminfo->id; // Sort by position in course. // . $this->section = $cminfo->sectionnum; $ssequence = self::getSectionSequence($cminfo->section); $this->sectionorder = array_search($cminfo->id, $ssequence); $this->link = "/mod/{$gi->itemmodule}/view.php?id={$cminfo->id}"; if ($gi->itemmodule == 'quiz') { $this->gradinglink = "/mod/{$gi->itemmodule}/report.php?id={$cminfo->id}&mode=grading"; } else if ($gi->itemmodule == "assign") { $this->gradinglink = $this->link ."&action=grading"; } else { $this->gradinglink = $this->link; } } $this->scale = $gi->load_scale(); $this->outcome = $gi->load_outcome(); $this->hidden = ($gi->hidden || (!empty($outcome) && $outcome->hidden)) ? true : false; $this->name = empty($outcome) ? $gi->itemname : $outcome->name; $this->typename = empty($contentitem) ? $gi->itemmodule : $contentitem->get_title()->get_value(); $this->gradingscanner = new gradingscanner($gi); $this->coursesort = $this->section * 1000 + $this->sectionorder; } public function is_selected() { global $DB; if ($this->studyitem) { // Check if selected for this studyitem. $r = $DB->get_record('local_treestudyplan_gradeinc', ['studyitem_id' => $this->studyitem->id(), 'grade_item_id' => $this->gradeitem->id]); if ($r && $r->include) { return(true); } } return(false); } public function is_required() { global $DB; if ($this->studyitem) { // Check if selected for this studyitem. $r = $DB->get_record('local_treestudyplan_gradeinc', ['studyitem_id' => $this->studyitem->id(), 'grade_item_id' => $this->gradeitem->id]); if ($r && $r->include && $r->required) { return(true); } } return(false); } public static function editor_structure($value=VALUE_REQUIRED) { return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'grade_item id'), "cmid" => new \external_value(PARAM_INT, 'course module id'), "name" => new \external_value(PARAM_TEXT, 'grade item name'), "typename" => new \external_value(PARAM_TEXT, 'grade item type name'), "outcome" => new \external_value(PARAM_BOOL, 'is outcome'), "selected" => new \external_value(PARAM_BOOL, 'is selected for current studyitem'), "icon" => new \external_value(PARAM_RAW, 'html for icon of related activity'), "link" => new \external_value(PARAM_TEXT, 'link to related activity'), "gradinglink" => new \external_value(PARAM_TEXT, 'link to related activity'), "grading" => gradingscanner::structure(), "required" => new \external_value(PARAM_BOOL, 'is required for current studyitem'), ], 'referenced course information', $value); } public function editor_model(studyitem $studyitem = null) { $model = [ "id" => $this->id, "cmid" => $this->cmid, "name" => $this->name, "typename" => $this->typename, "outcome" => isset($this->outcome), "selected" => $this->is_selected(), "icon" => $this->icon, "link" => $this->link, "gradinglink" => $this->gradinglink, "required" => $this->is_required(), ]; // Unfortunately, lazy loading of the completion data is off, since we need the data to show study item completion... if ($studyitem !== null && $this->is_selected() && has_capability('local/treestudyplan:viewuserreports', $studyitem->studyline()->studyplan()->context()) && $this->gradingscanner->is_available()) { $model['grading'] = $this->gradingscanner->model(); } return $model; } public static function user_structure($value=VALUE_REQUIRED) { return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'grade_item id'), "cmid" => new \external_value(PARAM_INT, 'course module id'), "name" => new \external_value(PARAM_TEXT, 'grade item name'), "typename" => new \external_value(PARAM_TEXT, 'grade item type name'), "grade" => new \external_value(PARAM_TEXT, 'is outcome'), "gradetype" => new \external_value(PARAM_TEXT, 'grade type (completion|grade)'), "feedback" => new \external_value(PARAM_RAW, 'html for feedback'), "completion" => new \external_value(PARAM_TEXT, 'completion state (incomplete|progress|completed|excellent)'), "icon" => new \external_value(PARAM_RAW, 'html for icon of related activity'), "link" => new \external_value(PARAM_TEXT, 'link to related activity'), "pendingsubmission" => new \external_value(PARAM_BOOL, 'is selected for current studyitem', VALUE_OPTIONAL), "required" => new \external_value(PARAM_BOOL, 'is required for current studyitem'), "selected" => new \external_value(PARAM_BOOL, 'is selected for current studyitem'), ], 'referenced course information', $value); } public function user_model($userid) { global $DB; $grade = $this->gradeitem->get_final($userid); // Convert scale grades to corresponding scale name. if (!empty($grade)) { if (!is_numeric($grade->finalgrade) && empty($grade->finalgrade)) { $finalgrade = "-"; } else if (isset($this->scale)) { $finalgrade = $this->scale->get_nearest_item($grade->finalgrade); } else { $finalgrade = round($grade->finalgrade, 1); } } else { $finalgrade = "-"; } // Retrieve the aggregator and determine completion. if (!isset($this->studyitem)) { throw new \UnexpectedValueException("Study item not set (null) for gradeinfo in report mode"); } $aggregator = $this->studyitem->studyline()->studyplan()->aggregator(); $completion = $aggregator->grade_completion($this, $userid); $model = [ "id" => $this->id, "cmid" => $this->cmid, "name" => $this->name, "typename" => $this->typename, "grade" => $finalgrade, "gradetype" => isset($this->scale) ? "completion" : "grade", "feedback" => empty($grade) ? null : $grade->feedback, "completion" => completion::label($completion), "icon" => $this->icon, "link" => $this->link, "pendingsubmission" => $this->gradingscanner->pending($userid), "required" => $this->is_required(), "selected" => $this->is_selected(), ]; return $model; } public function export_model() { return [ "name" => $this->name, "type" => $this->gradeitem->itemmodule, "selected" => $this->is_selected(), "required" => $this->is_required(), ]; } public static function import(studyitem $item, array $model) { if ($item->type() == studyitem::COURSE) { $courseid = $item->courseid(); $gradeitems= grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid]); foreach ($gradeitems as $gi) { $giname = empty($outcome) ? $gi->itemname : $outcome->name; $gitype = $gi->itemmodule; if ($giname == $model["name"] && $gitype == $model["type"]) { // We have a match. if (!isset($model["selected"])) { $model["selected"] = true; } if (!isset($model["required"])) { $model["required"] = false; } if ($model["selected"] || $model["required"]) { static::include_grade($gi->id, $item->id(), $model["selected"], $model["required"]); } } } } } public static function list_course_gradables($course, studyitem $studyitem=null) { $list = []; if (method_exists("\course_modinfo", "get_array_of_activities")) { $activities = \course_modinfo::get_array_of_activities($course); } else { // Deprecated in Moodle 4.0+, but not yet available in Moodle 3.11. $activities = get_array_of_activities($course->id); } foreach ($activities as $act) { if ($act->visible) { $gradeitems= grade_item::fetch_all(['itemtype' => 'mod', 'itemmodule' => $act->mod, 'iteminstance' => $act->id, 'courseid' => $course->id]); if (!empty($gradeitems)) { foreach ($gradeitems as $gi) { if (($gi->gradetype == GRADE_TYPE_VALUE || $gi->gradetype == GRADE_TYPE_SCALE)) { try { $gradable = new static($gi->id, $studyitem); $list[] = $gradable; } catch (\InvalidArgumentException $x) { } } } } } } usort($list, function($a, $b) { $course = $a->coursesort <=> $b->coursesort; return ($course != 0) ? $course : $a->gradeitem->sortorder <=> $b->gradeitem->sortorder; }); return $list; } public static function list_studyitem_gradables(studyitem $studyitem) { global $DB; $table = 'local_treestudyplan_gradeinc'; $list = []; $records = $DB->get_records($table, ['studyitem_id' => $studyitem->id()]); foreach ($records as $r) { if (isset($r->grade_item_id)) { try { if ($r->include || $r->required) { $list[] = new static($r->grade_item_id, $studyitem); } } catch (\InvalidArgumentException $x) { // On InvalidArgumentException, the grade_item id can no longer be found. // Remove the link to avoid database record hogging. $DB->delete_records($table, ['id' => $r->id]); } } } usort($list, function($a, $b) { $course = $a->coursesort <=> $b->coursesort; return ($course != 0) ? $course : $a->gradeitem->sortorder <=> $b->gradeitem->sortorder; }); return $list; } public static function include_grade(int $gradeid, int $itemid, bool $include, bool $required=false) { global $DB; $table = 'local_treestudyplan_gradeinc'; if ($include) { // Make sure a record exits. $r = $DB->get_record($table, ['studyitem_id' => $itemid, 'grade_item_id' => $gradeid]); if ($r) { $r->include = 1; $r->required = boolval($required)?1:0; $id = $DB->update_record($table, $r); } else { $DB->insert_record($table, [ 'studyitem_id' => $itemid, 'grade_item_id' => $gradeid, 'include' => 1, 'required' => boolval($required)?1:0] ); } } else { // Remove if it should not be included. $r = $DB->get_record($table, ['studyitem_id' => $itemid, 'grade_item_id' => $gradeid]); if ($r) { $DB->delete_records($table, ['id' => $r->id]); } } return success::success(); } }