. /** * Webservice related to courses * @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'); use \local_treestudyplan\courseinfo; use \local_treestudyplan\associationservice; use \local_treestudyplan\local\helpers\webservicehelper; use \local_treestudyplan\completionscanner; use \local_treestudyplan\gradingscanner; use \core_course_category; /** * Webservice related to courses */ class courseservice extends \external_api { /** * Capability required to edit study plans * @var string */ const CAP_EDIT = "local/treestudyplan:editstudyplan"; /** * Capability required to view studyplans (for other users) * @var string */ const CAP_VIEW = "local/treestudyplan:viewuserreports"; /** * Return value description for map_categories function */ public static function map_categories_parameters() : \external_function_parameters { return new \external_function_parameters( [ "root_id" => new \external_value(PARAM_INT, 'root category to use as base', VALUE_DEFAULT), ] ); } /** * Parameter description for map_categories function */ public static function map_categories_returns() : \external_description { return new \external_multiple_structure(static::map_category_structure(false)); } /** * Structure description for category map, used in a number of return descriptions * @param bool $lazy * @param int $value */ protected static function map_category_structure($lazy = false, $value = VALUE_REQUIRED) { $s = [ "id" => new \external_value(PARAM_INT, 'course category id'), "context_id" => new \external_value(PARAM_INT, 'course category context id'), "category" => contextinfo::structure(VALUE_OPTIONAL), "haschildren" => new \external_value(PARAM_BOOL, 'true if the category has child categories'), "hascourses" => new \external_value(PARAM_BOOL, 'true if the category contains courses'), "studyplancount" => new \external_value(PARAM_INT, 'number of linked studyplans', VALUE_OPTIONAL), ]; if (!$lazy > 0) { $s["courses"] = new \external_multiple_structure( courseinfo::editor_structure() ); $s["children"] = new \external_multiple_structure( static::map_category_structure(true)); } return new \external_single_structure($s, "CourseCat info", $value); } /** * Get a category map, and optionally specify a root category to search for * User's top category will be used if none specified * @param int $rootid Optional starting category for the map * @return array */ public static function map_categories($rootid = 0) { global $CFG, $DB; $root = \core_course_category::get($rootid); $context = $root->get_context(); // Make sure the user has access to the context for editing purposes. webservicehelper::require_capabilities(self::CAP_EDIT, $context); // Determine top categories from provided context. if ($root->id == 0) { // On the system level, determine the user's topmost allowed catecories. $usertop = \core_course_category::user_top(); if ($usertop->id == 0) { // Top category.. $children = $root->get_children(); // Returns a list of çore_course_category, let it overwrite $children. } else { $children = [$usertop]; } } else if ($root->is_uservisible()) { $children = [$root]; } foreach ($children as $cat) { $list[] = static::map_category($cat, false); } return $list; } /** * Return value description for get_category function */ public static function get_category_parameters() : \external_function_parameters { return new \external_function_parameters( [ "id" => new \external_value(PARAM_INT, 'id of category'), ] ); } /** * Parameter description for get_category function */ public static function get_category_returns() : \external_description { return static::map_category_structure(false); } /** * Get category info by id * @param mixed $id * @return array */ public static function get_category($id) { $cat = \core_course_category::get($id); return static::map_category($cat); } /** * Create a category map, given a specific category * @param core_course_category $cat The category to scan * @param bool $lazy If lazy loading, do not scan child categories * @return array */ protected static function map_category(core_course_category $cat, $lazy = false) { global $DB; $catcontext = $cat->get_context(); $ctxinfo = new contextinfo($catcontext); $children = $cat->get_children(); // Only shows children visible to the current user. $courses = $cat->get_courses(); $model = [ "id" => $cat->id, "context_id" => $catcontext->id, "category" => $ctxinfo->model(), "haschildren" => !empty($children), "hascourses" => !empty($courses), ]; if (!$lazy) { $model["courses"] = []; foreach ($courses as $course) { $courseinfo = new courseinfo($course->id); $model["courses"][] = $courseinfo->editor_model(); } $model["children"] = []; foreach ($children as $child) { $model["children"][] = static::map_category($child, true); } } return $model; } /** * Return value description for list_accessible_categories function */ public static function list_accessible_categories_parameters() : \external_function_parameters { return new \external_function_parameters( [ "operation" => new \external_value(PARAM_TEXT, 'type of operation ["view"|"edit"]', VALUE_DEFAULT), ] ); } /** * Parameter description for list_accessible_categories function */ public static function list_accessible_categories_returns() : \external_description { return new \external_multiple_structure(static::map_category_structure(true)); } /** * List all categories the user has access to * Used in the studyplan edit form * @param string $operation * @return array */ public static function list_accessible_categories($operation = "edit") { if ($operation == "edit") { $capability = self::CAP_EDIT; } else { // Operation == "view" || default. $capability = self::CAP_VIEW; } $cats = static::categories_by_capability($capability); $list = []; foreach ($cats as $cat) { $list[] = static::map_category($cat, true); } return $list; } /** * [Description for categories_by_capability] * @param mixed $capability * @param core_course_category|null $parent * @return array */ public static function categories_by_capability($capability, core_course_category $parent = null) { // List the categories in which the user has a specific capability. $list = []; // Initialize parent if needed. if ($parent == null) { $parent = \core_course_category::user_top(); if (has_capability($capability, $parent->get_context())) { $list[] = $parent; } } $children = $parent->get_children(); foreach ($children as $child) { // Check if we should add this category. if (has_capability($capability, $child->get_context())) { $list[] = $child; // For optimization purposes, we include all its children now, since they will have inherited the permission. // #PREMATURE_OPTIMIZATION ???. $list = array_merge($list, self::recursive_child_categories($child)); } else { if ($child->get_children_count() > 0) { $list = array_merge($list, self::categories_by_capability($capability, $child)); } } } return $list; } /** * Recursively create a list of all categories unter a specified parent * @param core_course_category $parent * @return core_course_category[] */ protected static function recursive_child_categories(core_course_category $parent) { $list = []; $children = $parent->get_children(); foreach ($children as $child) { $list[] = $child; if ($child->get_children_count() > 0) { $list = array_merge($list, self::recursive_child_categories($child)); } } return $list; } /** * Return value description for list_used_categories function */ public static function list_used_categories_parameters() : \external_function_parameters { return new \external_function_parameters( [ "operation" => new \external_value(PARAM_TEXT, 'type of operation ["view"|"edit"]', VALUE_DEFAULT), "refcontext_id" => new \external_value(PARAM_INT, 'id of reference context', VALUE_DEFAULT), ]); } /** * Parameter description for list_used_categories function */ public static function list_used_categories_returns() : \external_description { return new \external_multiple_structure(static::map_category_structure(true)); } /** * List all categories available to the current user for editing or viewing studyplans * @param string $operation The operation to scan usage for [edit, view] * @param int $refctxid Reference context id * @return array */ public static function list_used_categories($operation = 'edit', $refctxid = 0) { global $DB; if ($operation == "edit") { $capability = self::CAP_EDIT; } else { // Operation == "view" || default. $capability = self::CAP_VIEW; } $contextcounts = []; $contextids = []; $rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan} GROUP BY context_id"); foreach ($rs as $r) { $contextcounts[$r->context_id] = $r->num; $contextids[] = $r->context_id; } $rs->close(); $cats = []; // If the reference context id is not in the list, push it there if ($refctxid > 1 && !in_array($refctxid, $contextids)) { try { $refctx = \context::instance_by_id($refctxid); $refpath = $refctx->get_parent_context_ids(true); $found = false; foreach ($refpath as $i => $pid) { $idx = array_search($pid,$contextids); if($idx !== false) { $contextids = array_merge( array_slice($contextids, 0, $idx+1), array_reverse(array_slice($refpath,0,$i)), array_slice($contextids, $idx+1, count($contextids) - 1) ) ; $found = true; break; } } if(!$found) { array_unshift($contextids,$refctxid); } } catch(\dml_missing_record_exception $x) { // ignore context } } // we only have to check the contexts having a study plan for access permissions foreach ($contextids as $ctxid ) { try { $ctx = \context::instance_by_id($ctxid); if (has_capability($capability, $ctx)) { if ($ctx->contextlevel == CONTEXT_SYSTEM) { $cat = \core_course_category::top(); } else if ($ctx->contextlevel == CONTEXT_COURSECAT) { $cat = \core_course_category::get($ctx->instanceid); } $cats[] = $cat; if ($operation == "edit" && $ctxid == $refctxid) { // Include direct children for navigation purposes foreach ($cat->get_children() as $ccat) { $cats[] = $ccat; } } } } catch (\dml_missing_record_exception $x) { // ignore context } } $list = []; foreach ($cats as $cat) { $count = 0; $ctxid = $cat->get_context()->id; if (array_key_exists($ctxid, $contextcounts)) { $count = $contextcounts[$ctxid]; } $o = static::map_category($cat, true); $o["studyplancount"] = $count; $list[] = $o; } return $list; } /** * List all categories available to the current user for editing or viewing studyplans and add information about their usage * (Not a webservice function) * @param string $operation * @param int $refctxid Reference context id * @return stdClass[] */ public static function list_accessible_categories_with_usage($operation = 'edit', $refctxid = 0) { global $DB; if ($operation == "edit") { $capability = self::CAP_EDIT; } else { // Operation == "view" || default. $capability = self::CAP_VIEW; } // Retrieve context ids used. $contextcounts = []; $contextids = []; $rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan} GROUP BY context_id"); foreach ($rs as $r) { $contextcounts[$r->context_id] = $r->num; $contextids[] = $r->context_id; } $rs->close(); $cats = []; // If the reference context id is not in the list, push it there if ($refctxid > 1 && !in_array($refctxid, $contextids)) { try { $refctx = \context::instance_by_id($refctxid); $refpath = $refctx->get_parent_context_ids(true); $found = false; foreach ($refpath as $i => $pid) { $idx = array_search($pid,$contextids); if($idx !== false) { $contextids = array_merge( array_slice($contextids, 0, $idx+1), array_reverse(array_slice($refpath,0,$i)), array_slice($contextids, $idx+1, count($contextids) - 1) ) ; $found = true; break; } } if(!$found) { array_unshift($contextids,$refctxid); } } catch(\dml_missing_record_exception $x) { // ignore context } } // we only have to check these contexts for access permissions foreach ($contextids as $ctxid ) { try { $ctx = \context::instance_by_id($ctxid); if (has_capability($capability, $ctx)) { if ($ctx->contextlevel == CONTEXT_SYSTEM) { $cat = \core_course_category::top(); } else if ($ctx->contextlevel == CONTEXT_COURSECAT) { $cat = \core_course_category::get($ctx->instanceid); } $cats[] = $cat; if ($operation == "view" && $ctxid == $refctxid) { // Include direct children for navigation purposes foreach ($cat->get_children() as $ccat) { $cats[] = $ccat; } } } } catch (\dml_missing_record_exception $x) { // ignore context } } $list = []; foreach ($cats as $cat) { $count = 0; $ctxid = $cat->get_context()->id; if (array_key_exists($ctxid, $contextcounts)) { $count = $contextcounts[$ctxid]; } $o = new \stdClass(); $o->cat = $cat; $o->ctxid = $ctxid; $o->count = $count; $list[] = $o; } return $list; } /************************************** * * Progress scanners for teacherview * **************************************/ /** * Return value description for scan_grade_progress function * @return external_function_parameters */ public static function scan_grade_progress_parameters() : \external_function_parameters { return new \external_function_parameters( [ "gradeitemid" => new \external_value(PARAM_INT, 'Grade item ID to scan progress for', VALUE_DEFAULT), "studyplanid" => new \external_value(PARAM_INT, 'Study plan id to check progress in', VALUE_DEFAULT), ]); } /** * Parameter description for scan_grade_progress function */ public static function scan_grade_progress_returns() : \external_description { return gradingscanner::structure(VALUE_REQUIRED); } /** * Scan grade item for progress statistics * @param mixed $gradeitemid Grade item id * @param mixed $studyplanid Id of studyitem the grade is selected in * @return array */ public static function scan_grade_progress($gradeitemid, $studyplanid) { global $DB; // Verify access to the study plan. $o = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_VIEW, $o->context()); // Retrieve grade item. $gi = \grade_item::fetch(["id" => $gradeitemid]); // Validate course is linked to studyplan. $courseid = $gi->courseid; if (!$o->course_linked($courseid)) { throw new \webservice_access_exception( "Course {$courseid} linked to grade item {$gradeitemid} is not linked to studyplan {$o->id()}" ); } $scanner = new gradingscanner($gi); return $scanner->model(); } /** * Return value description for scan_completion_progress function */ public static function scan_completion_progress_parameters() : \external_function_parameters { return new \external_function_parameters( [ "criteriaid" => new \external_value(PARAM_INT, 'CriteriaID to scan progress for', VALUE_DEFAULT), "studyplanid" => new \external_value(PARAM_INT, 'Study plan id to check progress in', VALUE_DEFAULT), "courseid" => new \external_value(PARAM_INT, 'Course id of criteria', VALUE_DEFAULT), ]); } /** * Parameter description for scan_completion_progress function */ public static function scan_completion_progress_returns() : \external_description { return completionscanner::structure(VALUE_REQUIRED); } /** * Scan criterium for progress statistics * @param mixed $criteriaid Id of criterium * @param mixed $studyitemid Id of studyplan relevant to this criteria * @return array */ public static function scan_completion_progress($criteriaid, $studyitemid) { global $DB; // Verify access to the study plan. $item = studyitem::find_by_id($studyitemid); $o = $item->studyline()->studyplan(); webservicehelper::require_capabilities(self::CAP_VIEW, $o->context()); $crit = \completion_criteria::fetch(["id" => $criteriaid]); $scanner = new completionscanner($crit, $studyitemid); return $scanner->model(); } /** * Return value description for scan_badge_progress function */ public static function scan_badge_progress_parameters() : \external_function_parameters { return new \external_function_parameters( [ "badgeid" => new \external_value(PARAM_INT, 'Badge to scan progress for', VALUE_DEFAULT), "studyplanid" => new \external_value(PARAM_INT, 'Study plan id to limit progress search to (to determine which students to scan)', VALUE_DEFAULT), ]); } /** * Parameter description for scan_badge_progress function */ public static function scan_badge_progress_returns() : \external_description { return new \external_single_structure([ "total" => new \external_value(PARAM_INT, 'Total number of students scanned'), "issued" => new \external_value(PARAM_INT, 'Number of issued badges'), ]); } /** * Scan badge for completion progress statistica * @param mixed $badgeid ID of the badge * @param mixed $studyplanid ID of the relevant study plan * @return array */ public static function scan_badge_progress($badgeid, $studyplanid) { global $DB; // Check access to the study plan. $o = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_VIEW, $o->context()); // Validate that badge is linked to studyplan. if (!$o->badge_linked($badgeid)) { throw new \webservice_access_exception("Badge {$badgeid} is not linked to studyplan {$o->id()}"); } // Get badge info. $badge = new \core_badges\badge($badgeid); $badgeinfo = new badgeinfo($badge); // Get the connected users. $students = associationservice::all_associated($studyplanid); // Just get the user ids. $studentids = array_map(function ($a) { return $a["id"]; }, $students); return [ "total" => count($studentids), "issued" => $badgeinfo->count_issued($studentids), ]; } }