diff --git a/enrol/fee/classes/plugin.php b/enrol/fee/classes/plugin.php new file mode 100644 index 00000000000..71b9e7e524a --- /dev/null +++ b/enrol/fee/classes/plugin.php @@ -0,0 +1,413 @@ +. + +/** + * Fee enrolment plugin. + * + * This plugin allows you to set up paid courses. + * + * @package enrol_fee + * @copyright 2019 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Fee enrolment plugin implementation. + * + * @copyright 2019 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class enrol_fee_plugin extends enrol_plugin { + + /** + * Returns the list of currencies that the payment subsystem supports and therefore we can work with. + * + * @return array[currencycode => currencyname] + */ + public function get_possible_currencies(): array { + $codes = \core_payment\helper::get_supported_currencies(); + + $currencies = []; + foreach ($codes as $c) { + $currencies[$c] = new lang_string($c, 'core_currencies'); + } + + return $currencies; + } + + /** + * Returns optional enrolment information icons. + * + * This is used in course list for quick overview of enrolment options. + * + * We are not using single instance parameter because sometimes + * we might want to prevent icon repetition when multiple instances + * of one type exist. One instance may also produce several icons. + * + * @param array $instances all enrol instances of this type in one course + * @return array of pix_icon + */ + public function get_info_icons(array $instances) { + $found = false; + foreach ($instances as $instance) { + if ($instance->enrolstartdate != 0 && $instance->enrolstartdate > time()) { + continue; + } + if ($instance->enrolenddate != 0 && $instance->enrolenddate < time()) { + continue; + } + $found = true; + break; + } + if ($found) { + return array(new pix_icon('icon', get_string('pluginname', 'enrol_fee'), 'enrol_fee')); + } + return array(); + } + + public function roles_protected() { + // Users with role assign cap may tweak the roles later. + return false; + } + + public function allow_unenrol(stdClass $instance) { + // Users with unenrol cap may unenrol other users manually - requires enrol/fee:unenrol. + return true; + } + + public function allow_manage(stdClass $instance) { + // Users with manage cap may tweak period and status - requires enrol/fee:manage. + return true; + } + + public function show_enrolme_link(stdClass $instance) { + return ($instance->status == ENROL_INSTANCE_ENABLED); + } + + /** + * Returns true if the user can add a new instance in this course. + * @param int $courseid + * @return boolean + */ + public function can_add_instance($courseid) { + $context = context_course::instance($courseid, MUST_EXIST); + + if (!has_capability('moodle/course:enrolconfig', $context) or !has_capability('enrol/fee:config', $context)) { + return false; + } + + // Multiple instances supported - different cost for different roles. + return true; + } + + /** + * We are a good plugin and don't invent our own UI/validation code path. + * + * @return boolean + */ + public function use_standard_editing_ui() { + return true; + } + + /** + * Add new instance of enrol plugin. + * @param object $course + * @param array $fields instance fields + * @return int id of new instance, null if can not be created + */ + public function add_instance($course, array $fields = null) { + if ($fields && !empty($fields['cost'])) { + $fields['cost'] = unformat_float($fields['cost']); + } + return parent::add_instance($course, $fields); + } + + /** + * Update instance of enrol plugin. + * @param stdClass $instance + * @param stdClass $data modified instance fields + * @return boolean + */ + public function update_instance($instance, $data) { + if ($data) { + $data->cost = unformat_float($data->cost); + } + return parent::update_instance($instance, $data); + } + + /** + * Creates course enrol form, checks if form submitted + * and enrols user if necessary. It can also redirect. + * + * @param stdClass $instance + * @return string html text, usually a form in a text box + */ + public function enrol_page_hook(stdClass $instance) { + global $CFG, $USER, $OUTPUT, $PAGE, $DB; + + ob_start(); + + if ($DB->record_exists('user_enrolments', array('userid' => $USER->id, 'enrolid' => $instance->id))) { + return ob_get_clean(); + } + + if ($instance->enrolstartdate != 0 && $instance->enrolstartdate > time()) { + return ob_get_clean(); + } + + if ($instance->enrolenddate != 0 && $instance->enrolenddate < time()) { + return ob_get_clean(); + } + + $course = $DB->get_record('course', array('id' => $instance->courseid)); + $context = context_course::instance($course->id); + + $shortname = format_string($course->shortname, true, array('context' => $context)); + $strloginto = get_string("loginto", "", $shortname); + $strcourses = get_string("courses"); + + // Pass $view=true to filter hidden caps if the user cannot see them. + if ($users = get_users_by_capability($context, 'moodle/course:update', 'u.*', 'u.id ASC', + '', '', '', '', false, true)) { + $users = sort_by_roleassignment_authority($users, $context); + $teacher = array_shift($users); + } else { + $teacher = false; + } + + if ( (float) $instance->cost <= 0 ) { + $cost = (float) $this->get_config('cost'); + } else { + $cost = (float) $instance->cost; + } + + if (abs($cost) < 0.01) { // No cost, other enrolment methods (instances) should be used. + echo '

'.get_string('nocost', 'enrol_fee').'

'; + } else { + $localisedcost = format_float($cost, -1, true); + + if (isguestuser()) { // Force login only for guest user, not real users with guest role. + $wwwroot = $CFG->wwwroot; + echo '

'.get_string('paymentrequired').'

'; + echo '

'.get_string('cost').": $instance->currency $localisedcost".'

'; + echo '

'.get_string('loginsite').'

'; + echo '
'; + } else { + echo '
'; + $PAGE->requires->js_call_amd('profilefield_conditional/conditionconfig', 'init', array('#id_param1', + '#profilefield_conditional_conditionconfiguration', '#id_conditionconfigbutton', $fieldid)); + } + + } + + return $OUTPUT->box(ob_get_clean()); + } + + /** + * Restore instance and map settings. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $course + * @param int $oldid + */ + public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { + global $DB; + if ($step->get_task()->get_target() == backup::TARGET_NEW_COURSE) { + $merge = false; + } else { + $merge = array( + 'courseid' => $data->courseid, + 'enrol' => $this->get_name(), + 'roleid' => $data->roleid, + 'cost' => $data->cost, + 'currency' => $data->currency, + ); + } + if ($merge and $instances = $DB->get_records('enrol', $merge, 'id')) { + $instance = reset($instances); + $instanceid = $instance->id; + } else { + $instanceid = $this->add_instance($course, (array) $data); + } + $step->set_mapping('enrol', $oldid, $instanceid); + } + + /** + * Restore user enrolment. + * + * @param restore_enrolments_structure_step $step + * @param stdClass $data + * @param stdClass $instance + * @param int $oldinstancestatus + * @param int $userid + */ + public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { + $this->enrol_user($instance, $userid, null, $data->timestart, $data->timeend, $data->status); + } + + /** + * Return an array of valid options for the status. + * + * @return array + */ + protected function get_status_options() { + $options = array(ENROL_INSTANCE_ENABLED => get_string('yes'), + ENROL_INSTANCE_DISABLED => get_string('no')); + return $options; + } + + /** + * Return an array of valid options for the roleid. + * + * @param stdClass $instance + * @param context $context + * @return array + */ + protected function get_roleid_options($instance, $context) { + if ($instance->id) { + $roles = get_default_enrol_roles($context, $instance->roleid); + } else { + $roles = get_default_enrol_roles($context, $this->get_config('roleid')); + } + return $roles; + } + + + /** + * Add elements to the edit instance form. + * + * @param stdClass $instance + * @param MoodleQuickForm $mform + * @param context $context + * @return bool + */ + public function edit_instance_form($instance, MoodleQuickForm $mform, $context) { + + $mform->addElement('text', 'name', get_string('custominstancename', 'enrol')); + $mform->setType('name', PARAM_TEXT); + + $options = $this->get_status_options(); + $mform->addElement('select', 'status', get_string('status', 'enrol_fee'), $options); + $mform->setDefault('status', $this->get_config('status')); + + $mform->addElement('text', 'cost', get_string('cost', 'enrol_fee'), array('size' => 4)); + $mform->setType('cost', PARAM_RAW); + $mform->setDefault('cost', format_float($this->get_config('cost'), 2, true)); + + $supportedcurrencies = $this->get_possible_currencies(); + $mform->addElement('select', 'currency', get_string('currency', 'enrol_fee'), $supportedcurrencies); + $mform->setDefault('currency', $this->get_config('currency')); + + $roles = $this->get_roleid_options($instance, $context); + $mform->addElement('select', 'roleid', get_string('assignrole', 'enrol_fee'), $roles); + $mform->setDefault('roleid', $this->get_config('roleid')); + + $options = array('optional' => true, 'defaultunit' => 86400); + $mform->addElement('duration', 'enrolperiod', get_string('enrolperiod', 'enrol_fee'), $options); + $mform->setDefault('enrolperiod', $this->get_config('enrolperiod')); + $mform->addHelpButton('enrolperiod', 'enrolperiod', 'enrol_fee'); + + $options = array('optional' => true); + $mform->addElement('date_time_selector', 'enrolstartdate', get_string('enrolstartdate', 'enrol_fee'), $options); + $mform->setDefault('enrolstartdate', 0); + $mform->addHelpButton('enrolstartdate', 'enrolstartdate', 'enrol_fee'); + + $options = array('optional' => true); + $mform->addElement('date_time_selector', 'enrolenddate', get_string('enrolenddate', 'enrol_fee'), $options); + $mform->setDefault('enrolenddate', 0); + $mform->addHelpButton('enrolenddate', 'enrolenddate', 'enrol_fee'); + + if (enrol_accessing_via_instance($instance)) { + $warningtext = get_string('instanceeditselfwarningtext', 'core_enrol'); + $mform->addElement('static', 'selfwarn', get_string('instanceeditselfwarning', 'core_enrol'), $warningtext); + } + } + + /** + * Perform custom validation of the data used to edit the instance. + * + * @param array $data array of ("fieldname"=>value) of submitted data + * @param array $files array of uploaded files "element_name"=>tmp_file_path + * @param object $instance The instance loaded from the DB + * @param context $context The context of the instance we are editing + * @return array of "element_name"=>"error_description" if there are errors, + * or an empty array if everything is OK. + * @return void + */ + public function edit_instance_validation($data, $files, $instance, $context) { + $errors = array(); + + if (!empty($data['enrolenddate']) and $data['enrolenddate'] < $data['enrolstartdate']) { + $errors['enrolenddate'] = get_string('enrolenddaterror', 'enrol_fee'); + } + + $cost = str_replace(get_string('decsep', 'langconfig'), '.', $data['cost']); + if (!is_numeric($cost)) { + $errors['cost'] = get_string('costerror', 'enrol_fee'); + } + + $validstatus = array_keys($this->get_status_options()); + $validcurrency = array_keys($this->get_possible_currencies()); + $validroles = array_keys($this->get_roleid_options($instance, $context)); + $tovalidate = array( + 'name' => PARAM_TEXT, + 'status' => $validstatus, + 'currency' => $validcurrency, + 'roleid' => $validroles, + 'enrolperiod' => PARAM_INT, + 'enrolstartdate' => PARAM_INT, + 'enrolenddate' => PARAM_INT + ); + + $typeerrors = $this->validate_param_types($data, $tovalidate); + $errors = array_merge($errors, $typeerrors); + + return $errors; + } + + /** + * Execute synchronisation. + * @param progress_trace $trace + * @return int exit code, 0 means ok + */ + public function sync(progress_trace $trace) { + $this->process_expirations($trace); + return 0; + } + + /** + * Is it possible to delete enrol instance via standard UI? + * + * @param stdClass $instance + * @return bool + */ + public function can_delete_instance($instance) { + $context = context_course::instance($instance->courseid); + return has_capability('enrol/fee:config', $context); + } + + /** + * Is it possible to hide/show enrol instance via standard UI? + * + * @param stdClass $instance + * @return bool + */ + public function can_hide_show_instance($instance) { + $context = context_course::instance($instance->courseid); + return has_capability('enrol/fee:config', $context); + } +} diff --git a/enrol/fee/db/access.php b/enrol/fee/db/access.php new file mode 100644 index 00000000000..4dc1777f855 --- /dev/null +++ b/enrol/fee/db/access.php @@ -0,0 +1,61 @@ +. + +/** + * Capabilities for fee enrolment plugin. + * + * @package enrol_fee + * @copyright 2019 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'enrol/fee:config' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + ) + ), + + 'enrol/fee:manage' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + ) + ), + + 'enrol/fee:unenrol' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'manager' => CAP_ALLOW, + ) + ), + + 'enrol/fee:unenrolself' => array( + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + ) + ), + +); diff --git a/enrol/fee/lang/en/enrol_fee.php b/enrol/fee/lang/en/enrol_fee.php new file mode 100644 index 00000000000..a46e275b1ed --- /dev/null +++ b/enrol/fee/lang/en/enrol_fee.php @@ -0,0 +1,50 @@ +. + +/** + * Strings for component 'enrol_fee', language 'en' + * + * @package enrol_fee + * @copyright 2019 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['assignrole'] = 'Assign role'; +$string['cost'] = 'Enrolment fee'; +$string['costerror'] = 'The enrolment fee must be a number.'; +$string['currency'] = 'Currency'; +$string['defaultrole'] = 'Default role assignment'; +$string['defaultrole_desc'] = 'Select the role to assign to users after making a payment.'; +$string['enrolenddate'] = 'End date'; +$string['enrolenddate_help'] = 'If enabled, users can be enrolled until this date only.'; +$string['enrolenddaterror'] = 'The enrolment end date cannot be earlier than the start date.'; +$string['enrolperiod'] = 'Enrolment duration'; +$string['enrolperiod_desc'] = 'Default length of time that the enrolment is valid. If set to zero, the enrolment duration will be unlimited by default.'; +$string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user is enrolled. If disabled, the enrolment duration will be unlimited.'; +$string['enrolstartdate'] = 'Start date'; +$string['enrolstartdate_help'] = 'If enabled, users can only be enrolled from this date onwards.'; +$string['expiredaction'] = 'Enrolment expiry action'; +$string['expiredaction_help'] = 'Select the action to be performed when a user\'s enrolment expires. Please note that some user data and settings are deleted when a user is unenrolled.'; +$string['fee:config'] = 'Configure enrolment on payment enrol instances'; +$string['fee:manage'] = 'Manage enrolled users'; +$string['fee:unenrol'] = 'Unenrol users from course'; +$string['fee:unenrolself'] = 'Unenrol self from course'; +$string['nocost'] = 'There is no cost to enrol in this course!'; +$string['pluginname'] = 'Enrolment on payment'; +$string['pluginname_desc'] = 'The enrolment on payment enrolment method allows you to set up courses requiring a payment. If the fee for any course is set to zero, then students are not asked to pay for entry. There is a site-wide fee that you set here as a default for the whole site and then a course setting that you can set for each course individually. The course fee overrides the site fee.'; +$string['sendpaymentbutton'] = 'Select payment type'; +$string['status'] = 'Allow enrolment on payment enrolments'; +$string['status_desc'] = 'Allow users to make a payment to enrol into a course by default.'; diff --git a/enrol/fee/lib.php b/enrol/fee/lib.php new file mode 100644 index 00000000000..8e88922cfe9 --- /dev/null +++ b/enrol/fee/lib.php @@ -0,0 +1,25 @@ +. + +/** + * Fee enrolment plugin. + * + * This plugin allows you to set up paid courses. + * + * @package enrol_fee + * @copyright 2019 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ diff --git a/enrol/fee/pix/icon.svg b/enrol/fee/pix/icon.svg new file mode 100644 index 00000000000..4322ea4f3c0 --- /dev/null +++ b/enrol/fee/pix/icon.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/enrol/fee/settings.php b/enrol/fee/settings.php new file mode 100644 index 00000000000..0a21a50f967 --- /dev/null +++ b/enrol/fee/settings.php @@ -0,0 +1,68 @@ +. + +/** + * Settings for the Fee enrolment plugin + * + * @package enrol_fee + * @copyright 2019 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($ADMIN->fulltree) { + + $settings->add(new admin_setting_heading('enrol_fee_settings', '', get_string('pluginname_desc', 'enrol_fee'))); + + // Note: let's reuse the ext sync constants and strings here, internally it is very similar, + // it describes what should happen when users are not supposed to be enrolled any more. + $options = array( + ENROL_EXT_REMOVED_KEEP => get_string('extremovedkeep', 'enrol'), + ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'enrol'), + ENROL_EXT_REMOVED_UNENROL => get_string('extremovedunenrol', 'enrol'), + ); + $settings->add(new admin_setting_configselect( + 'enrol_fee/expiredaction', + get_string('expiredaction', 'enrol_fee'), + get_string('expiredaction_help', 'enrol_fee'), + ENROL_EXT_REMOVED_SUSPENDNOROLES, + $options)); + + $settings->add(new admin_setting_heading('enrol_fee_defaults', + get_string('enrolinstancedefaults', 'admin'), get_string('enrolinstancedefaults_desc', 'admin'))); + + $options = array(ENROL_INSTANCE_ENABLED => get_string('yes'), + ENROL_INSTANCE_DISABLED => get_string('no')); + $settings->add(new admin_setting_configselect('enrol_fee/status', + get_string('status', 'enrol_fee'), get_string('status_desc', 'enrol_fee'), ENROL_INSTANCE_DISABLED, $options)); + + $settings->add(new admin_setting_configtext('enrol_fee/cost', get_string('cost', 'enrol_fee'), '', 0, PARAM_FLOAT, 4)); + + $currencies = enrol_get_plugin('fee')->get_possible_currencies(); + $settings->add(new admin_setting_configselect('enrol_fee/currency', get_string('currency', 'enrol_fee'), '', '', $currencies)); + + if (!during_initial_install()) { + $options = get_default_enrol_roles(context_system::instance()); + $student = get_archetype_roles('student'); + $student = reset($student); + $settings->add(new admin_setting_configselect('enrol_fee/roleid', + get_string('defaultrole', 'enrol_fee'), get_string('defaultrole_desc', 'enrol_fee'), $student->id, $options)); + } + + $settings->add(new admin_setting_configduration('enrol_fee/enrolperiod', + get_string('enrolperiod', 'enrol_fee'), get_string('enrolperiod_desc', 'enrol_fee'), 0)); +} diff --git a/enrol/fee/version.php b/enrol/fee/version.php new file mode 100644 index 00000000000..dd716898623 --- /dev/null +++ b/enrol/fee/version.php @@ -0,0 +1,29 @@ +. + +/** + * Fee enrolment plugin version specification. + * + * @package enrol_fee + * @copyright 2019 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2020102700; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2020060900; // Requires this Moodle version. +$plugin->component = 'enrol_fee'; // Full name of the plugin (used for diagnostics). diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 6625b95d52f..36392ce5728 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1853,7 +1853,7 @@ class core_plugin_manager { 'enrol' => array( 'category', 'cohort', 'database', 'flatfile', 'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet', - 'paypal', 'self' + 'paypal', 'self', 'fee', ), 'filter' => array(