. /** * Collect, process and display information about gradable items * @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 core_course\local\entity\content_item; use \grade_item; use \grade_scale; use \grade_outcome; use \core_plugin_manager; /** * Collect, process and display information about gradable items */ class gradeinfo { /** @var studyitem */ private $studyitem = null; /** @var int */ private $id; /** @var grade_item */ private $gradeitem; /** @var string */ private $icon; /** @var string */ private $link; /** @var string */ private $gradinglink; /** @var grade_scale*/ private $scale; /** @var grade_outcome*/ private $outcome; /** @var bool*/ private $hidden = false; /** @var string */ private $name; /** @var string */ private $typename; /** @var int */ private $section; /** @var int */ private $sectionorder; /** @var int */ private $cmid; /** @var int */ private $coursesort; /** @var array */ private static $contentitems = null; /** @var gradingscanner */ private $gradingscanner; /** @var array */ private static $sections = []; /** * Get the sequence of activities for a given section id * @param mixed $sectionid Id of section * @return int[] Sequence of cms in a section */ protected static function get_sectionsequence($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]; } /** * Get the grade_item * @return grade_item */ public function get_gradeitem() : grade_item { return $this->gradeitem; } /** * Get the gradingscanner * @return gradingscanner */ public function get_gradingscanner() : gradingscanner { return $this->gradingscanner; } /** * Get the grade's scale if applicable * @return grade_scale|null */ public function get_scale() : ?grade_scale { return $this->scale; } /** * Get content items (activity icons) from the repository * @return content_item[] */ protected static function get_contentitems() : array { global $PAGE; if (empty(static::$contentitems)) { $PAGE->set_context(\context_system::instance()); static::$contentitems = (new content_item_readonly_repository())->find_all(); } return static::$contentitems; } /** * Get specific contentitem (activity icons) by name * @param mixed $name Name of content item * @return content_item|null */ public static function get_contentitem($name) : ?content_item { $contentitems = static::get_contentitems(); for ($i = 0; $i < count($contentitems); $i++) { if ($contentitems[$i]->get_name() == $name) { return $contentitems[$i]; } } return null; } /** * Get a specific course context from grade item id * @param int $id Grade item id * @return context_course * @throws InvalidArgumentException if grade id is not found */ public static function get_coursecontext_by_id($id) : \context_course { $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);; } /** * Create new object around a grade_item * @param int $id Grade item id of the grade item to use as base * @param studyitem|null $studyitem Studyitem containg the course that references this grade */ 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::get_sectionsequence($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; } /** * Check if this gradable item is selected in the studyitem * @return bool */ public function is_selected() : bool { 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); } /** * Check if this gradable item is marked required in the studyitem * @return bool */ 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); } /** * Webservice structure for editor info * @param int $value Webservice requirement constant * @return external_single_structure Webservice output structure */ /** * Webservice structure for editor info * @param int $value Webservice requirement constant * @return \external_single_structure Webservice output structure */ 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); } /** * Webservice model for editor info * @param studyitem $studyitem Related studyitem to check for * @return array Webservice data model */ 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; } /** * Webservice structure for user info * @param int $value Webservice requirement constant * @return external_single_structure Webservice output structure */ /** * Webservice structure for userinfo * @param int $value Webservice requirement constant * @return \external_single_structure Webservice output structure */ 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); } /** * Webservice model for user info * @param int $userid ID of user to check specific info for * @return array Webservice data model */ 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; } /** * Export essential information for export * @return array information model */ public function export_model() : array { return [ "name" => $this->name, "type" => $this->gradeitem->itemmodule, "selected" => $this->is_selected(), "required" => $this->is_required(), ]; } /** * Import data from exported model into database * @param studyitem $item Studyitem related to this gradable * @param array $model Model data previously exported */ 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"]); } } } } } /** * Get a list if all gradable activities in a given course * @param \stdClass $course Course database record * @param studyitem|null $studyitem Studyitem linked to the course which can be linked to created gradeinfo objects * @return self[] Array of gradeinfo */ public static function list_course_gradables($course, studyitem $studyitem = null) : array { $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) { // Pass and do not add to the list if an error occurs. $gradable = null; // Clean up gradable variable. } } } } } } usort($list, function($a, $b) { $course = $a->coursesort <=> $b->coursesort; return ($course != 0) ? $course : $a->gradeitem->sortorder <=> $b->gradeitem->sortorder; }); return $list; } /** * List all gradables enabled for a given study item * @param studyitem $studyitem The studyitem to search for * @return self[] Array of gradeinfo */ public static function list_studyitem_gradables(studyitem $studyitem) : array { 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; } /** * Webservice executor to include grade with studyitem or not. * if both $inclue and $required are false, any existing DB record will be removed * @param int $gradeid ID of the grade_item * @param int $itemid ID of the study item * @param bool $include Select grade_item for studyitem or not * @param bool $required Mark grade_item as required or not * @return success Always returns successful success object */ 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(); } }