Skip to content

Commit

Permalink
Feature: Allow the admin to configure cohorts which should be ignored…
Browse files Browse the repository at this point in the history
… by the tool.
  • Loading branch information
abias committed Aug 1, 2024
1 parent f149d2a commit 2c732bc
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.1-r1

* 2024-07-28 - Upgrade: Fix a Behat test which broke von Moodle 4.1.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
79 changes: 79 additions & 0 deletions classes/admin_setting_configmultiselect_autocomplete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
// 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 <http://www.gnu.org/licenses/>.

/**
* Admin tool "Hard life cycle for self-signup users" - Settings class file
*
* @package tool_selfsignuphardlifecycle
* @copyright 2024 Alexander Bias, lern.link GmbH <[email protected]>
* @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 <[email protected]>
* @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;
}
}
11 changes: 8 additions & 3 deletions classes/userlist_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions lang/en/tool_selfsignuphardlifecycle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 href="{$a->url}">{$a->linktitle}</a> 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';
Expand Down
68 changes: 65 additions & 3 deletions locallib.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);


/**
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
55 changes: 55 additions & 0 deletions settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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',
'',
Expand Down Expand Up @@ -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.
Expand Down
51 changes: 49 additions & 2 deletions tests/behat/tool_selfsignuphardlifecycle.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Loading

0 comments on commit 2c732bc

Please sign in to comment.