. /** * 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 ($displayfield == "fullname") { $fullname = trim(preg_replace("/\s+/u", " ", $this->course->fullname)); if (strlen($fullname) > 0) { return $fullname; } } 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), "competency" => coursecompetencyinfo::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'), "extrafields" => self::extrafields_structure(), ], 'referenced course information', $value); } /** * Webservice model for editor info * @return array Webservice data model */ public function editor_model() { $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", 'extrafields' => $this->extrafields_model(true), 'grades' => [], 'numenrolled' => $numenrolled, ]; if (isset($this->studyitem)) { $aggregator = $this->studyitem->studyline()->studyplan()->aggregator(); if ($aggregator->use_manualactivityselection()) { $gradables = gradeinfo::list_course_gradables($this->course, $this->studyitem ); foreach ($gradables as $gradable) { $info['grades'][] = $gradable->editor_model($this->studyitem); } } if ($aggregator->use_corecompletioninfo()) { $cc = new corecompletioninfo($this->course, $this->studyitem); $studentlist = $this->studyitem->studyline()->studyplan()->find_linked_userids(); $info['completion'] = $cc->editor_model($studentlist); } if ($aggregator->use_coursecompetencies()) { $ci = new coursecompetencyinfo($this->course, $this->studyitem); $studentlist = $this->studyitem->studyline()->studyplan()->find_linked_userids(); $info['competency'] = $ci->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), "competency" => coursecompetencyinfo::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'), "extrafields" => self::extrafields_structure(), "showprogressbar" => new \external_value(PARAM_BOOL, "Whether to show the progress bar in the header"), ], '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 * @return array Webservice data model */ public function user_model($userid) { 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), 'extrafields' => $this->extrafields_model(), 'showprogressbar' => get_config("local_treestudyplan", "courseprogressbar"), ]; if (isset($this->studyitem)) { $aggregator = $this->studyitem->studyline()->studyplan()->aggregator(); if ($aggregator->use_manualactivityselection()) { $gradables = gradeinfo::list_studyitem_gradables($this->studyitem); foreach ($gradables as $gi) { $info['grades'][] = $gi->user_model($userid); } } if ($aggregator->use_corecompletioninfo()) { $cc = new corecompletioninfo($this->course, $this->studyitem); $info['completion'] = $cc->user_model($userid); } if ($aggregator->use_coursecompetencies()) { $ci = new coursecompetencyinfo($this->course, $this->studyitem); $info['competency'] = $ci->user_model($userid); } } return $info; } /** * Webservice structure for extra fields * @param int $value Webservice requirement constant */ public static function extrafields_structure($value = VALUE_REQUIRED): \external_description { return new \external_multiple_structure(new \external_single_structure([ "title" => new \external_value(PARAM_RAW, 'title'), "value" => new \external_value(PARAM_RAW, 'value'), "position" => new \external_value(PARAM_TEXT, 'position'), "type" => new \external_value(PARAM_TEXT, 'value type'), "fieldname" => new \external_value(PARAM_TEXT, 'field name'), "checked" => new \external_value(PARAM_BOOL, 'checkbox value', VALUE_OPTIONAL), "courseid" => new \external_value(PARAM_TEXT, 'course id number'), ], 'referenced course information'), $value); } /** * Webservice model for basic info * @param bool $includeteachervisible Set to true if in teacher mode, so fields set to visible as teacher will show * @return array Webservice data model */ public function extrafields_model($includeteachervisible=false) { $list = []; for ($i = 1; $i <= 5; $i++) { $field = get_config('local_treestudyplan', 'courseinfo'.$i.'_field'); if ($field) { $title = self::extrafields_localize_title(get_config('local_treestudyplan', 'courseinfo'.$i.'_title')); $pos = get_config('local_treestudyplan', 'courseinfo'.$i.'_position'); [$value, $type, $raw] = $this->extrafields_value($field, $includeteachervisible); if ($type) { $list[] = [ "title" => $title, "value" => $value, "position" => $pos, "type" => $type, "fieldname" => $field, "courseid" => $this->course->id, "checked" => ($type == "checkbox") ? ($raw ? true : false) : null, ]; } } } return $list; } /** * Localize title for extra field * @param string $field */ protected static function extrafields_localize_title($field) { $lang = trim(current_language()); $lines = explode("\n", $field); $title = ""; $fallback = ""; // Fallback to first title. foreach ($lines as $l) { $parts = explode("|", $l, 2); if (count($parts) > 0) { // Set the first line as fallback. if (empty($firsttitle) && !empty($parts[0])) { $fallback = $parts[0]; } if (count($parts) == 1 && empty($title)) { // Set line without language as default if no localized line found. $title = trim($parts[0]); } else if (trim($parts[1]) == $lang) { return trim($parts[0]); } } } // Return default title or fall back to first localizef title. return (strlen($title) > 0) ? $title : $fallback; } /** * Determine value and type of an extra field for this course * @param string $fieldname The name if the extra field * @param bool $includeteachervisible Set to true if in teacher mode, so fields set to visible as teacher will show * @return array [value, type] of the field for this */ protected function extrafields_value($fieldname, $includeteachervisible=false) { global $PAGE; if ($fieldname == "description") { // Process embedded files. $value = \file_rewrite_pluginfile_urls( // The description content. $this->course()->summary, // The pluginfile URL which will serve the request. 'pluginfile.php', // The combination of contextid / component / filearea / itemid. // Form the virtual bucket that file are stored in. $this->coursecontext->id, // System instance is always used for this. 'course', 'summary', '' ); return [$value, "textarea", $this->course()->summary]; } else if ($fieldname == "idnumber") { $idnumber = trim(preg_replace("/\s+/u", " ", $this->course->idnumber)); return [$idnumber, "text", $this->course->idnumber]; } else if ($fieldname == "contacts") { $cle = new \core_course_list_element($this->course()); $contacts = $cle->get_course_contacts(); $value = ""; foreach ($contacts as $uid => $contact) { if (strlen($value) > 0) { $value .= ", "; } $value .= $contact["username"]; } if (empty($value)) { $value = get_string("none"); } return [$value, "text", $value]; } else if (strpos( $fieldname , "customfield_") === 0) { $fieldshortname = substr($fieldname, strlen("customfield_")); $handler = \core_customfield\handler::get_handler('core_course', 'course'); $datas = $handler->get_instance_data($this->course->id); foreach ($datas as $data) { $field = $data->get_field(); $fshortname = $field->get('shortname'); if ($fshortname == $fieldshortname) { $visibility = $field->get_configdata_property("visibility"); $raw = $data->get_value(); $type = $field->get('type'); // Only show if visibility is "Everyone" or ("Teachers" and in teacher view ). if ($visibility > 0 && ($visibility == 2 || $includeteachervisible)) { if ($type == "date") { // Date should be converted to YYYY-MM-DD so the javascript can properly format it. if ($raw == 0) { $value = ""; } else { // Convert to YYYY-MM-DD format. $value = date("Y-m-d", $raw); } } else { if (empty($PAGE->context)) { $PAGE->set_context(\context_system::instance()); } // Everything else can just use the export value. $value = $data->export_value(); } return [$value, $type, $raw]; } } } } // Fallback to empty if finding a match fails. return [null, null, null]; } }