diff --git a/classes/local/allocation.php b/classes/local/allocation.php
index e895233..887e1db 100644
--- a/classes/local/allocation.php
+++ b/classes/local/allocation.php
@@ -19,6 +19,7 @@
use enrol_programs\local\source\approval;
use enrol_programs\local\source\base;
use enrol_programs\local\source\cohort;
+use enrol_programs\local\source\profile;
use enrol_programs\local\source\ecommerce;
use enrol_programs\local\source\manual;
use enrol_programs\local\source\selfallocation;
@@ -51,6 +52,7 @@ public static function get_source_classes(): array {
selfallocation::get_type() => selfallocation::class,
approval::get_type() => approval::class,
cohort::get_type() => cohort::class,
+ profile::get_type() => profile::class,
];
if (ecommerce::is_commerce_enabled()) {
diff --git a/classes/local/event_observer.php b/classes/local/event_observer.php
index 1ee2b04..30b3afe 100644
--- a/classes/local/event_observer.php
+++ b/classes/local/event_observer.php
@@ -67,10 +67,27 @@ public static function course_category_deleted(\core\event\course_category_delet
}
}
+ // When new user profile is created.
+ public static function user_created(\core\event\user_created $event) {
+ $updated = \enrol_programs\local\source\profile::fix_allocations(null, $event->relateduserid);
+ if ($updated) {
+ allocation::fix_user_enrolments(null, $event->relateduserid);
+ }
+ }
+
+ // When user profile is updated.
+ public static function user_updated(\core\event\user_updated $event) {
+ $updated = \enrol_programs\local\source\profile::fix_allocations(null, $event->relateduserid);
+ if ($updated) {
+ allocation::fix_user_enrolments(null, $event->relateduserid);
+ }
+ }
+
public static function user_deleted(\core\event\user_deleted $event) {
allocation::deleted_user_cleanup($event->objectid);
}
+
public static function cohort_member_added(\core\event\cohort_member_added $event) {
$updated = \enrol_programs\local\source\cohort::fix_allocations(null, $event->relateduserid);
if ($updated) {
diff --git a/classes/local/form/source_profile_edit.php b/classes/local/form/source_profile_edit.php
new file mode 100644
index 0000000..44ec09e
--- /dev/null
+++ b/classes/local/form/source_profile_edit.php
@@ -0,0 +1,107 @@
+.
+
+namespace enrol_programs\local\form;
+
+use enrol_programs\local\program;
+use enrol_programs\local\allocation;
+use enrol_programs\local\source\profile;
+
+/**
+ * Edit cohort allocation settings.
+ *
+ * @package enrol_programs
+ * @copyright 2025
+ * @author Johnny Tsheke
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+final class source_profile_edit extends \local_openlms\dialog_form {
+
+ protected function definition() {
+ global $DB, $CFG, $PAGE, $COURSE;
+ $mform = $this->_form;
+ $context = $this->_customdata['context'];
+ $source = $this->_customdata['source'];
+ $program = $this->_customdata['program'];
+
+ // Daily version for javascript files to load. This is to avoid cache issues
+ //$todayversion = 'tv='. mktime(0, 0, 0, date("m"), date("d"), date("Y")).''; //timestanp
+ $todayversion = 'tv='.time().'';
+
+ $mform->addElement('select', 'enable', get_string('active'), ['1' => get_string('yes'), '0' => get_string('no')]);
+ $mform->setDefault('enable', $source->enable);
+ if ($source->hasallocations) {
+ $mform->hardFreeze('enable');
+ }
+
+ // load jquery librairy for the pop up
+ $urlstyle= new \moodle_url($CFG->wwwroot . '/enrol/programs/style-profile.css'.'?'."$todayversion");
+ $mform->addElement('html', " ");
+ $plugins = [];
+ require($CFG->libdir . '/jquery/plugins.php');
+ foreach ($plugins as $ptype => $files) {
+ foreach ($files['files'] as $file) {
+ if(file_exists($CFG->libdir . '/jquery/' . $file)){
+ $ulrfile = new \moodle_url($CFG->wwwroot . '/lib/jquery/'.$file.'?'."$todayversion");
+ if($ptype == 'ui-css'){
+ $mform->addElement('html', " ");
+ }else{
+ $mform->addElement('html', "");
+ }
+
+ break;
+ }
+ }
+ }
+ //end style
+ $mform->addElement('textarea', 'datajson', get_string('attrsyntax', 'enrol_programs'));
+ $mform->addHelpButton('datajson', 'attrsyntax', 'enrol_programs');
+
+ $mform->addElement('html', '
' . get_string('listitem_description', 'enrol_programs') . '
');
+
+ $mform->addElement('hidden', 'programid');
+ $mform->setType('programid', PARAM_INT);
+ $mform->setDefault('programid', $program->id);
+ $mform->addElement('hidden', 'courseid');
+ $mform->setType('courseid', PARAM_INT);
+ $mform->setDefault('courseid', $COURSE->id);
+
+ $mform->addElement('hidden', 'type');
+ $mform->setType('type', PARAM_ALPHANUMEXT);
+ $mform->setDefault('type', $source->type);
+
+ $this->add_action_buttons(true, get_string('update'));
+ $this->set_data($source);
+ //----
+
+ //load javascript data for profile rules settings
+ $urljsparams= new \moodle_url($CFG->wwwroot . '/enrol/programs/jsparams.php'.'?'."$todayversion");
+ $mform->addElement('html', "");
+ $ulreditor = new \moodle_url($CFG->wwwroot . '/enrol/programs/js/jquery.booleanEditor.min.js'.'?'."$todayversion");
+ $mform->addElement('html', "");
+ $urljs = new \moodle_url($CFG->wwwroot . '/enrol/programs/js/javascript.min.js'.'?'."$todayversion");
+ $mform->addElement('html', "");
+ $urlstr = new \moodle_url($CFG->wwwroot . '/lib/javascript-static.js'.'?'."$todayversion");
+ $mform->addElement('html', "");
+ }
+
+ public function validation($data, $files) {
+ $errors = parent::validation($data, $files);
+
+ return $errors;
+ }
+}
diff --git a/classes/local/source/profile.php b/classes/local/source/profile.php
new file mode 100644
index 0000000..1085ba2
--- /dev/null
+++ b/classes/local/source/profile.php
@@ -0,0 +1,548 @@
+.
+
+namespace enrol_programs\local\source;
+
+use stdClass;
+
+/**
+ * Program allocation for all visible cohort members.
+ *
+ * @package enrol_programs
+ * @copyright 2025
+ * @author Johnny Tsheke
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+final class profile extends base {
+ /**
+ * Return short type name of source, it is used in database to identify this source.
+ *
+ * NOTE: this must be unique and ite cannot be changed later
+ *
+ * @return string
+ */
+ public static function get_type(): string {
+ return 'profile';
+ }
+
+ /**
+ * Can settings of this source be imported to other program?
+ *
+ /**
+ * Can settings of this source be imported to other program?
+ *
+ * @param stdClass $fromprogram
+ * @param stdClass $targetprogram
+ * @return bool
+ */
+ public static function is_import_allowed(stdClass $fromprogram, stdClass $targetprogram): bool {
+ global $DB;
+
+ if (!$DB->record_exists('enrol_programs_sources', ['type' => static::get_type(), 'programid' => $fromprogram->id])) {
+ return false;
+ }
+
+ if (!$DB->record_exists('enrol_programs_sources', ['type' => static::get_type(), 'programid' => $targetprogram->id])) {
+ if (!static::is_new_allowed($targetprogram)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Import source data from one program to another.
+ *
+ * @param int $fromprogramid
+ * @param int $targetprogramid
+ * @return stdClass created or updated source record
+ */
+ public static function import_source_data(int $fromprogramid, int $targetprogramid): stdClass {
+ global $DB;
+
+ $targetsource = parent::import_source_data($fromprogramid, $targetprogramid);
+
+ $sql = "SELECT fc.*
+ FROM {enrol_programs_src_cohorts} fc
+ JOIN {enrol_programs_sources} fs ON fs.id = fc.sourceid AND fs.programid = :fromprogramid AND fs.type = 'cohort'
+ LEFT JOIN {enrol_programs_src_cohorts} tc ON tc.cohortid = fc.cohortid AND tc.sourceid = :targetsourceid
+ WHERE tc.id IS NULL
+ ORDER BY fc.id ASC";
+ $params = ['fromprogramid' => $fromprogramid, 'targetsourceid' => $targetsource->id];
+ $records = $DB->get_records_sql($sql, $params);
+ foreach ($records as $record) {
+ unset($record->id);
+ $record->sourceid = $targetsource->id;
+ $DB->insert_record('enrol_programs_src_cohorts', $record);
+ }
+
+ return $targetsource;
+ }
+
+ /**
+ * Render details about this enabled source in a program management ui.
+ *
+ * @param stdClass $program
+ * @param stdClass|null $source
+ * @return string
+ */
+ public static function render_status_details(stdClass $program, ?stdClass $source): string {
+ $result = parent::render_status_details($program, $source);
+
+ if ($source) {
+ /*$profiles = cohort::fetch_allocation_profiles_menu($source->id);
+ \core_collator::asort($profiles);
+ if ($profiles) {
+ $profiles = array_map('format_string', $profiles);
+ $result .= ' (' . implode(', ', $profiles) .')';
+ }*/
+ }
+
+ return $result;
+ }
+
+ /**
+ * Is it possible to manually edit user allocation?
+ *
+ * @param stdClass $program
+ * @param stdClass $source
+ * @param stdClass $allocation
+ * @return bool
+ */
+ public static function allocation_edit_supported(stdClass $program, stdClass $source, stdClass $allocation): bool {
+ return true;
+ }
+
+ /**
+ * Is it possible to manually delete user allocation?
+ *
+ * @param stdClass $program
+ * @param stdClass $source
+ * @param stdClass $allocation
+ * @return bool
+ */
+ public static function allocation_delete_supported(stdClass $program, stdClass $source, stdClass $allocation): bool {
+ if ($allocation->archived) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Callback method for source updates.
+ *
+ * @param stdClass|null $oldsource
+ * @param stdClass $data
+ * @param stdClass|null $source
+ * @return void
+ */
+ public static function after_update(?stdClass $oldsource, stdClass $data, ?stdClass $source): void {
+ global $DB;
+
+ if (!$source) {
+ // Just deleted or not enabled at all.
+ return;
+ }
+ return;
+ //$oldcohorts = profile::fetch_allocation_profiles_menu($source->id);
+ //$sourceid = $DB->get_field('enrol_programs_sources', 'id', ['programid' => $data->programid, 'type' => 'profile']);
+ $record = $DB->get_record('enrol_programs_sources', ['programid' => $data->programid, 'type' => 'profile']);
+ if($record) {
+ $arraysyntax = self::attrsyntax_toarray($record->datajson);
+ $arraysql = self::arraysyntax_tosql($arraysyntax);
+ $select = 'SELECT DISTINCT u.id FROM {user} u';
+ $where = ' WHERE u.id=' . $user_enrolment->userid . ' AND u.deleted=0 AND ';
+ }
+ //continuer ici
+ /*
+ $data->cohorts = $data->cohorts ?? [];
+ foreach ($data->cohorts as $cid) {
+ if (isset($oldcohorts[$cid])) {
+ unset($oldcohorts[$cid]);
+ continue;
+ }
+ $record = (object)['sourceid' => $sourceid, 'cohortid' => $cid];
+ $DB->insert_record('enrol_programs_src_cohorts', $record);
+ }
+ foreach ($oldcohorts as $cid => $unused) {
+ $DB->delete_records('enrol_programs_src_cohorts', ['sourceid' => $sourceid, 'cohortid' => $cid]);
+ }*/
+ }
+
+ /**
+ * Fetch cohorts that allow program allocation automatically.
+ *
+ * @param int $sourceid
+ * @return array
+ */
+ public static function fetch_allocation_cohorts_menu(int $sourceid): array {
+ global $DB;
+ return [];
+ $sql = "SELECT c.id, c.name
+ FROM {cohort} c
+ JOIN {enrol_programs_src_cohorts} pc ON c.id = pc.cohortid
+ WHERE pc.sourceid = :sourceid
+ ORDER BY c.name ASC, c.id ASC";
+ $params = ['sourceid' => $sourceid];
+
+ return $DB->get_records_sql_menu($sql, $params);
+ }
+
+ /**
+ * Make sure users are allocated properly.
+ *
+ * This is expected to be called from cron and when
+ * program allocation settings are updated.
+ *
+ * @param int|null $programid
+ * @param int|null $userid
+ * @return bool true if anything updated
+ */
+ public static function fix_allocations(?int $programid, ?int $userid): bool {
+ global $DB, $CFG;
+
+ $updated = false;
+
+ // Allocate all missing users and revert archived allocations.
+ $params = [];
+ $programselect = '';
+ $condselect = '';
+ $condwhere = '';
+ $params = [];
+ if ($programid) { // specific program
+ $psrecord = $DB->get_record('enrol_programs_sources', ['programid' => $programid, 'type' => 'profile']);
+
+ if($psrecord) {
+ $psrecord = self::decode_datajson($psrecord);
+ $arraysyntax = self::attrsyntax_toarray($psrecord->datajson);
+ $arraysql = self::arraysyntax_tosql($arraysyntax);
+ $condselect = $arraysql['select'] ?? '';
+ $condwhere = trim($arraysql['where']) ? ' AND '.$arraysql['where'] : '' ;
+ $params = array_merge($params, $arraysql['params']) ;
+ }
+
+ $programselect = ' AND ( p.id = :programid )';
+ $params['programid'] = $programid;
+ } else { // if no program specified, fix allocation for all programs not archived
+
+ $rsps = $DB->get_recordset('enrol_programs_sources', ['type' => 'profile']);
+
+ foreach ($rsps as $srecord) {
+ if (self::fix_allocations($srecord->programid, $userid)) {
+ $updated = true;
+ }
+ }
+ $rsps->close();
+ return ($updated);
+ }
+ $userselect = '';
+ if ($userid) {
+ // $condwhere = " AND (u.id = :userid) $condwhere";
+ $userselect = ' AND ( u.id = :userid )';
+ $params['userid'] = $userid;
+ }
+ $now = time();
+ $params['now1'] = $now;
+ $params['now2'] = $now;
+
+ $sql = "SELECT DISTINCT p.id programid, u.id userid, s.id AS sourceid, pa.id AS allocationid, pa.archived allocationarchived
+ FROM {user} u
+ $condselect
+ JOIN {enrol_programs_sources} s ON s.type = 'profile'
+ JOIN {enrol_programs_programs} p ON p.id = s.programid
+ LEFT JOIN {enrol_programs_allocations} pa ON pa.programid = p.id AND pa.userid = u.id
+ WHERE (pa.id IS NULL OR (pa.archived = 1 AND pa.sourceid = s.id))
+ AND (p.archived = 0)
+ AND (p.timeallocationstart IS NULL OR p.timeallocationstart <= :now1)
+ AND (p.timeallocationend IS NULL OR p.timeallocationend > :now2)
+ $condwhere
+ $programselect $userselect
+ ORDER BY p.id ASC, u.id ASC, s.id ASC";
+
+ $rs = $DB->get_recordset_sql($sql, $params);
+ $lastprogram = null;
+ $lastsource = null;
+ $program = null;
+ $source = null;
+
+ foreach ($rs as $record) {
+
+ if ($record->allocationid) {
+ if ($record->allocationarchived !== 0 ){
+ $DB->set_field('enrol_programs_allocations', 'archived', 0, ['id' => $record->allocationid]);
+ }
+
+ } else {
+ if ($lastprogram && $lastprogram->id == $record->programid) {
+ $program = $lastprogram;
+ } else {
+ $program = $DB->get_record('enrol_programs_programs', ['id' => $record->programid], '*', MUST_EXIST);
+ $lastprogram = $program;
+ }
+ if ($lastsource && $lastsource->id == $record->sourceid) {
+ $source = $lastsource;
+ } else {
+ $source = $DB->get_record('enrol_programs_sources', ['id' => $record->sourceid], '*', MUST_EXIST);
+ $lastsource = $source;
+ }
+ self::allocate_user($program, $source, $record->userid, []);
+ $updated = true;
+ }
+ }
+ $rs->close();
+
+ // Archive allocations if user not member.
+ //$params = [];
+ $programselect = '';
+ if ($programid) {
+ $programselect = 'AND p.id = :programid';
+ $params['programid'] = $programid;
+ }
+ $userselect = '';
+ if ($userid) {
+ $userselect = ' AND pa.userid = :userid';
+ $params['userid'] = $userid;
+ }
+ $now = time();
+ $params['now1'] = $now;
+ $params['now2'] = $now;
+
+ $sql = "SELECT pa.id as allocationid, pa.userid as allocationuserid
+ FROM {enrol_programs_allocations} pa
+ JOIN {enrol_programs_programs} p ON p.id = pa.programid
+ JOIN {enrol_programs_sources} s ON s.programid = pa.programid AND s.type = 'profile' AND s.id = pa.sourceid
+ WHERE (p.archived = 0) AND (pa.archived = 0)
+ AND NOT EXISTS (
+ SELECT 1
+ FROM {user} u
+ $condselect
+ WHERE (u.id = pa.userid)
+ $condwhere
+ )
+ AND (p.timeallocationstart IS NULL OR p.timeallocationstart <= :now1)
+ AND (p.timeallocationend IS NULL OR p.timeallocationend > :now2)
+ $programselect $userselect
+ ORDER BY pa.id ASC";
+
+ $rspa = $DB->get_recordset_sql($sql, $params);
+ foreach ($rspa as $pa) {
+ // NOTE: it is expected that enrolment fixing is executed right after this method.
+ $DB->set_field('enrol_programs_allocations', 'archived', 1, ['id' => $pa->allocationid]);
+ $updated = true;
+ }
+ $rspa->close();
+
+ return $updated;
+ }
+
+ /**
+ * Decode extra source settings.
+ *
+ * @param stdClass $source
+ * @return stdClass
+ */
+ public static function decode_datajson(stdClass $source): stdClass {
+ if(is_object($source) and property_exists($source,'datajson')) {
+ $source->datajson = json_decode($source->datajson, true, 512, JSON_THROW_ON_ERROR);
+ }
+ return $source;
+ }
+
+ /**
+ * Encode extra source settings.
+ * @param stdClass $formdata
+ * @return string
+ */
+ public static function encode_datajson(stdClass $formdata): string {
+
+ if(is_object($formdata) and property_exists($formdata,'datajson')){
+ return json_encode($formdata->datajson, JSON_THROW_ON_ERROR);
+
+ }
+ return \enrol_programs\local\util::json_encode([]);
+ }
+
+ /**
+ * inspired from enrol_attributes
+ * return an array
+ */
+
+ public static function attrsyntax_toarray($attrsyntax) { // TODO : protected
+ global $DB;
+
+ $attrsyntax_object = $attrsyntax ?? json_decode('{}');
+ $returnval = [];
+ $rules = [];
+ if(is_string($attrsyntax)) {
+ $attrsyntax_object = json_decode($attrsyntax);
+
+ }
+
+ $rules = $attrsyntax_object->rules ?? [];
+
+ $standardfields = \availability_profile\condition::get_standard_profile_fields();
+ $customuserfields = [];
+ foreach ($DB->get_records('user_info_field') as $customfieldrecord) {
+ // $customuserfields[$customfieldrecord->id] = $customfieldrecord->shortname;
+ $customuserfields[$customfieldrecord->shortname] = $customfieldrecord->id;
+ }
+
+ $returnval['rules'] = $rules;
+ $returnval['customuserfields'] = $customuserfields;
+ $returnval['standardfields'] = $standardfields;
+
+ return ($returnval);
+ }
+
+ /*
+ * initial version from enrol_attributes
+ * @param arraysyntax an array containing kys:
+ * 'customuserfields', 'standardfields' and 'rules'
+ * return an array
+ */
+
+ public static function arraysyntax_tosql($arraysyntax, &$join_id = 0) {
+ global $DB;
+ $select = '';
+ $where = '1=1';
+ $params = [];
+ $customuserfields = $arraysyntax['customuserfields'];
+ $standardfields = $arraysyntax['standardfields'];
+ foreach ($arraysyntax['rules'] as $rule) {
+ if (isset($rule->cond_op)) {
+ $where .= ' ' . strtoupper($rule->cond_op) . ' ';
+ }
+ else {
+ $where .= ' AND ';
+ }
+ // first just check if we have a value 'ANY' to enroll all people :
+ if (isset($rule->value) && $rule->value === 'ANY') {
+ $where .= '1=1';
+ continue;
+ }
+ if (isset($rule->rules)) {
+ $sub_arraysyntax = array(
+ 'customuserfields' => $customuserfields,
+ 'standardfields' => $standardfields,
+ 'rules' => $rule->rules
+ );
+ $sub_sql = self::arraysyntax_tosql($sub_arraysyntax, $join_id);
+ $select .= ' ' . $sub_sql['select'] . ' ';
+ $where .= ' ( ' . $sub_sql['where'] . ' ) ';
+ $params = array_merge($params, $sub_sql['params']);
+ } elseif ($customkey = array_search($rule->param, array_flip($customuserfields), true)) {
+ // custom user field actually exists
+ $join_id++;
+ $data = 'd' . $join_id . '.data';
+ $select .= ' INNER JOIN {user_info_data} d' . $join_id . ' ON d' . $join_id . '.userid = u.id AND d' . $join_id . '.fieldid = ' . $customkey;
+
+ if (isset($rule->comp_op) && $rule->comp_op === 'contains') {
+ $where .= ' (' . $DB->sql_like($DB->sql_compare_text($data), ':contains'.$join_id.'0') . ')';
+ $params['contains'.$join_id.'0'] = '%' . $rule->value . '%';
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'doesnotcontain') {
+ $where .= ' (' . $DB->sql_compare_text($data) . ' NOT lIKE ' . $DB->sql_compare_text(':doesnotcontain'.$join_id.'0') . ')';
+ $params['doesnotcontain'.$join_id.'0'] = '%' . $rule->value . '%';
+ }elseif (isset($rule->comp_op) && $rule->comp_op === 'isequalto') {
+ $where .= ' (' . $DB->sql_compare_text($data) . ' = ' . $DB->sql_compare_text(':isequalto'.$join_id.'0') . ')';
+ $params['isequalto'.$join_id.'0'] = '' . $rule->value . '';
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'startswith') {
+ $where .= ' (' . $DB->sql_like( $DB->sql_compare_text($data), ':startswith'.$join_id.'0' ) . ')';
+ $params['startswith'.$join_id.'0'] = $rule->value . '%';
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'endswith') {
+ $where .= ' (' . $DB->sql_like( $DB->sql_compare_text($data), ':endswith'.$join_id.'0' ) . ')';
+ $params['endswith'.$join_id.'0'] = '%' . $rule->value . '';
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'isempty') {
+ $where .= ' (' . 'TRIM( COALESCE('. $DB->sql_compare_text($data). ', :isempty'.$join_id.'0'.') )' . ' = '. ' :isempty'.$join_id.'1'.' )';
+ $params['isempty'.$join_id.'0'] = $DB->sql_like_escape('');
+ $params['isempty'.$join_id.'1'] = $DB->sql_like_escape('');
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'isnotempty') {
+ $where .= ' (' . 'NOT TRIM( COALESCE('. $DB->sql_compare_text($data). ', :isnotempty'.$join_id.'0'.') )' . ' = '. ' :isnotempty'.$join_id.'1'.' )';
+ $params['isnotempty'.$join_id.'0'] = $DB->sql_like_escape('');
+ $params['isnotempty'.$join_id.'1'] = $DB->sql_like_escape('');
+ } elseif (isset($rule->comp_op) && $rule->comp_op !== 'listitem') {
+ $where .= ' (' . $DB->sql_compare_text($data) . ' ' . strtoupper($rule->comp_op) . ' ' . $DB->sql_compare_text(':listitem'.$join_id.'0') . ')';
+ $params['listitem'.$join_id.'0'] = $rule->value;
+
+ } else {
+ $where .= ' (' . $DB->sql_compare_text($data) . ' = ' . $DB->sql_compare_text(
+ ':other'.$join_id.'0'
+ ) . ' OR ' . $DB->sql_like(
+ $DB->sql_compare_text($data),
+ ':other'.$join_id.'1'
+ ) . ' OR ' . $DB->sql_like(
+ $DB->sql_compare_text($data),
+ ':other'.$join_id.'2'
+ ) . ' OR ' . $DB->sql_like(
+ $DB->sql_compare_text($data),
+ ':other'.$join_id.'3')
+ . ')';
+
+ $params['other'.$join_id.'0'] = $rule->value;
+ $params['other'.$join_id.'1'] = '%' . $rule->value;
+ $params['other'.$join_id.'2'] = $rule->value . '%';
+ $params['other'.$join_id.'3'] = '%' . $rule->value . '%';
+
+ }
+ } elseif ($standardfield = array_search($rule->param, array_combine(array_keys($standardfields), array_keys($standardfields)), true)) {
+ // standard field actually exists
+ $join_id++;
+ $data = 'u.' . $standardfield . ''; // user tables field
+ if (isset($rule->comp_op) && $rule->comp_op === 'contains') {
+ $where .= ' (' . $DB->sql_like($DB->sql_compare_text($data), ':contains'.$join_id.'0') . ')';
+ $params['contains'.$join_id.'0'] = '%' . $rule->value . '%';
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'doesnotcontain') {
+ $where .= ' (' . $DB->sql_compare_text($data) . ' NOT lIKE ' . $DB->sql_compare_text(':doesnotcontain'.$join_id.'0') . ')';
+ $params['doesnotcontain'.$join_id.'0'] = '%' . $rule->value . '%';
+ }elseif (isset($rule->comp_op) && $rule->comp_op === 'isequalto') {
+ $where .= ' (' . $DB->sql_compare_text($data) . ' = ' . $DB->sql_compare_text(':isequalto'.$join_id.'0') . ' )';
+ $params['isequalto'.$join_id.'0'] = '' . $rule->value . '';
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'startswith') {
+ $where .= ' (' . $DB->sql_like( $DB->sql_compare_text($data), ':startswith'.$join_id.'0' ) . ')';
+ $params['startswith'.$join_id.'0'] = $rule->value . '%';
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'endswith') {
+ $where .= ' (' . $DB->sql_like( $DB->sql_compare_text($data), ':endswith'.$join_id.'0' ) . ')';
+ $params['endswith'.$join_id.'0'] = '%' . $rule->value . '';
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'isempty') {
+ $where .= ' (' . 'TRIM( COALESCE('. $DB->sql_compare_text($data). ', :isempty'.$join_id.'0'.') )' . ' = '. ' :isempty'.$join_id.'1'.' )';
+ $params['isempty'.$join_id.'0'] = $DB->sql_like_escape('');
+ $params['isempty'.$join_id.'1'] = $DB->sql_like_escape('');
+ } elseif (isset($rule->comp_op) && $rule->comp_op === 'isnotempty') {
+ $where .= ' (' . 'NOT TRIM( COALESCE('. $DB->sql_compare_text($data). ', :isnotempty'.$join_id.'0'.') )' . ' = '. ' :isnotempty'.$join_id.'0'.')';
+ $params['isnotempty'.$join_id.'0'] = $DB->sql_like_escape('');
+ $params['isnotempty'.$join_id.'1'] = $DB->sql_like_escape('');
+ }
+
+ }
+ }
+ $where = preg_replace('/^1=1 AND ?/', '', $where);
+ $where = preg_replace('/^1=1 OR/', '', $where);
+ $where = preg_replace('/^1=1/', '', $where);
+
+ if($where === '') {
+ // Must be FALSE in any database without causing syntax error
+ $where = '1=0';
+ } else {
+ $where = " ( $where ) ";
+ }
+
+ return array(
+ 'select' => $select,
+ 'where' => $where,
+ 'params' => $params
+ );
+ }
+}
diff --git a/db/events.php b/db/events.php
index b35e792..04d769a 100644
--- a/db/events.php
+++ b/db/events.php
@@ -38,6 +38,14 @@
'eventname' => \core\event\course_category_deleted::class,
'callback' => \enrol_programs\local\event_observer::class . '::course_category_deleted',
],
+ [
+ 'eventname' => \core\event\user_created::class,
+ 'callback' => \enrol_programs\local\event_observer::class . '::user_created',
+ ],
+ [
+ 'eventname' => \core\event\user_updated::class,
+ 'callback' => \enrol_programs\local\event_observer::class . '::user_updated',
+ ],
[
'eventname' => \core\event\user_deleted::class,
'callback' => \enrol_programs\local\event_observer::class . '::user_deleted',
diff --git a/js/javascript.js b/js/javascript.js
new file mode 100644
index 0000000..3ff1b2b
--- /dev/null
+++ b/js/javascript.js
@@ -0,0 +1,89 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// 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 ').attr('id', 'enrol-programs-shib-rules'),
+ $textarea = $("textarea[name=datajson]").eq(0);
+ // $textarea = $("#id_customtext1");
+
+ try {
+ var shib_boolconfig = eval('(' + $textarea.val() + ')');
+ }
+ catch (e) {
+ var shib_boolconfig = {"rules": ''};
+ }
+
+ $textarea
+ .hide()
+ .parent().append($shib_rules);
+
+ $shib_rules.booleanEditor({
+ rules: shib_boolconfig.rules,
+ change: enrol_programs_updateExpr
+ });
+
+ if ($('input[name=id]').val() && $('input[name=courseid]').val()) {
+ // "Purge" button
+ enrol_programs_purge = function (msg) {
+ if (confirm(msg)) {
+ var datasend = 'courseid=' + $('input[name=courseid]').val() + '&sesskey=' + M.cfg.sesskey + '&instanceid=' + $('input[name=id]').val();
+ $.post('purge.php', datasend, function (data) {
+ alert(data);
+ });
+ }
+ }
+ // "Force" button
+ enrol_programs_force = function (msg) {
+ if (confirm(msg)) {
+ var datasend = 'courseid=' + $('input[name=courseid]').val() + '&sesskey=' + M.cfg.sesskey + '&instanceid=' + $('input[name=id]').val();
+ $.post('force.php', datasend, function (data) {
+ alert(data);
+ });
+ }
+ }
+ }
+ else {
+ $('#id_purge, #id_force').remove();
+ }
+
+ });
+
+
+ function enrol_programs_updateExpr() {
+ var expressionStr = $(this).booleanEditor('getExpression'),
+ serializedObj = $(this).booleanEditor('serialize'),
+ serializedJson = $(this).booleanEditor('serialize', {mode: 'json'});
+
+ //$("#id_customtext1").val(serializedJson);
+ $("textarea[name='datajson']").eq(0).val(serializedJson);
+ }
+
+})(jQuery)
+
diff --git a/js/javascript.min.js b/js/javascript.min.js
new file mode 100644
index 0000000..1481aaf
--- /dev/null
+++ b/js/javascript.min.js
@@ -0,0 +1,5 @@
+var enrol_programs_purge=enrol_programs_force=function(){alert('please wait for page to finish loading');};(function($){$(document).ready(function(){var $shib_rules=$('
').attr('id','enrol-programs-shib-rules'),$textarea=$("textarea[name=datajson]").eq(0);try{var shib_boolconfig=eval('('+$textarea.val()+')');}
+catch(e){var shib_boolconfig={"rules":''};}
+$textarea.hide().parent().append($shib_rules);$shib_rules.booleanEditor({rules:shib_boolconfig.rules,change:enrol_programs_updateExpr});if($('input[name=id]').val()&&$('input[name=courseid]').val()){enrol_programs_purge=function(msg){if(confirm(msg)){var datasend='courseid='+$('input[name=courseid]').val()+'&sesskey='+M.cfg.sesskey+'&instanceid='+$('input[name=id]').val();$.post('purge.php',datasend,function(data){alert(data);});}}
+enrol_programs_force=function(msg){if(confirm(msg)){var datasend='courseid='+$('input[name=courseid]').val()+'&sesskey='+M.cfg.sesskey+'&instanceid='+$('input[name=id]').val();$.post('force.php',datasend,function(data){alert(data);});}}}
+else{$('#id_purge, #id_force').remove();}});function enrol_programs_updateExpr(){var expressionStr=$(this).booleanEditor('getExpression'),serializedObj=$(this).booleanEditor('serialize'),serializedJson=$(this).booleanEditor('serialize',{mode:'json'});$("textarea[name='datajson']").eq(0).val(serializedJson);}})(jQuery)
diff --git a/js/jquery.booleanEditor.js b/js/jquery.booleanEditor.js
new file mode 100644
index 0000000..ec7dfdd
--- /dev/null
+++ b/js/jquery.booleanEditor.js
@@ -0,0 +1,359 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// 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
.
+
+/**
+ * @package enrol_programs
+ * @author Johnny Tsheke
+ * @copyright 2025, inspired from enrol_attributes
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * minifier: python3 -m jsmin jquery.booleanEditor.js >jquery.booleanEditor.min.js
+ */
+
+;
+(function ($) {
+
+ $.booleanEditor = {
+ defaults: {
+ rules: [],
+ change: null
+ },
+ paramList: M.enrol_programs.paramList,
+ operatorList: M.enrol_programs.operatorList
+ // operatorList: [
+ // {label: " = ", value: "=="},
+// {label: "=/=", value: "!="},
+ // {label: "contains", value: "contains"}
+// ]
+ };
+
+ $.fn.extend({
+
+ booleanEditor: function (options) {
+ var isMethodCall = (typeof options == 'string'), // is it a method call or booleanEditor instantiation ?
+ args = Array.prototype.slice.call(arguments, 1);
+
+
+ if (isMethodCall) switch (options) {
+ case 'serialize':
+ var mode = ( args[0] ) ? args[0].mode : '',
+ ser_obj = serialize(this);
+
+ switch (mode) {
+ case 'json':
+ return JSON.stringify(ser_obj);
+ break;
+
+ case 'object':
+ default:
+ return ser_obj;
+ }
+ break;
+ case 'getExpression':
+ return getBooleanExpression(this);
+ break;
+ default:
+ return;
+ }
+
+ settings = $.extend({}, $.booleanEditor.defaults, options);
+
+ return this.each(function () {
+ if (settings.change) {
+ $(this).data('change', settings.change)
+ }
+ $(this)
+ .addClass("enrol-programs-boolean-editor")
+ .append(createRuleList($('
'), settings.rules));
+ changed(this);
+ });
+ }
+
+ });
+
+
+ function serialize(root_elem) {
+ var ser_obj = {rules: []};
+ var group_c_op = $("select:first[name='cond-operator']", root_elem).val();
+ if (group_c_op)
+ ser_obj.cond_op = group_c_op;
+
+ $("ul:first > li", root_elem).each(function () {
+ r = $(this);
+ if (r.hasClass('group')) {
+ ser_obj['rules'].push(serialize(this));
+ } else {
+ var cond_obj = {
+ param: $("select[name='comparison-param'] option:selected", r).val(),
+ comp_op: $("select[name='comparison-operator'] option:selected", r).val(),
+ value: $("input[name='value']", r).val()
+ };
+ var cond_op = $("select[name='cond-operator']", r).val();
+ if (cond_op)
+ cond_obj.cond_op = cond_op;
+ ser_obj['rules'].push(cond_obj);
+ }
+ });
+ return ser_obj;
+ }
+
+
+ function getBooleanExpression(editor) {
+ var expression = "";
+ $("ul:first > li", editor).each(function () {
+ r = $(this);
+ var c_op = $("select[name='cond-operator']", r).val();
+ if (c_op != undefined) c_op = '
' + c_op + ' ';
+
+ if (r.hasClass('group')) {
+ expression += c_op + '
( ' + getBooleanExpression(this) + '
) ';
+ } else {
+ expression += [
+ c_op,
+ '
( ',
+ '
' + $("select[name='comparison-param'] option:selected", r).text() + ' ',
+ '
' + $("select[name='comparison-operator'] option:selected", r).val() + ' ',
+ '
' + '\'' + $("input[name='value']", r).val() + '\'' + ' ',
+ '
) '
+ ].join("");
+ }
+ });
+ return expression;
+ }
+
+
+ function changed(o) {
+ $o = $(o);
+ if (!$o.hasClass('enrol-programs-boolean-editor')) {
+ $o = $o.parents('.enrol-programs-boolean-editor').eq(0);
+ }
+ if ($o.data('change')) {
+ $o.data('change').apply($o.get(0));
+ }
+ }
+
+
+ function createRuleList(list_elem, rules) {
+
+ if (list_elem.parent("li").eq(0).hasClass("group")) {
+ console.log("inside a group");
+ return;
+ }
+
+ if (rules.length == 0) {
+ // No rules, create a new one
+ list_elem.append(getRuleConditionElement({first: true}));
+
+ } else {
+ // Read all rules
+ for (var r_idx = 0; r_idx < rules.length; r_idx++) {
+ var r = rules[r_idx];
+ r['first'] = (r_idx == 0);
+
+ // If the rule is an array, create a group of rules
+ if (r.rules && (typeof r.rules[0] == 'object')) {
+ r.group = true;
+ var rg = getRuleConditionElement(r);
+ list_elem.append(rg);
+ createRuleList($("ul:first", rg), r.rules);
+ }
+ else {
+ list_elem.append(getRuleConditionElement(r));
+ }
+ }
+ }
+
+ return list_elem;
+ };
+
+
+ /**
+ * Build the HTML code for editing a rule condition.
+ * A rule is composed of one or more rule conditions linked by boolean operators
+ */
+ function getRuleConditionElement(config) {
+ config = $.extend({},
+ {
+ first: false,
+ group: false,
+ cond_op: null,
+ param: null,
+ comp_op: null,
+ value: ''
+ },
+ config
+ );
+
+
+ // If group flag is set, wrap content with
, content is obtained by a recursive call
+ // to the function, passing a copy of config with flag group set to false
+ var cond_block_content = $('
');
+ if (config.group) {
+ cond_block_content.append('
');
+ } else {
+ cond_block_content
+ .append(makeSelectList({ // The list of parameters to be compared
+ name: 'comparison-param',
+ params: $.booleanEditor.paramList,
+ selected_value: config.param
+ }).addClass("comp-param"))
+ //.append($('
').addClass("comp-op").text('=')) // MUHC
+ .append(makeSelectList({ // The comparison operator
+ name: 'comparison-operator',
+ params: $.booleanEditor.operatorList,
+ selected_value: config.comp_op
+ }).addClass("comp-op"))
+ .append($(' ')
+ .change(function () {
+ changed(this)
+ })
+ ); // The value of the comparions
+ }
+
+ var ruleConditionElement = $(' ')
+ .addClass((config.group) ? 'group' : 'rule')
+ .append(createRuleOperatorSelect(config))
+ .append(cond_block_content)
+ .append(createButtonPannel())
+
+
+ return ruleConditionElement;
+ };
+
+
+ function createRuleOperatorSelect(config) {
+ return (config.first) ? '' :
+ makeSelectList({
+ 'name': 'cond-operator',
+ params: [
+ {label: 'AND', value: 'and'},
+ {label: 'OR', value: 'or'}
+ ],
+ selected_value: config.cond_op
+ }).addClass('sre-condition-rule-operator');
+ }
+
+
+ function createButtonPannel() {
+ var buttonPannel = $('
')
+ .append($(''+ M.util.get_string('addcondition', 'enrol_programs') +' ')
+ .click(function () {
+ addNewConditionAfter($(this).parents('li').get(0));
+ })
+ )
+ .append($(''+ M.util.get_string('addgroup', 'enrol_programs') +' ')
+ .click(function () {
+ addNewGroupAfter($(this).parents('li').get(0));
+ })
+ )
+ .append($(''+ M.util.get_string('deletecondition', 'enrol_programs') +' ')
+ .click(function () {
+ deleteCondition($(this).parents('li').eq(0));
+ })
+ );
+ $('button', buttonPannel).each(function () {
+ $(this)
+ .focus(function () {
+ this.blur()
+ })
+ .attr("title", $(this).text())
+ .wrapInner(' ');
+ });
+ return buttonPannel;
+ }
+
+
+ function makeSelectList(config) {
+ config = $.extend({},
+ {
+ name: 'list_name',
+ params: [{label: 'label', value: 'value'}],
+ selected_value: null
+ },
+ config);
+
+ var selectList = $(' ')
+ .change(function () {
+ changed(this);
+ });
+ $.each(config.params, function (i, p) {
+ var p_obj = $(' ')
+ .attr({label: p.label, value: p.value})
+ .text(p.label);
+ if (p.value == config.selected_value) {
+ p_obj.attr("selected", "selected");
+ }
+ p_obj.appendTo(selectList);
+ });
+
+ return selectList;
+ }
+
+
+ //
+ // -->> Conditions manipulation <<--
+ //
+ function addNewConditionAfter(elem, config) {
+ getRuleConditionElement(config)
+ .hide()
+ .insertAfter(elem)
+ .fadeIn("normal", function () {
+ changed(elem)
+ });
+
+ }
+
+ function addNewGroupAfter(elem, config) {
+ getRuleConditionElement({group: true})
+ .hide()
+ .insertAfter(elem)
+ .find("ul:first")
+ .append(getRuleConditionElement($.extend({}, config, {first: true})))
+ .end()
+ .fadeIn("normal", function () {
+ changed(elem)
+ });
+ }
+
+ /*
+ *
+ * Supprimer une condition : supprimer éventuellement le parent si dernier enfant,
+ * mettre à jour le parent dans tous les cas.
+ *
+ */
+ function deleteCondition(elem) {
+ if (elem.parent().parent().hasClass('enrol-programs-boolean-editor')) {
+ // Level 1
+ if (elem.siblings().length == 0) {
+ return;
+ }
+
+ } else {
+ // Higher level
+ if (elem.siblings().length == 0) {
+ // The last cond of the group, target the group itself, to be removed
+ elem = elem.parents('li').eq(0);
+ }
+ }
+ p = elem.parent();
+ elem.fadeOut("normal", function () {
+ $(this).remove();
+ $("li:first .sre-condition-rule-operator", ".enrol-programs-boolean-editor ul").remove();
+ changed(p);
+ });
+ }
+
+
+})(jQuery);
+
diff --git a/js/jquery.booleanEditor.min.js b/js/jquery.booleanEditor.min.js
new file mode 100644
index 0000000..ed32233
--- /dev/null
+++ b/js/jquery.booleanEditor.min.js
@@ -0,0 +1,24 @@
+;(function($){$.booleanEditor={defaults:{rules:[],change:null},paramList:M.enrol_programs.paramList,operatorList:M.enrol_programs.operatorList
+};$.fn.extend({booleanEditor:function(options){var isMethodCall=(typeof options=='string'),args=Array.prototype.slice.call(arguments,1);if(isMethodCall)switch(options){case'serialize':var mode=(args[0])?args[0].mode:'',ser_obj=serialize(this);switch(mode){case'json':return JSON.stringify(ser_obj);break;case'object':default:return ser_obj;}
+break;case'getExpression':return getBooleanExpression(this);break;default:return;}
+settings=$.extend({},$.booleanEditor.defaults,options);return this.each(function(){if(settings.change){$(this).data('change',settings.change)}
+$(this).addClass("enrol-programs-boolean-editor").append(createRuleList($(''),settings.rules));changed(this);});}});function serialize(root_elem){var ser_obj={rules:[]};var group_c_op=$("select:first[name='cond-operator']",root_elem).val();if(group_c_op)
+ser_obj.cond_op=group_c_op;$("ul:first > li",root_elem).each(function(){r=$(this);if(r.hasClass('group')){ser_obj['rules'].push(serialize(this));}else{var cond_obj={param:$("select[name='comparison-param'] option:selected",r).val(),comp_op:$("select[name='comparison-operator'] option:selected",r).val(),value:$("input[name='value']",r).val()};var cond_op=$("select[name='cond-operator']",r).val();if(cond_op)
+cond_obj.cond_op=cond_op;ser_obj['rules'].push(cond_obj);}});return ser_obj;}
+function getBooleanExpression(editor){var expression="";$("ul:first > li",editor).each(function(){r=$(this);var c_op=$("select[name='cond-operator']",r).val();if(c_op!=undefined)c_op=' '+c_op+' ';if(r.hasClass('group')){expression+=c_op+'( '+getBooleanExpression(this)+') ';}else{expression+=[c_op,'( ',''+$("select[name='comparison-param'] option:selected",r).text()+' ',' '+$("select[name='comparison-operator'] option:selected",r).val()+' ',''+'\''+$("input[name='value']",r).val()+'\''+' ',') '].join("");}});return expression;}
+function changed(o){$o=$(o);if(!$o.hasClass('enrol-programs-boolean-editor')){$o=$o.parents('.enrol-programs-boolean-editor').eq(0);}
+if($o.data('change')){$o.data('change').apply($o.get(0));}}
+function createRuleList(list_elem,rules){if(list_elem.parent("li").eq(0).hasClass("group")){console.log("inside a group");return;}
+if(rules.length==0){list_elem.append(getRuleConditionElement({first:true}));}else{for(var r_idx=0;r_idx ');if(config.group){cond_block_content.append('
');}else{cond_block_content.append(makeSelectList({name:'comparison-param',params:$.booleanEditor.paramList,selected_value:config.param}).addClass("comp-param"))
+.append(makeSelectList({name:'comparison-operator',params:$.booleanEditor.operatorList,selected_value:config.comp_op}).addClass("comp-op")).append($('
').change(function(){changed(this)}));}
+var ruleConditionElement=$('
').addClass((config.group)?'group':'rule').append(createRuleOperatorSelect(config)).append(cond_block_content).append(createButtonPannel())
+return ruleConditionElement;};function createRuleOperatorSelect(config){return(config.first)?'':makeSelectList({'name':'cond-operator',params:[{label:'AND',value:'and'},{label:'OR',value:'or'}],selected_value:config.cond_op}).addClass('sre-condition-rule-operator');}
+function createButtonPannel(){var buttonPannel=$('
').append($('
'+M.util.get_string('addcondition','enrol_programs')+' ').click(function(){addNewConditionAfter($(this).parents('li').get(0));})).append($('
'+M.util.get_string('addgroup','enrol_programs')+' ').click(function(){addNewGroupAfter($(this).parents('li').get(0));})).append($('
'+M.util.get_string('deletecondition','enrol_programs')+' ').click(function(){deleteCondition($(this).parents('li').eq(0));}));$('button',buttonPannel).each(function(){$(this).focus(function(){this.blur()}).attr("title",$(this).text()).wrapInner('
');});return buttonPannel;}
+function makeSelectList(config){config=$.extend({},{name:'list_name',params:[{label:'label',value:'value'}],selected_value:null},config);var selectList=$('
').change(function(){changed(this);});$.each(config.params,function(i,p){var p_obj=$('
').attr({label:p.label,value:p.value}).text(p.label);if(p.value==config.selected_value){p_obj.attr("selected","selected");}
+p_obj.appendTo(selectList);});return selectList;}
+function addNewConditionAfter(elem,config){getRuleConditionElement(config).hide().insertAfter(elem).fadeIn("normal",function(){changed(elem)});}
+function addNewGroupAfter(elem,config){getRuleConditionElement({group:true}).hide().insertAfter(elem).find("ul:first").append(getRuleConditionElement($.extend({},config,{first:true}))).end().fadeIn("normal",function(){changed(elem)});}
+function deleteCondition(elem){if(elem.parent().parent().hasClass('enrol-programs-boolean-editor')){if(elem.siblings().length==0){return;}}else{if(elem.siblings().length==0){elem=elem.parents('li').eq(0);}}
+p=elem.parent();elem.fadeOut("normal",function(){$(this).remove();$("li:first .sre-condition-rule-operator",".enrol-programs-boolean-editor ul").remove();changed(p);});}})(jQuery);
diff --git a/jsparams.php b/jsparams.php
new file mode 100644
index 0000000..9c613ad
--- /dev/null
+++ b/jsparams.php
@@ -0,0 +1,85 @@
+.
+
+/**
+ * @package enrol_programs
+ * @author Johnny Tsheke
+ * @copyright 2025, inspired from enrol_attributes
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(dirname(dirname(__FILE__))) . '/config.php');
+require_once(__DIR__.'/lib.php');
+
+header('Content-type: application/javascript');
+
+$customfieldrecords = $DB->get_records('user_info_field');
+$customfields = array();
+foreach ($customfieldrecords as $customfieldrecord) {
+ $customfields[$customfieldrecord->shortname] = $customfieldrecord->name;
+}
+
+$customfieldsrecs = \availability_profile\condition::get_custom_profile_fields(); // array of stdclass objects
+
+$customfields = array_map(function($rec){return(format_string($rec->name)?? format_string($rec->shortname));}, $customfieldsrecs);
+$standardfields = \availability_profile\condition::get_standard_profile_fields();
+
+$allprofilefields = array_merge($standardfields, $customfields);
+
+$items = [];
+
+$profilefields = explode(',', get_config('enrol_programs', 'profilefields'));
+
+foreach ($profilefields as $profilefield) {
+ if (array_key_exists($profilefield, $allprofilefields)) {
+ $items[] = array(
+ 'value' => $profilefield,
+ 'label' => $allprofilefields[$profilefield]
+ );
+ }
+}
+
+$jsvar = json_encode($items);
+
+$operators = ['isequalto', 'contains', 'doesnotcontain', 'startswith', 'endswith',
+ 'isempty', 'isnotempty'];
+$operators = array_combine($operators,
+ array_map(fn($param): string => format_string(get_string("op_$param", 'availability_profile')),
+ $operators));
+
+$opitems = [];// comparaison operators
+foreach($operators as $op => $label){
+ $opitems[] = [
+ 'value' => $op,
+ 'label' => $label
+ ];
+}
+$oplist = json_encode($opitems); // operators list
+
+//string item
+$stritems = [];
+$stritems['addcondition'] = format_string(get_string('addcondition', 'enrol_programs'));
+$stritems['addgroup'] = format_string(get_string('addgroup', 'enrol_programs'));
+$stritems['deletecondition'] = format_string(get_string('deletecondition', 'enrol_programs'));
+$strlist = json_encode($stritems);
+echo <<
These rules can only use custom user profile fields.';
$string['benefitname'] = '{$a}: Program allocation';
$string['calendarprogramend'] = '{$a} ends';
$string['calendarprogramdue'] = '{$a} is due';
@@ -103,6 +108,7 @@
$string['item'] = 'Item';
$string['itemcompletion'] = 'Program item completion';
$string['itempoints'] = 'Points';
+$string['listitem_description'] = 'The "equals" operator checks for strict equality. The "listitem" operator allows to check if a value is in a list of values. The list of values is a semicolon separated list of values. The listitem operator is case sensitive.';
$string['management'] = 'Program management';
$string['messageprovider:allocation_notification'] = 'Program allocation notification';
$string['messageprovider:approval_request_notification'] = 'Program approval request notification';
@@ -121,6 +127,8 @@
$string['movebefore'] = 'Move "{$a->item}" before "{$a->target}"';
$string['moveinto'] = 'Move "{$a->item}" into "{$a->target}"';
$string['myprograms'] = 'My programs';
+$string['no_custom_field'] = 'There seems to be no custom field. Head to user settings to add one.';
+$string['no_profile_field_selected'] = 'No profile field has been selected in the enrol_attributes plugin settings.';
$string['notification_allocation'] = 'User allocated';
$string['notification_allocation_subject'] = 'Program allocation notification';
$string['notification_allocation_body'] = 'Hello {$a->user_fullname},
@@ -234,6 +242,10 @@
$string['privacy:metadata:table:enrol_programs_src_commholds'] = 'Commerce allocation reservations';
$string['privacy:metadata:field:quantity'] = 'Quantity';
+$string['profilefields'] = 'Profile fields to be used in the selector';
+$string['profilefields_desc'] =
+ 'Which user profile fields can be used when configuring an enrolment instance?
+ If you don\'t select any attribute here, this makes the plugin moot and hence disables its use in courses.
The feature below may however still be used in this case.';
$string['program'] = 'Program';
$string['programautofix'] = 'Auto repair program';
$string['programdue'] = 'Program due';
@@ -353,6 +365,9 @@
$string['source_manual_userupload_allocated'] = 'Allocated to \'{$a}\'';
$string['source_manual_userupload_alreadyallocated'] = 'Already allocated to \'{$a}\'';
$string['source_manual_userupload_invalidprogram'] = 'Cannot allocate to \'{$a}\'';
+$string['source_profile'] = 'Automatic allocation by user profile';
+$string['source_profile_allownew'] = 'Allow allocation by user profile';
+$string['source_profile_allownew_desc'] = 'Allow adding new _auto allocation by user profile_ sources to programs';
$string['source_selfallocation'] = 'Self allocation';
$string['source_selfallocation_allocate'] = 'Sign up';
$string['source_selfallocation_allownew'] = 'Allow self allocation';
diff --git a/pix/add.png b/pix/add.png
new file mode 100644
index 0000000..6332fef
Binary files /dev/null and b/pix/add.png differ
diff --git a/pix/bullet_add.png b/pix/bullet_add.png
new file mode 100644
index 0000000..41ff833
Binary files /dev/null and b/pix/bullet_add.png differ
diff --git a/pix/delete.png b/pix/delete.png
new file mode 100644
index 0000000..08f2493
Binary files /dev/null and b/pix/delete.png differ
diff --git a/pix/group_add.png b/pix/group_add.png
new file mode 100644
index 0000000..7265a87
Binary files /dev/null and b/pix/group_add.png differ
diff --git a/settings.php b/settings.php
index 2221efe..d17ec2f 100644
--- a/settings.php
+++ b/settings.php
@@ -79,6 +79,35 @@
new lang_string('source_ecommerce_allownew', 'enrol_programs'),
new lang_string('source_ecommerce_allownew_desc', 'enrol_programs'), 0));
}
+ // allocation by user profile fields
+ $settings->add(new admin_setting_configcheckbox('enrol_programs/source_profile_allownew',
+ new lang_string('source_profile_allownew', 'enrol_programs'),
+ new lang_string('source_profile_allownew_desc', 'enrol_programs'), 1));
+ // Fields to use in the selector
+
+ $customfieldsrecs = \availability_profile\condition::get_custom_profile_fields(); // array of stdclass objects
+
+ $customfields = array_map(function($rec){return(format_string($rec->name)??
+ format_string($rec->shortname));}, $customfieldsrecs);
+ $standardfields = \availability_profile\condition::get_standard_profile_fields();
+
+ $allprofilefields = array_merge($standardfields, $customfields);
+
+ $epfields = explode(',', get_config('enrol_programs', 'profilefields')) ?? [];
+ $epfields = array_combine($epfields, $epfields);
+ $profilefieldselected = array_intersect_key($epfields, $allprofilefields) ? true : false;
+
+ if (!$profilefieldselected && !(defined('PHPUNIT_TEST') && PHPUNIT_TEST) && enrol_is_enabled('programs')) {
+ \core\notification::warning(
+ get_string('no_profile_field_selected', 'enrol_programs', $CFG->wwwroot . '/user/profile/index.php')
+ );
+ }
+
+ asort($allprofilefields);
+ $settings->add(new admin_setting_configmultiselect('enrol_programs/profilefields',
+ get_string('profilefields', 'enrol_programs'), get_string('profilefields_desc', 'enrol_programs'),
+ [], $allprofilefields));
+
}
unset($programsenabled);
diff --git a/style-profile.css b/style-profile.css
new file mode 100644
index 0000000..1265802
--- /dev/null
+++ b/style-profile.css
@@ -0,0 +1,178 @@
+/* Css for Shibboleth Rules Editor */
+
+.enrol-programs-boolean-editor {
+ width: 600px;
+}
+
+.enrol-programs-boolean-editor ul {
+ list-style: none;
+ padding-left: 10px;
+}
+
+.enrol-programs-boolean-editor li {
+ position: relative;
+ margin-bottom: 45px;
+ white-space: nowrap;
+}
+
+.enrol-programs-boolean-editor li.rule {
+ background-color: #eee;
+}
+
+.enrol-programs-boolean-editor li .sre-condition-rule-operator {
+ left: 0;
+ top: -33px;
+ position: absolute;
+}
+
+.enrol-programs-boolean-editor li .sre-condition-box {
+ padding: 10px;
+ border: solid 1px #666;
+ overflow: hidden;
+}
+
+.enrol-programs-boolean-editor li:hover > .sre-condition-box {
+ border-width: 2px;
+ padding: 9px;
+}
+
+.enrol-programs-boolean-editor li.group > .sre-condition-box {
+ border-style: dashed;
+}
+
+/*
+.enrol-attributes-boolean-editor li .button-pannel {
+ position: absolute;
+ right: 5px;
+ bottom: -15px;
+ padding: 2px;
+ line-height: 0;
+ border: 1px dotted #666;
+ background: #E2E2E2;
+ z-index: 100;
+}
+.enrol-attributes-boolean-editor li:hover > .button-pannel {
+ border-style: solid;
+}
+*/
+
+.enrol-programs-boolean-editor li .button-pannel {
+ position: absolute;
+ right: 5px;
+ bottom: -12px;
+ padding: 1px 5px;
+ height: 10px;
+ line-height: 0;
+ border: 1px solid #666;
+ border-width: 0 1px 1px;
+ background: #eee;
+}
+
+.enrol-programs-boolean-editor li.group > .button-pannel {
+ border-style: dashed;
+ background-color: #fff;
+}
+
+.enrol-programs-boolean-editor li:hover > .button-pannel {
+ border-width: 0 2px 2px;
+ padding: 1px 4px;
+}
+
+.enrol-programs-boolean-editor .button-pannel button {
+ float: left;
+ margin-top: -10px;
+}
+
+.enrol-programs-boolean-editor li:hover > .button-pannel button {
+ margin-top: -9px;
+}
+
+.enrol-programs-boolean-editor .button-pannel button {
+ border: 1px solid #999;
+ background: #ccc;
+ margin-right: 3px;
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+}
+
+.enrol-programs-boolean-editor .button-pannel button:hover {
+ border-color: #bbb #999 #666;
+}
+
+.enrol-programs-boolean-editor .button-pannel button:active {
+ border-color: #c00;
+}
+
+.enrol-programs-boolean-editor .button-pannel button:last-child {
+ margin-right: 0;
+}
+
+.enrol-programs-boolean-editor .button-pannel button span {
+ display: none;
+}
+
+.enrol-programs-boolean-editor .button-pannel .button-add-cond {
+ /*background-image: url([[pix:enrol_programs|add]]);*/
+ background-image: url(pix/add.png);
+}
+
+.enrol-programs-boolean-editor .button-pannel .button-add-group {
+ /*background-image: url([[pix:enrol_programs|group_add]]);*/
+ background-image: url(pix/group_add.png);
+}
+
+.enrol-programs-boolean-editor .button-pannel .button-del-cond {
+ /* background-image: url([[pix:enrol_programs|delete]]);*/
+ background-image: url(pix/delete.png);
+}
+
+#shib-expression .expr {
+ margin-left: 10px;
+ font-family: monospace;
+ font-size: 12px;
+ line-height: 16px;
+ width: 600px;
+ background-color: #eee;
+}
+
+#shib-expression .expr span {
+ margin-right: 2px;
+}
+
+#shib-expression .expr .cond-op {
+ color: #c00;
+}
+
+#shib-expression .expr .group-op {
+ color: #666;
+ font-size: 12px;
+}
+
+#shib-expression .expr .group-group {
+ font-size: 16px;
+ color: #666;
+}
+
+#shib-expression .expr .comp-param {
+ color: blue;
+}
+
+#shib-expression .expr .comp-op {
+ color: #0a0;
+}
+
+#shib-expression .expr .comp-val {
+ color: purple;
+}
+
+.enrol-programs-boolean-editor .button-pannel button {
+ -moz-border-radius: 4px;
+ -webkit-border-radius: 4px;
+ border-radius: 4px;
+}
+
+span.comp-op {
+ padding: 0 10px;
+}
+