2023-05-17 21:19:14 +02:00
|
|
|
<?php
|
2023-08-24 23:02:41 +02:00
|
|
|
// This file is part of the Studyplan plugin for Moodle
|
|
|
|
//
|
|
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
/**
|
2023-08-27 21:23:39 +02:00
|
|
|
* Webservice related to courses
|
2023-08-24 23:02:41 +02:00
|
|
|
* @package local_treestudyplan
|
|
|
|
* @copyright 2023 P.M. Kuipers
|
|
|
|
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
|
*/
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
namespace local_treestudyplan;
|
2023-08-25 12:04:27 +02:00
|
|
|
defined('MOODLE_INTERNAL') || die();
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
require_once($CFG->libdir.'/externallib.php');
|
|
|
|
|
2024-06-02 18:47:23 +02:00
|
|
|
use local_treestudyplan\courseinfo;
|
|
|
|
use local_treestudyplan\associationservice;
|
|
|
|
use local_treestudyplan\local\helpers\webservicehelper;
|
|
|
|
use local_treestudyplan\completionscanner;
|
|
|
|
use local_treestudyplan\gradingscanner;
|
|
|
|
use local_treestudyplan\debug;
|
|
|
|
use core_course_category;
|
2024-02-05 23:32:22 +01:00
|
|
|
use moodle_exception;
|
2023-05-17 21:19:14 +02:00
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Webservice related to courses
|
|
|
|
*/
|
2023-08-25 10:41:56 +02:00
|
|
|
class courseservice extends \external_api {
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Capability required to edit study plans
|
|
|
|
* @var string
|
|
|
|
*/
|
2023-06-16 13:49:47 +02:00
|
|
|
const CAP_EDIT = "local/treestudyplan:editstudyplan";
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Capability required to view studyplans (for other users)
|
|
|
|
* @var string
|
|
|
|
*/
|
2023-06-16 13:49:47 +02:00
|
|
|
const CAP_VIEW = "local/treestudyplan:viewuserreports";
|
2023-05-17 21:19:14 +02:00
|
|
|
|
|
|
|
|
2024-02-03 23:50:58 +01:00
|
|
|
/**
|
|
|
|
* Get the topmost categories for the specicied user.
|
|
|
|
* Most of the work is offloaded to an SQL query in the interest of speed, but moodle functions are used to double check access permissions.
|
|
|
|
* @param int $userid Id of the user
|
|
|
|
* @return array of core_course_category
|
|
|
|
*/
|
2024-02-04 23:18:11 +01:00
|
|
|
public static function user_tops($userid=null,$capability='moodle/category:viewcourselist') {
|
|
|
|
global $DB, $USER;
|
|
|
|
if ($userid == null) {
|
|
|
|
$userid = $USER->id;
|
|
|
|
}
|
2024-02-03 23:50:58 +01:00
|
|
|
$tops = [];
|
|
|
|
|
2024-02-05 23:32:22 +01:00
|
|
|
if (has_capability($capability,\context_system::instance(),$userid)) {
|
|
|
|
if ($capability == 'moodle/category:viewcourselist') {
|
|
|
|
// We are now just looking for the visible main level categories.
|
|
|
|
// Add all categories of depth = 1;
|
|
|
|
$rs = $DB->get_recordset("course_categories",["depth" => 1],'sortorder');
|
|
|
|
foreach( $rs as $rcat) {
|
|
|
|
// Get the category, and double check if the category is visible to the current user.
|
|
|
|
// Just in case it is a hidden category and the user does not have the viewhidden permission.
|
|
|
|
$cat = \core_course_category::get($rcat->id, \IGNORE_MISSING, false, $userid);
|
|
|
|
if ($cat !== null) {
|
|
|
|
// Register the category.
|
|
|
|
array_push($tops,$cat);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$rs->close();
|
|
|
|
|
|
|
|
} else {
|
2024-03-25 23:43:27 +01:00
|
|
|
// Context is system, but system may not be visible
|
2024-02-05 23:32:22 +01:00
|
|
|
// Return the top visible categories for this user.
|
|
|
|
// Recurses only once.
|
|
|
|
return self::user_tops($userid);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// We need to search for the permissions on an individial context level.
|
|
|
|
// This part finds all top categories with a certain permission that are also visible for the user.
|
|
|
|
|
|
|
|
$sql = "SELECT UNIQUE ctx.* FROM {context} AS ctx
|
|
|
|
INNER JOIN {role_assignments} AS ra ON ra.contextid = ctx.id
|
|
|
|
INNER JOIN {role_capabilities} AS rc ON ra.roleid = rc.roleid
|
|
|
|
LEFT JOIN {course_categories} AS cat ON ctx.instanceid = cat.id
|
|
|
|
WHERE ( ctx.contextlevel = :ctxl_coursecat )
|
|
|
|
AND ra.userid = :userid AND rc.capability = :capability
|
|
|
|
ORDER BY ctx.depth ASC, cat.sortorder ASC";
|
|
|
|
|
|
|
|
// Use recordset to handle the eventuality of a really big and complex moodle setup.
|
2024-03-25 23:43:27 +01:00
|
|
|
$recordset = $DB->get_records_sql($sql, ["userid" => $userid, "capability" => $capability,
|
2024-02-05 23:32:22 +01:00
|
|
|
"ctxl_coursecat" => \CONTEXT_COURSECAT,]);
|
2024-03-25 23:43:27 +01:00
|
|
|
$params = ["userid" => $userid, "capability" => $capability,
|
|
|
|
"ctxl_coursecat" => \CONTEXT_COURSECAT,];
|
2024-02-05 23:32:22 +01:00
|
|
|
$contextids = [];
|
|
|
|
foreach ($recordset as $r) {
|
|
|
|
// Get the paths as an array.
|
|
|
|
$parents = explode("/",$r->path);
|
|
|
|
// Strip the first item, since it is an empty one.
|
|
|
|
array_shift($parents);
|
|
|
|
// Strip the last item, since it refers to self.
|
|
|
|
array_pop($parents);
|
|
|
|
// Figure out if any of the remaining parent contexts are already contexts with permission.
|
|
|
|
$intersect = array_intersect($contextids,$parents);
|
|
|
|
if (count($intersect) == 0) {
|
|
|
|
// Double check permissions according to the moodle capability system.
|
|
|
|
$ctx = \context::instance_by_id($r->id);
|
|
|
|
if (has_capability($capability,$ctx,$userid)) {
|
|
|
|
// Get the category, and double check if the category is visible to the current user.
|
|
|
|
// Just in case it is a hidden category and the user does not have the viewhidden permission.
|
|
|
|
$cat = \core_course_category::get($r->instanceid, \IGNORE_MISSING, false, $userid);
|
|
|
|
if ($cat !== null) {
|
|
|
|
// Register the context id in the list now, since we know the category is really visible.
|
|
|
|
array_push($contextids,$r->id);
|
|
|
|
// Register the category.
|
|
|
|
array_push($tops,$cat);
|
|
|
|
} else {
|
|
|
|
// The category is not visible. Add the first known visible subcategories.
|
|
|
|
$children = self::get_first_visible_children($r->id,$userid);
|
|
|
|
foreach ($children as $cat) {
|
|
|
|
array_push($tops,$cat);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-03-25 23:43:27 +01:00
|
|
|
//$recordset->close();
|
2024-02-05 23:32:22 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $tops;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the top-most child categories for a given category that are visible.
|
|
|
|
*
|
|
|
|
* @param int $parentid The category to search for
|
|
|
|
* @return array of \core_course_category
|
|
|
|
*/
|
|
|
|
private static function get_first_visible_children($parentid, $userid) {
|
|
|
|
global $DB;
|
2024-02-03 23:50:58 +01:00
|
|
|
$capability = 'moodle/category:viewcourselist';
|
|
|
|
|
2024-02-05 23:32:22 +01:00
|
|
|
$tops = [];
|
|
|
|
$path_like = $DB->sql_like('ctx.path',':pathsearch');
|
|
|
|
|
2024-02-03 23:50:58 +01:00
|
|
|
$sql = "SELECT UNIQUE ctx.* FROM {context} AS ctx
|
|
|
|
INNER JOIN {role_assignments} AS ra ON ra.contextid = ctx.id
|
|
|
|
INNER JOIN {role_capabilities} AS rc ON ra.roleid = rc.roleid
|
|
|
|
LEFT JOIN {course_categories} AS cat ON ctx.instanceid = cat.id
|
2024-02-05 23:32:22 +01:00
|
|
|
WHERE ( ctx.contextlevel = :ctxl_coursecat )
|
2024-02-03 23:50:58 +01:00
|
|
|
AND ra.userid = :userid AND rc.capability = :capability
|
2024-02-05 23:32:22 +01:00
|
|
|
AND {$path_like}
|
2024-02-03 23:50:58 +01:00
|
|
|
ORDER BY ctx.depth ASC, cat.sortorder ASC";
|
|
|
|
|
|
|
|
// Use recordset to handle the eventuality of a really big and complex moodle setup.
|
2024-02-05 23:32:22 +01:00
|
|
|
$recordset = $DB->get_recordset_sql($sql, ["userid" => $userid,
|
|
|
|
"capability" => $capability,
|
|
|
|
"ctxl_coursecat" => \CONTEXT_COURSECAT,
|
|
|
|
"pathsearch" => "%/{$parentid}/%",
|
|
|
|
]);
|
2024-02-03 23:50:58 +01:00
|
|
|
|
|
|
|
$contextids = [];
|
|
|
|
foreach ($recordset as $r) {
|
|
|
|
// Get the paths as an array.
|
|
|
|
$parents = explode("/",$r->path);
|
|
|
|
// Strip the first item, since it is an empty one.
|
|
|
|
array_shift($parents);
|
|
|
|
// Strip the last item, since it refers to self.
|
|
|
|
array_pop($parents);
|
|
|
|
// Figure out if any of the remaining parent contexts are already contexts with permission.
|
|
|
|
$intersect = array_intersect($contextids,$parents);
|
|
|
|
if (count($intersect) == 0) {
|
|
|
|
// Double check permissions according to the moodle capability system.
|
|
|
|
$ctx = \context::instance_by_id($r->id);
|
|
|
|
if (has_capability($capability,$ctx,$userid)) {
|
2024-02-05 23:32:22 +01:00
|
|
|
// Get the category, and double check if the category is visible to the current user.
|
|
|
|
// Just in case it is a hidden category and the user does not have the viewhidden permission.
|
|
|
|
$cat = \core_course_category::get($r->instanceid, \IGNORE_MISSING, false, $userid);
|
|
|
|
if ($cat !== null) {
|
|
|
|
// Register the context id in the list now, since we know the category is really visible.
|
|
|
|
array_push($contextids,$r->id);
|
|
|
|
// Register the category.
|
|
|
|
array_push($tops,$cat);
|
|
|
|
}
|
|
|
|
}
|
2024-02-03 23:50:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
$recordset->close();
|
|
|
|
return $tops;
|
|
|
|
}
|
|
|
|
|
2024-02-05 23:32:22 +01:00
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Return value description for map_categories function
|
|
|
|
*/
|
2023-08-28 08:51:52 +02:00
|
|
|
public static function map_categories_parameters() : \external_function_parameters {
|
2023-05-17 21:19:14 +02:00
|
|
|
return new \external_function_parameters( [
|
2024-03-09 23:29:58 +01:00
|
|
|
"studyplan_id" => new \external_value(PARAM_INT, 'ID of studyplan to map the categories for', VALUE_DEFAULT),
|
2023-05-17 21:19:14 +02:00
|
|
|
] );
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Parameter description for map_categories function
|
|
|
|
*/
|
2023-08-28 11:26:14 +02:00
|
|
|
public static function map_categories_returns() : \external_description {
|
2023-05-17 21:19:14 +02:00
|
|
|
return new \external_multiple_structure(static::map_category_structure(false));
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Structure description for category map, used in a number of return descriptions
|
|
|
|
* @param bool $lazy
|
|
|
|
* @param int $value
|
|
|
|
*/
|
2023-08-25 13:04:19 +02:00
|
|
|
protected static function map_category_structure($lazy = false, $value = VALUE_REQUIRED) {
|
2023-05-17 21:19:14 +02:00
|
|
|
$s = [
|
|
|
|
"id" => new \external_value(PARAM_INT, 'course category id'),
|
|
|
|
"context_id" => new \external_value(PARAM_INT, 'course category context id'),
|
2023-08-25 10:41:56 +02:00
|
|
|
"category" => contextinfo::structure(VALUE_OPTIONAL),
|
2023-08-25 12:16:51 +02:00
|
|
|
"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'),
|
2023-08-25 10:41:56 +02:00
|
|
|
"studyplancount" => new \external_value(PARAM_INT, 'number of linked studyplans', VALUE_OPTIONAL),
|
2023-05-17 21:19:14 +02:00
|
|
|
];
|
|
|
|
|
2023-08-24 23:02:41 +02:00
|
|
|
if (!$lazy > 0) {
|
2023-05-17 21:19:14 +02:00
|
|
|
$s["courses"] = new \external_multiple_structure( courseinfo::editor_structure() );
|
|
|
|
$s["children"] = new \external_multiple_structure( static::map_category_structure(true));
|
|
|
|
}
|
2023-08-24 23:02:41 +02:00
|
|
|
return new \external_single_structure($s, "CourseCat info", $value);
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2024-03-09 23:29:58 +01:00
|
|
|
public static function map_categories($studyplanid = 0) {
|
2024-02-03 23:50:58 +01:00
|
|
|
global $USER;
|
2023-05-17 21:19:14 +02:00
|
|
|
|
|
|
|
|
2023-08-24 23:02:41 +02:00
|
|
|
// Determine top categories from provided context.
|
2023-05-17 21:19:14 +02:00
|
|
|
|
2024-03-09 23:29:58 +01:00
|
|
|
if ($studyplanid == 0) {
|
2023-08-25 09:44:34 +02:00
|
|
|
// On the system level, determine the user's topmost allowed catecories.
|
2024-02-03 23:50:58 +01:00
|
|
|
// This uses a custom function, since moodle's "core_course_category::user_top()" is somewhat deficient.
|
2024-02-05 23:32:22 +01:00
|
|
|
$children = self::user_tops();
|
|
|
|
if (count($children) == 0) {
|
|
|
|
throw new moodle_exception("error:nocategoriesvisible","local_treestudyplan");
|
|
|
|
}
|
2024-02-03 23:50:58 +01:00
|
|
|
} else {
|
2024-03-09 23:29:58 +01:00
|
|
|
if (\get_config("local_treestudyplan","limitcourselist")) { // TODO: Insert config setting here
|
|
|
|
$studyplan = studyplan::find_by_id($studyplanid);
|
|
|
|
$context = $studyplan->context();
|
|
|
|
if ($context->contextlevel == \CONTEXT_SYSTEM) {
|
|
|
|
$children = self::user_tops();
|
|
|
|
} else if ($context->contextlevel == \CONTEXT_COURSECAT) {
|
|
|
|
$cat = \core_course_category::get($context->instanceid,\MUST_EXIST,true);
|
|
|
|
if ($cat->is_uservisible()) {
|
|
|
|
$children = [$cat];
|
|
|
|
} else {
|
|
|
|
$ci = new contextinfo($context);
|
|
|
|
$contextname = $ci->pathstr();
|
|
|
|
throw new moodle_exception("error:cannotviewcategory","local_treestudyplan",'',$contextname);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$children = [];
|
|
|
|
}
|
2024-02-05 23:32:22 +01:00
|
|
|
} else {
|
2024-03-09 23:29:58 +01:00
|
|
|
$children = self::user_tops();
|
|
|
|
if (count($children) == 0) {
|
|
|
|
throw new moodle_exception("error:nocategoriesvisible","local_treestudyplan");
|
|
|
|
}
|
2024-02-05 23:32:22 +01:00
|
|
|
}
|
2023-08-24 23:02:41 +02:00
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
|
2024-02-02 23:36:56 +01:00
|
|
|
$list = [];
|
2023-08-24 23:02:41 +02:00
|
|
|
foreach ($children as $cat) {
|
|
|
|
$list[] = static::map_category($cat, false);
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
return $list;
|
2023-08-24 23:02:41 +02:00
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Return value description for get_category function
|
|
|
|
*/
|
2023-08-28 08:51:52 +02:00
|
|
|
public static function get_category_parameters() : \external_function_parameters {
|
2023-05-17 21:19:14 +02:00
|
|
|
return new \external_function_parameters( [
|
|
|
|
"id" => new \external_value(PARAM_INT, 'id of category'),
|
|
|
|
] );
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Parameter description for get_category function
|
|
|
|
*/
|
2023-08-28 11:26:14 +02:00
|
|
|
public static function get_category_returns() : \external_description {
|
2023-05-17 21:19:14 +02:00
|
|
|
return static::map_category_structure(false);
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Get category info by id
|
|
|
|
* @param mixed $id
|
|
|
|
* @return array
|
|
|
|
*/
|
2023-08-24 23:02:41 +02:00
|
|
|
public static function get_category($id) {
|
2023-05-17 21:19:14 +02:00
|
|
|
$cat = \core_course_category::get($id);
|
|
|
|
return static::map_category($cat);
|
2023-08-24 23:02:41 +02:00
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* 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) {
|
2023-05-17 21:19:14 +02:00
|
|
|
global $DB;
|
|
|
|
$catcontext = $cat->get_context();
|
2023-08-25 09:33:42 +02:00
|
|
|
$ctxinfo = new contextinfo($catcontext);
|
2023-08-25 10:41:56 +02:00
|
|
|
$children = $cat->get_children(); // Only shows children visible to the current user.
|
2023-05-17 21:19:14 +02:00
|
|
|
$courses = $cat->get_courses();
|
|
|
|
$model = [
|
|
|
|
"id" => $cat->id,
|
|
|
|
"context_id" => $catcontext->id,
|
2023-08-25 09:33:42 +02:00
|
|
|
"category" => $ctxinfo->model(),
|
2023-05-17 21:19:14 +02:00
|
|
|
"haschildren" => !empty($children),
|
|
|
|
"hascourses" => !empty($courses),
|
|
|
|
];
|
|
|
|
|
2023-08-24 23:02:41 +02:00
|
|
|
if (!$lazy) {
|
2023-05-17 21:19:14 +02:00
|
|
|
$model["courses"] = [];
|
2023-08-24 23:02:41 +02:00
|
|
|
foreach ($courses as $course) {
|
2023-05-17 21:19:14 +02:00
|
|
|
$courseinfo = new courseinfo($course->id);
|
|
|
|
$model["courses"][] = $courseinfo->editor_model();
|
|
|
|
}
|
|
|
|
|
|
|
|
$model["children"] = [];
|
2023-08-24 23:02:41 +02:00
|
|
|
foreach ($children as $child) {
|
|
|
|
$model["children"][] = static::map_category($child, true);
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $model;
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
2024-02-04 23:18:11 +01:00
|
|
|
* List all user visible categories the current user has a given capability for.
|
2023-08-27 21:23:39 +02:00
|
|
|
* @param mixed $capability
|
|
|
|
* @param core_course_category|null $parent
|
|
|
|
* @return array
|
|
|
|
*/
|
2024-02-04 23:18:11 +01:00
|
|
|
public static function categories_by_capability($capability) {
|
|
|
|
global $USER;
|
2023-08-24 23:02:41 +02:00
|
|
|
// List the categories in which the user has a specific capability.
|
2023-05-17 21:19:14 +02:00
|
|
|
$list = [];
|
2024-02-04 23:18:11 +01:00
|
|
|
$parents = self::user_tops($USER->id,$capability);
|
|
|
|
array_merge($list,$parents);
|
2023-08-24 23:02:41 +02:00
|
|
|
|
2024-02-04 23:18:11 +01:00
|
|
|
foreach ($parents as $parent) {
|
|
|
|
// 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($parent));
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $list;
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* 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) {
|
2023-05-17 21:19:14 +02:00
|
|
|
$list = [];
|
|
|
|
$children = $parent->get_children();
|
2023-08-24 23:02:41 +02:00
|
|
|
foreach ($children as $child) {
|
2023-05-17 21:19:14 +02:00
|
|
|
$list[] = $child;
|
2023-08-24 23:02:41 +02:00
|
|
|
if ($child->get_children_count() > 0) {
|
|
|
|
$list = array_merge($list, self::recursive_child_categories($child));
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return $list;
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
2024-02-04 23:18:11 +01:00
|
|
|
* Return value description for list_available_categories function
|
2023-08-27 21:23:39 +02:00
|
|
|
*/
|
2024-02-04 23:18:11 +01:00
|
|
|
public static function list_available_categories_parameters() : \external_function_parameters {
|
2023-05-17 21:19:14 +02:00
|
|
|
return new \external_function_parameters( [
|
2023-08-24 23:02:41 +02:00
|
|
|
"operation" => new \external_value(PARAM_TEXT, 'type of operation ["view"|"edit"]', VALUE_DEFAULT),
|
2023-12-13 23:49:06 +01:00
|
|
|
"refcontext_id" => new \external_value(PARAM_INT, 'id of reference context', VALUE_DEFAULT),
|
2023-05-17 21:19:14 +02:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
2024-02-04 23:18:11 +01:00
|
|
|
* Parameter description for list_available_categories function
|
2023-08-27 21:23:39 +02:00
|
|
|
*/
|
2024-02-04 23:18:11 +01:00
|
|
|
public static function list_available_categories_returns() : \external_description {
|
2023-05-17 21:19:14 +02:00
|
|
|
return new \external_multiple_structure(static::map_category_structure(true));
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* List all categories available to the current user for editing or viewing studyplans
|
|
|
|
* @param string $operation The operation to scan usage for [edit, view]
|
2023-12-13 23:49:06 +01:00
|
|
|
* @param int $refctxid Reference context id
|
2023-08-27 21:23:39 +02:00
|
|
|
* @return array
|
|
|
|
*/
|
2024-02-04 23:18:11 +01:00
|
|
|
public static function list_available_categories($operation = 'edit', $refctxid = 0) {
|
2023-05-17 21:19:14 +02:00
|
|
|
global $DB;
|
2023-08-24 23:02:41 +02:00
|
|
|
if ($operation == "edit") {
|
2023-06-16 13:49:47 +02:00
|
|
|
$capability = self::CAP_EDIT;
|
2023-08-25 09:44:34 +02:00
|
|
|
} else { // Operation == "view" || default.
|
2023-06-16 13:49:47 +02:00
|
|
|
$capability = self::CAP_VIEW;
|
2023-08-24 23:02:41 +02:00
|
|
|
}
|
2024-02-04 23:18:11 +01:00
|
|
|
|
|
|
|
// Get the context ids of all categories the user has access to view and wich have the given permission.
|
2023-08-25 09:33:42 +02:00
|
|
|
$contextids = [];
|
2024-02-04 23:18:11 +01:00
|
|
|
$tops = self::user_tops(null,$capability);
|
|
|
|
foreach ($tops as $cat) {
|
|
|
|
$ctx = \context_coursecat::instance($cat->id);
|
|
|
|
$contextids[] = $ctx->id;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now get an overview of the number of study plans in a given context.
|
|
|
|
$contextcounts = [];
|
|
|
|
$insertctxs = [];
|
2023-08-24 23:02:41 +02:00
|
|
|
$rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan}
|
2023-05-17 21:19:14 +02:00
|
|
|
GROUP BY context_id");
|
2023-08-24 23:02:41 +02:00
|
|
|
foreach ($rs as $r) {
|
2024-02-04 23:18:11 +01:00
|
|
|
// Build the counts.
|
2023-12-13 23:49:06 +01:00
|
|
|
$contextcounts[$r->context_id] = $r->num;
|
2024-02-04 23:18:11 +01:00
|
|
|
// Add any of the categories containing studyplans to the list.
|
|
|
|
$ctx = \context::instance_by_id($r->context_id);
|
|
|
|
if (has_capability($capability,$ctx) && !in_array($r->context_id,$contextids)) {
|
|
|
|
$insertctxs[] = $ctx;
|
|
|
|
}
|
2023-12-14 21:42:34 +01:00
|
|
|
}
|
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
$rs->close();
|
|
|
|
|
2023-12-13 23:49:06 +01:00
|
|
|
$cats = [];
|
2024-02-04 23:18:11 +01:00
|
|
|
|
|
|
|
// If the reference context id is not in the list, push it there if the user has proper permissions in that context
|
2023-12-13 23:49:06 +01:00
|
|
|
if ($refctxid > 1 && !in_array($refctxid, $contextids)) {
|
|
|
|
try {
|
2024-02-04 23:18:11 +01:00
|
|
|
// Get the context.
|
2023-12-13 23:49:06 +01:00
|
|
|
$refctx = \context::instance_by_id($refctxid);
|
2024-02-04 23:18:11 +01:00
|
|
|
// Double check permissions.
|
|
|
|
if (has_capability($capability,$refctx)) {
|
|
|
|
$insertctxs[] = $refctx;
|
2023-12-13 23:49:06 +01:00
|
|
|
}
|
|
|
|
} catch(\dml_missing_record_exception $x) {
|
|
|
|
// ignore context
|
|
|
|
}
|
|
|
|
}
|
2024-02-04 23:18:11 +01:00
|
|
|
|
|
|
|
foreach ($insertctxs as $ictx) {
|
|
|
|
// Place this context and all relevant direct parents in the correct spots.
|
|
|
|
$ipath = $ictx->get_parent_context_ids(true);
|
|
|
|
$found = false;
|
|
|
|
foreach ($ipath as $i => $pid) {
|
|
|
|
$idx = array_search($pid,$contextids);
|
|
|
|
if($idx !== false) {
|
|
|
|
|
|
|
|
$contextids = array_merge(
|
|
|
|
array_slice($contextids, 0, $idx+1),
|
|
|
|
array_reverse(array_slice($ipath,0,$i)),
|
|
|
|
array_slice($contextids, $idx+1, count($contextids) - 1)
|
|
|
|
) ;
|
|
|
|
|
|
|
|
$found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!$found) {
|
|
|
|
array_unshift($contextids,$ictx->id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Now translate this to the list of categories.
|
2023-12-13 23:49:06 +01:00
|
|
|
foreach ($contextids as $ctxid ) {
|
|
|
|
try {
|
|
|
|
$ctx = \context::instance_by_id($ctxid);
|
2024-02-04 23:18:11 +01:00
|
|
|
if ($ctx->contextlevel == CONTEXT_SYSTEM) {
|
|
|
|
$cat = \core_course_category::top();
|
|
|
|
} else if ($ctx->contextlevel == CONTEXT_COURSECAT) {
|
|
|
|
$cat = \core_course_category::get($ctx->instanceid,\MUST_EXIST,false);
|
|
|
|
}
|
|
|
|
$cats[] = $cat;
|
|
|
|
// In edit mode, also include direct children of the currently selected context.
|
|
|
|
if ($operation == "edit" && $ctxid == $refctxid) {
|
|
|
|
// Include direct children for navigation purposes.
|
|
|
|
foreach ($cat->get_children() as $ccat) {
|
|
|
|
$ccatctx = \context_coursecat::instance($ccat->id);
|
|
|
|
if (!in_array($ccatctx->id,$contextids)) {
|
|
|
|
$cats[] = $ccat;
|
2023-12-13 23:49:06 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (\dml_missing_record_exception $x) {
|
|
|
|
// ignore context
|
|
|
|
}
|
|
|
|
}
|
2023-05-17 21:19:14 +02:00
|
|
|
|
2024-02-04 23:18:11 +01:00
|
|
|
// And finally build the proper models, including studyplan count in the category context.
|
2023-05-17 21:19:14 +02:00
|
|
|
$list = [];
|
2023-08-24 23:02:41 +02:00
|
|
|
foreach ($cats as $cat) {
|
2023-05-17 21:19:14 +02:00
|
|
|
$count = 0;
|
|
|
|
$ctxid = $cat->get_context()->id;
|
2023-12-13 23:49:06 +01:00
|
|
|
if (array_key_exists($ctxid, $contextcounts)) {
|
|
|
|
$count = $contextcounts[$ctxid];
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
|
2023-08-24 23:02:41 +02:00
|
|
|
$o = static::map_category($cat, true);
|
2023-05-17 21:19:14 +02:00
|
|
|
$o["studyplancount"] = $count;
|
|
|
|
$list[] = $o;
|
|
|
|
}
|
|
|
|
return $list;
|
2023-12-13 23:49:06 +01:00
|
|
|
|
2023-05-17 21:19:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-06-16 13:49:47 +02:00
|
|
|
/**************************************
|
2023-08-27 22:20:17 +02:00
|
|
|
*
|
2023-06-16 13:49:47 +02:00
|
|
|
* Progress scanners for teacherview
|
2023-08-27 22:20:17 +02:00
|
|
|
*
|
2023-06-16 13:49:47 +02:00
|
|
|
**************************************/
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Return value description for scan_grade_progress function
|
|
|
|
* @return external_function_parameters
|
|
|
|
*/
|
2023-08-28 08:51:52 +02:00
|
|
|
public static function scan_grade_progress_parameters() : \external_function_parameters {
|
2023-06-16 13:49:47 +02:00
|
|
|
return new \external_function_parameters( [
|
2023-08-24 23:02:41 +02:00
|
|
|
"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),
|
2023-06-16 13:49:47 +02:00
|
|
|
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Parameter description for scan_grade_progress function
|
|
|
|
*/
|
2023-08-28 11:26:14 +02:00
|
|
|
public static function scan_grade_progress_returns() : \external_description {
|
2023-08-25 13:04:19 +02:00
|
|
|
return gradingscanner::structure(VALUE_REQUIRED);
|
2023-06-16 13:49:47 +02:00
|
|
|
}
|
2023-08-24 23:02:41 +02:00
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Scan grade item for progress statistics
|
2023-08-27 22:20:17 +02:00
|
|
|
* @param mixed $gradeitemid Grade item id
|
2023-08-27 21:23:39 +02:00
|
|
|
* @param mixed $studyplanid Id of studyitem the grade is selected in
|
|
|
|
* @return array
|
|
|
|
*/
|
2023-08-24 23:02:41 +02:00
|
|
|
public static function scan_grade_progress($gradeitemid, $studyplanid) {
|
2023-06-16 13:49:47 +02:00
|
|
|
global $DB;
|
2023-08-24 23:02:41 +02:00
|
|
|
// Verify access to the study plan.
|
2023-08-25 17:33:20 +02:00
|
|
|
$o = studyplan::find_by_id($studyplanid);
|
2023-08-24 23:02:41 +02:00
|
|
|
webservicehelper::require_capabilities(self::CAP_VIEW, $o->context());
|
|
|
|
|
|
|
|
// Retrieve grade item.
|
2023-06-16 13:49:47 +02:00
|
|
|
$gi = \grade_item::fetch(["id" => $gradeitemid]);
|
|
|
|
|
2023-08-24 23:02:41 +02:00
|
|
|
// Validate course is linked to studyplan.
|
2023-06-16 13:49:47 +02:00
|
|
|
$courseid = $gi->courseid;
|
2023-08-24 23:02:41 +02:00
|
|
|
if (!$o->course_linked($courseid)) {
|
2023-08-25 11:52:05 +02:00
|
|
|
throw new \webservice_access_exception(
|
|
|
|
"Course {$courseid} linked to grade item {$gradeitemid} is not linked to studyplan {$o->id()}"
|
|
|
|
);
|
2023-06-16 13:49:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$scanner = new gradingscanner($gi);
|
|
|
|
return $scanner->model();
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Return value description for scan_completion_progress function
|
|
|
|
*/
|
2023-08-28 08:51:52 +02:00
|
|
|
public static function scan_completion_progress_parameters() : \external_function_parameters {
|
2023-06-16 13:49:47 +02:00
|
|
|
return new \external_function_parameters( [
|
2023-08-24 23:02:41 +02:00
|
|
|
"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),
|
2023-06-16 13:49:47 +02:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Parameter description for scan_completion_progress function
|
|
|
|
*/
|
2023-08-28 11:26:14 +02:00
|
|
|
public static function scan_completion_progress_returns() : \external_description {
|
2023-06-16 13:49:47 +02:00
|
|
|
return completionscanner::structure(VALUE_REQUIRED);
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Scan criterium for progress statistics
|
|
|
|
* @param mixed $criteriaid Id of criterium
|
2023-09-08 12:47:29 +02:00
|
|
|
* @param mixed $studyitemid Id of studyplan relevant to this criteria
|
2023-08-27 21:23:39 +02:00
|
|
|
* @return array
|
|
|
|
*/
|
2023-08-31 07:40:55 +02:00
|
|
|
public static function scan_completion_progress($criteriaid, $studyitemid) {
|
2023-06-16 13:49:47 +02:00
|
|
|
global $DB;
|
2023-08-24 23:02:41 +02:00
|
|
|
// Verify access to the study plan.
|
2023-08-31 07:40:55 +02:00
|
|
|
$item = studyitem::find_by_id($studyitemid);
|
|
|
|
$o = $item->studyline()->studyplan();
|
2023-08-24 23:02:41 +02:00
|
|
|
webservicehelper::require_capabilities(self::CAP_VIEW, $o->context());
|
2023-06-16 13:49:47 +02:00
|
|
|
|
|
|
|
$crit = \completion_criteria::fetch(["id" => $criteriaid]);
|
2023-08-31 07:40:55 +02:00
|
|
|
$scanner = new completionscanner($crit, $studyitemid);
|
2023-06-16 13:49:47 +02:00
|
|
|
return $scanner->model();
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Return value description for scan_badge_progress function
|
|
|
|
*/
|
2023-08-28 08:51:52 +02:00
|
|
|
public static function scan_badge_progress_parameters() : \external_function_parameters {
|
2023-06-16 13:49:47 +02:00
|
|
|
return new \external_function_parameters( [
|
2023-08-24 23:02:41 +02:00
|
|
|
"badgeid" => new \external_value(PARAM_INT, 'Badge to scan progress for', VALUE_DEFAULT),
|
2023-08-25 11:52:05 +02:00
|
|
|
"studyplanid" => new \external_value(PARAM_INT,
|
|
|
|
'Study plan id to limit progress search to (to determine which students to scan)', VALUE_DEFAULT),
|
2023-06-16 13:49:47 +02:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
|
|
|
* Parameter description for scan_badge_progress function
|
|
|
|
*/
|
2023-08-28 11:26:14 +02:00
|
|
|
public static function scan_badge_progress_returns() : \external_description {
|
2023-06-16 13:49:47 +02:00
|
|
|
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'),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:23:39 +02:00
|
|
|
/**
|
2023-08-27 22:20:17 +02:00
|
|
|
* Scan badge for completion progress statistica
|
2023-08-27 21:23:39 +02:00
|
|
|
* @param mixed $badgeid ID of the badge
|
|
|
|
* @param mixed $studyplanid ID of the relevant study plan
|
|
|
|
* @return array
|
|
|
|
*/
|
2023-08-25 10:41:56 +02:00
|
|
|
public static function scan_badge_progress($badgeid, $studyplanid) {
|
2023-06-16 13:49:47 +02:00
|
|
|
global $DB;
|
2023-08-24 23:02:41 +02:00
|
|
|
// Check access to the study plan.
|
2023-08-25 17:33:20 +02:00
|
|
|
$o = studyplan::find_by_id($studyplanid);
|
2023-08-24 23:02:41 +02:00
|
|
|
webservicehelper::require_capabilities(self::CAP_VIEW, $o->context());
|
2023-06-16 13:49:47 +02:00
|
|
|
|
2023-08-24 23:02:41 +02:00
|
|
|
// Validate that badge is linked to studyplan.
|
|
|
|
if (!$o->badge_linked($badgeid)) {
|
2023-06-16 13:49:47 +02:00
|
|
|
throw new \webservice_access_exception("Badge {$badgeid} is not linked to studyplan {$o->id()}");
|
|
|
|
}
|
|
|
|
|
2023-08-24 23:02:41 +02:00
|
|
|
// Get badge info.
|
2023-06-16 13:49:47 +02:00
|
|
|
$badge = new \core_badges\badge($badgeid);
|
|
|
|
$badgeinfo = new badgeinfo($badge);
|
|
|
|
|
2023-08-25 09:44:34 +02:00
|
|
|
// Get the connected users.
|
2023-06-16 13:49:47 +02:00
|
|
|
$students = associationservice::all_associated($studyplanid);
|
2023-08-24 23:02:41 +02:00
|
|
|
// Just get the user ids.
|
2023-08-25 13:04:19 +02:00
|
|
|
$studentids = array_map(function ($a) {
|
|
|
|
return $a["id"];
|
|
|
|
}, $students);
|
2023-06-16 13:49:47 +02:00
|
|
|
|
|
|
|
return [
|
|
|
|
"total" => count($studentids),
|
|
|
|
"issued" => $badgeinfo->count_issued($studentids),
|
|
|
|
];
|
|
|
|
}
|
2023-08-25 11:52:05 +02:00
|
|
|
}
|