From c06da88cf82e38e603028e597f393fe4cafe711f Mon Sep 17 00:00:00 2001 From: James R Date: Sat, 14 Jul 2012 22:19:55 +1000 Subject: [PATCH] Multiple Enrol Meta --- enrol/meta/addmultiple.php | 113 ++++++++++++++ enrol/meta/addmultiple_form.php | 82 ++++++++++ enrol/meta/lang/en/enrol_meta.php | 8 +- enrol/meta/lib.php | 4 + enrol/meta/module.js | 248 ++++++++++++++++++++++++++++++ enrol/meta/search.php | 78 ++++++++++ enrol/meta/settings.php | 5 + 7 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 enrol/meta/addmultiple.php create mode 100644 enrol/meta/addmultiple_form.php create mode 100644 enrol/meta/module.js create mode 100644 enrol/meta/search.php diff --git a/enrol/meta/addmultiple.php b/enrol/meta/addmultiple.php new file mode 100644 index 0000000000000..a044435d8babb --- /dev/null +++ b/enrol/meta/addmultiple.php @@ -0,0 +1,113 @@ +. + +/** + * Adds new instance of enrol_meta to specified course. + * + * @package enrol + * @subpackage meta + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../../config.php'); +require_once("$CFG->dirroot/enrol/meta/addmultiple_form.php"); +require_once("$CFG->dirroot/enrol/meta/locallib.php"); + +$id = required_param('id', PARAM_INT); // course id + +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); +$context = get_context_instance(CONTEXT_COURSE, $course->id, MUST_EXIST); + +$PAGE->set_url('/enrol/meta/addmultiple.php', array('id'=>$course->id)); +$pageurl = new moodle_url('/enrol/meta/addmultiple.php', array('id'=>$course->id)); + +$PAGE->set_pagelayout('admin'); + +navigation_node::override_active_url(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); + +require_login($course); +require_capability('moodle/course:enrolconfig', $context); + +$searchtext = optional_param('links_searchtext', '', PARAM_RAW); + +if (optional_param('links_clearbutton', 0, PARAM_RAW) && confirm_sesskey()) { + redirect($pageurl); +} + + +$enrol = enrol_get_plugin('meta'); +if (!$enrol->get_newinstance_link($course->id)) { + redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); +} + +//$mform = new enrol_meta_addmultiple_form(NULL, $course); + +// row limit unlimited if not set in config +$rowlimit = $enrol->get_config('addmultiple_rowlimit', 0); + +function get_valid_courses($courses, $course) { + $available_courses = array(); + foreach ($courses as $c) { + if ($c->id == SITEID or $c->id == $course->id or isset($existing[$c->id])) { + continue; + } + $coursecontext = get_context_instance(CONTEXT_COURSE, $c->id); + if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) { + continue; + } + $available_courses[$c->id] = format_string($c->fullname) . ' ['.$c->shortname.']'; + } + return $available_courses; +} + +$existing = $DB->get_records('enrol', array('enrol'=>'meta', 'courseid'=>$course->id), '', 'customint1, id'); +if (!empty($searchtext)) { + $courses = get_courses_search(explode(" ", $searchtext), 'shortname ASC', 0, 99999, $rowlimit); + $availablecourses = get_valid_courses($courses, $course); +} else { + $rs = $DB->get_recordset('course', array('visible' => 1), 'shortname ASC', 'id, fullname, shortname', 0, $rowlimit); + $availablecourses = get_valid_courses($rs, $course); + $rs->close(); +} + +if (!$enrol->get_newinstance_link($course->id)) { + redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id, ''))); +} + +$mform = new enrol_meta_addmultiple_form($pageurl->out(false), array('course'=>$course, 'availablecourses'=>$availablecourses)); +if ($mform->is_cancelled()) { + redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); + +} else if ($data = $mform->get_data()) { + if (!empty($data->links)) { //todo + foreach ($data->links as $link) { + $eid = $enrol->add_instance($course, array('customint1'=>$link)); + } + enrol_meta_sync($course->id); + } + redirect(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); +} + +$PAGE->set_heading($course->fullname); +$PAGE->set_title(get_string('pluginname', 'enrol_meta')); + +echo $OUTPUT->header(); + +$mform->display(); + +echo $OUTPUT->footer(); diff --git a/enrol/meta/addmultiple_form.php b/enrol/meta/addmultiple_form.php new file mode 100644 index 0000000000000..e815cb40e0510 --- /dev/null +++ b/enrol/meta/addmultiple_form.php @@ -0,0 +1,82 @@ +. + +/** + * Adds instance form + * + * @package enrol + * @subpackage meta + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +require_once("$CFG->libdir/formslib.php"); + +class enrol_meta_addmultiple_form extends moodleform { + protected $course; + private static $jsmodule = array( + 'name' => 'course_selector', + 'fullpath' => '/enrol/meta/module.js', + 'requires' => array('node', 'event-custom', 'datasource', 'json')); + + function definition() { + global $CFG, $DB, $PAGE; + + $mform = $this->_form; + $course = $this->_customdata['course']; + $availablecourses = $this->_customdata['availablecourses']; + $this->course = $course; + + + $mform->addElement('header','general', get_string('pluginname', 'enrol_meta')); + $mform->addElement('select', 'links', get_string('linkcourses', 'enrol_meta'), $availablecourses, array('size'=>12, 'multiple'=>true)); + $mform->addRule('links', get_string('required'), 'required', null, 'client'); + + $searchgroup = array(); + $searchgroup[] = &$mform->createElement('text', 'links_searchtext'); + $searchgroup[] = &$mform->createElement('submit', 'links_searchbutton', get_string('search')); + $mform->registerNoSubmitButton('links_searchbutton'); + $searchgroup[] = &$mform->createElement('submit', 'links_clearbutton', get_string('clear')); + $mform->registerNoSubmitButton('links_clearbutton'); + $mform->addGroup($searchgroup, 'searchgroup', get_string('search') , array(' '), false); + + $mform->addElement('hidden', 'id', null); + $mform->setType('id', PARAM_INT); + + $this->add_action_buttons(true, get_string('add')); + + $this->set_data(array('id'=>$course->id)); + $PAGE->requires->js_init_call('M.core_enrol.init_course_selector', array('links', $course->id), true, self::$jsmodule); + + } + + function validation($data, $files) { + global $DB, $CFG; + $errors = array(); + // TODO: this is duplicated here because it may be necessary one we implement ajax course selection element + + $errors = parent::validation($data, $files); + if (!isset($data['links'])){ + $errors['links'] = get_string('required'); + } + // TODO: context and capability checks maybe + return $errors; + } +} diff --git a/enrol/meta/lang/en/enrol_meta.php b/enrol/meta/lang/en/enrol_meta.php index 4f64889de3478..6fabae91bdd43 100644 --- a/enrol/meta/lang/en/enrol_meta.php +++ b/enrol/meta/lang/en/enrol_meta.php @@ -25,6 +25,8 @@ */ $string['linkedcourse'] = 'Link course'; +$string['linkcourses'] = 'Link courses'; +$string['linkmultiplecourses'] = 'Link multiple courses'; $string['meta:config'] = 'Configure meta enrol instances'; $string['meta:selectaslinked'] = 'Select course as meta linked'; $string['meta:unenrol'] = 'Unenrol suspended users'; @@ -33,4 +35,8 @@ $string['pluginname'] = 'Course meta link'; $string['pluginname_desc'] = 'Course meta link enrolment plugin synchronises enrolments and roles in two different courses.'; $string['syncall'] = 'Synchronise all enrolled users'; -$string['syncall_desc'] = 'If enabled all enrolled users are synchronised even if they have no role in parent course, if disabled only users that have at least one synchronised role are enrolled in child course.'; \ No newline at end of file +$string['syncall_desc'] = 'If enabled all enrolled users are synchronised even if they have no role in parent course, if disabled only users that have at least one synchronised role are enrolled in child course.'; +$string['addmultiple'] = 'Allow selection of Multiple linked classes'; +$string['addmultiple_desc'] = 'Allow selection of Multiple linked classes.'; +$string['addmultiple_rowlimit'] = 'Max number of courses to show.'; +$string['addmultiple_rowlimit_desc'] = 'Set the max number of courses to show. Set to 0 for unlimited'; diff --git a/enrol/meta/lib.php b/enrol/meta/lib.php index c400bc3263032..1070d8a1c6af8 100644 --- a/enrol/meta/lib.php +++ b/enrol/meta/lib.php @@ -63,6 +63,10 @@ public function get_newinstance_link($courseid) { return NULL; } // multiple instances supported - multiple parent courses linked + $plugin = enrol_get_plugin('meta'); + if ($plugin->get_config('addmultiple')) { + return new moodle_url('/enrol/meta/addmultiple.php', array('id'=>$courseid)); + } return new moodle_url('/enrol/meta/addinstance.php', array('id'=>$courseid)); } diff --git a/enrol/meta/module.js b/enrol/meta/module.js new file mode 100644 index 0000000000000..fb77abfede203 --- /dev/null +++ b/enrol/meta/module.js @@ -0,0 +1,248 @@ +/** + * JavaScript for course selector. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package courseselector + */ +// Define the core_enrol namespace if it has not already been defined +M.core_enrol = M.core_enrol || {}; +// Define a selectors array for against namespace +M.core_enrol.course_selectors = []; +/** + * Retrieves an instantiated course selector or null if there isn't one by the requested name + * @param {string} name The name of the selector to retrieve + * @return bool + */ +M.core_enrol.get_course_selector = function (name) { + return this.course_selectors[name] || null; +}; + +/** + * Initialise a new course selector. + * + * @param {YUI} Y The YUI3 instance + * @param {string} name the control name/id. + * @param {string} courseid the courseid. + * @param {string} lastsearch The last search that took place + */ +M.core_enrol.init_course_selector = function (Y, name, courseid, lastsearch) { + // Creates a new course_selector object + var course_selector = { + courseid : courseid, + /** This id/name used for this control in the HTML. */ + name : name, + /** Number of seconds to delay before submitting a query request */ + querydelay : 0.5, + /** The input element that contains the search term. */ + searchfield : Y.one('#id_'+name + '_searchtext'), + /** The clear button. */ + clearbutton : null, + /** The select element that contains the list. */ + listbox : Y.one('#id_'+name), + /** Used to hold the timeout id of the timeout that waits before doing a search. */ + timeoutid : null, + /** The last string that we searched for, so we can avoid unnecessary repeat searches. */ + lastsearch : lastsearch, + /** Whether any options where selected last time we checked. Used by + * handle_selection_change to track when this status changes. */ + selectionempty : true, + /** + * Initialises the course selector object + * @constructor + */ + init : function() { + // Hide the search button and replace it with a label. + + var searchbutton = Y.one('#id_'+this.name + '_searchbutton'); + //this.searchfield.insert(Y.Node.create(''), this.searchfield); + searchbutton.remove(); + var clearbutton = Y.one('#id_'+this.name + '_clearbutton'); + //clearbutton.remove(); + // Hook up the event handler for when the search text changes. + this.searchfield.on('keyup', this.handle_keyup, this); + + // Hook up the event handler for when the selection changes. + this.listbox.on('keyup', this.handle_selection_change, this); + this.listbox.on('click', this.handle_selection_change, this); + this.listbox.on('change', this.handle_selection_change, this); + // Define our custom event. + //this.selectionempty = this.is_selection_empty(); + + // Replace the Clear submit button with a clone that is not a submit button. + //var clearbtn = Y.one('#'+this.name + '_clearbutton'); + //this.clearbutton = Y.Node.create(''); + //clearbtn.replace(Y.Node.getDOMNode(this.clearbutton)); + //this.clearbutton.set('id',+this.name+"_clearbutton"); + //this.clearbutton.on('click', this.handle_clear, this); + + this.send_query(false); + }, + /** + * Key up hander for the search text box. + * @param {Y.Event} e the keyup event. + */ + handle_keyup : function(e) { + // Trigger an ajax search after a delay. + this.cancel_timeout(); + this.timeoutid = Y.later(this.querydelay*1000, e, function(obj){obj.send_query(false)}, this); + + // Enable or diable the clear button. + //this.clearbutton.set('disabled', (this.get_search_text() == '')); + + // If enter was pressed, prevent a form submission from happening. + if (e.keyCode == 13) { + e.halt(); + } + }, + /** + * Handles when the selection has changed. If the selection has changed from + * empty to not-empty, or vice versa, then fire the event handlers. + */ + handle_selection_change : function() { + var isselectionempty = this.is_selection_empty(); + if (isselectionempty !== this.selectionempty) { + this.fire('course_selector:selectionchanged', isselectionempty); + } + this.selectionempty = isselectionempty; + }, + + /** + * Click handler for the clear button.. + */ + handle_clear : function() { + this.searchfield.set('value', ''); + //this.clearbutton.set('disabled',true); + this.send_query(false); + }, + /** + * Fires off the ajax search request. + */ + send_query : function(forceresearch) { + // Cancel any pending timeout. + this.cancel_timeout(); + + var value = this.get_search_text(); + + this.searchfield.removeClass('error'); + if (this.lastsearch == value && !forceresearch) { + return; + } + + Y.io(M.cfg.wwwroot + '/enrol/meta/search.php', { + method: 'POST', + data: 'sesskey='+M.cfg.sesskey+'&searchtext='+value+'&id='+this.courseid, + on: { + success:this.handle_response, + failure:this.handle_failure + }, + context:this + }); + + this.lastsearch = value; + this.listbox.setStyle('background','url(' + M.util.image_url('i/loading', 'moodle') + ') no-repeat center center'); + }, + /** + * Handle what happens when we get some data back from the search. + * @param {int} requestid not used. + * @param {object} response the list of courses that was returned. + */ + handle_response : function(requestid, response) { + try { + this.listbox.setStyle('background',''); + var data = Y.JSON.parse(response.responseText); + this.output_list(data); + } catch (e) { + this.handle_failure(); + } + }, + /** + * Handles what happens when the ajax request fails. + */ + handle_failure : function() { + this.listbox.setStyle('background',''); + this.searchfield.addClass('error'); + // If we are in developer debug mode, output a link to help debug the failure. + if (M.cfg.developerdebug) { + this.searchfield.insert(Y.Node.create('Ajax call failed. Click here to try the search call directly.')); + } + }, + output_list : function(data) { + var courses = {}; + this.listbox.all('option').each(function(option){ + if (option.get('selected')) { + courses[option.get('value')] = { + id : option.get('value'), + name : option.get('innerText') || option.get('textContent'), + disabled: option.get('disabled') + } + } + option.remove(); + }, this); + + count = 0; + for (var courseid in data.results) { + var course = data.results[courseid]; + var option = Y.Node.create(''); + + if (course.disabled) { + option.set('disabled', true); + } else if (courses===true || courses[courseid]) { + option.set('selected', true); + } else { + option.set('selected', false); + } + count++; + this.listbox.append(option); + + } + this.handle_selection_change(); + + }, + /** + * Replace + * @param {string} str + * @param {string} search The search term + * @return string + */ + insert_search_into_str : function(str, search) { + return str.replace("%%SEARCHTERM%%", search); + }, + /** + * Gets the search text + * @return String the value to search for, with leading and trailing whitespace trimmed. + */ + get_search_text : function() { + return this.searchfield.get('value').toString().replace(/^ +| +$/, ''); + }, + /** + * Returns true if the selection is empty (nothing is selected) + * @return Boolean check all the options and return whether any are selected. + */ + is_selection_empty : function() { + var selection = false; + this.listbox.all('option').each(function(){ + if (this.get('selected')) { + selection = true; + } + }); + return !(selection); + }, + /** + * Cancel the search delay timeout, if there is one. + */ + cancel_timeout : function() { + if (this.timeoutid) { + clearTimeout(this.timeoutid); + this.timeoutid = null; + } + } + }; + // Augment the course selector with the EventTarget class so that we can use + // custom events + Y.augment(course_selector, Y.EventTarget, null, null, {}); + // Initialise the course selector + course_selector.init(); + // Store the course selector so that it can be retrieved + this.course_selectors[name] = course_selector; + // Return the course selector + return course_selector; +}; \ No newline at end of file diff --git a/enrol/meta/search.php b/enrol/meta/search.php new file mode 100644 index 0000000000000..38695244a9331 --- /dev/null +++ b/enrol/meta/search.php @@ -0,0 +1,78 @@ +. + +define('AJAX_SCRIPT', true); +require_once(dirname(__FILE__) . '/../../config.php'); + + +$PAGE->set_context(get_system_context()); +$PAGE->set_url('/enrol/meta/search.php'); + +header('Content-type: application/json; charset=utf-8'); + +// Check access. +if (!isloggedin()) { + print_error('mustbeloggedin'); +} +if (!confirm_sesskey()) { + $error = array('error'=>get_string('invalidsesskey', 'error')); + die(json_encode($error)); +} + +$id = required_param('id', PARAM_INT);// course id +$searchtext = required_param('searchtext', PARAM_RAW);// Get the search parameter. + +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); +$context = get_context_instance(CONTEXT_COURSE, $course->id, MUST_EXIST); + +// row limit unlimited if not set in config +$enrol = enrol_get_plugin('meta'); +$rowlimit = $enrol->get_config('addmultiple_rowlimit', 0); + +$existing = $DB->get_records('enrol', array('enrol'=>'meta', 'courseid'=>$id), '', 'customint1, id'); + +function get_valid_courses($courses, $cid) { + + $valid_courses = array(); + foreach ($courses as $c) { + if (isset($existing[$c->id]) || $c->id == $cid || $c->id == SITEID) { + continue; + } + $coursecontext = get_context_instance(CONTEXT_COURSE, $c->id); + if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) { + continue; + } + $valid = new stdClass(); + $valid->id = $c->id; + $valid->shortname = $c->shortname; + $valid->fullname = $c->fullname; + // omitting $c->id from the key preserves query order (shortname ASC) + $valid_courses[] = $valid; + } + return $valid_courses; +} + +if (!empty($searchtext)) { + $courses = get_courses_search(explode(" ", $searchtext), 'shortname ASC', 0, 99999, $rowlimit); + $results = get_valid_courses($courses, $id); +} else { + $rs = $DB->get_recordset('course', array('visible' => 1), 'shortname ASC', 'id, shortname, fullname', 0, $rowlimit); + $results = get_valid_courses($rs, $id); + $rs->close(); +} + +echo json_encode(array('results'=>$results)); diff --git a/enrol/meta/settings.php b/enrol/meta/settings.php index c88ea264c9388..13483ddefe0db 100644 --- a/enrol/meta/settings.php +++ b/enrol/meta/settings.php @@ -38,6 +38,11 @@ } $settings->add(new admin_setting_configmultiselect('enrol_meta/nosyncroleids', get_string('nosyncroleids', 'enrol_meta'), get_string('nosyncroleids_desc', 'enrol_meta'), array(), $allroles)); + $settings->add(new admin_setting_configcheckbox('enrol_meta/addmultiple', + get_string('addmultiple', 'enrol_meta'), get_string('addmultiple_desc', 'enrol_meta'), 1)); + $settings->add(new admin_setting_configtext('enrol_meta/addmultiple_rowlimit', + get_string('addmultiple_rowlimit', 'enrol_meta'), get_string('addmultiple_rowlimit_desc', 'enrol_meta'), 250, PARAM_INT)); + $settings->add(new admin_setting_configcheckbox('enrol_meta/syncall', get_string('syncall', 'enrol_meta'), get_string('syncall_desc', 'enrol_meta'), 1)); $options = array(