. /** * Webservice class for handling associations of cohorts and users to a studyplan * @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(); use local_treestudyplan\local\helpers\webservicehelper; require_once($CFG->libdir.'/externallib.php'); /** * Webservice class for handling associations of cohorts and users to a studyplan */ class associationservice 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"; /** * Webservice structure to use in describing a user */ public static function user_structure() : \external_description { return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'user id'), "username" => new \external_value(PARAM_TEXT, 'username'), "firstname" => new \external_value(PARAM_TEXT, 'first name'), "lastname" => new \external_value(PARAM_TEXT, 'last name'), "idnumber" => new \external_value(PARAM_TEXT, 'id number'), "email" => new \external_value(PARAM_TEXT, 'email address'), ]); } /** * Make a webservice user model for a given user * @param stdClass $r User DB record */ public static function make_user_model($r) { return [ "id" => $r->id, "username" => $r->username, "firstname" => $r->firstname, "lastname" => $r->lastname, "idnumber" => $r->idnumber, "email" => $r->email, ]; } /** * Webservice structure to use in describing a cohort */ public static function cohort_structure() : \external_description { return new \external_single_structure([ "id" => new \external_value(PARAM_INT, 'cohort id'), "name" => new \external_value(PARAM_TEXT, 'name'), "idnumber" => new \external_value(PARAM_TEXT, 'id number'), "description" => new \external_value(PARAM_TEXT, 'description'), "visible" => new \external_value(PARAM_BOOL, 'is visible'), "context" => new \external_single_structure([ "name" => new \external_value(PARAM_TEXT, 'context name'), "shortname" => new \external_value(PARAM_TEXT, 'context short name'), "path" => new \external_multiple_structure( new \external_value(PARAM_TEXT)), "shortpath" => new \external_multiple_structure( new \external_value(PARAM_TEXT)), ], 'context information', VALUE_OPTIONAL), ]); } /** * Make a webservice cohort model for a given cohort * @param stdClass $r Cohort DB record */ public static function make_cohort_model($r) { global $DB; $ctx = \context::instance_by_id($r->contextid); $ctxpath = array_reverse($ctx->get_parent_context_ids(true)); if (count($ctxpath) > 1 && $ctxpath[0] == 1) { array_shift($ctxpath); } $result = [ "id" => $r->id, "name" => $r->name, "idnumber" => $r->idnumber, "description" => $r->description, "visible" => $r->visible, "context" => [ "name" => $ctx->get_context_name(false, false), "shortname" => $ctx->get_context_name(false, true), "path" => array_map(function($c) { return \context::instance_by_id($c)->get_context_name(false, false); }, $ctxpath), "shortpath" => array_map(function($c) { return \context::instance_by_id($c)->get_context_name(false, true); }, $ctxpath), ] ]; return $result; } /** * Parameter description for webservice function list_cohort */ public static function list_cohort_parameters() : \external_function_parameters { return new \external_function_parameters( [ 'like' => new \external_value(PARAM_TEXT, 'search text', VALUE_OPTIONAL), 'exclude_id' => new \external_value(PARAM_INT, 'exclude members of this studyplan', VALUE_OPTIONAL), 'context_ic' => new \external_value(PARAM_INT, 'context for this request', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function list_cohort */ public static function list_cohort_returns() : \external_description { return new \external_multiple_structure(self::cohort_structure()); } /** * Search cohorts for matching string * @param string $like String to match cohorts with * @param null $excludeid Do not include these cohorts * @param int $contextid Context to search (default system) * @return array */ public static function list_cohort($like = '', $excludeid = null, $contextid = 1) { global $CFG, $DB; // Only allow this if the user has the right to edit in this context. $context = webservicehelper::find_context($contextid); webservicehelper::require_capabilities(self::CAP_EDIT, $context); $pattern = "%{$like}%"; $params = ["pattern_nm" => $pattern, "pattern_id" => $pattern, ]; $sql = "SELECT DISTINCT c.* from {cohort} c LEFT JOIN {local_treestudyplan_cohort} j ON c.id = j.cohort_id WHERE c.visible = 1 AND(name LIKE :pattern_nm OR idnumber LIKE :pattern_id)"; if (isset($excludeid) && is_numeric($excludeid)) { $sql .= " AND (j.studyplan_id IS NULL OR j.studyplan_id != :exclude_id)"; $params['exclude_id'] = $excludeid; } if ($contextid > 1) { // System context returns all cohorts, including system cohorts. // Otherwise, . $sql .= " AND contextid = :context_id"; $params['context_id'] = $contextid; } $cohorts = []; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $r) { $cohorts[] = static::make_cohort_model($r); } $rs->close(); return $cohorts; } /** * Parameter description for webservice function find_user */ public static function find_user_parameters() : \external_function_parameters { return new \external_function_parameters( [ 'like' => new \external_value(PARAM_TEXT, 'search text'), 'exclude_id' => new \external_value(PARAM_INT, 'exclude members of this studyplan', VALUE_OPTIONAL), 'context_id' => new \external_value(PARAM_INT, 'context for this request', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function find_user */ public static function find_user_returns() : \external_description { return new \external_multiple_structure(self::user_structure()); } /** * Search users for match * @param string $like String to match user firstname/lastname with * @param null $excludeid Do not include these users * @param int $contextid Context to search (default system) * @return array */ public static function find_user($like, $excludeid = null, $contextid = 1) { global $CFG, $DB; // Only allow this if the user has the right to edit in this context. $context = webservicehelper::find_context($contextid); webservicehelper::require_capabilities(self::CAP_EDIT, $context); $pattern = "%{$like}%"; $params = ["pattern_fn" => $pattern, "pattern_ln" => $pattern, "pattern_un" => $pattern, ]; $sql = "SELECT DISTINCT u.* from {user} u LEFT JOIN {local_treestudyplan_user} j ON u.id = j.user_id WHERE u.deleted != 1 AND (firstname LIKE :pattern_fn OR lastname LIKE :pattern_ln OR username LIKE :pattern_un)"; if (isset($excludeid) && is_numeric($excludeid)) { $sql .= " AND (j.studyplan_id IS NULL OR j.studyplan_id != :exclude_id)"; $params['exclude_id'] = $excludeid; } $users = []; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $r) { $users[] = static::make_user_model($r); } $rs->close(); self::sortusermodels($users); return $users; } /** * Parameter description for webservice function connect_cohort */ public static function connect_cohort_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), "cohort_id" => new \external_value(PARAM_INT, 'id of cohort to link', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function connect_cohort */ public static function connect_cohort_returns() : \external_description { return new \external_single_structure([ "success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'), "msg" => new \external_value(PARAM_TEXT, 'message'), ]); } /** * Connect a cohort to a studyplan * @param mixed $studyplanid Id of studyplan * @param mixed $cohortid Id of cohort * @return array Success/fail model */ public static function connect_cohort($studyplanid, $cohortid) { global $CFG, $DB; $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_EDIT, $studyplan->context()); if (!$DB->record_exists('local_treestudyplan_cohort', ['studyplan_id' => $studyplanid, 'cohort_id' => $cohortid])) { $id = $DB->insert_record('local_treestudyplan_cohort', [ 'studyplan_id' => $studyplanid, 'cohort_id' => $cohortid, ]); $studyplan->mark_csync_changed(); return ['success' => true, 'msg' => 'Cohort connected']; } else { return ['success' => true, 'msg' => 'Cohort already connected']; } } /** * Parameter description for webservice function disconnect_cohort */ public static function disconnect_cohort_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), "cohort_id" => new \external_value(PARAM_INT, 'id of cohort to link', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function disconnect_cohort */ public static function disconnect_cohort_returns() : \external_description { return new \external_single_structure([ "success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'), "msg" => new \external_value(PARAM_TEXT, 'message'), ]); } /** * Disconnect a cohort from a studyplan * @param mixed $studyplanid Id of studyplan * @param mixed $cohortid Id of cohort * @return array Success/fail model */ public static function disconnect_cohort($studyplanid, $cohortid) { global $CFG, $DB; $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_EDIT, $studyplan->context()); if ($DB->record_exists('local_treestudyplan_cohort', ['studyplan_id' => $studyplanid, 'cohort_id' => $cohortid])) { $DB->delete_records('local_treestudyplan_cohort', [ 'studyplan_id' => $studyplanid, 'cohort_id' => $cohortid, ]); $studyplan->mark_csync_changed(); return ['success' => true, 'msg' => 'Cohort Disconnected']; } else { return ['success' => true, 'msg' => 'Connection does not exist']; } } /** * Parameter description for webservice function connect_user */ public static function connect_user_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), "user_id" => new \external_value(PARAM_INT, 'id of user to link', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function connect_user */ public static function connect_user_returns() : \external_description { return new \external_single_structure([ "success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'), "msg" => new \external_value(PARAM_TEXT, 'message'), ]); } /** * Connect a user to a studyplan * @param mixed $studyplanid Id of studyplan * @param mixed $userid Id of user * @return array Success/fail model */ public static function connect_user($studyplanid, $userid) { global $CFG, $DB; $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_EDIT, $studyplan->context()); if (!$DB->record_exists('local_treestudyplan_user', ['studyplan_id' => $studyplanid, 'user_id' => $userid])) { $id = $DB->insert_record('local_treestudyplan_user', [ 'studyplan_id' => $studyplanid, 'user_id' => $userid, ]); $studyplan->mark_csync_changed(); return ['success' => true, 'msg' => 'Cohort connected']; } else { return ['success' => true, 'msg' => 'Cohort already connected']; } } /** * Parameter description for webservice function disconnect_user */ public static function disconnect_user_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), "user_id" => new \external_value(PARAM_INT, 'id of user to link', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function disconnect_user */ public static function disconnect_user_returns() : \external_description { return new \external_single_structure([ "success" => new \external_value(PARAM_BOOL, 'operation completed succesfully'), "msg" => new \external_value(PARAM_TEXT, 'message'), ]); } /** * Disconnect a user from a studyplan * @param mixed $studyplanid Id of studyplan * @param mixed $userid Id of user * @return array Success/fail model */ public static function disconnect_user($studyplanid, $userid) { global $CFG, $DB; $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_EDIT, $studyplan->context()); if ($DB->record_exists('local_treestudyplan_user', ['studyplan_id' => $studyplanid, 'user_id' => $userid])) { $DB->delete_records('local_treestudyplan_user', [ 'studyplan_id' => $studyplanid, 'user_id' => $userid, ]); $studyplan->mark_csync_changed(); return ['success' => true, 'msg' => 'User Disconnected']; } else { return ['success' => true, 'msg' => 'Connection does not exist']; } } /** * Parameter description for webservice function associated_users */ public static function associated_users_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function associated_users */ public static function associated_users_returns() : \external_description { return new \external_multiple_structure(self::user_structure()); } /** * List all users associated to a studyplan * @param mixed $studyplanid Id of studyplan * @return array */ public static function associated_users($studyplanid) { global $CFG, $DB; $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_VIEW, $studyplan->context()); $sql = "SELECT DISTINCT u.* FROM {user} u INNER JOIN {local_treestudyplan_user} j ON j.user_id = u.id WHERE j.studyplan_id = :studyplan_id ORDER BY u.lastname, u.firstname"; $rs = $DB->get_recordset_sql($sql, ['studyplan_id' => $studyplanid]); $users = []; foreach ($rs as $u) { $users[] = self::make_user_model($u); } $rs->close(); self::sortusermodels($users); return $users; } /** * Parameter description for webservice function associated_cohorts */ public static function associated_cohorts_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function associated_cohorts */ public static function associated_cohorts_returns() : \external_description { return new \external_multiple_structure(self::cohort_structure()); } /** * List all cohorts associated to a studyplan * @param mixed $studyplanid Id of studyplan * @return array */ public static function associated_cohorts($studyplanid) { global $CFG, $DB; $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_VIEW, $studyplan->context()); $sql = "SELECT DISTINCT c.* FROM {cohort} c INNER JOIN {local_treestudyplan_cohort} j ON j.cohort_id = c.id WHERE j.studyplan_id = :studyplan_id"; $rs = $DB->get_recordset_sql($sql, ['studyplan_id' => $studyplanid]); $cohorts = []; foreach ($rs as $c) { $cohorts[] = self::make_cohort_model($c); } $rs->close(); return $cohorts; } /** * Parameter description for webservice function all_associated */ public static function all_associated_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function all_associated */ public static function all_associated_returns() : \external_description { return new \external_multiple_structure(self::user_structure()); } /** * List all users associated to a studyplan through either a cohort or directly * @param mixed $studyplanid Id of studyplan * @return array */ public static function all_associated($studyplanid) { global $CFG, $DB; $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_VIEW, $studyplan->context()); $users = []; // SQL JOIN script selecting all users that have a cohort linked to this studyplan . // Or are directly linked. $sql = "SELECT DISTINCT u.id, u.username, u.firstname, u.lastname, u.idnumber, u.email FROM {user} u LEFT JOIN {cohort_members} cm ON u.id = cm.userid LEFT JOIN {local_treestudyplan_cohort} tc ON cm.cohortid = tc.cohort_id LEFT JOIN {local_treestudyplan_user} tu ON u.id = tu.user_id WHERE tc.studyplan_id = :studyplanid OR tu.studyplan_id = :studyplanidtoo ORDER BY u.lastname, u.firstname"; $rs = $DB->get_recordset_sql($sql, ["studyplanid" => $studyplan->id(), "studyplanidtoo" => $studyplan->id()]); foreach ($rs as $u) { $users[] = self::make_user_model($u); } $rs->close(); self::sortusermodels($users); return $users; } /** * Parameter description for webservice function all_associated */ public static function all_associated_grouped_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function all_associated */ public static function all_associated_grouped_returns() : \external_description { return new \external_multiple_structure(new \external_single_structure([ 'id' => new \external_value(PARAM_INT, 'id of group'), 'label' => new \external_value(PARAM_TEXT,'group label'), 'users' => new \external_multiple_structure(self::user_structure()), ])); } /** * List all users associated to a studyplan through either a cohort or directly * @param mixed $studyplanid Id of studyplan * @return array */ public static function all_associated_grouped($studyplanid) { global $CFG, $DB; $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_VIEW, $studyplan->context()); $userlist = [ [ 'id' => 0, 'label' => get_string("individuals",'local_treestudyplan'), 'users' => self::associated_users($studyplanid), ] ]; $sql = "SELECT DISTINCT c.* FROM {cohort} c INNER JOIN {local_treestudyplan_cohort} j ON j.cohort_id = c.id WHERE j.studyplan_id = :studyplan_id"; $crs = $DB->get_recordset_sql($sql, ['studyplan_id' => $studyplanid]); foreach ($crs as $c) { $users = []; $sql = "SELECT DISTINCT u.id, u.username, u.firstname, u.lastname, u.idnumber, u.email FROM {user} u LEFT JOIN {cohort_members} cm ON u.id = cm.userid WHERE cm.cohortid = :cohortid ORDER BY u.lastname, u.firstname"; $rs = $DB->get_recordset_sql($sql, ["cohortid" => $c->id]); foreach ($rs as $u) { $users[] = self::make_user_model($u); } $rs->close(); $userlist[] = [ 'id' => $c->id, 'label' => $c->name, 'users' => $users, ]; } $crs->close(); return $userlist; } /** * Sort a list of user models by firstname->lastname * @param array $list Reference to list of user models */ public static function sortusermodels(&$list) { return usort($list, function($a, $b) { $m = []; if (preg_match("/.*?([A-Z].*)/", $a['lastname'], $m)) { $sortlna = $m[1]; } else { $sortlna = $a['lastname']; } if (preg_match("/.*?([A-Z].*)/", $b['lastname'], $m)) { $sortlnb = $m[1]; } else { $sortlnb = $b['lastname']; } $cmp = $sortlna <=> $sortlnb; return ($cmp != 0) ? $cmp : $a['firstname'] <=> $b['firstname']; }); } /** * Parameter description for webservice function cascade_cohortsync */ public static function cascade_cohortsync_parameters() : \external_function_parameters { return new \external_function_parameters( [ "studyplan_id" => new \external_value(PARAM_INT, 'id of studyplan', VALUE_OPTIONAL), ] ); } /** * Return value description for webservice function cascade_cohortsync */ public static function cascade_cohortsync_returns() : \external_description { return success::structure(); } /** * Perform cascading cohort sync for a given studyplan * This adds a cohort sync enrolment to all linked cohorts for each course listed in the studyplan * @param mixed $studyplanid Id of studyplan * @return array Success/fail model */ public static function cascade_cohortsync($studyplanid) { $studyplan = studyplan::find_by_id($studyplanid); webservicehelper::require_capabilities(self::CAP_EDIT, $studyplan->context()); $enroller = new cascadecohortsync($studyplan); $enroller->sync(); if (get_config("local_treestudyplan", "csync_users")) { $userenroller = new cascadeusersync($studyplan); $userenroller->sync(); } $studyplan->clear_csync_changed(); // Clear the csync required flag. return success::success()->model(); } }