. /** * Class to process information about a 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_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 process information about a course */ class courseinfo { /** * Table name used in this class * @var string */ const TABLE = 'course'; /** @var stdClass */ private $course; /** @var \context */ private $context; /** @var \context */ private $coursecontext; /** @var studyitem */ private $studyitem; /** @var array */ private static $contentitems = null; /** * Cache of enrolled students in a particular course * @var array */ private static $coursestudents = []; /** * Return database identifier * @return int */ public function id() { return $this->course->id; } /** * Return short name * @return string */ public function shortname() { return $this->course->shortname; } /** * Return course record * @return stdClass */ public function course() { return $this->course; } /** * Return course context * @return \context */ public function course_context() { return $this->coursecontext; } /** * Return course's category context * @return \context */ public function category_context() { return $this->context; } /** * Get content items (activity icons) from the repository * @return content_item[] */ protected 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; } /** * Check if current user is teacher in this course * @return book */ protected function am_teacher() : bool { global $USER; return is_enrolled($this->coursecontext, $USER, 'mod/assign:grade'); } /** * Check if specified user can select gradables in this course * @param int $userid User id to check for . Leave empty to check current user */ protected function i_can_select_gradables($userid = -1) { global $USER, $DB; if ($userid <= 0) { $usr = $USER; } else { $usr = $DB->get_record('user', ['id' => $userid, 'deleted' => 0]); } return($usr && is_enrolled($this->coursecontext, $usr, 'local/treestudyplan:selectowngradables')); } /** * Get specific contentitem (activity icons) by name * @param mixed $name Name of content item * @return content_item|null */ 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; } /** * Construct courseinfo based on course id and * @param int $id Course id * @param studyitem|null $studyitem Studyitem linking this course (if applicable) */ public function __construct($id, studyitem $studyitem = null) { global $DB; $this->studyitem = $studyitem; $this->course = \get_course($id); $this->context = \context_coursecat::instance($this->course->category); $this->coursecontext = \context_course::instance($this->course->id); } /** * Check if a course with the given ID exists * @param int $id Course id * @return bool */ public static function exists($id) { global $DB; return is_numeric($id) && $DB->record_exists(self::TABLE, ['id' => $id]); } /** * Find course id from shortname * @param string $shortname Shortname of the course * @return int Course id */ public static function id_from_shortname($shortname) { global $DB; return $DB->get_field(self::TABLE, "id", ['shortname' => $shortname]); } /** * Determine course timing [future, present or past] based on a course date * @param stdClass $course Course database record * @return string 'future', 'present' or 'past' */ public static function coursetiming($course) { $now = time(); if ($now > $course->startdate) { if ($course->enddate > 0 && $now > $course->enddate) { return "past"; } else { return "present"; } } else { return "future"; } } /** * Determine course timing for this course [future, present or past] * @return string 'future', 'present' or 'past' */ public function timing() { return self::coursetiming($this->course); } /** * Determine proper display name for this course based on config settings, custom fields etc... * @return string Display name for the course */ public function displayname() { $displayfield = get_config("local_treestudyplan", "display_field"); if ($displayfield == "idnumber") { $idnumber = trim(preg_replace("/\s+/u", " ", $this->course->idnumber)); if (strlen($idnumber) > 0) { return $this->course->idnumber; } } else if (strpos( $displayfield , "customfield_") === 0) { $fieldname = substr($displayfield, strlen("customfield_")); $handler = \core_customfield\handler::get_handler('core_course', 'course'); $datas = $handler->get_instance_data($this->course->id); foreach ($datas as $data) { if ($data->get_field()->get('shortname') == $fieldname) { $value = trim(preg_replace("/\s+/u", " ", $data->get_value())); if (strlen($value) > 0) { return $value; } } } } // Fallback to shortname when the specified display field fails, since shortname is never empty. return $this->course->shortname; } /** * Webservice structure for basic info * @param int $value Webservice requirement constant */ public static function simple_structure($value = VALUE_REQUIRED) : \external_description { return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'linked course id'), "fullname" => new \external_value(PARAM_TEXT, 'linked course name'), "shortname" => new \external_value(PARAM_TEXT, 'linked course shortname'), "displayname" => new \external_value(PARAM_TEXT, 'linked course displayname'), "context" => contextinfo::structure(VALUE_OPTIONAL), ], 'referenced course information', $value); } /** * Webservice model for basic info * @return array Webservice data model */ public function simple_model() { $contextinfo = new contextinfo($this->context); $info = [ 'id' => $this->course->id, 'fullname' => $this->course->fullname, 'shortname' => $this->course->shortname, 'displayname' => $this->displayname(), 'context' => $contextinfo->model() ]; return $info; } /** * 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([ "id" => new \external_value(PARAM_INT, 'linked course id'), "fullname" => new \external_value(PARAM_TEXT, 'linked course name'), "shortname" => new \external_value(PARAM_TEXT, 'linked course shortname'), "displayname" => new \external_value(PARAM_TEXT, 'linked course displayname'), "context" => contextinfo::structure(VALUE_OPTIONAL), "ctxid" => new \external_value(PARAM_INT, 'course context id name'), "grades" => new \external_multiple_structure(gradeinfo::editor_structure(), 'grade list (legacy list)', VALUE_OPTIONAL), "completion" => corecompletioninfo::editor_structure(VALUE_OPTIONAL), "timing" => new \external_value(PARAM_TEXT, '(past|present|future)'), "startdate" => new \external_value(PARAM_TEXT, 'Course start date'), "enddate" => new \external_value(PARAM_TEXT, 'Course end date'), "amteacher" => new \external_value(PARAM_BOOL, 'Requesting user is teacher in this course'), "canupdatecourse" => new \external_value(PARAM_BOOL, "If the current user can update this course"), "canselectgradables" => new \external_value(PARAM_BOOL, 'Requesting user can change selected gradables'), "numenrolled" => new \external_value(PARAM_INT, 'number of students from this studyplan enrolled in the course'), "tag" => new \external_value(PARAM_TEXT, 'Tag'), ], 'referenced course information', $value); } /** * Webservice model for editor info * @param studyitem $studyitem Specify a specific study item to check gradable selections for. Leave empty to use default * @param bool $usecorecompletioninfo Whether to use corecompletion info instead of custom selected gradables * @return array Webservice data model */ public function editor_model($usecorecompletioninfo = false) { $contextinfo = new contextinfo($this->context); $timing = $this->timing(); if (isset($this->studyitem)) { $numenrolled = $this->count_enrolled_students($this->studyitem->studyline()->studyplan()->find_linked_userids()); } else { $numenrolled = 0; } $info = [ 'id' => $this->course->id, 'fullname' => $this->course->fullname, 'shortname' => $this->course->shortname, 'displayname' => $this->displayname(), 'context' => $contextinfo->model(), 'ctxid' => $this->coursecontext->id, 'timing' => $timing, 'startdate' => date("Y-m-d", $this->course->startdate, ), 'enddate' => date("Y-m-d", $this->course->enddate), 'amteacher' => $this->am_teacher(), 'canupdatecourse' => \has_capability("moodle/course:update", $this->coursecontext), 'canselectgradables' => $this->i_can_select_gradables(), 'tag' => "Editormodel", 'grades' => [], 'numenrolled' => $numenrolled, ]; if (isset($this->studyitem)) { if (!$usecorecompletioninfo) { $gradables = gradeinfo::list_course_gradables($this->course, $this->studyitem ); foreach ($gradables as $gradable) { $info['grades'][] = $gradable->editor_model($this->studyitem); } } else { $cc = new corecompletioninfo($this->course, $this->studyitem); $studentlist = $this->studyitem->studyline()->studyplan()->find_linked_userids(); $info['completion'] = $cc->editor_model($studentlist); } } return $info; } /** * 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([ "id" => new \external_value(PARAM_INT, 'linked course id'), "fullname" => new \external_value(PARAM_TEXT, 'linked course name'), "shortname" => new \external_value(PARAM_TEXT, 'linked course shortname'), "displayname" => new \external_value(PARAM_TEXT, 'linked course displayname'), "context" => contextinfo::structure(VALUE_OPTIONAL), "ctxid" => new \external_value(PARAM_INT, 'course context id name'), "grades" => new \external_multiple_structure(gradeinfo::user_structure(), 'grade list (legacy list)', VALUE_OPTIONAL), "completion" => corecompletioninfo::user_structure(VALUE_OPTIONAL), "timing" => new \external_value(PARAM_TEXT, '(past|present|future)'), "startdate" => new \external_value(PARAM_TEXT, 'Course start date'), "enddate" => new \external_value(PARAM_TEXT, 'Course end date'), "enrolled" => new \external_value(PARAM_BOOL, 'True if student is enrolled as student in this course'), ], 'course information', $value); } /** * List all users enrolled in a course as student by userid * @param int $courseid Course id of the course to check * @return int[] Array if user ids */ public static function get_course_students($courseid) : array { global $CFG; if (!array_key_exists($courseid, self::$coursestudents)) { $students = []; $context = \context_course::instance($courseid); foreach (explode(', ', $CFG->gradebookroles) as $roleid) { $roleid = trim($roleid); $students = array_keys(get_role_users($roleid, $context, false, 'u.id', 'u.id ASC')); } self::$coursestudents[$courseid] = $students; } return self::$coursestudents[$courseid]; } /** * Check if a user is enrolled as a student in this course * (Has a gradebook role) * @param int $userid The user Id to check */ public function is_enrolled_student($userid) : bool { global $CFG; foreach (explode(', ', $CFG->gradebookroles) as $roleid) { if (user_has_role_assignment($userid, $roleid, $this->coursecontext->id)) { return true; } } return false; } /** * List how many users from a list also enrolled as students * (Has a gradebook role) * @param int[] $userids The user Ids to check */ private function count_enrolled_students(array $userids) { $count = 0; foreach ($userids as $userid) { if ($this->is_enrolled_student($userid)) { $count++; } } return $count; } /** * Webservice model for user info * @param int $userid ID of user to check specific info for * @param bool $usecorecompletioninfo Whether to use corecompletion info instead of custom selected gradables * @return array Webservice data model */ public function user_model($userid, $usecorecompletioninfo = false) { global $DB; $contextinfo = new contextinfo($this->context); $timing = $this->timing(); $info = [ 'id' => $this->course->id, 'fullname' => $this->course->fullname, 'shortname' => $this->course->shortname, 'displayname' => $this->displayname(), 'context' => $contextinfo->model(), 'ctxid' => $this->coursecontext->id, 'timing' => $timing, 'startdate' => date("Y-m-d", $this->course->startdate), 'enddate' => date("Y-m-d", $this->course->enddate), 'grades' => [], 'enrolled' => $this->is_enrolled_student($userid), ]; if (isset($this->studyitem)) { if (!$usecorecompletioninfo) { $gradables = gradeinfo::list_studyitem_gradables($this->studyitem); foreach ($gradables as $gi) { $info['grades'][] = $gi->user_model($userid); } } else if (isset($this->studyitem)) { $cc = new corecompletioninfo($this->course, $this->studyitem); $info['completion'] = $cc->user_model($userid); } } return $info; } }