Skip to content

Commit

Permalink
MBS-9448: Add role filter for rights config table (#28)
Browse files Browse the repository at this point in the history
* MBS-9448: Add role filter for rights config table

* MBS-9448: Move current filter data to session

* MBS-9448: Trying(!) to implement autocomplete

* MBS-9448: redirect for resetting filters.

* MBS-9448: Final refactoring and cleanup

* MBS-9448: Fix fetching correct filter data

---------

Co-authored-by: Andreas Wagner <[email protected]>
  • Loading branch information
PhMemmel and Andreas Wagner authored Nov 25, 2024
1 parent 217e6e7 commit bae2fe0
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 63 deletions.
98 changes: 77 additions & 21 deletions classes/form/rights_config_filter_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,109 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

/**
* User config config form.
*
* This form handles the locking and unlocking of users on the statistics overview pages.
*
* @package local_ai_manager
* @copyright 2024 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace local_ai_manager\form;

use local_ai_manager\local\userinfo;

defined('MOODLE_INTERNAL') || die;

global $CFG;
require_once($CFG->libdir . '/formslib.php');

/**
* A form for filtering IDM groups.
* A form for filtering for roles and whatever is being injected by a hook.
*
* @copyright 2021, ISB Bayern
* @package local_ai_manager
* @copyright 2024 ISB Bayern
* @author Philipp Memmel
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rights_config_filter_form extends \moodleform {

/** @var string the filter identifier for the filter provided by the usertable_filter hook. */
const FILTER_IDENTIFIER_HOOK_FILTER = 'hookfilter';

/** @var string the filter identifier for the role filter. */
const FILTER_IDENTIFIER_ROLE_FILTER = 'rolefilter';

/**
* Form definition.
*/
public function definition() {
$tenant = \core\di::get(\local_ai_manager\local\tenant::class);
$mform = &$this->_form;
$filteroptions = $this->_customdata['filteroptions'];

$attributes = $mform->getAttributes();
$attributes['class'] = $attributes['class'] . ' col-md-12';
$mform->setAttributes($attributes);
$hookfilteroptions = $this->_customdata['hookfilteroptions'];
$hookfilterlabel = $this->_customdata['hookfilterlabel'];
$mform->addElement('hidden', 'tenant', $tenant->get_identifier());
$mform->setType('tenant', PARAM_ALPHANUM);

$elementarray = [];
if (!empty($hookfilteroptions)) {
$filteroptionsautocomplete =
$mform->createElement('autocomplete', 'hookfilterids', '', $hookfilteroptions,
['multiple' => true, 'noselectionstring' => $hookfilterlabel]);
$filteroptionsautocomplete->setMultiple(true);
$elementarray[] = $filteroptionsautocomplete;
}

$filteroptionsmultiselect = $mform->createElement('select', 'filterids', '', $filteroptions,
['size' => 2, 'class' => 'local_ai_manager-filter_select pr-1']);
$filteroptionsmultiselect->setMultiple(true);
$elementarray[] = $filteroptionsmultiselect;
$rolefilteroptions =
[
userinfo::ROLE_BASIC => get_string(userinfo::get_role_as_string(userinfo::ROLE_BASIC), 'local_ai_manager'),
userinfo::ROLE_EXTENDED => get_string(userinfo::get_role_as_string(userinfo::ROLE_EXTENDED),
'local_ai_manager'),
userinfo::ROLE_UNLIMITED => get_string(userinfo::get_role_as_string(userinfo::ROLE_UNLIMITED),
'local_ai_manager'),
];
$rolefilterautocomplete =
$mform->createElement('autocomplete', 'rolefilterids', '', $rolefilteroptions,
['multiple' => true, 'noselectionstring' => get_string('filterroles', 'local_ai_manager')]);
$rolefilterautocomplete->setMultiple(true);
$elementarray[] = $rolefilterautocomplete;

$elementarray[] = $mform->createElement('submit', 'applyfilter', get_string('applyfilter', 'local_ai_manager'));
$elementarray[] = $mform->createElement('cancel', 'resetfilter', get_string('resetfilter', 'local_ai_manager'));
$mform->addGroup($elementarray, 'elementarray', get_string('filterheading', 'local_ai_manager'), [' '], false);
$elementarray[] = $mform->createElement('submit', 'resetfilter', get_string('resetfilter', 'local_ai_manager'));
$mform->addGroup($elementarray, 'elementarray', '', [' '], false);
}

/**
* Store filterids and rolefilterids in session.
*
* @param string $filteridentifier the identifier of the filter
* @param array $filterids the ids to store for the filter
*/
public function store_filterids(string $filteridentifier, array $filterids) {
global $SESSION;
$key = 'local_ai_manager_' . $filteridentifier;

// Ensure attribute exists for following lines.
if (!isset($SESSION->{$key})) {
$SESSION->{$key} = [];
}

if ($SESSION->{$key} !== $filterids) {
$SESSION->{$key} = $filterids;
}
}

/**
* Get currently selected filters from user session.
*
* @param string $filteridentifier the identifier of the filter
* @return array of the form [1,3] containing the ids for the filter
*/
public function get_stored_filterids(string $filteridentifier): array {
global $SESSION;
$key = 'local_ai_manager_' . $filteridentifier;

// Ensure attribute exists for following lines.
if (!isset($SESSION->{$key})) {
$SESSION->{$key} = [];
}

return $SESSION->{$key};
}

}
21 changes: 21 additions & 0 deletions classes/hook/usertable_filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class usertable_filter {
/** @var array associative array for providing filter options to the filter component of the rights config table */
private array $filteroptions = [];

/** @var string String for providing a label for the filter selection form element */
private string $filterlabel = '';

/**
* Constructor for the hook.
* @param tenant $tenant the tenant for which the user table is being shown
Expand Down Expand Up @@ -74,4 +77,22 @@ public function get_filter_options(): array {
public function set_filter_options(array $filteroptions): void {
$this->filteroptions = $filteroptions;
}

/**
* Standard getter for retrieving the label which should be shown above the filter form element.
*
* @return string the localized string to show above the filter form element
*/
public function get_filter_label(): string {
return $this->filterlabel;
}

/**
* Standard setter for the label which should be shown above the filter form element.
*
* @param string $label The localized string to show above the filter form element.
*/
public function set_filter_label(string $label): void {
$this->filterlabel = $label;
}
}
22 changes: 14 additions & 8 deletions classes/local/access_manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,18 @@ public function require_tenant_manager(): void {
}

