diff --git a/CHANGES.md b/CHANGES.md index 41fcbf7..a6109ea 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,10 @@ moodle-tool_selfsignuphardlifecycle Changes ------- +### Unreleased + +* 2024-07-30 - Feature: Allow the admin to configure cohorts which should be ignored by the tool. + ### v4.3-r1 * 2024-07-28 - Prepare compatibility for Moodle 4.3. diff --git a/README.md b/README.md index ef2a624..d895cdd 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ Here, you can optionally configure the number of days after which a user will be Here, you can allow the admin to override deletion and suspension dates for individual users. +#### 1.6 Cohort exceptions + +Here, you can optionally configure cohorts which should be ignored by the tool. + ### 2. User list On this page, there is a list which shows all users which are covered by this tool according to the current configuration. You will also see the current status of each user and when the next step of the user's hard lifecycle will happen. diff --git a/classes/admin_setting_configmultiselect_autocomplete.php b/classes/admin_setting_configmultiselect_autocomplete.php new file mode 100644 index 0000000..3f46a82 --- /dev/null +++ b/classes/admin_setting_configmultiselect_autocomplete.php @@ -0,0 +1,79 @@ +. + +/** + * Admin tool "Hard life cycle for self-signup users" - Settings class file + * + * @package tool_selfsignuphardlifecycle + * @copyright 2024 Alexander Bias, lern.link GmbH + * @copyright based on admin_setting_configselect_autocomplete in Moodle core. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_selfsignuphardlifecycle; + +/** + * Class used for selecting multiple options with autocompletion. + * + * @package tool_selfsignuphardlifecycle + * @copyright 2024 Alexander Bias, lern.link GmbH + * @copyright based on admin_setting_configselect_autocomplete in Moodle core. + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_setting_configmultiselect_autocomplete extends \admin_setting_configmultiselect { + // In this class, we simply inherited from admin_setting_configmultiselect and copied everything from + // admin_setting_configselect_autocomplete. And the multiselect widget worked automagically with autocompletion. + + /** @var bool $tags Should we allow typing new entries to the field? */ + protected $tags = false; + /** @var string $ajax Name of an AMD module to send/process ajax requests. */ + protected $ajax = ''; + /** @var string $placeholder Placeholder text for an empty list. */ + protected $placeholder = ''; + /** @var bool $casesensitive Whether the search has to be case-sensitive. */ + protected $casesensitive = false; + /** @var bool $showsuggestions Show suggestions by default - but this can be turned off. */ + protected $showsuggestions = true; + /** @var string $noselectionstring String that is shown when there are no selections. */ + protected $noselectionstring = ''; + + /** + * Returns XHTML select field and wrapping div(s) + * + * @param array $data Array of values to select by default + * @param string $query + * @return string XHTML field and wrapping div + */ + public function output_html($data, $query='') { + global $PAGE; + + $html = parent::output_html($data, $query); + + if ($html === '') { + return $html; + } + + $this->placeholder = get_string('search'); + + $params = ['#' . $this->get_id(), $this->tags, $this->ajax, + $this->placeholder, $this->casesensitive, $this->showsuggestions, $this->noselectionstring]; + + // Load autocomplete wrapper for select2 library. + $PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params); + + return $html; + } +} diff --git a/classes/userlist_table.php b/classes/userlist_table.php index 84844b1..1cafda2 100644 --- a/classes/userlist_table.php +++ b/classes/userlist_table.php @@ -55,15 +55,20 @@ public function __construct($uniqueid) { // Get SQL snippets for excludings admins and guests. list($admininsql, $adminsqlparams) = tool_selfsignuphardlifecycle_get_adminandguest_sql(); + // Get SQL subquery for ignoring cohorts. + list($cohortexceptionswhere, $cohortexceptionsparams) = + tool_selfsignuphardlifecycle_get_cohort_exceptions_sql(); + // Get plugin config. $config = get_config('tool_selfsignuphardlifecycle'); // Set the sql for the table. $sqlfields = 'id, firstname, lastname, username, email, auth, suspended, timecreated'; - $sqlwhere = 'deleted = :deleted AND auth '.$authinsql.' AND id '.$admininsql; - $sqlparams = array_merge($authsqlparams, $adminsqlparams); + $sqlfrom = '{user}'; + $sqlwhere = 'deleted = :deleted AND auth '.$authinsql.' AND id '.$admininsql.' '.$cohortexceptionswhere; + $sqlparams = array_merge($authsqlparams, $adminsqlparams, $cohortexceptionsparams); $sqlparams['deleted'] = 0; - $this->set_sql($sqlfields, '{user}', $sqlwhere, $sqlparams); + $this->set_sql($sqlfields, $sqlfrom, $sqlwhere, $sqlparams); // Set the table columns (depending if user overrides are enabled or not). if (tool_selfsignuphardlifecycle_user_overrides_enabled_and_configured() == true) { diff --git a/lang/en/tool_selfsignuphardlifecycle.php b/lang/en/tool_selfsignuphardlifecycle.php index 54b19d7..7800823 100644 --- a/lang/en/tool_selfsignuphardlifecycle.php +++ b/lang/en/tool_selfsignuphardlifecycle.php @@ -45,8 +45,14 @@ $string['profileedit'] = 'Edit'; $string['profileview'] = 'View'; $string['setting_authmethodsheading'] = 'Authentication methods'; +$string['setting_cohortexceptionsheading'] = 'Cohort exceptions'; +$string['setting_cohortexceptions'] = 'Cohorts to ignore'; +$string['setting_cohortexceptions_desc'] = 'With this setting, you can configure the cohorts whose members should be ignored. Each member of one of the selected cohorts will be completely ignored by this tool.'; +$string['setting_cohortexceptionsnocohortyet_desc'] = 'With this setting, you can configure the cohorts whose members should be ignored. There isn\'t any usable cohort yet. Please go to {$a->linktitle} and create a cohort first.'; $string['setting_coveredauth'] = 'Covered authentication methods'; $string['setting_coveredauth_desc'] = 'With this setting, you can configure which users are covered by this tool. If you select a particular authentication method, all users with this authentication method will become candidates for (suspension and) deletion. If you do not select a particular authentication method, all users with this authentication method will not be touched by this tool in any way.'; +$string['setting_enablecohortexceptions'] = 'Enable cohort exceptions'; +$string['setting_enablecohortexceptions_desc'] = 'With this setting, you can define cohort exceptions.'; $string['setting_enableuseroverrides'] = 'Enable user overrides'; $string['setting_enableuseroverrides_desc'] = 'With this setting, you can allow the admin to override deletion and suspension dates for individual users.'; $string['setting_enableusersuspension'] = 'Enable user suspension before deletion'; diff --git a/locallib.php b/locallib.php index a5f02f6..b6c0c4f 100644 --- a/locallib.php +++ b/locallib.php @@ -27,6 +27,7 @@ define('TOOL_SELFSIGNUPHARDLIFECYCLLE_SUSPENSIONPERIOD_DEFAULT', 100); define('TOOL_SELFSIGNUPHARDLIFECYCLLE_ENABLESUSPENSION_DEFAULT', 1); define('TOOL_SELFSIGNUPHARDLIFECYCLLE_ENABLEOVERRIDES_DEFAULT', 0); +define('TOOL_SELFSIGNUPHARDLIFECYCLLE_ENABLECOHORTEXCEPTIONS_DEFAULT', 0); /** @@ -51,6 +52,10 @@ function tool_selfsignuphardlifecycle_process_lifecycle() { // Get SQL snippets for covered auth methods. list($authinsql, $authsqlparams) = tool_selfsignuphardlifecycle_get_auth_sql(); + // Get SQL subquery for ignoring cohorts. + list($cohortexceptionswhere, $cohortexceptionsparams) = + tool_selfsignuphardlifecycle_get_cohort_exceptions_sql(); + // PHASE 1: Overridden users. // Do only if user override is enabled. @@ -61,6 +66,7 @@ function tool_selfsignuphardlifecycle_process_lifecycle() { $usersparams['deleted'] = 0; $usersparams['deletionoverridefieldid'] = $config->userdeletionoverridefield; $usersparams['suspensionoverridefieldid'] = $config->usersuspensionoverridefield; + $usersparams = array_merge($usersparams, $cohortexceptionsparams); $userssql = 'SELECT u.*, (SELECT uid.data FROM {user_info_data} uid @@ -73,7 +79,7 @@ function tool_selfsignuphardlifecycle_process_lifecycle() { AND uid.fieldid = :suspensionoverridefieldid ) AS suspensionoverride FROM {user} u - WHERE u.auth '.$authinsql.' + WHERE u.auth '.$authinsql.' '.$cohortexceptionswhere.' AND u.deleted = :deleted ORDER BY u.id ASC'; $usersrs = $DB->get_recordset_sql($userssql, $usersparams); @@ -194,11 +200,13 @@ function tool_selfsignuphardlifecycle_process_lifecycle() { $deleteusersparams['timecreated'] = $userdeletiondatets; $deleteusersparams['suspended'] = 1; $deleteusersparams['deleted'] = 0; + $deleteusersparams = array_merge($deleteusersparams, $cohortexceptionsparams); $deleteuserssql = 'SELECT * FROM {user} WHERE auth '.$authinsql.' AND timecreated < :timecreated '. - $suspendedsqlsnippet.' + $suspendedsqlsnippet.' '. + $cohortexceptionswhere.' AND deleted = :deleted ORDER BY id ASC'; $deleteusersrs = $DB->get_recordset_sql($deleteuserssql, $deleteusersparams); @@ -263,10 +271,12 @@ function tool_selfsignuphardlifecycle_process_lifecycle() { $suspendusersparams['timecreated'] = $usersuspensiondatets; $suspendusersparams['suspended'] = 0; $suspendusersparams['deleted'] = 0; + $suspendusersparams = array_merge($suspendusersparams, $cohortexceptionsparams); $suspenduserssql = 'SELECT * FROM {user} WHERE auth ' . $authinsql . ' - AND timecreated < :timecreated + AND timecreated < :timecreated '. + $cohortexceptionswhere.' AND suspended = :suspended AND deleted = :deleted ORDER BY id ASC'; @@ -702,3 +712,55 @@ function tool_selfsignuphardlifecycle_user_overrides_enabled_and_configured() { // Return the result. return $retvalue; } + +/** + * Helper function to get the SQL WHERE subquery for the cohorts which should be ignored by the plugin. + * + * @return array An array with two elements: + * The first element is a SQL WHERE snippet. + * The second element is a param array. + */ +function tool_selfsignuphardlifecycle_get_cohort_exceptions_sql() { + global $DB; + + // Get plugin config. + $config = get_config('tool_selfsignuphardlifecycle'); + + // If cohort exceptions are not enabled, return an all-empty array. + if ($config->enablecohortexceptions == false) { + return ['', []]; + } + + // Use a static array to cache the results of this function as it might be called multiple times per user. + static $cohortexceptions = []; + static $staticcachebuilt = false; + + // If we did not compose the cohort exceptions yet. + if ($staticcachebuilt === false) { + // Explode the cohort exception configuration. + $cohorts = explode(',', $config->cohortexceptions); + + // If no cohorts are set, return an all-empty array. + if (count($cohorts) < 1) { + return ['', []]; + } + + // Compose the WHERE subquery. + list($whereinsql, $whereinparams) = $DB->get_in_or_equal($cohorts, SQL_PARAMS_NAMED, 'cohort', true); + $where = 'AND NOT EXISTS ( + SELECT 1 + FROM {cohort_members} + WHERE {cohort_members}.userid = {user}.id + AND {cohort_members}.cohortid '.$whereinsql.' + )'; + + // Compose the cohort exceptions array. + $cohortexceptions = [$where, $whereinparams]; + + // Remember that we have built it. + $staticcachebuilt = true; + } + + // Return the cohort exceptions array. + return $cohortexceptions; +} diff --git a/settings.php b/settings.php index f36acf9..5571e0f 100644 --- a/settings.php +++ b/settings.php @@ -22,6 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use tool_selfsignuphardlifecycle\admin_setting_configmultiselect_autocomplete; + defined('MOODLE_INTERNAL') || die; global $CFG; @@ -39,6 +41,9 @@ // Require the necessary libraries. require_once($CFG->dirroot . '/admin/tool/selfsignuphardlifecycle/locallib.php'); + // Require cohort library. + require_once($CFG->dirroot . '/cohort/lib.php'); + // Create hard life cycle description static widget. $setting = new admin_setting_heading('tool_selfsignuphardlifecycle/userlifecyclestatic', '', @@ -177,6 +182,56 @@ 'tool_selfsignuphardlifecycle/enableusersuspension'); } unset($userprofilefieldoptions); + + // Create cohort exceptions heading widget. + $setting = new admin_setting_heading('tool_selfsignuphardlifecycle/cohortexceptionsheading', + get_string('setting_cohortexceptionsheading', 'tool_selfsignuphardlifecycle', null, true), + ''); + $page->add($setting); + + // Create enable cohort exceptions widget. + $setting = new admin_setting_configcheckbox('tool_selfsignuphardlifecycle/enablecohortexceptions', + get_string('setting_enablecohortexceptions', 'tool_selfsignuphardlifecycle', null, true), + get_string('setting_enablecohortexceptions_desc', 'tool_selfsignuphardlifecycle', null, true), + TOOL_SELFSIGNUPHARDLIFECYCLLE_ENABLECOHORTEXCEPTIONS_DEFAULT); + $page->add($setting); + + // Get cohort options. + $cohortdata = cohort_get_all_cohorts(0, 0); + $cohortoptions = []; + foreach ($cohortdata['cohorts'] as $cohort) { + $cohortoptions[$cohort->id] = $cohort->name; + } + + // If there aren't any cohorts yet. + if (count($cohortoptions) < 1) { + // Build settings page link. + $url = new moodle_url('/cohort/index.php'); + $link = ['url' => $url->out(), 'linktitle' => get_string('cohorts', 'core_cohort', null, true)]; + + // Create empty cohort exceptions field widget to trigger a settings entry in the database. + $setting = new admin_setting_configempty('tool_selfsignuphardlifecycle/cohortexceptions', + get_string('setting_cohortexceptions', 'tool_selfsignuphardlifecycle', null, true), + get_string('setting_cohortexceptionsnocohortyet_desc', 'tool_selfsignuphardlifecycle', $link, true)); + $page->add($setting); + $page->hide_if('tool_selfsignuphardlifecycle/cohortexceptions', + 'tool_selfsignuphardlifecycle/enablecohortexceptions'); + + unset ($link, $url); + + // Otherwise, if there are cohorts. + } else { + // Create user deletion override field widget. + $setting = new admin_setting_configmultiselect_autocomplete('tool_selfsignuphardlifecycle/cohortexceptions', + get_string('setting_cohortexceptions', 'tool_selfsignuphardlifecycle', null, true), + get_string('setting_cohortexceptions_desc', 'tool_selfsignuphardlifecycle', null, true), + [], + $cohortoptions); + $page->add($setting); + $page->hide_if('tool_selfsignuphardlifecycle/cohortexceptions', + 'tool_selfsignuphardlifecycle/enablecohortexceptions'); + } + unset($cohortoptions); } // Add settings page to navigation category. diff --git a/tests/behat/tool_selfsignuphardlifecycle.feature b/tests/behat/tool_selfsignuphardlifecycle.feature index da1afbb..b9340ca 100644 --- a/tests/behat/tool_selfsignuphardlifecycle.feature +++ b/tests/behat/tool_selfsignuphardlifecycle.feature @@ -6,8 +6,8 @@ Feature: The hard life cycle for self-signup users tool allows admins to get rid Background: Given the following config values are set as admin: - | coveredauth | email | tool_selfsignuphardlifecycle | - | userdeletionperiod | 200 | tool_selfsignuphardlifecycle | + | coveredauth | email | tool_selfsignuphardlifecycle | + | userdeletionperiod | 200 | tool_selfsignuphardlifecycle | Scenario: Manual authenticated users remain untouched by the tool Given the following "users" exist: @@ -249,3 +249,50 @@ Feature: The hard life cycle for self-signup users tool allows admins to get rid And I should not see "User 3" in the "#users" "css_element" And I should see "User 4" in the "#users" "css_element" And ".usersuspended" "css_element" should exist in the "User 4" "table_row" + + @javascript + Scenario: Users from ignored cohorts remain untouched by the tool + Given the following "users" exist: + | username | firstname | lastname | email | auth | suspended | timecreated | + # User 1 will be ignored as he is a member of an ignored cohort. + | user1 | User | 1 | user1@example.com | email | 0 | ## 201 days ago ## | + # User 2 will be suspended as he is not a member of an ignored cohort. + | user2 | User | 2 | user2@example.com | email | 0 | ## 201 days ago ## | + # User 3 will be suspended as he is not a member of any cohort at all. + | user3 | User | 3 | user3@example.com | email | 0 | ## 201 days ago ## | + And the following "cohorts" exist: + | name | idnumber | + | Cohort 1 | C1 | + | Cohort 2 | C2 | + | Cohort 3 | C3 | + And the following "cohort members" exist: + | user | cohort | + | user1 | C1 | + | user2 | C2 | + And I log in as "admin" + And I navigate to "Users > Hard life cycle for self-signup users > Settings" in site administration + And I set the field "Enable cohort exceptions" to "1" + And I click on ".form-autocomplete-downarrow" "css_element" in the "#admin-cohortexceptions" "css_element" + And I click on "Cohort 1" item in the autocomplete list + And I click on "Cohort 3" item in the autocomplete list + And I press the escape key + And I click on "Save changes" "button" + + When I navigate to "Users > Hard life cycle for self-signup users > User list" in site administration + Then I should not see "user1" in the "#region-main" "css_element" + And I should see "user2" in the "#region-main" "css_element" + And I should see "user2" in the "#region-main" "css_element" + + And I navigate to "Users > Accounts > Browse list of users" in site administration + Then I should see "User 1" in the "#users" "css_element" + And I should see "User 2" in the "#users" "css_element" + And I should see "User 3" in the "#users" "css_element" + And ".usersuspended" "css_element" should not exist in the "User 1" "table_row" + And ".usersuspended" "css_element" should not exist in the "User 2" "table_row" + And ".usersuspended" "css_element" should not exist in the "User 3" "table_row" + And I run the scheduled task "tool_selfsignuphardlifecycle\task\process_lifecycle" + And I reload the page + Then I should see "User 1" in the "#users" "css_element" + And I should not see "User 2" in the "#users" "css_element" + And I should not see "User 3" in the "#users" "css_element" + And ".usersuspended" "css_element" should not exist in the "User 1" "table_row" diff --git a/version.php b/version.php index f8a8d0b..31efdbf 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'tool_selfsignuphardlifecycle'; -$plugin->version = 2023100900; +$plugin->version = 2023100901; $plugin->release = 'v4.3-r1'; $plugin->requires = 2023100900; $plugin->supported = [403, 403];