Implemented enrollable lines in cohort and user sync

This commit is contained in:
PMKuipers 2024-03-08 11:54:39 +01:00
parent d713e24e32
commit 35afe06a91
11 changed files with 382 additions and 162 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -995,7 +995,7 @@ export default {
:data-studyline="value.id" ref="mainEl"
><div class="r-studyline-handle" :style="'background-color: ' + value.color"></div>
<div class="r-studyline-title"><div>
<abbr v-b-tooltip.hover :title="value.name">{{ value.shortname }}</abbr>
<abbr v-b-tooltip.hover :title="value.name">{{ value.shortname }}</abbr><br>
<template v-if="premiumenabled() && enrollable">
<template v-if="teachermode">
<a v-if="!can_enrol"

View File

@ -24,6 +24,7 @@ namespace local_treestudyplan;
defined('MOODLE_INTERNAL') || die();
use local_treestudyplan\local\helpers\webservicehelper;
use local_treestudyplan\task\autocohortsync;
require_once($CFG->libdir.'/externallib.php');
@ -698,14 +699,7 @@ class associationservice extends \external_api {
$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.
autocohortsync::syncplan($studyplan);
return success::success()->model();

View File

@ -39,6 +39,14 @@ class cascadecohortsync {
private $studyplan;
/** @var int */
private $studyplanid;
/** @var object */
private $enrol;
/** @var object */
private $manualenrol;
/** @var int */
private $roleid;
/** @var array */
private $cohortids;
/**
* Create a synchronization task for a studyplan
@ -47,6 +55,12 @@ class cascadecohortsync {
public function __construct(studyplan $studyplan) {
$this->studyplan = $studyplan;
$this->studyplanid = $studyplan->id();
$this->enrol = \enrol_get_plugin(self::METHOD);
$this->manualenrol = \enrol_get_plugin("manual");
// Get the roleid to use for synchronizations.
$this->roleid = get_config("local_treestudyplan", "csync_roleid");
// And find the cohorts that are linked to this studyplan.
$this->cohortids = $this->studyplan->get_linked_cohort_ids();
}
/**
@ -91,6 +105,58 @@ class cascadecohortsync {
return $groupid;
}
/**
* Find all cohort sync instances for a specific course
* @param int courseid
*/
private function list_cohortsyncs($courseid) {
global $DB;
// Find all cohort syncs for this course.
$searchparams = [
'courseid' => $courseid,
'enrol' => self::METHOD,
'roleid' => get_config("local_treestudyplan", "csync_roleid"),
];
$list = [];
$records = $DB->get_records("enrol", $searchparams);
foreach ($records as $instance) {
if (!empty($instance->customtext4)) {
// Only add to the list if customtext4 is not empty
$list[] = $instance;
}
}
return $list;
}
/**
* Remove this studyplan reference from cohort sync
* @param object $enrollinstance
*/
private function unlink_cohortsync($enrolinstance) {
// So it may or may not need to be removed.
$plans = json_decode($enrolinstance->customtext4);
if ($plans !== null && is_array($plans)) {
// If a valid array is not returned, better leave it be, we don't want to mess with it.
// Otherwise, check if we should remove it.
if (in_array($this->studyplanid, $plans)) {
// If this plan was referenced before.
// First remove the link.
$fplans = self::array_remove_value($plans, $this->studyplanid);
if (count($fplans) == 0) {
// Delete the sync if there are no studyplan references left.
$this->enrol->delete_instance($enrolinstance);
} else {
// Otherwise just update the references so this studyplan is no longer linked.
$this->enrol->update_instance($enrolinstance, (object)["customtext4" => json_encode($fplans)]);
}
}
}
}
/**
* Enroll all cohorts associated to the studyplan in the courses linked to this studyplan
*/
@ -106,140 +172,180 @@ class cascadecohortsync {
removed outside of this script, it was determined to be the simplest and cleanest solution.
*/
$enrol = \enrol_get_plugin(self::METHOD);
// Find the study lines associated to this studyplan.
$lines = $this->studyplan->get_all_studylines();
debug::write("Starting cohort sync cascading");
debug::dump($lines);
foreach($lines as $line) {
$this->syncline($line);
}
}
/**
* Enroll all cohorts associated to the studyplan in the courses linked to the specified study line
*/
public function syncline(studyline $line){
global $DB;
debug::dump($line->simple_model());
// Find the courses that need to be synced to the associated cohorts.
$courseids = $this->studyplan->get_linked_course_ids();
// And find the cohorts that are linked to this studyplan.
$cohortids = $this->studyplan->get_linked_cohort_ids();
$courseids = $line->get_linked_course_ids();
foreach ($courseids as $courseid) {
$course = \get_course($courseid);
// First create any nonexistent links.
foreach ($cohortids as $cohortid) {
$cohort = $DB->get_record('cohort', ['id' => $cohortid]);
$instanceparams = [
'courseid' => $courseid,
'customint1' => $cohortid,
'enrol' => self::METHOD,
'roleid' => get_config("local_treestudyplan", "csync_roleid"),
];
$instancenewparams = [
'customint1' => $cohortid,
'enrol' => self::METHOD,
'roleid' => get_config("local_treestudyplan", "csync_roleid"),
];
// Create group: .
// 1: check if a link exists.
// If not, make it (maybe use some of the custom text to list the studyplans involved).
if ($instance = $DB->get_record('enrol', $instanceparams)) {
// It already exists.
// Check if this studyplan is already referenced in customtext4 in json format.
// TODO: Check this code - Maybe add option to not remember manually added stuff .
$plans = json_decode($instance->customtext4);
if ($plans == false || !is_array(($plans))) {
// If the data was not an array (null or garbled), count it as manually added.
// This will prevent it's deletion upon.
if (get_config("local_treestudyplan", "csync_remember_manual_csync")) {
$plans = ["manual"];
if ($line->enrollable()) {
debug::write("Checking sycn for enrollable line {$line->name}");
// Since the studyline is enrollable, we need to cascade by student.
foreach ($courseids as $courseid) {
/* 1: Associate the users by individual association */
$course = \get_course($courseid);
debug::write(" processing course {$course->shortname}");
$userids = $line->get_enrolled_userids();
debug::write(" Will attempt to enrol the following userids");
debug::dump($userids);
if (count($userids) > 0) {
// Get the manual enrol instance for this course.
$instanceparams = ['courseid' => $courseid, 'enrol' => 'manual'];
if (!($instance = $DB->get_record('enrol', $instanceparams))) {
if ($instanceid = $this->manualenrol->add_default_instance($course)) {
$instance = $DB->get_record('enrol', array('id' => $instanceid));
} else {
$plans = [];
// Instance not added for some reason, so report an error somewhere.
// (or not).
$instance = null;
}
}
if (!in_array($this->studyplanid , $plans)) {
// If not, add it to the reference.
$plans[] = (int)($this->studyplanid);
$enrol->update_instance($instance, (object)["customtext4" => json_encode($plans)]);
}
} else {
// If method members should be added to a group, create it or get its ID.
if (get_config("local_treestudyplan", "csync_creategroup")) {
// Make or get new new cohort group - but only on creating of instances.
$groupname = $cohort->name." ".strtolower(\get_string('defaultgroupname', 'core_group'));
// And make sure the .
$instancenewparams['customint2'] = self::uploadenrolmentmethods_get_group($courseid, $groupname);
}
if ($instanceid = $enrol->add_instance($course, $instancenewparams)) {
// Also record the (as of yet only) studyplans id requiring this association.
// In the customtext4 field in json format.
$instance = $DB->get_record('enrol', array('id' => $instanceid));
$enrol->update_instance($instance, (object)["customtext4" => json_encode([(int)($this->studyplanid)])]);
// Successfully added a valid new instance, so now instantiate it.
// First synchronise the enrolment.
$cohorttrace = new \null_progress_trace();
$result = enrol_cohort_sync($cohorttrace, $cohortid);
$cohorttrace->finished();
if ($instance !== null) {
debug::write("Got manual enrol instance for this course");
debug::dump($instance);
foreach ($userids as $uid) {
// Try a manual registration - it will just be updated if it is already there....
$this->manualenrol->enrol_user($instance, $uid, $this->roleid);
}
}
}
/* 2: Remove the cohort sync for this studyplan for this line if it exists, since this course should not have cohort sync form this studyplan
Then - if no one uses the link anymore, deactivate it...
This does not remove the sync from courses that are unlinked from a studplan.
But maybe we do not want that anyway, since it is generally a good idea to keep student
access to courses available.
*/
if (get_config("local_treestudyplan", "csync_autoremove")) {
// Only try the autoremove if the option is enabled.
debug::write("Checking for existing cohort syncs");
foreach($this->list_cohortsyncs($courseid) as $instance) {
debug::write("Got existing cohort sync, attempting to remove");
debug::dump($instance);
$this->unlink_cohortsync($instance);
}
}
}
} else {
// Studyline is not enrollable, we can enrol by cohort...
foreach ($courseids as $courseid) {
$course = \get_course($courseid);
/* 2: Check if there are cohort links for this studyplan in this course that should be removed.
A: Check if there are cohort links that are no longer related to this studyplan.
B: Check if these links are valid through another studyplan...
If no one uses the link anymore, deactivate it...
// First create any nonexistent links.
foreach ($this->cohortids as $cohortid) {
$cohort = $DB->get_record('cohort', ['id' => $cohortid]);
This does not remove the sync from courses that are unlinked from a studplan.
But maybe we do not want that anyway, since it is generally a good idea to keep student
access to courses available .
*/
$instanceparams = [
'courseid' => $courseid,
'customint1' => $cohortid,
'enrol' => self::METHOD,
'roleid' => get_config("local_treestudyplan", "csync_roleid"),
];
if (get_config("local_treestudyplan", "csync_autoremove")) {
// Only try the autoremove if the option is enabled.
$instancenewparams = [
'customint1' => $cohortid,
'enrol' => self::METHOD,
'roleid' => get_config("local_treestudyplan", "csync_roleid"),
];
// Find all cohort syncs for this course.
$searchparams = [
'courseid' => $courseid,
'enrol' => self::METHOD,
'roleid' => get_config("local_treestudyplan", "csync_roleid"),
];
// Create group: .
$records = $DB->get_records("enrol", $searchparams);
foreach ($records as $instance) {
if (!empty($instance->customtext4)) {
// Only check the records that have studyplan information in the customtext4 field.
// First check if the cohort is not one of the cohort id's we have associated.
if (!in_array($instance->customint1, $cohortids)) {
// 1: check if a link exists.
// If not, make it (maybe use some of the custom text to list the studyplans involved).
if ($instance = $DB->get_record('enrol', $instanceparams)) {
// So it may or may not need to be removed.
$plans = json_decode($instance->customtext4);
if ($plans !== null && is_array($plans)) {
// If a valid array is not returned, better leave it be, we don't want to mess with it.
// Otherwise, check if we should remove it.
if (in_array($this->studyplanid, $plans)) {
// It already exists.
// Check if this studyplan is already referenced in customtext4 in json format.
// If this plan was referenced before.
// First remove the link.
$fplans = self::array_remove_value($plans, $this->studyplanid);
if (count($fplans) == 0) {
// Delete the sync if there are no studyplan references left.
$enrol->delete_instance($instance);
} else {
// Otherwise just update the references so this studyplan is no longer linked.
$enrol->update_instance($instance, (object)["customtext4" => json_encode($fplans)]);
}
}
// TODO: Check this code - Maybe add option to not remember manually added stuff .
$plans = json_decode($instance->customtext4);
if ($plans == false || !is_array(($plans))) {
// If the data was not an array (null or garbled), count it as manually added.
// This will prevent it's deletion upon.
if (get_config("local_treestudyplan", "csync_remember_manual_csync")) {
$plans = ["manual"];
} else {
$plans = [];
}
}
if (!in_array($this->studyplanid , $plans)) {
// If not, add it to the reference.
$plans[] = (int)($this->studyplanid);
$this->enrol->update_instance($instance, (object)["customtext4" => json_encode($plans)]);
}
} else {
// If method members should be added to a group, create it or get its ID.
if (get_config("local_treestudyplan", "csync_creategroup")) {
// Make or get new new cohort group - but only on creating of instances.
$groupname = $cohort->name." ".strtolower(\get_string('defaultgroupname', 'core_group'));
// And make sure the .
$instancenewparams['customint2'] = self::uploadenrolmentmethods_get_group($courseid, $groupname);
}
if ($instanceid = $this->enrol->add_instance($course, $instancenewparams)) {
// Also record the (as of yet only) studyplans id requiring this association.
// In the customtext4 field in json format.
$instance = $DB->get_record('enrol', array('id' => $instanceid));
$this->enrol->update_instance($instance, (object)["customtext4" => json_encode([(int)($this->studyplanid)])]);
// Successfully added a valid new instance, so now instantiate it.
// First synchronise the enrolment.
$cohorttrace = new \null_progress_trace();
$result = enrol_cohort_sync($cohorttrace, $cohortid);
$cohorttrace->finished();
}
}
}
/* 2: Check if there are cohort links for this studyplan in this course that should be removed.
A: Check if there are cohort links that are no longer related to this studyplan.
B: Check if these links are valid through another studyplan...
If no one uses the link anymore, deactivate it...
This does not remove the sync from courses that are unlinked from a studplan.
But maybe we do not want that anyway, since it is generally a good idea to keep student
access to courses available .
*/
if (get_config("local_treestudyplan", "csync_autoremove")) {
// Only try the autoremove if the option is enabled.
foreach($this->list_cohortsyncs($courseid) as $instance){
// Only check the records that have studyplan information in the customtext4 field.
// First check if the cohort is not one of the cohort id's we have associated.
if (!in_array($instance->customint1, $this->cohortids)) {
$this->unlink_cohortsync($instance);
}
}
}
}
}
}
}

View File

@ -38,6 +38,12 @@ class cascadeusersync {
private const METHOD = "manual";
/** @var studyplan */
private $studyplan;
/** @var object */
private $enrol;
/** @var int */
private $roleid;
/** @var array */
private $userids;
/**
* Create a synchronization task for a studyplan
@ -45,6 +51,11 @@ class cascadeusersync {
*/
public function __construct(studyplan $studyplan) {
$this->studyplan = $studyplan;
$this->enrol = \enrol_get_plugin(self::METHOD);
// Get the roleid to use for synchronizations.
$this->roleid = get_config("local_treestudyplan", "csync_roleid");
// And find the users that are linked to this studyplan.
$this->userids = $this->studyplan->get_linked_user_ids();
}
@ -63,39 +74,63 @@ class cascadeusersync {
outside of this script, it was determined to be the simplest and cleanest solution.
*/
$enrol = \enrol_get_plugin(self::METHOD);
// Find the courses that need to be synced to the associated cohorts.
$courseids = $this->studyplan->get_linked_course_ids();
// Find the study lines associated to this studyplan.
$lines = $this->studyplan->get_all_studylines();
// And find the users that are linked to this studyplan.
$userids = $this->studyplan->get_linked_user_ids();
// Get the roleid to use for synchronizations.
$roleid = get_config("local_treestudyplan", "csync_roleid");
// Next, for each course that is linked:.
foreach ($courseids as $courseid) {
$course = \get_course($courseid);
if (count($userids) > 0) {
// Get the manual enrol instance for this course.
$instanceparams = ['courseid' => $courseid, 'enrol' => 'manual'];
if (!($instance = $DB->get_record('enrol', $instanceparams))) {
if ($instanceid = $enrol->add_default_instance($course)) {
$instance = $DB->get_record('enrol', array('id' => $instanceid));
} else {
// Instance not added for some reason, so report an error somewhere.
// (or not).
$instance = null;
}
}
if ($instance !== null) {
foreach ($userids as $uid) {
// Try a manual registration - it will just be updated if it is already there....
$enrol->enrol_user($instance, $uid, $roleid);
}
foreach($lines as $line) {
$this->syncline($line);
}
}
/**
* Enroll all cohorts associated to the studyplan in the courses linked to the specified study line
*/
public function syncline(studyline $line){
// Find the courses that need to be synced to the associated cohorts.
$courseids = $line->get_linked_course_ids();
if ($line->enrollable()) {
$lineuids = $line->get_enrolled_userids();
// Next, for each course that is linked:.
foreach ($courseids as $courseid) {
$this->perform_enrol($courseid,$lineuids);
// We do not do any autoremoval for user syncs, to avoid students losing access to the course data.
}
} else {
// Next, for each course that is linked:.
foreach ($courseids as $courseid) {
$this->perform_enrol($courseid,$this->userids);
// We do not do any autoremoval for user syncs, to avoid students losing access to the course data.
}
}
}
/** Enrol a list of users into a specific course
*
*
*/
private function perform_enrol($courseid,$userids) {
global $DB;
$course = \get_course($courseid);
if (count($userids) > 0) {
// Get the manual enrol instance for this course.
$instanceparams = ['courseid' => $courseid, 'enrol' => 'manual'];
if (!($instance = $DB->get_record('enrol', $instanceparams))) {
if ($instanceid = $this->enrol->add_default_instance($course)) {
$instance = $DB->get_record('enrol', array('id' => $instanceid));
} else {
// Instance not added for some reason, so report an error somewhere.
// (or not).
$instance = null;
}
}
if ($instance !== null) {
foreach ($userids as $uid) {
// Try a manual registration - it will just be updated if it is already there....
$this->enrol->enrol_user($instance, $uid, $this->roleid);
}
}
// We do not do any autoremoval for user syncs, to avoid students losing access to the course data.
}
}
}

View File

@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/externallib.php');
use \local_treestudyplan\local\helpers\webservicehelper;
use local_treestudyplan\task\autocohortsync;
require_once($CFG->libdir.'/badgeslib.php');
/**
@ -628,6 +629,9 @@ class studentstudyplanservice extends \external_api {
$o->enrol($userid);
}
// Trigger immediate cohort synchronization for this line only
autocohortsync::syncline($o);
return $o->enrol_info_model($userid);
}

View File

@ -606,6 +606,25 @@ class studyline {
return $model;
}
/** Get the users enrolled in this studyline if relevant
* @return array of Userids linked
*/
public function get_enrolled_userids() {
$userids = $this->studyplan()->find_linked_userids();
if($this->enrollable()) {
$list = [];
foreach($userids as $uid) {
if ( $this->isenrolled($uid)) {
$list[] = $uid;
}
}
return $list;
} else {
return $userids;
}
}
/**
* Check if student is enrolled in the line.
* @param int $userid ID of user to check specific info for
@ -628,6 +647,24 @@ class studyline {
}
}
}
/**
* List the course id is linked in this studyplan
* Used for cohort enrolment cascading
* @return int[]
*/
public function get_linked_course_ids() : array {
global $DB;
$sql = "SELECT i.course_id
FROM {local_treestudyplan_line} l
INNER JOIN {local_treestudyplan_item} i ON l.id = i.line_id
WHERE l.id = :studyline_id AND i.type = :itemtype";
$fields = $DB->get_fieldset_sql($sql, ["studyline_id" => $this->id, "itemtype" => studyitem::COURSE]);
return $fields;
}
/**
* Enrol student from this line (if line enrollable)
* NOTE: This function does not check if the current user should be allowed to do this,
@ -661,6 +698,8 @@ class studyline {
$r->enrolledby = $USER->id;
$DB->insert_record("local_treestudyplan_lineuser",$r);
}
$this->studyplan()->mark_csync_changed();
}
}
@ -686,6 +725,8 @@ class studyline {
$r->timeenrolled = time(); // Regi
$r->enrolledby = $USER->id;
$DB->update_record("local_treestudyplan_lineuser",$r);
$this->studyplan()->mark_csync_changed();
}
// Otherwise, ignore the request.

View File

@ -957,23 +957,27 @@ class studyplan {
return ($count > 0) ? true : false;
}
/**
* List the course id is linked in this studyplan
* Get all study lines linked to this plan (quickly)
* Used for cohort enrolment cascading
* @return int[]
* @return studyline[]
*/
public function get_linked_course_ids() : array {
public function get_all_studylines() : array {
global $DB;
$sql = "SELECT i.course_id
$sql = "SELECT l.id
FROM {local_treestudyplan} p
INNER JOIN {local_treestudyplan_page} pg ON p.id = pg.studyplan_id
INNER JOIN {local_treestudyplan_line} l ON pg.id = l.page_id
INNER JOIN {local_treestudyplan_item} i ON l.id = i.line_id
WHERE p.id = :studyplan_id AND i.type = :itemtype";
$fields = $DB->get_fieldset_sql($sql, ["studyplan_id" => $this->id, "itemtype" => studyitem::COURSE]);
WHERE p.id = :studyplan_id";
$fields = $DB->get_fieldset_sql($sql, ["studyplan_id" => $this->id]);
return $fields;
$list = [];
foreach($fields as $id) {
$list[] = studyline::find_by_id($id);
}
return $list;
}
/**

View File

@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die();
use external_single_structure;
use local_treestudyplan\local\helpers\webservicehelper;
use local_treestudyplan\task\autocohortsync;
require_once($CFG->libdir.'/externallib.php');
require_once($CFG->libdir.'/badgeslib.php');
@ -2019,6 +2020,8 @@ class studyplanservice extends \external_api {
}
}
// Trigger immediate cohort synchronization for this line only
autocohortsync::syncline($o);
return $list;
}
@ -2067,7 +2070,9 @@ class studyplanservice extends \external_api {
$o->unenrol($userid);
$list[] = self::student_enrol_status_model($userid,$o);
}
// Trigger immediate cohort synchronization for this line only
autocohortsync::syncline($o);
return $list;
}

View File

@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/course/externallib.php');
use local_treestudyplan\studyplan;
use local_treestudyplan\studyline;
use local_treestudyplan\cascadecohortsync;
use local_treestudyplan\cascadeusersync;
@ -70,4 +71,34 @@ class autocohortsync extends \core\task\scheduled_task {
\mtrace("Automatic csync cascading disabled");
}
}
/**
* Perform immediate syncronization on a single studyplan.
*/
public static function syncplan(studyplan $studyplan) {
$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.
}
/**
* Perform immediate syncronization on a single studyline.
*/
public static function syncline(studyline $line) {
$plan = $line->studyplan();
$enroller = new cascadecohortsync($plan);
$enroller->sync($line);
if (get_config("local_treestudyplan", "csync_users")) {
$userenroller = new cascadeusersync($plan);
$userenroller->sync($line);
}
// Leave the csync required flag for the next auto update
}
}