/**
* Determines if the current user is a tenant manager.
* Determines if a user is a tenant manager.
*
* @param int $userid the user id of the user to check, or empty/0 if current user should be used
* @param ?tenant $tenant the tenant to use, if not passed or null the currently used tenant is being used
* @return bool true if the current user is a tenant manager
*/
public function is_tenant_manager(?tenant $tenant = null): bool {
public function is_tenant_manager(int $userid = 0, ?tenant $tenant = null): bool {
global $USER;
if (has_capability('local/ai_manager:managetenants', \context_system::instance())) {
if (empty($userid)) {
$userid = $USER->id;
}
if (has_capability('local/ai_manager:managetenants', \context_system::instance(), $userid)) {
return true;
}

Expand All @@ -77,15 +81,16 @@ public function is_tenant_manager(?tenant $tenant = null): bool {
// In case of default tenant we get system context here, admin should have all capabilities, so we need no admin check.
$tenantcontext = $tenant->get_context();

$user = empty($userid) ? $USER : \core_user::get_user($userid);
if ($tenantcontext === \context_system::instance()) {
// If the context of the tenant is systemwide, we distinguish between the capabilities "manage" and "managetenants":
// If someone has the manage capability on system context, he/she will also have to be member of the tenant to be able
// to manage it.
$tenantfield = get_config('local_ai_manager', 'tenantcolumn');
return has_capability('local/ai_manager:manage', $tenantcontext) && $tenant->is_tenant_allowed()
&& $USER->{$tenantfield} === $tenant->get_identifier();
return has_capability('local/ai_manager:manage', $tenantcontext, $user) && $tenant->is_tenant_allowed()
&& $user->{$tenantfield} === $tenant->get_sql_identifier();
}
return has_capability('local/ai_manager:manage', $tenantcontext) && $tenant->is_tenant_allowed();
return has_capability('local/ai_manager:manage', $tenantcontext, $user) && $tenant->is_tenant_allowed();
}

/**
Expand All @@ -106,7 +111,7 @@ public function require_tenant_member(): void {
\core\di::get(\core\hook\manager::class)->dispatch($customtenant);

$tenantfield = get_config('local_ai_manager', 'tenantcolumn');
if (empty($USER->{$tenantfield}) || $USER->{$tenantfield} !== $this->tenant->get_identifier()) {
if (empty($USER->{$tenantfield}) || $USER->{$tenantfield} !== $this->tenant->get_sql_identifier()) {
throw new \moodle_exception('You must not access information for the tenant '
. $this->tenant->get_identifier() . '!');
}
Expand All @@ -119,10 +124,11 @@ public function require_tenant_member(): void {
* @return bool true if the current user is allowed to manage the instance
*/
public function can_manage_connectorinstance(base_instance $instance) {
global $USER;
if (has_capability('local/ai_manager:managetenants', \context_system::instance())) {
return true;
}
if ($this->is_tenant_manager(new tenant($instance->get_tenant()))) {
if ($this->is_tenant_manager($USER->id, new tenant($instance->get_tenant()))) {
return has_capability('local/ai_manager:manage', $this->tenant->get_context());
}
return false;
Expand Down
45 changes: 38 additions & 7 deletions classes/local/rights_config_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ class rights_config_table extends table_sql {
* @param tenant $tenant the tenant to display the table for
* @param moodle_url $baseurl the current base url on which the table is being displayed
* @param array $filterids list of ids to filter
* @param array $rolefilterids list of role ids to filter
*/
public function __construct(
string $uniqid,
tenant $tenant,
moodle_url $baseurl,
array $filterids,
array $rolefilterids
) {
global $DB;
parent::__construct($uniqid);
$this->set_attribute('id', $uniqid);
$this->define_baseurl($baseurl);
Expand All @@ -68,11 +71,37 @@ public function __construct(

$tenantfield = get_config('local_ai_manager', 'tenantcolumn');

// This is a nightmare concerning performance, but showing the rights config table while also filtering roles does not
// happen very often, so we should be fine.
// On the other hand we cannot just apply the filter SQL to the table sql, because there is no SQL way to determine the
// roles for users who do not have a userinfo record yet.
$rolewhere = '';
$roleparams = [];
if (!empty($rolefilterids)) {
$rolefiltersql = "SELECT u.id as userid, ui.role as role FROM {user} u "
. "LEFT JOIN {local_ai_manager_userinfo} ui ON u.id = ui.userid "
. "WHERE u.deleted != 1 AND u.suspended != 1 AND " . $tenantfield . " = :tenant";
$rolefilterparams = ['tenant' => $tenant->get_sql_identifier()];
$records = $DB->get_records_sql($rolefiltersql, $rolefilterparams);
$roleuserids = [];
foreach ($records as $record) {
$userinfo = new userinfo($record->userid);
$role = $record->role === null ? $userinfo->get_default_role() : $record->role;
if (in_array($role, $rolefilterids)) {
$roleuserids[] = $record->userid;
}
}
if (!empty($roleuserids)) {
[$insql, $roleparams] = $DB->get_in_or_equal($roleuserids, SQL_PARAMS_NAMED);
$rolewhere = ' AND u.id ' . $insql;
}
}

$fields = 'u.id as id, lastname, firstname, role, locked, ui.confirmed';
$from =
'{user} u LEFT JOIN {local_ai_manager_userinfo} ui ON u.id = ui.userid';
$where = 'u.deleted != 1 AND u.suspended != 1 AND ' . $tenantfield . ' = :tenant';
$params = ['tenant' => $tenant->get_sql_identifier()];
$where = 'u.deleted != 1 AND u.suspended != 1 AND ' . $tenantfield . ' = :tenant' . $rolewhere;
$params = array_merge(['tenant' => $tenant->get_sql_identifier()], $roleparams);

$usertableextend = new usertable_extend($tenant, $columns, $headers, $filterids, $fields, $from, $where, $params);
\core\di::get(\core\hook\manager::class)->dispatch($usertableextend);
Expand All @@ -82,17 +111,19 @@ public function __construct(
$this->define_headers($usertableextend->get_headers());

$this->no_sorting('checkbox');
$this->no_sorting('role');
$this->no_sorting('locked');
$this->no_sorting('confirmed');
$this->collapsible(false);
$this->sortable(true, 'lastname');

$this->set_count_sql(
"SELECT COUNT(DISTINCT id) FROM {user} WHERE " . $tenantfield . " = :tenant",
['tenant' => $tenant->get_sql_identifier()]
);

$this->set_sql($usertableextend->get_fields(), $usertableextend->get_from(),
$usertableextend->get_where() . ' GROUP BY u.id',
$usertableextend->get_params());
// We need to use this because we are using "GROUP BY" which is not being expected by the sql table.
$this->set_count_sql("SELECT COUNT(*) FROM (SELECT " . $usertableextend->get_fields() . " FROM "
. $usertableextend->get_from() . " WHERE " . $usertableextend->get_where() . " GROUP BY u.id) AS subquery",
$usertableextend->get_params());
parent::setup();
}

Expand Down
2 changes: 1 addition & 1 deletion classes/local/userinfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public function load(): void {
public function get_default_role() {
$accessmanager = \core\di::get(access_manager::class);
if (\core\di::get(tenant::class)->is_default_tenant()) {
return $accessmanager->is_tenant_manager() ? self::ROLE_UNLIMITED : self::ROLE_BASIC;
return $accessmanager->is_tenant_manager($this->userid) ? self::ROLE_UNLIMITED : self::ROLE_BASIC;
}

$userinfoextend = new userinfo_extend($this->userid);
Expand Down
2 changes: 1 addition & 1 deletion lang/de/local_ai_manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
$string['exception_http429'] = 'Es wurden zu viele oder zu große Anfragen in einem bestimmten Zeitraum an das KI-Tool gesendet. Bitte versuchen Sie es später erneut.';
$string['exception_http500'] = 'Ein interner Serverfehler des KI-Tools ist aufgetreten.';
$string['female'] = 'Weiblich';
$string['filterheading'] = 'Filter';
$string['filterroles'] = 'Rollen filtern';
$string['formvalidation_editinstance_azureapiversion'] = 'Sie müssen die API-Version Ihrer Azure-Resource eingeben';
$string['formvalidation_editinstance_azuredeploymentid'] = 'Sie müssen die Deployment-ID Ihrer Azure-Resource eingeben';
$string['formvalidation_editinstance_azureresourcename'] = 'Sie müssen den Namen Ihrer Azure-Resource eingeben';
Expand Down
2 changes: 1 addition & 1 deletion lang/en/local_ai_manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
$string['exception_http429'] = 'There have been sent too many or too big requests to the AI tool in a certain amount of time. Please try again later.';
$string['exception_http500'] = 'An internal server error of the AI tool occurred';
$string['female'] = 'Female';
$string['filterheading'] = 'Filter';
$string['filterroles'] = 'Filter roles';
$string['formvalidation_editinstance_azureapiversion'] = 'You must provide the api version of your Azure Resource';
$string['formvalidation_editinstance_azuredeploymentid'] = 'You must provide the deployment id of your Azure Resource';
$string['formvalidation_editinstance_azureresourcename'] = 'You must provide the resource name of your Azure Resource';
Expand Down
Loading

0 comments on commit bae2fe0

Please sign in to comment.