From 7d87592f5dfe822260de4e357a02cb358210659e Mon Sep 17 00:00:00 2001 From: Johnny Tsheke Date: Thu, 17 Apr 2025 13:03:12 -0400 Subject: [PATCH] Allocation by user profile attributes --- classes/local/allocation.php | 2 + classes/local/event_observer.php | 17 + classes/local/form/source_profile_edit.php | 107 ++++ classes/local/source/profile.php | 548 +++++++++++++++++++++ db/events.php | 8 + js/javascript.js | 89 ++++ js/javascript.min.js | 5 + js/jquery.booleanEditor.js | 359 ++++++++++++++ js/jquery.booleanEditor.min.js | 24 + jsparams.php | 85 ++++ lang/en/enrol_programs.php | 15 + pix/add.png | Bin 0 -> 733 bytes pix/bullet_add.png | Bin 0 -> 286 bytes pix/delete.png | Bin 0 -> 715 bytes pix/group_add.png | Bin 0 -> 457 bytes settings.php | 29 ++ style-profile.css | 178 +++++++ 17 files changed, 1466 insertions(+) create mode 100644 classes/local/form/source_profile_edit.php create mode 100644 classes/local/source/profile.php create mode 100644 js/javascript.js create mode 100644 js/javascript.min.js create mode 100644 js/jquery.booleanEditor.js create mode 100644 js/jquery.booleanEditor.min.js create mode 100644 jsparams.php create mode 100644 pix/add.png create mode 100644 pix/bullet_add.png create mode 100644 pix/delete.png create mode 100644 pix/group_add.png create mode 100644 style-profile.css 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', ''); + + $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 . + +/** + * @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 javascript.js >javascript.min.js + */ + +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); + // $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($('') + .click(function () { + addNewConditionAfter($(this).parents('li').get(0)); + }) + ) + .append($('') + .click(function () { + addNewGroupAfter($(this).parents('li').get(0)); + }) + ) + .append($('') + .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($('').click(function(){addNewConditionAfter($(this).parents('li').get(0));})).append($('').click(function(){addNewGroupAfter($(this).parents('li').get(0));})).append($('').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 <<
            +
            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 0000000000000000000000000000000000000000..6332fefea4be19eeadf211b0b202b272e8564898 GIT binary patch literal 733 zcmV<30wVp1P)9VHk(~TedF+gQSL8D5xnVSSWAVY>J9b+m>@{iq7_KE}go~11+5s4;8hc+i0Xa zI1j@EX5!S+Me6HNqKzU5YQwL;-W5$p%ZMKMeR<%zp69-~?<4?8|C8S?bklXr4v&Ov zb&06v2|-x?qB`90yn>Qi%Sh2^G4n)$ZdyvTPf9}1)_buUT7>`e2G&2VU@~Bb(o+Mz zi4)>IxlSY${Dj4k={-9RzU^W5g9|2V5RZ2ZulL9s2xQbZ@r6eP9Ra5u(s|C0Nj#&4>wTSkb?%#=9?@ z^oxDy-O@tyN{L@by(WWvQ3%CyEu8x{+#Jb4-h&K9Owi)2pgg+heWDyked|3R$$kL@A z#sp1v-r+=G4B8D6DqsDH0@7OztA7aT9qc1Py{()w`m``?Y0&gi2=ROcc-9+nU^I6< zT=e_Y=vSnG@?3Ue{BW5ONFttcE!R-R_W4O01|0-|K-YNXLo2`4Qv z`r1LxR6#yf3FB%T95gJnaKKivA~Z}S9A(ZxEDK}O3T04USJ P00000NkvXXu0mjf^IS-S literal 0 HcmV?d00001 diff --git a/pix/bullet_add.png b/pix/bullet_add.png new file mode 100644 index 0000000000000000000000000000000000000000..41ff8335b06be000bc6912c2bbd9d3c572c8a9da GIT binary patch literal 286 zcmV+(0pb3MP)C4}Mrzlg<+1Y8PEBfUp0jJpx4B>@E+cy3`^(Gw`Mf+2&yxZm<$to~Vpgvg&QKNR z_f#1(r6svZt%iF?s+n<8X?B&!h3g9Dbb8_=MX}!;HiQSAh`bp^WMl~Z-44teO7W_Y zV4thSL{h;rJY7!l3%5J4H1!tIzB`Dv+YxO(haWeausGZYkI8^hWj6mzo=L0{%;yxzh{5!Htr?51 zvG|W62MzC8BZ76hRpCyO2zOn<%e)K>NHge!-~)Ap33OdWw6hsLYbCxGNt0%wk_2z7 zfyYvXheSG)5HRK1VB~%mq7Dmurw#bi@hEcOr3&G1ZiF*$M=&9nB#VNf&Q^r$4G5kp zTURh&s)E0%5&hyVD}sp<72~zmAY`Y(9aqO6CXF%=zFHGzO-A&I(pE}v70YQxCPJ{Y z4L+?5-crdLn3ZRPEs!A4ehEY3ZRpL~w9>@aMN+{F4dI@v&>(QDHQum!mG~E^$OS8l z!7?%Uwib*ROP67Hw`ika)gX-(8Ia`-u_IEhxG7U<13kSsMW+$lbb2dUMm5p6pa}cjgA+U$^mJ^AjD?&bdi)8~y+Q002ovPDHLkV1g8IMc@Dc literal 0 HcmV?d00001 diff --git a/pix/group_add.png b/pix/group_add.png new file mode 100644 index 0000000000000000000000000000000000000000..7265a87428dc49b33aa88985204bb865c065775b GIT binary patch literal 457 zcmV;)0XF`LP)S6 zy9c$<0@(l^D1Dg-17MaYAse7XMg%H?2o^$7Trf4N=0C&#vLC;`^M3yJ@$=J%&let= zeG{$*X24f$@t-*%s6kRjpx#*1gh56~m0|zY4ZJ6~-{z>e3f#Q2`@;!jFWf_l4LJDv z<8!%zhCahzroRmOlCBJY8Gkca=vpxdsc@>m^e`dCFGg6r|MHfDm63HyF@}!aiC})mu0)2ruP!nC|HtqL-31`cv!O{9 z?u~!{|Mi}@bc}%w7*v}tPh((XWMMdR;UL4`-~VRA)G&fVnvszaIjz5fr3a9lMUGSl zkeLOadd(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; +} +