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" :data-studyline="value.id" ref="mainEl"
><div class="r-studyline-handle" :style="'background-color: ' + value.color"></div> ><div class="r-studyline-handle" :style="'background-color: ' + value.color"></div>
<div class="r-studyline-title"><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="premiumenabled() && enrollable">
<template v-if="teachermode"> <template v-if="teachermode">
<a v-if="!can_enrol" <a v-if="!can_enrol"

View file

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

View file

@ -39,6 +39,14 @@ class cascadecohortsync {
private $studyplan; private $studyplan;
/** @var int */ /** @var int */
private $studyplanid; 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 * Create a synchronization task for a studyplan
@ -47,6 +55,12 @@ class cascadecohortsync {
public function __construct(studyplan $studyplan) { public function __construct(studyplan $studyplan) {
$this->studyplan = $studyplan; $this->studyplan = $studyplan;
$this->studyplanid = $studyplan->id(); $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; 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 * Enroll all cohorts associated to the studyplan in the courses linked to this studyplan
*/ */
@ -106,17 +172,85 @@ class cascadecohortsync {
removed outside of this script, it was determined to be the simplest and cleanest solution. 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.
// Find the courses that need to be synced to the associated cohorts. $lines = $this->studyplan->get_all_studylines();
$courseids = $this->studyplan->get_linked_course_ids();
// And find the cohorts that are linked to this studyplan.
$cohortids = $this->studyplan->get_linked_cohort_ids();
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 = $line->get_linked_course_ids();
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 {
// Instance not added for some reason, so report an error somewhere.
// (or not).
$instance = null;
}
}
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) { foreach ($courseids as $courseid) {
$course = \get_course($courseid); $course = \get_course($courseid);
// First create any nonexistent links. // First create any nonexistent links.
foreach ($cohortids as $cohortid) { foreach ($this->cohortids as $cohortid) {
$cohort = $DB->get_record('cohort', ['id' => $cohortid]); $cohort = $DB->get_record('cohort', ['id' => $cohortid]);
$instanceparams = [ $instanceparams = [
@ -156,7 +290,7 @@ class cascadecohortsync {
// If not, add it to the reference. // If not, add it to the reference.
$plans[] = (int)($this->studyplanid); $plans[] = (int)($this->studyplanid);
$enrol->update_instance($instance, (object)["customtext4" => json_encode($plans)]); $this->enrol->update_instance($instance, (object)["customtext4" => json_encode($plans)]);
} }
} else { } else {
@ -171,12 +305,12 @@ class cascadecohortsync {
$instancenewparams['customint2'] = self::uploadenrolmentmethods_get_group($courseid, $groupname); $instancenewparams['customint2'] = self::uploadenrolmentmethods_get_group($courseid, $groupname);
} }
if ($instanceid = $enrol->add_instance($course, $instancenewparams)) { if ($instanceid = $this->enrol->add_instance($course, $instancenewparams)) {
// Also record the (as of yet only) studyplans id requiring this association. // Also record the (as of yet only) studyplans id requiring this association.
// In the customtext4 field in json format. // In the customtext4 field in json format.
$instance = $DB->get_record('enrol', array('id' => $instanceid)); $instance = $DB->get_record('enrol', array('id' => $instanceid));
$enrol->update_instance($instance, (object)["customtext4" => json_encode([(int)($this->studyplanid)])]); $this->enrol->update_instance($instance, (object)["customtext4" => json_encode([(int)($this->studyplanid)])]);
// Successfully added a valid new instance, so now instantiate it. // Successfully added a valid new instance, so now instantiate it.
// First synchronise the enrolment. // First synchronise the enrolment.
@ -201,45 +335,17 @@ class cascadecohortsync {
if (get_config("local_treestudyplan", "csync_autoremove")) { if (get_config("local_treestudyplan", "csync_autoremove")) {
// Only try the autoremove if the option is enabled. // Only try the autoremove if the option is enabled.
// Find all cohort syncs for this course. foreach($this->list_cohortsyncs($courseid) as $instance){
$searchparams = [
'courseid' => $courseid,
'enrol' => self::METHOD,
'roleid' => get_config("local_treestudyplan", "csync_roleid"),
];
$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. // 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. // First check if the cohort is not one of the cohort id's we have associated.
if (!in_array($instance->customint1, $cohortids)) { if (!in_array($instance->customint1, $this->cohortids)) {
$this->unlink_cohortsync($instance);
}
}
// 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)) {
// 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)]);
}
}
}
}
}
}
}
}
}
} }

View file

@ -38,6 +38,12 @@ class cascadeusersync {
private const METHOD = "manual"; private const METHOD = "manual";
/** @var studyplan */ /** @var studyplan */
private $studyplan; private $studyplan;
/** @var object */
private $enrol;
/** @var int */
private $roleid;
/** @var array */
private $userids;
/** /**
* Create a synchronization task for a studyplan * Create a synchronization task for a studyplan
@ -45,6 +51,11 @@ class cascadeusersync {
*/ */
public function __construct(studyplan $studyplan) { public function __construct(studyplan $studyplan) {
$this->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,22 +74,50 @@ class cascadeusersync {
outside of this script, it was determined to be the simplest and cleanest solution. 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.
// Find the courses that need to be synced to the associated cohorts. $lines = $this->studyplan->get_all_studylines();
$courseids = $this->studyplan->get_linked_course_ids();
// And find the users that are linked to this studyplan. // And find the users that are linked to this studyplan.
$userids = $this->studyplan->get_linked_user_ids(); $userids = $this->studyplan->get_linked_user_ids();
// Get the roleid to use for synchronizations.
$roleid = get_config("local_treestudyplan", "csync_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:. // Next, for each course that is linked:.
foreach ($courseids as $courseid) { 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); $course = \get_course($courseid);
if (count($userids) > 0) { if (count($userids) > 0) {
// Get the manual enrol instance for this course. // Get the manual enrol instance for this course.
$instanceparams = ['courseid' => $courseid, 'enrol' => 'manual']; $instanceparams = ['courseid' => $courseid, 'enrol' => 'manual'];
if (!($instance = $DB->get_record('enrol', $instanceparams))) { if (!($instance = $DB->get_record('enrol', $instanceparams))) {
if ($instanceid = $enrol->add_default_instance($course)) { if ($instanceid = $this->enrol->add_default_instance($course)) {
$instance = $DB->get_record('enrol', array('id' => $instanceid)); $instance = $DB->get_record('enrol', array('id' => $instanceid));
} else { } else {
// Instance not added for some reason, so report an error somewhere. // Instance not added for some reason, so report an error somewhere.
@ -89,13 +128,9 @@ class cascadeusersync {
if ($instance !== null) { if ($instance !== null) {
foreach ($userids as $uid) { foreach ($userids as $uid) {
// Try a manual registration - it will just be updated if it is already there.... // Try a manual registration - it will just be updated if it is already there....
$enrol->enrol_user($instance, $uid, $roleid); $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'); require_once($CFG->libdir.'/externallib.php');
use \local_treestudyplan\local\helpers\webservicehelper; use \local_treestudyplan\local\helpers\webservicehelper;
use local_treestudyplan\task\autocohortsync;
require_once($CFG->libdir.'/badgeslib.php'); require_once($CFG->libdir.'/badgeslib.php');
/** /**
@ -628,6 +629,9 @@ class studentstudyplanservice extends \external_api {
$o->enrol($userid); $o->enrol($userid);
} }
// Trigger immediate cohort synchronization for this line only
autocohortsync::syncline($o);
return $o->enrol_info_model($userid); return $o->enrol_info_model($userid);
} }

View file

@ -606,6 +606,25 @@ class studyline {
return $model; 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. * Check if student is enrolled in the line.
* @param int $userid ID of user to check specific info for * @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) * Enrol student from this line (if line enrollable)
* NOTE: This function does not check if the current user should be allowed to do this, * 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; $r->enrolledby = $USER->id;
$DB->insert_record("local_treestudyplan_lineuser",$r); $DB->insert_record("local_treestudyplan_lineuser",$r);
} }
$this->studyplan()->mark_csync_changed();
} }
} }
@ -686,6 +725,8 @@ class studyline {
$r->timeenrolled = time(); // Regi $r->timeenrolled = time(); // Regi
$r->enrolledby = $USER->id; $r->enrolledby = $USER->id;
$DB->update_record("local_treestudyplan_lineuser",$r); $DB->update_record("local_treestudyplan_lineuser",$r);
$this->studyplan()->mark_csync_changed();
} }
// Otherwise, ignore the request. // Otherwise, ignore the request.

View file

@ -957,23 +957,27 @@ class studyplan {
return ($count > 0) ? true : false; 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 * Used for cohort enrolment cascading
* @return int[] * @return studyline[]
*/ */
public function get_linked_course_ids() : array { public function get_all_studylines() : array {
global $DB; global $DB;
$sql = "SELECT i.course_id $sql = "SELECT l.id
FROM {local_treestudyplan} p FROM {local_treestudyplan} p
INNER JOIN {local_treestudyplan_page} pg ON p.id = pg.studyplan_id 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_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";
WHERE p.id = :studyplan_id AND i.type = :itemtype"; $fields = $DB->get_fieldset_sql($sql, ["studyplan_id" => $this->id]);
$fields = $DB->get_fieldset_sql($sql, ["studyplan_id" => $this->id, "itemtype" => studyitem::COURSE]);
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 external_single_structure;
use local_treestudyplan\local\helpers\webservicehelper; use local_treestudyplan\local\helpers\webservicehelper;
use local_treestudyplan\task\autocohortsync;
require_once($CFG->libdir.'/externallib.php'); require_once($CFG->libdir.'/externallib.php');
require_once($CFG->libdir.'/badgeslib.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; return $list;
} }
@ -2068,6 +2071,8 @@ class studyplanservice extends \external_api {
$list[] = self::student_enrol_status_model($userid,$o); $list[] = self::student_enrol_status_model($userid,$o);
} }
// Trigger immediate cohort synchronization for this line only
autocohortsync::syncline($o);
return $list; return $list;
} }

View file

@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/course/externallib.php'); require_once($CFG->dirroot.'/course/externallib.php');
use local_treestudyplan\studyplan; use local_treestudyplan\studyplan;
use local_treestudyplan\studyline;
use local_treestudyplan\cascadecohortsync; use local_treestudyplan\cascadecohortsync;
use local_treestudyplan\cascadeusersync; use local_treestudyplan\cascadeusersync;
@ -70,4 +71,34 @@ class autocohortsync extends \core\task\scheduled_task {
\mtrace("Automatic csync cascading disabled"); \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
}
} }