diff --git a/enrol/manual/cli/sync.php b/enrol/manual/cli/sync.php index c18488e366c..acdb2ebe214 100644 --- a/enrol/manual/cli/sync.php +++ b/enrol/manual/cli/sync.php @@ -42,7 +42,7 @@ if ($unrecognized) { if ($options['help']) { $help = - "Execute manual enrolments expiration sync. + "Execute manual enrolments expiration sync and send notifications. Options: -v, --verbose Print verbose progress information @@ -62,4 +62,6 @@ $plugin = enrol_get_plugin('manual'); $result = $plugin->sync(null, $verbose); +$plugin->send_notifications($verbose); + exit($result); diff --git a/enrol/manual/db/messages.php b/enrol/manual/db/messages.php new file mode 100644 index 00000000000..a576217c0e4 --- /dev/null +++ b/enrol/manual/db/messages.php @@ -0,0 +1,29 @@ +. + +/** + * Defines message providers for manual enrolments. + * + * @package enrol_manual + * @copyright 2012 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$messageproviders = array ( + + 'expiry_notification' => array(), + +); diff --git a/enrol/manual/db/upgrade.php b/enrol/manual/db/upgrade.php new file mode 100644 index 00000000000..6bc2602995e --- /dev/null +++ b/enrol/manual/db/upgrade.php @@ -0,0 +1,44 @@ +. + +/** + * This file keeps track of upgrades to the manual enrolment plugin + * + * @package enrol_manual + * @copyright 2012 Petr Skoda {@link http://skodak.org + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +function xmldb_enrol_manual_upgrade($oldversion) { + global $CFG, $DB, $OUTPUT; + + $dbman = $DB->get_manager(); + + // Moodle v2.3.0 release upgrade line + // Put any upgrade step following this + + if ($oldversion < 2012100702) { + // Set default expiry threshold to 1 day. + $DB->execute("UPDATE {enrol} SET expirythreshold = 86400 WHERE enrol = 'manual' AND expirythreshold = 0"); + upgrade_plugin_savepoint(true, 2012100702, 'enrol', 'manual'); + } + + return true; +} + + diff --git a/enrol/manual/edit.php b/enrol/manual/edit.php index c4ff53d5049..d6d01e2d123 100644 --- a/enrol/manual/edit.php +++ b/enrol/manual/edit.php @@ -52,36 +52,64 @@ if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol $plugin->delete_instance($del); } } + // Merge these two settings to one value for the single selection element. + if ($instance->notifyall and $instance->expirynotify) { + $instance->expirynotify = 2; + } + unset($instance->notifyall); + } else { require_capability('moodle/course:enrolconfig', $context); // No instance yet, we have to add new instance. navigation_node::override_active_url(new moodle_url('/enrol/instances.php', array('id'=>$course->id))); $instance = new stdClass(); - $instance->id = null; - $instance->courseid = $course->id; + $instance->id = null; + $instance->courseid = $course->id; + $instance->expirynotify = $plugin->get_config('expirynotify'); + $instance->expirythreshold = $plugin->get_config('expirythreshold'); } -$mform = new enrol_manual_edit_form(NULL, array($instance, $plugin, $context)); +$mform = new enrol_manual_edit_form(null, array($instance, $plugin, $context)); if ($mform->is_cancelled()) { redirect($return); } else if ($data = $mform->get_data()) { + if ($data->expirynotify == 2) { + $data->expirynotify = 1; + $data->notifyall = 1; + } else { + $data->notifyall = 0; + } + if (!$data->expirynotify) { + // Keep previous/default value of disabled expirythreshold option. + $data->expirythreshold = $instance->expirythreshold; + } if ($instance->id) { - $reset = ($instance->status != $data->status); + $instance->roleid = $data->roleid; + $instance->enrolperiod = $data->enrolperiod; + $instance->expirynotify = $data->expirynotify; + $instance->notifyall = $data->notifyall; + $instance->expirythreshold = $data->expirythreshold; + $instance->timemodified = time(); - $instance->status = $data->status; - $instance->enrolperiod = $data->enrolperiod; - $instance->roleid = $data->roleid; - $instance->timemodified = time(); $DB->update_record('enrol', $instance); - if ($reset) { + // Use standard API to update instance status. + if ($instance->status != $data->status) { + $instance = $DB->get_record('enrol', array('id'=>$instance->id)); + $plugin->update_status($instance, $data->status); $context->mark_dirty(); } } else { - $fields = array('status'=>$data->status, 'enrolperiod'=>$data->enrolperiod, 'roleid'=>$data->roleid); + $fields = array( + 'status' => $data->status, + 'roleid' => $data->roleid, + 'enrolperiod' => $data->enrolperiod, + 'expirynotify' => $data->expirynotify, + 'notifyall' => $data->notifyall, + 'expirythreshold' => $data->expirythreshold); $plugin->add_instance($course, $fields); } diff --git a/enrol/manual/edit_form.php b/enrol/manual/edit_form.php index eb210991167..23b7f4e38d7 100644 --- a/enrol/manual/edit_form.php +++ b/enrol/manual/edit_form.php @@ -42,10 +42,6 @@ class enrol_manual_edit_form extends moodleform { $mform->addHelpButton('status', 'status', 'enrol_manual'); $mform->setDefault('status', $plugin->get_config('status')); - $mform->addElement('duration', 'enrolperiod', get_string('defaultperiod', 'enrol_manual'), array('optional' => true, 'defaultunit' => 86400)); - $mform->setDefault('enrolperiod', $plugin->get_config('enrolperiod')); - $mform->addHelpButton('enrolperiod', 'defaultperiod', 'enrol_manual'); - if ($instance->id) { $roles = get_default_enrol_roles($context, $instance->roleid); } else { @@ -54,10 +50,34 @@ class enrol_manual_edit_form extends moodleform { $mform->addElement('select', 'roleid', get_string('defaultrole', 'role'), $roles); $mform->setDefault('roleid', $plugin->get_config('roleid')); + $mform->addElement('duration', 'enrolperiod', get_string('defaultperiod', 'enrol_manual'), array('optional' => true, 'defaultunit' => 86400)); + $mform->setDefault('enrolperiod', $plugin->get_config('enrolperiod')); + $mform->addHelpButton('enrolperiod', 'defaultperiod', 'enrol_manual'); + + $options = array(0 => get_string('no'), 1 => get_string('expirynotifyteacher', 'enrol_manual'), 2 => get_string('expirynotifyall', 'enrol_manual')); + $mform->addElement('select', 'expirynotify', get_string('expirynotify', 'enrol_manual'), $options); + $mform->addHelpButton('expirynotify', 'expirynotify', 'enrol_manual'); + + $mform->addElement('duration', 'expirythreshold', get_string('expirythreshold', 'enrol_manual'), array('optional' => false, 'defaultunit' => 86400)); + $mform->addHelpButton('expirythreshold', 'expirythreshold', 'enrol_manual'); + $mform->disabledIf('expirythreshold', 'expirynotify', 'eq', 0); + $mform->addElement('hidden', 'courseid'); $this->add_action_buttons(true, ($instance->id ? null : get_string('addinstance', 'enrol'))); $this->set_data($instance); } + + function validation($data, $files) { + global $DB; + + $errors = parent::validation($data, $files); + + if ($data['expirynotify'] > 0 and $data['expirythreshold'] < 86400) { + $errors['expirythreshold'] = get_string('errorthresholdlow', 'enrol_manual'); + } + + return $errors; + } } diff --git a/enrol/manual/lang/en/enrol_manual.php b/enrol/manual/lang/en/enrol_manual.php index 7abb510d69a..b4494f28e3c 100644 --- a/enrol/manual/lang/en/enrol_manual.php +++ b/enrol/manual/lang/en/enrol_manual.php @@ -1,5 +1,4 @@ . /** - * Strings for component 'enrol_manual', language 'en', branch 'MOODLE_20_STABLE' + * Strings for component 'enrol_manual', language 'en'. * - * @package enrol - * @subpackage manual + * @package enrol_manual * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -37,13 +35,35 @@ $string['editenrolment'] = 'Edit enrolment'; $string['editselectedusers'] = 'Edit selected user enrolments'; $string['enrolledincourserole'] = 'Enrolled in "{$a->course}" as "{$a->role}"'; $string['enrolusers'] = 'Enrol users'; +$string['errorthresholdlow'] = 'Notification threshold must be at least 1 day.'; $string['expiredaction'] = 'Enrolment expiration action'; $string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.'; +$string['expirymessageenrollersubject'] = 'Enrolment expiry notification'; +$string['expirymessageenrollerbody'] = 'Enrolment in the course \'{$a->course}\' will expire within the next {$a->threshold} for the following users: + +{$a->users} + +To extend their enrolment, go to {$a->extendurl}'; +$string['expirymessageenrolledsubject'] = 'Enrolment expiry notification'; +$string['expirymessageenrolledbody'] = 'Dear {$a->user}, + +This is a notification that your enrolment in the course \'{$a->course}\' is due to expire on {$a->timeend}. + +If you need help, please contact {$a->enroller}.'; +$string['expirynotify'] = 'Notify before enrolment expires'; +$string['expirynotify_help'] = 'This setting determines whether enrolment expiry notification messages are sent.'; +$string['expirynotifyall'] = 'Enroller and enrolled user'; +$string['expirynotifyteacher'] = 'Enroller only'; +$string['expirythreshold'] = 'Notification threshold'; +$string['expirythreshold_help'] = 'This setting specifies the number of days before enrolment expiry that a notification message is sent.'; +$string['expirythreshold_help'] = 'How long before expiration should be users notified?'; $string['manual:config'] = 'Configure manual enrol instances'; $string['manual:enrol'] = 'Enrol users'; $string['manual:manage'] = 'Manage user enrolments'; $string['manual:unenrol'] = 'Unenrol users from the course'; $string['manual:unenrolself'] = 'Unenrol self from the course'; +$string['messageprovider:expiry_notification'] = 'Manual enrolment expiry notifications'; +$string['notifyhour'] = 'Hour to send enrolment expiry notifications'; $string['pluginname'] = 'Manual enrolments'; $string['pluginname_desc'] = 'The manual enrolments plugin allows users to be enrolled manually via a link in the course administration settings, by a user with appropriate permissions such as a teacher. The plugin should normally be enabled, since certain other enrolment plugins, such as self enrolment, require it.'; $string['status'] = 'Enable manual enrolments'; diff --git a/enrol/manual/lib.php b/enrol/manual/lib.php index 7fe0f56b2e8..e3b041d9adf 100644 --- a/enrol/manual/lib.php +++ b/enrol/manual/lib.php @@ -26,6 +26,9 @@ defined('MOODLE_INTERNAL') || die(); class enrol_manual_plugin extends enrol_plugin { + protected $lasternoller = null; + protected $lasternollercourseid = 0; + public function roles_protected() { // Users may tweak the roles later. return false; @@ -147,7 +150,14 @@ class enrol_manual_plugin extends enrol_plugin { * @return int id of new instance, null if can not be created */ public function add_default_instance($course) { - $fields = array('status'=>$this->get_config('status'), 'enrolperiod'=>$this->get_config('enrolperiod', 0), 'roleid'=>$this->get_config('roleid', 0)); + $fields = array( + 'status' => $this->get_config('status'), + 'roleid' => $this->get_config('roleid', 0), + 'enrolperiod' => $this->get_config('enrolperiod', 0), + 'expirynotify' => $this->get_config('expirynotify', 0), + 'notifyall' => $this->get_config('notifyall', 0), + 'expirythreshold' => $this->get_config('expirythreshold', 86400), + ); return $this->add_instance($course, $fields); } @@ -263,6 +273,7 @@ class enrol_manual_plugin extends enrol_plugin { */ public function cron() { $this->sync(null, true); + $this->send_notifications(true); } /** @@ -357,6 +368,239 @@ class enrol_manual_plugin extends enrol_plugin { return 0; } + /** + * Send notifications. + * + * @param bool $verbose verbose CLI output + */ + public function send_notifications($verbose = false) { + global $DB, $CFG; + + // Unfortunately this may take a long time, it should not be interrupted, + // otherwise users get duplicate notification. + + @set_time_limit(0); + raise_memory_limit(MEMORY_HUGE); + + $notifylast = $this->get_config('notifylast', 0); + $notifyhour = $this->get_config('notifyhour', 6); + $timenow = time(); + + $notifytime = usergetmidnight($timenow, $CFG->timezone) + ($notifyhour * 3600); + + if ($notifylast > $notifytime) { + if ($verbose) { + mtrace('Manual enrolment notifications were already sent today at '.userdate($notifylast, '', $CFG->timezone).'.'); + } + return; + } else if ($timenow < $notifytime) { + if ($verbose) { + mtrace('Manual enrolment notifications will be sent at '.userdate($notifytime, '', $CFG->timezone).'.'); + } + return; + } + + if ($verbose) { + mtrace('Processing manual enrolment notifications...'); + } + + // Notify users responsible for enrolment once every day. + $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname + FROM {user_enrolments} ue + JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'manual' AND e.expirynotify > 0 AND e.status = :enabled) + JOIN {course} c ON (c.id = e.courseid) + JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0) + WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2) + ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC"; + $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'now1'=>$timenow, 'now2'=>$timenow); + + $rs = $DB->get_recordset_sql($sql, $params); + + $lastenrollid = 0; + $users = array(); + + foreach($rs as $ue) { + if ($lastenrollid and $lastenrollid != $ue->enrolid) { + $this->notify_expiry_enroller($ue->enrolid, $users, $verbose); + } + $lastenrollid = $ue->enrolid; + + $enroller = $this->get_enroller($ue->courseid); + $context = context_course::instance($ue->courseid); + + $user = $DB->get_record('user', array('id'=>$ue->userid)); + + $users[] = array('fullname'=>fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend'=>$ue->timeend); + + if (!$ue->notifyall) { + continue; + } + + if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) { + // Notify enrolled users only once at the start of the threshold. + continue; + } + + $this->notify_expiry_enrolled($user, $ue, $verbose); + } + $rs->close(); + + if ($lastenrollid and $users) { + $this->notify_expiry_enroller($lastenrollid, $users, $verbose); + } + + if ($verbose) { + mtrace('...notification processing finished.'); + } + $this->set_config('notifylast', $timenow); + } + + /** + * Returns the user who is resposible for manual enrolments in given course + * @param int $courseid + * @return stdClass user record + */ + protected function get_enroller($courseid) { + if ($this->lasternollercourseid == $courseid and $this->lasternoller) { + return $this->lasternoller; + } + + $context = context_course::instance($courseid); + if ($users = get_enrolled_users($context, 'enrol/manual:manage')) { + $users = sort_by_roleassignment_authority($users, $context); + $this->lasternoller = reset($users); + unset($users); + } else { + $this->lasternoller = get_admin(); + } + + $this->lasternollercourseid = $courseid; + + return $this->lasternoller; + } + + /** + * Notify user about incoming expiration of their enrolment. + * @param stdClass $user + * @param stdClass $ue + * @param bool $verbose + */ + protected function notify_expiry_enrolled($user, $ue, $verbose) { + global $CFG, $SESSION; + + // Some nasty hackery to get strings and dates localised for target user. + $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null; + if (get_string_manager()->translation_exists($user->lang, false)) { + $SESSION->lang = $user->lang; + moodle_setlocale(); + } + + $enroller = $this->get_enroller($ue->courseid); + $context = context_course::instance($ue->courseid); + + $subject = get_string('expirymessageenrolledsubject', 'enrol_manual', null); + $a = new stdClass(); + $a->course = format_string($ue->fullname, true, array('context'=>$context)); + $a->user = fullname($user, true); + $a->timeend = userdate($ue->timeend, '', $user->timezone); + $a->enroller = fullname($enroller, has_capability('moodle/site:viewfullnames', $context, $user)); + $body = get_string('expirymessageenrolledbody', 'enrol_manual', $a); + + $message = new stdClass(); + $message->notification = 1; + $message->component = 'enrol_manual'; + $message->name = 'expiry_notification'; + $message->userfrom = $enroller; + $message->userto = $user; + $message->subject = $subject; + $message->fullmessage = $body; + $message->fullmessageformat = FORMAT_MARKDOWN; + $message->fullmessagehtml = markdown_to_html($body); + $message->smallmessage = $subject; + $message->contexturlname = $a->course; + $message->contexturl = (string)new moodle_url('/course/view.php', array('id'=>$ue->courseid)); + + if (message_send($message)) { + if ($verbose) { + mtrace(" notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone)); + } + } else { + if ($verbose) { + mtrace(" error notifying user $ue->userid that enrolment in course $ue->courseid expires on ".userdate($ue->timeend, '', $CFG->timezone)); + } + } + + if ($SESSION->lang !== $sessionlang) { + $SESSION->lang = $sessionlang; + moodle_setlocale(); + } + } + + /** + * Notify person responsible for enrolments that some user enrolments will be expired soon. + * @param int $eid + * @param array $users + * @param bool $verbose + */ + protected function notify_expiry_enroller($eid, $users, $verbose) { + global $DB, $SESSION; + + $instance = $DB->get_record('enrol', array('id'=>$eid, 'enrol'=>'manual')); + $context = context_course::instance($instance->courseid); + $course = $DB->get_record('course', array('id'=>$instance->courseid)); + + $enroller = $this->get_enroller($instance->courseid); + $admin = get_admin(); + + // Some nasty hackery to get strings and dates localised for target user. + $sessionlang = isset($SESSION->lang) ? $SESSION->lang : null; + if (get_string_manager()->translation_exists($enroller->lang, false)) { + $SESSION->lang = $enroller->lang; + moodle_setlocale(); + } + + foreach($users as $key=>$info) { + $users[$key] = '* '.$info['fullname'].' - '.userdate($info['timeend'], '', $enroller->timezone); + } + + $subject = get_string('expirymessageenrollersubject', 'enrol_manual', null); + $a = new stdClass(); + $a->course = format_string($course->fullname, true, array('context'=>$context)); + $a->threshold = get_string('numdays', '', $instance->expirythreshold / (60*60*24)); + $a->users = implode("\n", $users); + $a->extendurl = (string)new moodle_url('/enrol/users.php', array('id'=>$instance->courseid)); + $body = get_string('expirymessageenrollerbody', 'enrol_manual', $a); + + $message = new stdClass(); + $message->notification = 1; + $message->component = 'enrol_manual'; + $message->name = 'expiry_notification'; + $message->userfrom = $admin; + $message->userto = $enroller; + $message->subject = $subject; + $message->fullmessage = $body; + $message->fullmessageformat = FORMAT_MARKDOWN; + $message->fullmessagehtml = markdown_to_html($body); + $message->smallmessage = $subject; + $message->contexturlname = $a->course; + $message->contexturl = $a->extendurl; + + if (message_send($message)) { + if ($verbose) { + mtrace(" notifying user $enroller->id about all expiring manual enrolments in course $instance->courseid"); + } + } else { + if ($verbose) { + mtrace(" error notifying user $enroller->id about all expiring manual enrolments in course $instance->courseid"); + } + } + + if ($SESSION->lang !== $sessionlang) { + $SESSION->lang = $sessionlang; + moodle_setlocale(); + } + } + /** * Gets an array of the user enrolment actions. * diff --git a/enrol/manual/settings.php b/enrol/manual/settings.php index f7934850e30..d27d26585be 100644 --- a/enrol/manual/settings.php +++ b/enrol/manual/settings.php @@ -38,6 +38,12 @@ if ($ADMIN->fulltree) { ); $settings->add(new admin_setting_configselect('enrol_manual/expiredaction', get_string('expiredaction', 'enrol_manual'), get_string('expiredaction_help', 'enrol_manual'), ENROL_EXT_REMOVED_KEEP, $options)); + $options = array(); + for ($i=0; $i<24; $i++) { + $options[$i] = $i; + } + $settings->add(new admin_setting_configselect('enrol_manual/notifyhour', get_string('notifyhour', 'enrol_manual'), '', 6, $options)); + //--- enrol instance defaults ---------------------------------------------------------------------------- $settings->add(new admin_setting_heading('enrol_manual_defaults', @@ -51,9 +57,6 @@ if ($ADMIN->fulltree) { $settings->add(new admin_setting_configselect('enrol_manual/status', get_string('status', 'enrol_manual'), get_string('status_desc', 'enrol_manual'), ENROL_INSTANCE_ENABLED, $options)); - $settings->add(new admin_setting_configduration('enrol_manual/enrolperiod', - get_string('defaultperiod', 'enrol_manual'), get_string('defaultperiod_desc', 'enrol_manual'), 0)); - if (!during_initial_install()) { $options = get_default_enrol_roles(context_system::instance()); $student = get_archetype_roles('student'); @@ -61,4 +64,15 @@ if ($ADMIN->fulltree) { $settings->add(new admin_setting_configselect('enrol_manual/roleid', get_string('defaultrole', 'role'), '', $student->id, $options)); } + + $settings->add(new admin_setting_configduration('enrol_manual/enrolperiod', + get_string('defaultperiod', 'enrol_manual'), get_string('defaultperiod_desc', 'enrol_manual'), 0)); + + $options = array(0 => get_string('no'), 1 => get_string('expirynotifyteacher', 'enrol_manual'), 2 => get_string('expirynotifyall', 'enrol_manual')); + $settings->add(new admin_setting_configselect('enrol_manual/expirynotify', + get_string('expirynotify', 'enrol_manual'), get_string('expirynotify_help', 'enrol_manual'), 0, $options)); + + $settings->add(new admin_setting_configduration('enrol_manual/expirythreshold', + get_string('expirythreshold', 'enrol_manual'), get_string('expirythreshold_help', 'enrol_manual'), 86400, 86400)); + } diff --git a/enrol/manual/version.php b/enrol/manual/version.php index a878cb2b2be..6d041337004 100644 --- a/enrol/manual/version.php +++ b/enrol/manual/version.php @@ -24,7 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2012091500; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2012091400; // Requires this Moodle version +$plugin->version = 2012100705; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2012100500; // Requires this Moodle version $plugin->component = 'enrol_manual'; // Full name of the plugin (used for diagnostics) $plugin->cron = 600;