Merge branch 'MDL-68612-master-integration' of git://github.com/mickhawkins/moodle

This commit is contained in:
Jun Pataleta 2020-06-03 15:39:36 +08:00
commit 7fcdd9d983
44 changed files with 1629 additions and 797 deletions

View File

@ -30,10 +30,11 @@ Feature: Rename roles within a course
Then "Tutor" "button" should exist
And "Learner" "button" should exist
And I navigate to course participants
And I open the autocomplete suggestions list
And I should see "Role: Tutor" in the ".form-autocomplete-suggestions" "css_element"
And I should see "Role: Learner" in the ".form-autocomplete-suggestions" "css_element"
And I should not see "Role: Student" in the ".form-autocomplete-suggestions" "css_element"
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I should see "Tutor" in the ".form-autocomplete-suggestions" "css_element"
And I should see "Learner" in the ".form-autocomplete-suggestions" "css_element"
And I should not see "Student" in the ".form-autocomplete-suggestions" "css_element"
And I am on "Course 1" course homepage
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
@ -45,6 +46,7 @@ Feature: Rename roles within a course
And "Student" "button" should exist
And "Learner" "button" should not exist
And I navigate to course participants
And I open the autocomplete suggestions list
And I should see "Role: Non-editing teacher" in the ".form-autocomplete-suggestions" "css_element"
And I should see "Role: Student" in the ".form-autocomplete-suggestions" "css_element"
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I should see "Non-editing teacher" in the ".form-autocomplete-suggestions" "css_element"
And I should see "Student" in the ".form-autocomplete-suggestions" "css_element"

View File

@ -49,14 +49,17 @@ Feature: Organize students into groups
And the "members" select box should not contain "Student 0 (student0@example.com)"
And the "members" select box should not contain "Student 1 (student1@example.com)"
And I navigate to course participants
And I open the autocomplete suggestions list
And I click on "Group: Group 1" item in the autocomplete list
And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Group 1" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 0"
And I should see "Student 1"
And I should not see "Student 2"
And I click on "Group: Group 1" "text" in the ".form-autocomplete-selection" "css_element"
And I open the autocomplete suggestions list
And I click on "Group: Group 2" item in the autocomplete list
And I click on "Remove \"Group 1\" from filter" "button" in the "Filter 1" "fieldset"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Group 2" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 2"
And I should see "Student 3"
And I should not see "Student 0"

View File

@ -41,24 +41,32 @@ Feature: The description of a group can be viewed by students and teachers
And I add "Student 2 (student2@example.com)" user to "Group B" group members
And I am on "Course 1" course homepage
And I navigate to course participants
And I open the autocomplete suggestions list
And I click on "Group: Group A" item in the autocomplete list
And I click on "Student 1" "link" in the "participants" "table"
And I click on "Group A" "link"
And I should see "Description for Group A"
And ".groupinfobox" "css_element" should exist
And I should see "Description for Group A"
And I click on "Group: Group A" "autocomplete_selection"
And I open the autocomplete suggestions list
And I click on "Group: Group B" item in the autocomplete list
And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Group B" "list_item"
And I click on "Apply filters" "button"
And I click on "Student 2" "link" in the "participants" "table"
And I click on "Group B" "link"
And I should see "Student 2" in the "participants" "table"
And ".groupinfobox" "css_element" should not exist
And I log out
When I log in as "student1"
And I am on "Course 1" course homepage
And I navigate to course participants
And I click on "Student 1" "link" in the "participants" "table"
And I click on "Group A" "link"
Then I should see "Description for Group A"
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I navigate to course participants
And I click on "Student 2" "link" in the "participants" "table"
And I click on "Group B" "link"
And I should see "Student 2" in the "participants" "table"
And ".groupinfobox" "css_element" should not exist
@javascript
@ -83,22 +91,31 @@ Feature: The description of a group can be viewed by students and teachers
And I add "Student 2 (student2@example.com)" user to "Group B" group members
And I am on "Course 1" course homepage
And I navigate to course participants
And I open the autocomplete suggestions list
And I click on "Group: Group A" item in the autocomplete list
And I click on "Student 1" "link" in the "participants" "table"
And I click on "Group A" "link"
And I should see "Description for Group A"
And ".groupinfobox" "css_element" should exist
And I click on "Group: Group A" "autocomplete_selection"
And I open the autocomplete suggestions list
And I click on "Group: Group B" item in the autocomplete list
And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Group B" "list_item"
And I click on "Apply filters" "button"
And I click on "Student 2" "link" in the "participants" "table"
And I click on "Group B" "link"
And ".groupinfobox" "css_element" should not exist
And I log out
When I log in as "student1"
And I am on "Course 1" course homepage
And I navigate to course participants
Then I should not see "Description for Group A"
And I click on "Student 1" "link" in the "participants" "table"
And I click on "Group A" "link"
And I should see "Student 1" in the "participants" "table"
And I should not see "Description for Group A"
And ".groupinfobox" "css_element" should not exist
And I log out
And I log in as "student2"
And I am on "Course 1" course homepage
And I navigate to course participants
And I click on "Student 2" "link" in the "participants" "table"
And I click on "Group B" "link"
And I should see "Student 2" in the "participants" "table"
And ".groupinfobox" "css_element" should not exist

View File

@ -144,3 +144,4 @@ pacific/yap,core_timezones
editsettings,core_badges
availablelicenses,core_admin
managelicenses,core_admin
userfilterplaceholder,core

View File

@ -2166,7 +2166,6 @@ $string['userdescription'] = 'Description';
$string['userdescription_help'] = 'This box enables you to enter some text about yourself which will then be displayed on your profile page for others to view.';
$string['userdetails'] = 'User details';
$string['userfiles'] = 'User files';
$string['userfilterplaceholder'] = 'Search keyword or select filter';
$string['userlist'] = 'User list';
$string['usermenu'] = 'User menu';
$string['username'] = 'Username';
@ -2292,3 +2291,4 @@ $string['sitemessage'] = 'Message users';
// Deprecated since Moodle 3.9.
$string['participantscount'] = 'Number of participants: {$a}';
$string['userfilterplaceholder'] = 'Search keyword or select filter';

View File

@ -29,7 +29,9 @@ $string['adverbfor_or'] = 'or';
$string['applyfilters'] = 'Apply filters';
$string['clearfilterrow'] = 'Remove filter row';
$string['clearfilters'] = 'Clear filters';
$string['clearfilterselection'] = 'Remove "{$a}" from filter';
$string['countparticipantsfound'] = '{$a} participants found';
$string['filterrowlegend'] = 'Filter {$a}';
$string['filtersetmatchdescription'] = 'How multiple filters should be combined';
$string['match'] = 'Match';
$string['matchofthefollowing'] = 'of the following:';

View File

@ -230,7 +230,12 @@ class behat_core_generator extends behat_generator_base {
'datagenerator' => 'setup_backpack_connected',
'required' => ['user', 'externalbackpack'],
'switchids' => ['user' => 'userid', 'externalbackpack' => 'externalbackpackid']
]
],
'last access times' => [
'datagenerator' => 'last_access_times',
'required' => ['user', 'course', 'lastaccess'],
'switchids' => ['user' => 'userid', 'course' => 'courseid'],
],
];
}
@ -951,4 +956,100 @@ class behat_core_generator extends behat_generator_base {
$backpack->externalbackpackid = $data['externalbackpackid'];
$DB->insert_record('badge_backpack', $backpack);
}
/**
* Creates user last access data within given courses.
*
* @param array $data
* @return void
*/
protected function process_last_access_times(array $data) {
global $DB;
if (!isset($data['userid'])) {
throw new Exception('\'last acces times\' requires the field \'user\' to be specified');
}
if (!isset($data['courseid'])) {
throw new Exception('\'last acces times\' requires the field \'course\' to be specified');
}
if (!isset($data['lastaccess'])) {
throw new Exception('\'last acces times\' requires the field \'lastaccess\' to be specified');
}
$userdata = [];
$userdata['old'] = $DB->get_record('user', ['id' => $data['userid']], 'firstaccess, lastaccess, lastlogin, currentlogin');
$userdata['new'] = [
'firstaccess' => $userdata['old']->firstaccess,
'lastaccess' => $userdata['old']->lastaccess,
'lastlogin' => $userdata['old']->lastlogin,
'currentlogin' => $userdata['old']->currentlogin,
];
// Check for lastaccess data for this course.
$lastaccessdata = [
'userid' => $data['userid'],
'courseid' => $data['courseid'],
];
$lastaccessid = $DB->get_field('user_lastaccess', 'id', $lastaccessdata);
$dbdata = (object) $lastaccessdata;
$dbdata->timeaccess = $data['lastaccess'];
// Set the course last access time.
if ($lastaccessid) {
$dbdata->id = $lastaccessid;
$DB->update_record('user_lastaccess', $dbdata);
} else {
$DB->insert_record('user_lastaccess', $dbdata);
}
// Store changes to other user access times as needed.
// Update first access if this is the user's first login, or this access is earlier than their current first access.
if (empty($userdata['new']['firstaccess']) ||
$userdata['new']['firstaccess'] > $data['lastaccess']) {
$userdata['new']['firstaccess'] = $data['lastaccess'];
}
// Update last access if it is the user's most recent access.
if (empty($userdata['new']['lastaccess']) ||
$userdata['new']['lastaccess'] < $data['lastaccess']) {
$userdata['new']['lastaccess'] = $data['lastaccess'];
}
// Update last and current login if it is the user's most recent access.
if (empty($userdata['new']['lastlogin']) ||
$userdata['new']['lastlogin'] < $data['lastaccess']) {
$userdata['new']['lastlogin'] = $data['lastaccess'];
$userdata['new']['currentlogin'] = $data['lastaccess'];
}
$updatedata = [];
if ($userdata['new']['firstaccess'] != $userdata['old']->firstaccess) {
$updatedata['firstaccess'] = $userdata['new']['firstaccess'];
}
if ($userdata['new']['lastaccess'] != $userdata['old']->lastaccess) {
$updatedata['lastaccess'] = $userdata['new']['lastaccess'];
}
if ($userdata['new']['lastlogin'] != $userdata['old']->lastlogin) {
$updatedata['lastlogin'] = $userdata['new']['lastlogin'];
}
if ($userdata['new']['currentlogin'] != $userdata['old']->currentlogin) {
$updatedata['currentlogin'] = $userdata['new']['currentlogin'];
}
// Only update user access data if there have been any changes.
if (!empty($updatedata)) {
$updatedata['id'] = $data['userid'];
$updatedata = (object) $updatedata;
$DB->update_record('user', $updatedata);
}
}
}

View File

@ -3559,3 +3559,267 @@ function cron_bc_hack_plugin_functions($plugintype, $plugins) {
return $plugins;
}
/**
* Returns the SQL used by the participants table.
*
* @deprecated since Moodle 3.9 MDL-68612 - See \core_user\table\participants_search for an improved way to fetch participants.
* @param int $courseid The course id
* @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
* @param int $accesssince The time since last access, 0 means any time
* @param int $roleid The role id, 0 means all roles and -1 no roles
* @param int $enrolid The enrolment id, 0 means all enrolment methods will be returned.
* @param int $statusid The user enrolment status, -1 means all enrolments regardless of the status will be returned, if allowed.
* @param string|array $search The search that was performed, empty means perform no search
* @param string $additionalwhere Any additional SQL to add to where
* @param array $additionalparams The additional params
* @return array
*/
function user_get_participants_sql($courseid, $groupid = 0, $accesssince = 0, $roleid = 0, $enrolid = 0, $statusid = -1,
$search = '', $additionalwhere = '', $additionalparams = array()) {
global $DB, $USER, $CFG;
$deprecatedtext = __FUNCTION__ . '() is deprecated. ' .
'Please use \core\table\participants_search::class with table filtersets instead.';
debugging($deprecatedtext, DEBUG_DEVELOPER);
// Get the context.
$context = \context_course::instance($courseid, MUST_EXIST);
$isfrontpage = ($courseid == SITEID);
// Default filter settings. We only show active by default, especially if the user has no capability to review enrolments.
$onlyactive = true;
$onlysuspended = false;
if (has_capability('moodle/course:enrolreview', $context) && (has_capability('moodle/course:viewsuspendedusers', $context))) {
switch ($statusid) {
case ENROL_USER_ACTIVE:
// Nothing to do here.
break;
case ENROL_USER_SUSPENDED:
$onlyactive = false;
$onlysuspended = true;
break;
default:
// If the user has capability to review user enrolments, but statusid is set to -1, set $onlyactive to false.
$onlyactive = false;
break;
}
}
list($esql, $params) = get_enrolled_sql($context, null, $groupid, $onlyactive, $onlysuspended, $enrolid);
$joins = array('FROM {user} u');
$wheres = array();
$userfields = get_extra_user_fields($context);
$userfieldssql = user_picture::fields('u', $userfields);
if ($isfrontpage) {
$select = "SELECT $userfieldssql, u.lastaccess";
$joins[] = "JOIN ($esql) e ON e.id = u.id"; // Everybody on the frontpage usually.
if ($accesssince) {
$wheres[] = user_get_user_lastaccess_sql($accesssince);
}
} else {
$select = "SELECT $userfieldssql, COALESCE(ul.timeaccess, 0) AS lastaccess";
$joins[] = "JOIN ($esql) e ON e.id = u.id"; // Course enrolled users only.
// Not everybody has accessed the course yet.
$joins[] = 'LEFT JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = :courseid)';
$params['courseid'] = $courseid;
if ($accesssince) {
$wheres[] = user_get_course_lastaccess_sql($accesssince);
}
}
// Performance hacks - we preload user contexts together with accounts.
$ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
$ccjoin = 'LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)';
$params['contextlevel'] = CONTEXT_USER;
$select .= $ccselect;
$joins[] = $ccjoin;
// Limit list to users with some role only.
if ($roleid) {
// We want to query both the current context and parent contexts.
list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true),
SQL_PARAMS_NAMED, 'relatedctx');
// Get users without any role.
if ($roleid == -1) {
$wheres[] = "u.id NOT IN (SELECT userid FROM {role_assignments} WHERE contextid $relatedctxsql)";
$params = array_merge($params, $relatedctxparams);
} else {
$wheres[] = "u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)";
$params = array_merge($params, array('roleid' => $roleid), $relatedctxparams);
}
}
if (!empty($search)) {
if (!is_array($search)) {
$search = [$search];
}
foreach ($search as $index => $keyword) {
$searchkey1 = 'search' . $index . '1';
$searchkey2 = 'search' . $index . '2';
$searchkey3 = 'search' . $index . '3';
$searchkey4 = 'search' . $index . '4';
$searchkey5 = 'search' . $index . '5';
$searchkey6 = 'search' . $index . '6';
$searchkey7 = 'search' . $index . '7';
$conditions = array();
// Search by fullname.
$fullname = $DB->sql_fullname('u.firstname', 'u.lastname');
$conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
// Search by email.
$email = $DB->sql_like('email', ':' . $searchkey2, false, false);
if (!in_array('email', $userfields)) {
$maildisplay = 'maildisplay' . $index;
$userid1 = 'userid' . $index . '1';
// Prevent users who hide their email address from being found by others
// who aren't allowed to see hidden email addresses.
$email = "(". $email ." AND (" .
"u.maildisplay <> :$maildisplay " .
"OR u.id = :$userid1". // User can always find himself.
"))";
$params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
$params[$userid1] = $USER->id;
}
$conditions[] = $email;
// Search by idnumber.
$idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false);
if (!in_array('idnumber', $userfields)) {
$userid2 = 'userid' . $index . '2';
// Users who aren't allowed to see idnumbers should at most find themselves
// when searching for an idnumber.
$idnumber = "(". $idnumber . " AND u.id = :$userid2)";
$params[$userid2] = $USER->id;
}
$conditions[] = $idnumber;
if (!empty($CFG->showuseridentity)) {
// Search all user identify fields.
$extrasearchfields = explode(',', $CFG->showuseridentity);
foreach ($extrasearchfields as $extrasearchfield) {
if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
// Already covered above. Search by country not supported.
continue;
}
$param = $searchkey3 . $extrasearchfield;
$condition = $DB->sql_like($extrasearchfield, ':' . $param, false, false);
$params[$param] = "%$keyword%";
if (!in_array($extrasearchfield, $userfields)) {
// User cannot see this field, but allow match if their own account.
$userid3 = 'userid' . $index . '3' . $extrasearchfield;
$condition = "(". $condition . " AND u.id = :$userid3)";
$params[$userid3] = $USER->id;
}
$conditions[] = $condition;
}
}
// Search by middlename.
$middlename = $DB->sql_like('middlename', ':' . $searchkey4, false, false);
$conditions[] = $middlename;
// Search by alternatename.
$alternatename = $DB->sql_like('alternatename', ':' . $searchkey5, false, false);
$conditions[] = $alternatename;
// Search by firstnamephonetic.
$firstnamephonetic = $DB->sql_like('firstnamephonetic', ':' . $searchkey6, false, false);
$conditions[] = $firstnamephonetic;
// Search by lastnamephonetic.
$lastnamephonetic = $DB->sql_like('lastnamephonetic', ':' . $searchkey7, false, false);
$conditions[] = $lastnamephonetic;
$wheres[] = "(". implode(" OR ", $conditions) .") ";
$params[$searchkey1] = "%$keyword%";
$params[$searchkey2] = "%$keyword%";
$params[$searchkey3] = "%$keyword%";
$params[$searchkey4] = "%$keyword%";
$params[$searchkey5] = "%$keyword%";
$params[$searchkey6] = "%$keyword%";
$params[$searchkey7] = "%$keyword%";
}
}
if (!empty($additionalwhere)) {
$wheres[] = $additionalwhere;
$params = array_merge($params, $additionalparams);
}
$from = implode("\n", $joins);
if ($wheres) {
$where = 'WHERE ' . implode(' AND ', $wheres);
} else {
$where = '';
}
return array($select, $from, $where, $params);
}
/**
* Returns the total number of participants for a given course.
*
* @deprecated since Moodle 3.9 MDL-68612 - See \core_user\table\participants_search for an improved way to fetch participants.
* @param int $courseid The course id
* @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
* @param int $accesssince The time since last access, 0 means any time
* @param int $roleid The role id, 0 means all roles
* @param int $enrolid The applied filter for the user enrolment ID.
* @param int $status The applied filter for the user's enrolment status.
* @param string|array $search The search that was performed, empty means perform no search
* @param string $additionalwhere Any additional SQL to add to where
* @param array $additionalparams The additional params
* @return int
*/
function user_get_total_participants($courseid, $groupid = 0, $accesssince = 0, $roleid = 0, $enrolid = 0, $statusid = -1,
$search = '', $additionalwhere = '', $additionalparams = array()) {
global $DB;
$deprecatedtext = __FUNCTION__ . '() is deprecated. ' .
'Please use \core\table\participants_search::class with table filtersets instead.';
debugging($deprecatedtext, DEBUG_DEVELOPER);
list($select, $from, $where, $params) = user_get_participants_sql($courseid, $groupid, $accesssince, $roleid, $enrolid,
$statusid, $search, $additionalwhere, $additionalparams);
return $DB->count_records_sql("SELECT COUNT(u.id) $from $where", $params);
}
/**
* Returns the participants for a given course.
*
* @deprecated since Moodle 3.9 MDL-68612 - See \core_user\table\participants_search for an improved way to fetch participants.
* @param int $courseid The course id
* @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
* @param int $accesssince The time since last access
* @param int $roleid The role id
* @param int $enrolid The applied filter for the user enrolment ID.
* @param int $status The applied filter for the user's enrolment status.
* @param string $search The search that was performed
* @param string $additionalwhere Any additional SQL to add to where
* @param array $additionalparams The additional params
* @param string $sort The SQL sort
* @param int $limitfrom return a subset of records, starting at this point (optional).
* @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
* @return moodle_recordset
*/
function user_get_participants($courseid, $groupid = 0, $accesssince, $roleid, $enrolid = 0, $statusid, $search,
$additionalwhere = '', $additionalparams = array(), $sort = '', $limitfrom = 0, $limitnum = 0) {
global $DB;
$deprecatedtext = __FUNCTION__ . '() is deprecated. ' .
'Please use \core\table\participants_search::class with table filtersets instead.';
debugging($deprecatedtext, DEBUG_DEVELOPER);
list($select, $from, $where, $params) = user_get_participants_sql($courseid, $groupid, $accesssince, $roleid, $enrolid,
$statusid, $search, $additionalwhere, $additionalparams);
return $DB->get_recordset_sql("$select $from $where $sort", $params, $limitfrom, $limitnum);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -158,6 +158,18 @@ export const updateTable = (tableRoot, {
}
};
/**
* Get the table dataset for the specified tableRoot, ensuring that the provided table is a dynamic table.
*
* @param {HTMLElement} tableRoot
* @returns {DOMStringMap}
*/
const getTableData = tableRoot => {
checkTableIsDynamic(tableRoot);
return tableRoot.dataset;
};
/**
* Update the specified table using the new filters.
*
@ -169,6 +181,18 @@ export const updateTable = (tableRoot, {
export const setFilters = (tableRoot, filters, refreshContent = true) =>
updateTable(tableRoot, {filters}, refreshContent);
/**
* Get the filter data for the specified table.
*
* @param {HTMLElement} tableRoot
* @returns {Object}
*/
export const getFilters = tableRoot => {
checkTableIsDynamic(tableRoot);
return getFiltersetFromTable(tableRoot);
};
/**
* Update the sort order.
*
@ -192,6 +216,14 @@ export const setSortOrder = (tableRoot, sortBy, sortOrder, refreshContent = true
export const setPageNumber = (tableRoot, pageNumber, refreshContent = true) =>
updateTable(tableRoot, {pageNumber}, refreshContent);
/**
* Get the current page number.
*
* @param {HTMLElement} tableRoot
* @returns {Number}
*/
export const getPageNumber = tableRoot => getTableData(tableRoot).tablePageNumber;
/**
* Set the page size.
*
@ -203,6 +235,14 @@ export const setPageNumber = (tableRoot, pageNumber, refreshContent = true) =>
export const setPageSize = (tableRoot, pageSize, refreshContent = true) =>
updateTable(tableRoot, {pageSize, pageNumber: 0}, refreshContent);
/**
* Get the current page size.
*
* @param {HTMLElement} tableRoot
* @returns {Number}
*/
export const getPageSize = tableRoot => getTableData(tableRoot).tablePageSize;
/**
* Update the first initial to show.
*
@ -214,6 +254,14 @@ export const setPageSize = (tableRoot, pageSize, refreshContent = true) =>
export const setFirstInitial = (tableRoot, firstInitial, refreshContent = true) =>
updateTable(tableRoot, {firstInitial}, refreshContent);
/**
* Get the current first initial filter.
*
* @param {HTMLElement} tableRoot
* @returns {String}
*/
export const getFirstInitial = tableRoot => getTableData(tableRoot).tableFirstInitial;
/**
* Update the last initial to show.
*
@ -225,6 +273,14 @@ export const setFirstInitial = (tableRoot, firstInitial, refreshContent = true)
export const setLastInitial = (tableRoot, lastInitial, refreshContent = true) =>
updateTable(tableRoot, {lastInitial}, refreshContent);
/**
* Get the current last initial filter.
*
* @param {HTMLElement} tableRoot
* @returns {String}
*/
export const getLastInitial = tableRoot => getTableData(tableRoot).tableLastInitial;
/**
* Hide a column in the participants table.
*

View File

@ -41,11 +41,8 @@ use Iterator;
*/
class filter implements Countable, Iterator, JsonSerializable {
/**
* @var in The default filter type (ALL)
* Note: This is for backwards compatibility with the old UI behaviour and will be set to JOINTYPE_ANY as part of MDL-68612.
*/
const JOINTYPE_DEFAULT = 2;
/** @var in The default filter type (ANY) */
const JOINTYPE_DEFAULT = 1;
/** @var int None of the following match */
const JOINTYPE_NONE = 0;

View File

@ -40,11 +40,8 @@ use moodle_exception;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class filterset implements JsonSerializable {
/**
* @var in The default filter type (ALL)
* Note: This is for backwards compatibility with the old UI behaviour and will be set to JOINTYPE_ANY as part of MDL-68612.
*/
const JOINTYPE_DEFAULT = 2;
/** @var in The default filter type (ANY) */
const JOINTYPE_DEFAULT = 1;
/** @var int None of the following match */
const JOINTYPE_NONE = 0;

View File

@ -1,2 +1,2 @@
define ("core_user/local/participantsfilter/filter",["exports","core/form-autocomplete","./selectors","core/str"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=e(b);c=e(c);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}function h(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function i(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function j(a,b,c){if(b)i(a.prototype,b);if(c)i(a,c);return a}var k=function(a){return a.querySelectorAll(":checked")},l=function(){function a(b,c){h(this,a);this.filterType=b;this.rootNode=c;this.addValueSelector()}j(a,[{key:"tearDown",value:function tearDown(){}},{key:"addValueSelector",value:function(){var a=g(regeneratorRuntime.mark(function a(){var c,d;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:c=this.getFilterValueNode();c.innerHTML=this.getSourceDataForFilter().outerHTML;d=c.querySelector("select");a.t0=b.default;a.t1=d;a.t2="1"==d.dataset.allowCustom;a.next=8;return this.placeholder;case 8:a.t3=a.sent;a.t4=this.showSuggestions;a.t5=!d.multiple;a.t6={items:"core_user/local/participantsfilter/autocomplete_selection_items",layout:"core_user/local/participantsfilter/autocomplete_layout",selection:"core_user/local/participantsfilter/autocomplete_selection"};a.t0.enhance.call(a.t0,a.t1,a.t2,null,a.t3,!1,a.t4,null,a.t5,a.t6);case 13:case"end":return a.stop();}}},a,this)}));return function addValueSelector(){return a.apply(this,arguments)}}()},{key:"getSourceDataForFilter",value:function getSourceDataForFilter(){var a=this.rootNode.querySelector(c.default.filterset.regions.datasource);return a.querySelector(c.default.data.fields.byName(this.filterType))}},{key:"getFilterValueNode",value:function getFilterValueNode(){return this.filterRoot.querySelector(c.default.filter.regions.values)}},{key:"placeholder",get:function get(){return(0,d.get_string)("placeholdertypeorselect","core_user")}},{key:"showSuggestions",get:function get(){return!0}},{key:"filterRoot",get:function get(){return this.rootNode.querySelector(c.default.filter.byName(this.filterType))}},{key:"name",get:function get(){return this.filterType}},{key:"jointype",get:function get(){return this.filterRoot.querySelector(c.default.filter.fields.join).value}},{key:"rawValues",get:function get(){var a=this.getFilterValueNode(),b=a.querySelector("select");return Object.values(k(b)).map(function(a){return a.value})}},{key:"values",get:function get(){return this.rawValues.map(function(a){return parseInt(a,10)})}},{key:"filterValue",get:function get(){return{name:this.name,jointype:this.jointype,values:this.values}}}]);return a}();a.default=l;return a.default});
define ("core_user/local/participantsfilter/filter",["exports","core/form-autocomplete","./selectors","core/str"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=e(b);c=e(c);function e(a){return a&&a.__esModule?a:{default:a}}function f(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function g(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var i=a.apply(b,c);function g(a){f(i,d,e,g,h,"next",a)}function h(a){f(i,d,e,g,h,"throw",a)}g(void 0)})}}function h(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function i(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function j(a,b,c){if(b)i(a.prototype,b);if(c)i(a,c);return a}var k=function(a){return a.querySelectorAll(":checked")},l=function(){function a(b,c,d){h(this,a);this.filterType=b;this.rootNode=c;this.addValueSelector(d)}j(a,[{key:"tearDown",value:function tearDown(){}},{key:"addValueSelector",value:function(){var a=g(regeneratorRuntime.mark(function a(){var c=this,d,e,f,g=arguments;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:d=0<g.length&&g[0]!==void 0?g[0]:[];e=this.getFilterValueNode();e.innerHTML=this.getSourceDataForFilter().outerHTML;f=e.querySelector("select");d.forEach(function(a){var b=f.querySelector("option[value=\"".concat(a,"\"]"));if(b){b.selected=!0}else if(!c.showSuggestions){b=document.createElement("option");b.value=a;b.innerHTML=a;b.selected=!0;f.append(b)}});a.t0=b.default;a.t1=f;a.t2="1"==f.dataset.allowCustom;a.next=10;return this.placeholder;case 10:a.t3=a.sent;a.t4=this.showSuggestions;a.t5=!f.multiple;a.t6={items:"core_user/local/participantsfilter/autocomplete_selection_items",layout:"core_user/local/participantsfilter/autocomplete_layout",selection:"core_user/local/participantsfilter/autocomplete_selection"};a.t0.enhance.call(a.t0,a.t1,a.t2,null,a.t3,!1,a.t4,null,a.t5,a.t6);case 15:case"end":return a.stop();}}},a,this)}));return function addValueSelector(){return a.apply(this,arguments)}}()},{key:"getSourceDataForFilter",value:function getSourceDataForFilter(){var a=this.rootNode.querySelector(c.default.filterset.regions.datasource);return a.querySelector(c.default.data.fields.byName(this.filterType))}},{key:"getFilterValueNode",value:function getFilterValueNode(){return this.filterRoot.querySelector(c.default.filter.regions.values)}},{key:"placeholder",get:function get(){return(0,d.get_string)("placeholdertypeorselect","core_user")}},{key:"showSuggestions",get:function get(){return!0}},{key:"filterRoot",get:function get(){return this.rootNode.querySelector(c.default.filter.byName(this.filterType))}},{key:"name",get:function get(){return this.filterType}},{key:"jointype",get:function get(){return this.filterRoot.querySelector(c.default.filter.fields.join).value}},{key:"rawValues",get:function get(){var a=this.getFilterValueNode(),b=a.querySelector("select");return Object.values(k(b)).map(function(a){return a.value})}},{key:"values",get:function get(){return this.rawValues.map(function(a){return parseInt(a,10)})}},{key:"filterValue",get:function get(){return{name:this.name,jointype:this.jointype,values:this.values}}}]);return a}();a.default=l;return a.default});
//# sourceMappingURL=filter.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
define ("core_user/local/participantsfilter/filtertypes/keyword",["exports","../filter","core/str"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function d(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){d=function(a){return typeof a}}else{d=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return d(a)}function e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function g(a,b,c){if(b)f(a.prototype,b);if(c)f(a,c);return a}function h(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)i(a,b)}function i(a,b){i=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return i(a,b)}function j(a){return function(){var b=n(a),c;if(m()){var d=n(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return k(this,c)}}function k(a,b){if(b&&("object"===d(b)||"function"==typeof b)){return b}return l(a)}function l(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function m(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function n(a){n=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return n(a)}var o=function(a){h(b,a);var d=j(b);function b(a,c){e(this,b);return d.call(this,a,c)}g(b,[{key:"values",get:function get(){return this.rawValues}},{key:"placeholder",get:function get(){return(0,c.get_string)("placeholdertype","core_user")}},{key:"showSuggestions",get:function get(){return!1}}]);return b}(b.default);a.default=o;return a.default});
define ("core_user/local/participantsfilter/filtertypes/keyword",["exports","../filter","core/str"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);function d(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){d=function(a){return typeof a}}else{d=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return d(a)}function e(a,b){if(!(a instanceof b)){throw new TypeError("Cannot call a class as a function")}}function f(a,b){for(var c=0,d;c<b.length;c++){d=b[c];d.enumerable=d.enumerable||!1;d.configurable=!0;if("value"in d)d.writable=!0;Object.defineProperty(a,d.key,d)}}function g(a,b,c){if(b)f(a.prototype,b);if(c)f(a,c);return a}function h(a,b){if("function"!=typeof b&&null!==b){throw new TypeError("Super expression must either be null or a function")}a.prototype=Object.create(b&&b.prototype,{constructor:{value:a,writable:!0,configurable:!0}});if(b)i(a,b)}function i(a,b){i=Object.setPrototypeOf||function(a,b){a.__proto__=b;return a};return i(a,b)}function j(a){return function(){var b=n(a),c;if(m()){var d=n(this).constructor;c=Reflect.construct(b,arguments,d)}else{c=b.apply(this,arguments)}return k(this,c)}}function k(a,b){if(b&&("object"===d(b)||"function"==typeof b)){return b}return l(a)}function l(a){if(void 0===a){throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}return a}function m(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{Date.prototype.toString.call(Reflect.construct(Date,[],function(){}));return!0}catch(a){return!1}}function n(a){n=Object.setPrototypeOf?Object.getPrototypeOf:function(a){return a.__proto__||Object.getPrototypeOf(a)};return n(a)}var o=function(a){h(b,a);var d=j(b);function b(){e(this,b);return d.apply(this,arguments)}g(b,[{key:"values",get:function get(){return this.rawValues}},{key:"placeholder",get:function get(){return(0,c.get_string)("placeholdertype","core_user")}},{key:"showSuggestions",get:function get(){return!1}}]);return b}(b.default);a.default=o;return a.default});
//# sourceMappingURL=keyword.min.js.map

View File

@ -1 +1 @@
{"version":3,"sources":["../../../../src/local/participantsfilter/filtertypes/keyword.js"],"names":["filterType","filterSet","rawValues","Filter"],"mappings":"mMAuBA,uD,gqDAII,WAAYA,CAAZ,CAAwBC,CAAxB,CAAmC,8BACzBD,CADyB,CACbC,CADa,CAElC,C,qCAOY,CACT,MAAO,MAAKC,SACf,C,uCAOiB,CACd,MAAO,iBAAU,iBAAV,CAA6B,WAA7B,CACV,C,2CAOqB,CAClB,QACH,C,cA9BwBC,S","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Keyword filter.\n *\n * @module core_user/local/participantsfilter/filtertypes/keyword\n * @package core_user\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from '../filter';\nimport {get_string as getString} from 'core/str';\n\nexport default class extends Filter {\n constructor(filterType, filterSet) {\n super(filterType, filterSet);\n }\n\n /**\n * For keywords the final value is an Array of strings.\n *\n * @returns {Object}\n */\n get values() {\n return this.rawValues;\n }\n\n /**\n * Get the placeholder to use when showing the value selector.\n *\n * @return {Promise} Resolving to a String\n */\n get placeholder() {\n return getString('placeholdertype', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return false;\n }\n}\n"],"file":"keyword.min.js"}
{"version":3,"sources":["../../../../src/local/participantsfilter/filtertypes/keyword.js"],"names":["rawValues","Filter"],"mappings":"mMAuBA,uD,2vDASiB,CACT,MAAO,MAAKA,SACf,C,uCAOiB,CACd,MAAO,iBAAU,iBAAV,CAA6B,WAA7B,CACV,C,2CAOqB,CAClB,QACH,C,cA1BwBC,S","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Keyword filter.\n *\n * @module core_user/local/participantsfilter/filtertypes/keyword\n * @package core_user\n * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Filter from '../filter';\nimport {get_string as getString} from 'core/str';\n\nexport default class extends Filter {\n /**\n * For keywords the final value is an Array of strings.\n *\n * @returns {Object}\n */\n get values() {\n return this.rawValues;\n }\n\n /**\n * Get the placeholder to use when showing the value selector.\n *\n * @return {Promise} Resolving to a String\n */\n get placeholder() {\n return getString('placeholdertype', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return false;\n }\n}\n"],"file":"keyword.min.js"}

View File

@ -1,2 +1,2 @@
define ("core_user/local/participantsfilter/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b=function(a){return"[data-filterregion=\"".concat(a,"\"]")},c=function(a){return"[data-filteraction=\"".concat(a,"\"]")},d=function(a){return"[data-filterfield=\"".concat(a,"\"]")},e={filter:{region:b("filter"),actions:{remove:c("remove")},fields:{join:d("join"),type:d("type")},regions:{values:b("value")},byName:function byName(a){return"".concat(b("filter"),"[data-filter-type=\"").concat(a,"\"]")}},filterset:{region:b("actions"),actions:{addRow:c("add"),applyFilters:c("apply"),resetFilters:c("reset")},regions:{filtermatch:b("filtermatch"),filterlist:b("filters"),datasource:b("filtertypedata")},fields:{join:"".concat(b("filtermatch")," ").concat(d("join"))}},data:{fields:{byName:function byName(a){return"[data-field-name=\"".concat(a,"\"]")},all:"".concat(b("filtertypedata")," [data-field-name]")},typeList:b("filtertypelist")}};a.default=e;return a.default});
define ("core_user/local/participantsfilter/selectors",["exports"],function(a){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var b=function(a){return"[data-filterregion=\"".concat(a,"\"]")},c=function(a){return"[data-filteraction=\"".concat(a,"\"]")},d=function(a){return"[data-filterfield=\"".concat(a,"\"]")},e={filter:{region:b("filter"),actions:{remove:c("remove")},fields:{join:d("join"),type:d("type")},regions:{values:b("value")},byName:function byName(a){return"".concat(b("filter"),"[data-filter-type=\"").concat(a,"\"]")}},filterset:{region:b("actions"),actions:{addRow:c("add"),applyFilters:c("apply"),resetFilters:c("reset")},regions:{filtermatch:b("filtermatch"),filterlist:b("filters"),datasource:b("filtertypedata")},fields:{join:"".concat(b("filtermatch")," ").concat(d("join"))}},data:{fields:{byName:function byName(a){return"[data-field-name=\"".concat(a,"\"]")},all:"".concat(b("filtertypedata")," [data-field-name]")},typeList:b("filtertypelist"),typeListSelect:"select".concat(b("filtertypelist"))}};a.default=e;return a.default});
//# sourceMappingURL=selectors.min.js.map

View File

@ -1 +1 @@
{"version":3,"sources":["../../../src/local/participantsfilter/selectors.js"],"names":["getFilterRegion","region","getFilterAction","action","getFilterField","field","filter","actions","remove","fields","join","type","regions","values","byName","name","filterset","addRow","applyFilters","resetFilters","filtermatch","filterlist","datasource","data","all","typeList"],"mappings":"iKAwBMA,CAAAA,CAAe,CAAG,SAAAC,CAAM,uCAA2BA,CAA3B,Q,CACxBC,CAAe,CAAG,SAAAC,CAAM,uCAA2BA,CAA3B,Q,CACxBC,CAAc,CAAG,SAAAC,CAAK,sCAA0BA,CAA1B,Q,GAEb,CACXC,MAAM,CAAE,CACJL,MAAM,CAAED,CAAe,CAAC,QAAD,CADnB,CAEJO,OAAO,CAAE,CACLC,MAAM,CAAEN,CAAe,CAAC,QAAD,CADlB,CAFL,CAKJO,MAAM,CAAE,CACJC,IAAI,CAAEN,CAAc,CAAC,MAAD,CADhB,CAEJO,IAAI,CAAEP,CAAc,CAAC,MAAD,CAFhB,CALJ,CASJQ,OAAO,CAAE,CACLC,MAAM,CAAEb,CAAe,CAAC,OAAD,CADlB,CATL,CAYJc,MAAM,CAAE,gBAAAC,CAAI,kBAAOf,CAAe,CAAC,QAAD,CAAtB,gCAAsDe,CAAtD,QAZR,CADG,CAeXC,SAAS,CAAE,CACPf,MAAM,CAAED,CAAe,CAAC,SAAD,CADhB,CAEPO,OAAO,CAAE,CACLU,MAAM,CAAEf,CAAe,CAAC,KAAD,CADlB,CAELgB,YAAY,CAAEhB,CAAe,CAAC,OAAD,CAFxB,CAGLiB,YAAY,CAAEjB,CAAe,CAAC,OAAD,CAHxB,CAFF,CAOPU,OAAO,CAAE,CACLQ,WAAW,CAAEpB,CAAe,CAAC,aAAD,CADvB,CAELqB,UAAU,CAAErB,CAAe,CAAC,SAAD,CAFtB,CAGLsB,UAAU,CAAEtB,CAAe,CAAC,gBAAD,CAHtB,CAPF,CAYPS,MAAM,CAAE,CACJC,IAAI,WAAKV,CAAe,CAAC,aAAD,CAApB,aAAuCI,CAAc,CAAC,MAAD,CAArD,CADA,CAZD,CAfA,CA+BXmB,IAAI,CAAE,CACFd,MAAM,CAAE,CACJK,MAAM,CAAE,gBAAAC,CAAI,qCAAyBA,CAAzB,QADR,CAEJS,GAAG,WAAKxB,CAAe,CAAC,gBAAD,CAApB,sBAFC,CADN,CAKFyB,QAAQ,CAAEzB,CAAe,CAAC,gBAAD,CALvB,CA/BK,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module containing the selectors for user filters.\n *\n * @module core_user/local/user_filter/selectors\n * @package core_user\n * @copyright 2020 Michael Hawkins <michaelh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst getFilterRegion = region => `[data-filterregion=\"${region}\"]`;\nconst getFilterAction = action => `[data-filteraction=\"${action}\"]`;\nconst getFilterField = field => `[data-filterfield=\"${field}\"]`;\n\nexport default {\n filter: {\n region: getFilterRegion('filter'),\n actions: {\n remove: getFilterAction('remove'),\n },\n fields: {\n join: getFilterField('join'),\n type: getFilterField('type'),\n },\n regions: {\n values: getFilterRegion('value'),\n },\n byName: name => `${getFilterRegion('filter')}[data-filter-type=\"${name}\"]`,\n },\n filterset: {\n region: getFilterRegion('actions'),\n actions: {\n addRow: getFilterAction('add'),\n applyFilters: getFilterAction('apply'),\n resetFilters: getFilterAction('reset'),\n },\n regions: {\n filtermatch: getFilterRegion('filtermatch'),\n filterlist: getFilterRegion('filters'),\n datasource: getFilterRegion('filtertypedata'),\n },\n fields: {\n join: `${getFilterRegion('filtermatch')} ${getFilterField('join')}`,\n },\n },\n data: {\n fields: {\n byName: name => `[data-field-name=\"${name}\"]`,\n all: `${getFilterRegion('filtertypedata')} [data-field-name]`,\n },\n typeList: getFilterRegion('filtertypelist'),\n },\n};\n"],"file":"selectors.min.js"}
{"version":3,"sources":["../../../src/local/participantsfilter/selectors.js"],"names":["getFilterRegion","region","getFilterAction","action","getFilterField","field","filter","actions","remove","fields","join","type","regions","values","byName","name","filterset","addRow","applyFilters","resetFilters","filtermatch","filterlist","datasource","data","all","typeList","typeListSelect"],"mappings":"iKAwBMA,CAAAA,CAAe,CAAG,SAAAC,CAAM,uCAA2BA,CAA3B,Q,CACxBC,CAAe,CAAG,SAAAC,CAAM,uCAA2BA,CAA3B,Q,CACxBC,CAAc,CAAG,SAAAC,CAAK,sCAA0BA,CAA1B,Q,GAEb,CACXC,MAAM,CAAE,CACJL,MAAM,CAAED,CAAe,CAAC,QAAD,CADnB,CAEJO,OAAO,CAAE,CACLC,MAAM,CAAEN,CAAe,CAAC,QAAD,CADlB,CAFL,CAKJO,MAAM,CAAE,CACJC,IAAI,CAAEN,CAAc,CAAC,MAAD,CADhB,CAEJO,IAAI,CAAEP,CAAc,CAAC,MAAD,CAFhB,CALJ,CASJQ,OAAO,CAAE,CACLC,MAAM,CAAEb,CAAe,CAAC,OAAD,CADlB,CATL,CAYJc,MAAM,CAAE,gBAAAC,CAAI,kBAAOf,CAAe,CAAC,QAAD,CAAtB,gCAAsDe,CAAtD,QAZR,CADG,CAeXC,SAAS,CAAE,CACPf,MAAM,CAAED,CAAe,CAAC,SAAD,CADhB,CAEPO,OAAO,CAAE,CACLU,MAAM,CAAEf,CAAe,CAAC,KAAD,CADlB,CAELgB,YAAY,CAAEhB,CAAe,CAAC,OAAD,CAFxB,CAGLiB,YAAY,CAAEjB,CAAe,CAAC,OAAD,CAHxB,CAFF,CAOPU,OAAO,CAAE,CACLQ,WAAW,CAAEpB,CAAe,CAAC,aAAD,CADvB,CAELqB,UAAU,CAAErB,CAAe,CAAC,SAAD,CAFtB,CAGLsB,UAAU,CAAEtB,CAAe,CAAC,gBAAD,CAHtB,CAPF,CAYPS,MAAM,CAAE,CACJC,IAAI,WAAKV,CAAe,CAAC,aAAD,CAApB,aAAuCI,CAAc,CAAC,MAAD,CAArD,CADA,CAZD,CAfA,CA+BXmB,IAAI,CAAE,CACFd,MAAM,CAAE,CACJK,MAAM,CAAE,gBAAAC,CAAI,qCAAyBA,CAAzB,QADR,CAEJS,GAAG,WAAKxB,CAAe,CAAC,gBAAD,CAApB,sBAFC,CADN,CAKFyB,QAAQ,CAAEzB,CAAe,CAAC,gBAAD,CALvB,CAMF0B,cAAc,iBAAW1B,CAAe,CAAC,gBAAD,CAA1B,CANZ,CA/BK,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Module containing the selectors for user filters.\n *\n * @module core_user/local/user_filter/selectors\n * @package core_user\n * @copyright 2020 Michael Hawkins <michaelh@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst getFilterRegion = region => `[data-filterregion=\"${region}\"]`;\nconst getFilterAction = action => `[data-filteraction=\"${action}\"]`;\nconst getFilterField = field => `[data-filterfield=\"${field}\"]`;\n\nexport default {\n filter: {\n region: getFilterRegion('filter'),\n actions: {\n remove: getFilterAction('remove'),\n },\n fields: {\n join: getFilterField('join'),\n type: getFilterField('type'),\n },\n regions: {\n values: getFilterRegion('value'),\n },\n byName: name => `${getFilterRegion('filter')}[data-filter-type=\"${name}\"]`,\n },\n filterset: {\n region: getFilterRegion('actions'),\n actions: {\n addRow: getFilterAction('add'),\n applyFilters: getFilterAction('apply'),\n resetFilters: getFilterAction('reset'),\n },\n regions: {\n filtermatch: getFilterRegion('filtermatch'),\n filterlist: getFilterRegion('filters'),\n datasource: getFilterRegion('filtertypedata'),\n },\n fields: {\n join: `${getFilterRegion('filtermatch')} ${getFilterField('join')}`,\n },\n },\n data: {\n fields: {\n byName: name => `[data-field-name=\"${name}\"]`,\n all: `${getFilterRegion('filtertypedata')} [data-field-name]`,\n },\n typeList: getFilterRegion('filtertypelist'),\n typeListSelect: `select${getFilterRegion('filtertypelist')}`,\n },\n};\n"],"file":"selectors.min.js"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -44,12 +44,13 @@ export default class {
*
* @param {String} filterType The type of filter that this relates to
* @param {HTMLElement} rootNode The root node for the participants filterset
* @param {Array} initialValues The initial values for the selector
*/
constructor(filterType, rootNode) {
constructor(filterType, rootNode, initialValues) {
this.filterType = filterType;
this.rootNode = rootNode;
this.addValueSelector();
this.addValueSelector(initialValues);
}
/**
@ -79,8 +80,10 @@ export default class {
/**
* Add the value selector to the filter row.
*
* @param {Array} initialValues
*/
async addValueSelector() {
async addValueSelector(initialValues = []) {
const filterValueNode = this.getFilterValueNode();
// Copy the data in place.
@ -88,6 +91,21 @@ export default class {
const dataSource = filterValueNode.querySelector('select');
// If there are any initial values then attempt to apply them.
initialValues.forEach(filterValue => {
let selectedOption = dataSource.querySelector(`option[value="${filterValue}"]`);
if (selectedOption) {
selectedOption.selected = true;
} else if (!this.showSuggestions) {
selectedOption = document.createElement('option');
selectedOption.value = filterValue;
selectedOption.innerHTML = filterValue;
selectedOption.selected = true;
dataSource.append(selectedOption);
}
});
Autocomplete.enhance(
// The source select element.
dataSource,

View File

@ -25,10 +25,6 @@ import Filter from '../filter';
import {get_string as getString} from 'core/str';
export default class extends Filter {
constructor(filterType, filterSet) {
super(filterType, filterSet);
}
/**
* For keywords the final value is an Array of strings.
*

View File

@ -63,5 +63,6 @@ export default {
all: `${getFilterRegion('filtertypedata')} [data-field-name]`,
},
typeList: getFilterRegion('filtertypelist'),
typeListSelect: `select${getFilterRegion('filtertypelist')}`,
},
};

View File

@ -25,6 +25,7 @@
import CourseFilter from './local/participantsfilter/filtertypes/courseid';
import * as DynamicTable from 'core_table/dynamic';
import GenericFilter from './local/participantsfilter/filter';
import {get_strings as getStrings} from 'core/str';
import Notification from 'core/notification';
import Selectors from './local/participantsfilter/selectors';
import Templates from 'core/templates';
@ -56,7 +57,8 @@ export const init = participantsRegionId => {
* @return {Promise}
*/
const addFilterRow = () => {
return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {})
const rownum = 1 + getFilterRegion().querySelectorAll(Selectors.filter.region).length;
return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {"rownumber": rownum})
.then(({html, js}) => {
const newContentNodes = Templates.appendNodeContents(getFilterRegion(), html, js);
@ -104,8 +106,10 @@ export const init = participantsRegionId => {
*
* @param {HTMLElement} filterRow
* @param {String} filterType
* @param {Array} initialFilterValues The initially selected values for the filter
* @returns {Filter}
*/
const addFilter = async(filterRow, filterType) => {
const addFilter = async(filterRow, filterType, initialFilterValues) => {
// Name the filter on the filter row.
filterRow.dataset.filterType = filterType;
@ -116,14 +120,17 @@ export const init = participantsRegionId => {
if (filterDataNode.dataset.filterTypeClass) {
Filter = await import(filterDataNode.dataset.filterTypeClass);
}
activeFilters[filterType] = new Filter(filterType, filterSet);
activeFilters[filterType] = new Filter(filterType, filterSet, initialFilterValues);
// Disable the select.
const typeField = filterRow.querySelector(Selectors.filter.fields.type);
typeField.value = filterType;
typeField.disabled = 'disabled';
// Update the list of available filter types.
updateFiltersOptions();
return activeFilters[filterType];
};
/**
@ -157,31 +164,40 @@ export const init = participantsRegionId => {
*
* @param {HTMLElement} filterRow
*/
const removeFilterRow = filterRow => {
const removeFilterRow = async filterRow => {
// Remove the filter object.
removeFilterObject(filterRow.dataset.filterType);
// Remove the actual filter HTML.
filterRow.remove();
// Update the list of available filter types.
updateFiltersOptions();
// Refresh the table.
updateTableFromFilter();
// Update the list of available filter types.
updateFiltersOptions();
// Update filter fieldset legends.
const filterLegends = await getAvailableFilterLegends();
getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {
filterRow.querySelector('legend').innerText = filterLegends[index];
});
};
/**
* Replace the specified filter row with a new one.
*
* @param {HTMLElement} filterRow
* @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).
* @return {Promise}
*/
const replaceFilterRow = filterRow => {
const replaceFilterRow = (filterRow, rowNum = 1) => {
// Remove the filter object.
removeFilterObject(filterRow.dataset.filterType);
return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {})
return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {"rownumber": rowNum})
.then(({html, js}) => {
const newContentNodes = Templates.replaceNode(filterRow, html, js);
@ -237,15 +253,28 @@ export const init = participantsRegionId => {
/**
* Remove all filters.
*
* @returns {Promise}
*/
const removeAllFilters = async() => {
const removeAllFilters = () => {
const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);
filters.forEach((filterRow) => {
removeOrReplaceFilterRow(filterRow);
});
filters.forEach(filterRow => removeOrReplaceFilterRow(filterRow));
// Refresh the table.
updateTableFromFilter();
return updateTableFromFilter();
};
/**
* Remove any empty filters.
*/
const removeEmptyFilters = () => {
const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);
filters.forEach(filterRow => {
const filterType = filterRow.querySelector(Selectors.filter.fields.type);
if (!filterType.value) {
removeOrReplaceFilterRow(filterRow);
}
});
};
/**
@ -287,6 +316,49 @@ export const init = participantsRegionId => {
}
};
/**
* Set the current filter options based on a provided configuration.
*
* @param {Object} config
* @param {Number} config.jointype
* @param {Object} config.filters
*/
const setFilterFromConfig = config => {
const filterConfig = Object.entries(config.filters);
if (!filterConfig.length) {
// There are no filters to set from.
return;
}
// Set the main join type.
filterSet.querySelector(Selectors.filterset.fields.join).value = config.jointype;
const filterPromises = filterConfig.map(([filterType, filterData]) => {
if (filterType === 'courseid') {
// The courseid is a special case.
return Promise.resolve();
}
const filterValues = filterData.values;
if (!filterValues.length) {
// There are no values for this filter.
// Skip it.
return Promise.resolve();
}
return addFilterRow().then(([filterRow]) => addFilter(filterRow, filterType, filterValues));
});
Promise.all(filterPromises).then(() => {
return removeEmptyFilters();
})
.then(updateFiltersOptions)
.then(updateTableFromFilter)
.catch();
};
/**
* Update the Dynamic table based upon the current filter.
*
@ -302,6 +374,33 @@ export const init = participantsRegionId => {
);
};
/**
* Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.
*
* @return {array}
*/
const getAvailableFilterLegends = async() => {
const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;
let requests = [];
[...Array(maxFilters)].forEach((_, rowIndex) => {
requests.push({
"key": "filterrowlegend",
"component": "core_user",
// Add 1 since rows begin at 1 (index begins at zero).
"param": rowIndex + 1
});
});
const legendStrings = await getStrings(requests)
.then(fetchedStrings => {
return fetchedStrings;
})
.catch(Notification.exception);
return legendStrings;
};
// Add listeners for the main actions.
filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {
if (e.target.closest(Selectors.filterset.actions.addRow)) {
@ -345,4 +444,11 @@ export const init = participantsRegionId => {
filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {
filterSet.dataset.filterverb = e.target.value;
});
const tableRoot = DynamicTable.getTableFromId(filterSet.dataset.tableRegion);
const initialFilters = DynamicTable.getFilters(tableRoot);
if (initialFilters) {
// Apply the initial filter configuration.
setFilterFromConfig(initialFilters);
}
};

View File

@ -16,6 +16,7 @@
/**
* Unified filter page JS module for the course participants page.
*
* @deprecated since Moodle 3.9 MDL-68612 - user unified filter replaced by participants filter.
* @module core_user/unified_filter
* @package core_user
* @copyright 2017 Jun Pataleta
@ -37,6 +38,7 @@ define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'],
/**
* Init function.
*
* @deprecated since Moodle 3.9 MDL-68612 - user unified filter replaced by participants filter.
* @method init
* @private
*/
@ -117,6 +119,7 @@ define(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'],
/**
* Return the unified user filter form.
*
* @deprecated since Moodle 3.9 MDL-68612 - user unified filter replaced by participants filter.
* @method getForm
* @return {DOMElement}
*/

View File

@ -15,6 +15,7 @@
/**
* Datasource for the core_user/unified_filter.
* @deprecated since Moodle 3.9 MDL-68612 - user unified filter replaced by participants filter.
*
* This module is compatible with core/form-autocomplete.
*

View File

@ -210,7 +210,8 @@ class participants_filter implements renderable, templatable {
$groups = groups_get_all_groups($this->course->id, $USER->id);
}
if (empty($groups)) {
// Return no data if no groups found (which includes if the only value is 'No group').
if (empty($groups) || (count($groups) === 1 && array_key_exists(-1, $groups))) {
return null;
}
@ -349,6 +350,7 @@ class participants_filter implements renderable, templatable {
'tableregionid' => $this->tableregionid,
'courseid' => $this->context->instanceid,
'filtertypes' => $this->get_filtertypes(),
'rownumber' => 1,
];
return $data;

View File

@ -17,6 +17,7 @@
/**
* Class containing the filter options data for rendering the unified filter autocomplete element for the course participants page.
*
* @deprecated since Moodle 3.9 MDL-68612 - Please use \core_user\table\participants_search::class and table filtersets instead.
* @package core_user
* @copyright 2017 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@ -34,8 +35,10 @@ defined('MOODLE_INTERNAL') || die();
/**
* Class containing the filter options data for rendering the unified filter autocomplete element for the course participants page.
*
* @deprecated since Moodle 3.9 MDL-68612 - Please use \core_user\table\participants_search::class and table filtersets instead.
* @copyright 2017 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*
*/
class unified_filter implements renderable, templatable {
@ -56,6 +59,10 @@ class unified_filter implements renderable, templatable {
* @param string|moodle_url $baseurl The url with params needed to call up this page.
*/
public function __construct($filteroptions, $selectedoptions, $baseurl = null) {
$deprecatedtext = __CLASS__ . ' class is deprecated. Please use \core\table\participants_search::class' .
' with table filtersets instead.';
debugging($deprecatedtext, DEBUG_DEVELOPER);
$this->filteroptions = $filteroptions;
$this->selectedoptions = $selectedoptions;
if (!empty($baseurl)) {

View File

@ -314,6 +314,8 @@ class participants_search {
* @return array SQL query data in the format ['sql' => '', 'forcedsql' => '', 'params' => []].
*/
protected function get_enrolled_sql(): array {
global $USER;
$isfrontpage = ($this->context->instanceid == SITEID);
$prefix = 'eu_';
$filteruid = "{$prefix}u.id";
@ -357,15 +359,43 @@ class participants_search {
$params = array_merge($params, $methodparams, $statusparams);
}
// Prepare any groups filtering.
$groupids = [];
if ($this->filterset->has_filter('groups')) {
$groupids = $this->filterset->get_filter('groups')->get_filter_values();
}
// Force additional groups filtering if required due to lack of capabilities.
// Note: This means results will always be limited to allowed groups, even if the user applies their own groups filtering.
$canaccessallgroups = has_capability('moodle/site:accessallgroups', $this->context);
$forcegroups = ($this->course->groupmode == SEPARATEGROUPS && !$canaccessallgroups);
if ($forcegroups) {
$allowedgroupids = array_keys(groups_get_all_groups($this->course->id, $USER->id));
// Users not in any group in a course with separate groups mode should not be able to access the participants filter.
if (empty($allowedgroupids)) {
// The UI does not support this, so it should not be reachable unless someone is trying to bypass the restriction.
throw new \coding_exception('User must be part of a group to filter by participants.');
}
$forceduid = "{$forcedprefix}u.id";
$forcedjointype = $this->get_groups_jointype(\core_table\local\filter\filter::JOINTYPE_ANY);
$forcedgroupjoin = groups_get_members_join($allowedgroupids, $forceduid, $this->context, $forcedjointype);
$forcedjoins[] = $forcedgroupjoin->joins;
$forcedwhere .= "AND ({$forcedgroupjoin->wheres})";
$params = array_merge($params, $forcedgroupjoin->params);
// Remove any filtered groups the user does not have access to.
$groupids = array_intersect($allowedgroupids, $groupids);
}
// Prepare any user defined groups filtering.
if ($groupids) {
$groupjoin = groups_get_members_join($groupids, $filteruid, $this->context, $this->get_groups_jointype());
$joins[] = $groupjoin->joins;
$params = array_merge($params, $groupjoin->params);
if (!empty($groupjoin->wheres)) {
@ -685,12 +715,28 @@ class participants_search {
* Fetch the groups filter's grouplib jointype, based on its filterset jointype.
* This mapping is to ensure compatibility between the two, should their values ever differ.
*
* @param int|null $forcedjointype If set, specifies the join type to fetch mapping for (used when applying forced filtering).
* If null, then user defined filter join type is used.
* @return int
*/
protected function get_groups_jointype(): int {
protected function get_groups_jointype(?int $forcedjointype = null): int {
// If applying forced groups filter and no manual groups filtering is applied, add an empty filter so we can map the join.
if (!is_null($forcedjointype) && !$this->filterset->has_filter('groups')) {
$this->filterset->add_filter(new \core_table\local\filter\integer_filter('groups'));
}
$groupsfilter = $this->filterset->get_filter('groups');
switch ($groupsfilter->get_join_type()) {
if (is_null($forcedjointype)) {
// Fetch join type mapping for a user supplied groups filtering.
$filterjointype = $groupsfilter->get_join_type();
} else {
// Fetch join type mapping for forced groups filtering.
$filterjointype = $forcedjointype;
}
switch ($filterjointype) {
case $groupsfilter::JOINTYPE_NONE:
$groupsjoin = GROUPS_JOIN_NONE;
break;

View File

@ -43,7 +43,7 @@ $contextid = optional_param('contextid', 0, PARAM_INT); // One of this or.
$courseid = optional_param('id', 0, PARAM_INT); // This are required.
$newcourse = optional_param('newcourse', false, PARAM_BOOL);
$roleid = optional_param('roleid', 0, PARAM_INT);
$groupparam = optional_param('group', 0, PARAM_INT);
$urlgroupid = optional_param('group', 0, PARAM_INT);
$PAGE->set_url('/user/index.php', array(
'page' => $page,
@ -102,137 +102,47 @@ if ($node) {
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('participants'));
// Get the currently applied filters.
$filtersapplied = optional_param_array('unified-filters', [], PARAM_NOTAGS);
$filterwassubmitted = optional_param('unified-filter-submitted', 0, PARAM_BOOL);
// If they passed a role make sure they can view that role.
if ($roleid) {
$viewableroles = get_profile_roles($context);
// Check if the user can view this role.
if (array_key_exists($roleid, $viewableroles)) {
$filtersapplied[] = USER_FILTER_ROLE . ':' . $roleid;
} else {
$roleid = 0;
}
}
// Default group ID.
$groupid = false;
$canaccessallgroups = has_capability('moodle/site:accessallgroups', $context);
if ($course->groupmode != NOGROUPS) {
if ($canaccessallgroups) {
// Change the group if the user can access all groups and has specified group in the URL.
if ($groupparam) {
$groupid = $groupparam;
}
} else {
// Otherwise, get the user's default group.
$groupid = groups_get_course_group($course, true);
if ($course->groupmode == SEPARATEGROUPS && !$groupid) {
// The user is not in the group so show message and exit.
echo $OUTPUT->notification(get_string('notingroup'));
echo $OUTPUT->footer();
exit;
}
}
}
$hasgroupfilter = false;
$lastaccess = 0;
$searchkeywords = [];
$enrolid = 0;
$filterset = new \core_user\table\participants_filterset();
$filterset->add_filter(new integer_filter('courseid', filter::JOINTYPE_DEFAULT, [(int)$course->id]));
$participanttable = new \core_user\table\participants("user-index-participants-{$course->id}");
$filterset = new \core_user\table\participants_filterset();
$filterset->add_filter(new integer_filter('courseid', filter::JOINTYPE_DEFAULT, [(int)$course->id]));
$enrolfilter = new integer_filter('enrolments');
$groupfilter = new integer_filter('groups');
$keywordfilter = new string_filter('keywords');
$lastaccessfilter = new integer_filter('accesssince');
$rolefilter = new integer_filter('roles');
$statusfilter = new integer_filter('status');
$canaccessallgroups = has_capability('moodle/site:accessallgroups', $context);
$filtergroupids = $urlgroupid ? [$urlgroupid] : [];
foreach ($filtersapplied as $filter) {
$filtervalue = explode(':', $filter, 2);
$value = null;
if (count($filtervalue) == 2) {
$key = clean_param($filtervalue[0], PARAM_INT);
$value = clean_param($filtervalue[1], PARAM_INT);
} else {
// Search string.
$key = USER_FILTER_STRING;
$value = clean_param($filtervalue[0], PARAM_TEXT);
}
// Force group filtering if user should only see a subset of groups' users.
if ($course->groupmode == SEPARATEGROUPS && !$canaccessallgroups) {
$filtergroupids = array_keys(groups_get_all_groups($course->id, $USER->id));
switch ($key) {
case USER_FILTER_ENROLMENT:
$enrolid = $value;
$enrolfilter->add_filter_value($value);
break;
case USER_FILTER_GROUP:
$groupid = $value;
$groupfilter->add_filter_value($value);
$hasgroupfilter = true;
break;
case USER_FILTER_LAST_ACCESS:
$lastaccess = $value;
$lastaccessfilter->add_filter_value($value);
break;
case USER_FILTER_ROLE:
$roleid = $value;
$rolefilter->add_filter_value($value);
break;
case USER_FILTER_STATUS:
// We only accept active/suspended statuses.
if ($value == ENROL_USER_ACTIVE || $value == ENROL_USER_SUSPENDED) {
$status = $value;
$statusfilter->add_filter_value($value);
}
break;
default:
// Search string.
$searchkeywords[] = $value;
$keywordfilter->add_filter_value($value);
break;
}
}
// If course supports groups we may need to set a default.
if (!empty($groupid)) {
if ($canaccessallgroups) {
// User can access all groups, let them filter by whatever was selected.
$filtersapplied[] = USER_FILTER_GROUP . ':' . $groupid;
$groupfilter->add_filter_value((int)$groupid);
} else if (!$filterwassubmitted && $course->groupmode == VISIBLEGROUPS) {
// If we are in a course with visible groups and the user has not submitted anything and does not have
// access to all groups, then set a default group.
$filtersapplied[] = USER_FILTER_GROUP . ':' . $groupid;
$groupfilter->add_filter_value((int)$groupid);
} else if (!$hasgroupfilter && $course->groupmode != VISIBLEGROUPS) {
// The user can't access all groups and has not set a group filter in a course where the groups are not visible
// then apply a default group filter.
$filtersapplied[] = USER_FILTER_GROUP . ':' . $groupid;
$groupfilter->add_filter_value((int)$groupid);
} else if (!$hasgroupfilter) { // No need for the group id to be set.
$groupid = false;
if (empty($filtergroupids)) {
// The user is not in a group so show message and exit.
echo $OUTPUT->notification(get_string('notingroup'));
echo $OUTPUT->footer();
exit();
}
}
if ($groupid > 0 && ($course->groupmode != SEPARATEGROUPS || $canaccessallgroups)) {
// Apply groups filter if included in URL or forced due to lack of capabilities.
if (!empty($filtergroupids)) {
$filterset->add_filter(new integer_filter('groups', filter::JOINTYPE_DEFAULT, $filtergroupids));
}
// Display single group information if requested in the URL.
if ($urlgroupid > 0 && ($course->groupmode != SEPARATEGROUPS || $canaccessallgroups)) {
$grouprenderer = $PAGE->get_renderer('core_group');
$groupdetailpage = new \core_group\output\group_details($groupid);
$groupdetailpage = new \core_group\output\group_details($urlgroupid);
echo $grouprenderer->group_details($groupdetailpage);
}
// Should use this variable so that we don't break stuff every time a variable is added or changed.
$baseurl = new moodle_url('/user/index.php', array(
'contextid' => $context->id,
'id' => $course->id,
'perpage' => $perpage));
// Filter by role if passed via URL (used on profile page).
if ($roleid) {
$viewableroles = get_profile_roles($context);
$participanttable = new \core_user\table\participants("user-index-participants-{$course->id}");
$participanttable->define_baseurl($baseurl);
// Apply filter if the user can view this role.
if (array_key_exists($roleid, $viewableroles)) {
$filterset->add_filter(new integer_filter('roles', filter::JOINTYPE_DEFAULT, [$roleid]));
}
}
// Manage enrolments.
$manager = new course_enrolment_manager($PAGE, $course);
@ -242,50 +152,18 @@ $enrolbuttonsout = '';
foreach ($enrolbuttons as $enrolbutton) {
$enrolbuttonsout .= $enrolrenderer->render($enrolbutton);
}
echo html_writer::div($enrolbuttonsout, 'd-flex justify-content-end', [
'data-region' => 'wrapper',
'data-table-uniqueid' => $participanttable->uniqueid,
]);
// Render the unified filter.
$renderer = $PAGE->get_renderer('core_user');
echo $renderer->unified_filter($course, $context, $filtersapplied, $baseurl);
// Render the user filters.
$userrenderer = $PAGE->get_renderer('core_user');
echo $userrenderer->participants_filter($context, $participanttable->uniqueid);
echo '<div class="userlist">';
// Add filters to the baseurl after creating unified_filter to avoid losing them.
foreach (array_unique($filtersapplied) as $filterix => $filter) {
$baseurl->param('unified-filters[' . $filterix . ']', $filter);
}
if (count($groupfilter)) {
$filterset->add_filter($groupfilter);
}
if (count($lastaccessfilter)) {
$filterset->add_filter($lastaccessfilter);
}
if (count($rolefilter)) {
$filterset->add_filter($rolefilter);
}
if (count($enrolfilter)) {
$filterset->add_filter($enrolfilter);
}
if (count($statusfilter)) {
$filterset->add_filter($statusfilter);
}
if (count($keywordfilter)) {
$filterset->add_filter($keywordfilter);
}
// Do this so we can get the total number of rows.
ob_start();
$participanttable->set_filterset($filterset);
@ -317,8 +195,10 @@ echo html_writer::tag(
echo $participanttablehtml;
$perpageurl = clone($baseurl);
$perpageurl->remove_params('perpage');
$perpageurl = new moodle_url('/user/index.php', [
'contextid' => $context->id,
'id' => $course->id,
]);
$perpagesize = DEFAULT_PAGE_SIZE;
$perpagevisible = false;
$perpagestring = '';

View File

@ -1287,255 +1287,6 @@ function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx
$exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
}
/**
* Returns the SQL used by the participants table.
*
* @param int $courseid The course id
* @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
* @param int $accesssince The time since last access, 0 means any time
* @param int $roleid The role id, 0 means all roles and -1 no roles
* @param int $enrolid The enrolment id, 0 means all enrolment methods will be returned.
* @param int $statusid The user enrolment status, -1 means all enrolments regardless of the status will be returned, if allowed.
* @param string|array $search The search that was performed, empty means perform no search
* @param string $additionalwhere Any additional SQL to add to where
* @param array $additionalparams The additional params
* @return array
*/
function user_get_participants_sql($courseid, $groupid = 0, $accesssince = 0, $roleid = 0, $enrolid = 0, $statusid = -1,
$search = '', $additionalwhere = '', $additionalparams = array()) {
global $DB, $USER, $CFG;
// Get the context.
$context = \context_course::instance($courseid, MUST_EXIST);
$isfrontpage = ($courseid == SITEID);
// Default filter settings. We only show active by default, especially if the user has no capability to review enrolments.
$onlyactive = true;
$onlysuspended = false;
if (has_capability('moodle/course:enrolreview', $context) && (has_capability('moodle/course:viewsuspendedusers', $context))) {
switch ($statusid) {
case ENROL_USER_ACTIVE:
// Nothing to do here.
break;
case ENROL_USER_SUSPENDED:
$onlyactive = false;
$onlysuspended = true;
break;
default:
// If the user has capability to review user enrolments, but statusid is set to -1, set $onlyactive to false.
$onlyactive = false;
break;
}
}
list($esql, $params) = get_enrolled_sql($context, null, $groupid, $onlyactive, $onlysuspended, $enrolid);
$joins = array('FROM {user} u');
$wheres = array();
$userfields = get_extra_user_fields($context);
$userfieldssql = user_picture::fields('u', $userfields);
if ($isfrontpage) {
$select = "SELECT $userfieldssql, u.lastaccess";
$joins[] = "JOIN ($esql) e ON e.id = u.id"; // Everybody on the frontpage usually.
if ($accesssince) {
$wheres[] = user_get_user_lastaccess_sql($accesssince);
}
} else {
$select = "SELECT $userfieldssql, COALESCE(ul.timeaccess, 0) AS lastaccess";
$joins[] = "JOIN ($esql) e ON e.id = u.id"; // Course enrolled users only.
// Not everybody has accessed the course yet.
$joins[] = 'LEFT JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = :courseid)';
$params['courseid'] = $courseid;
if ($accesssince) {
$wheres[] = user_get_course_lastaccess_sql($accesssince);
}
}
// Performance hacks - we preload user contexts together with accounts.
$ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
$ccjoin = 'LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)';
$params['contextlevel'] = CONTEXT_USER;
$select .= $ccselect;
$joins[] = $ccjoin;
// Limit list to users with some role only.
if ($roleid) {
// We want to query both the current context and parent contexts.
list($relatedctxsql, $relatedctxparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true),
SQL_PARAMS_NAMED, 'relatedctx');
// Get users without any role.
if ($roleid == -1) {
$wheres[] = "u.id NOT IN (SELECT userid FROM {role_assignments} WHERE contextid $relatedctxsql)";
$params = array_merge($params, $relatedctxparams);
} else {
$wheres[] = "u.id IN (SELECT userid FROM {role_assignments} WHERE roleid = :roleid AND contextid $relatedctxsql)";
$params = array_merge($params, array('roleid' => $roleid), $relatedctxparams);
}
}
if (!empty($search)) {
if (!is_array($search)) {
$search = [$search];
}
foreach ($search as $index => $keyword) {
$searchkey1 = 'search' . $index . '1';
$searchkey2 = 'search' . $index . '2';
$searchkey3 = 'search' . $index . '3';
$searchkey4 = 'search' . $index . '4';
$searchkey5 = 'search' . $index . '5';
$searchkey6 = 'search' . $index . '6';
$searchkey7 = 'search' . $index . '7';
$conditions = array();
// Search by fullname.
$fullname = $DB->sql_fullname('u.firstname', 'u.lastname');
$conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
// Search by email.
$email = $DB->sql_like('email', ':' . $searchkey2, false, false);
if (!in_array('email', $userfields)) {
$maildisplay = 'maildisplay' . $index;
$userid1 = 'userid' . $index . '1';
// Prevent users who hide their email address from being found by others
// who aren't allowed to see hidden email addresses.
$email = "(". $email ." AND (" .
"u.maildisplay <> :$maildisplay " .
"OR u.id = :$userid1". // User can always find himself.
"))";
$params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
$params[$userid1] = $USER->id;
}
$conditions[] = $email;
// Search by idnumber.
$idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false);
if (!in_array('idnumber', $userfields)) {
$userid2 = 'userid' . $index . '2';
// Users who aren't allowed to see idnumbers should at most find themselves
// when searching for an idnumber.
$idnumber = "(". $idnumber . " AND u.id = :$userid2)";
$params[$userid2] = $USER->id;
}
$conditions[] = $idnumber;
if (!empty($CFG->showuseridentity)) {
// Search all user identify fields.
$extrasearchfields = explode(',', $CFG->showuseridentity);
foreach ($extrasearchfields as $extrasearchfield) {
if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
// Already covered above. Search by country not supported.
continue;
}
$param = $searchkey3 . $extrasearchfield;
$condition = $DB->sql_like($extrasearchfield, ':' . $param, false, false);
$params[$param] = "%$keyword%";
if (!in_array($extrasearchfield, $userfields)) {
// User cannot see this field, but allow match if their own account.
$userid3 = 'userid' . $index . '3' . $extrasearchfield;
$condition = "(". $condition . " AND u.id = :$userid3)";
$params[$userid3] = $USER->id;
}
$conditions[] = $condition;
}
}
// Search by middlename.
$middlename = $DB->sql_like('middlename', ':' . $searchkey4, false, false);
$conditions[] = $middlename;
// Search by alternatename.
$alternatename = $DB->sql_like('alternatename', ':' . $searchkey5, false, false);
$conditions[] = $alternatename;
// Search by firstnamephonetic.
$firstnamephonetic = $DB->sql_like('firstnamephonetic', ':' . $searchkey6, false, false);
$conditions[] = $firstnamephonetic;
// Search by lastnamephonetic.
$lastnamephonetic = $DB->sql_like('lastnamephonetic', ':' . $searchkey7, false, false);
$conditions[] = $lastnamephonetic;
$wheres[] = "(". implode(" OR ", $conditions) .") ";
$params[$searchkey1] = "%$keyword%";
$params[$searchkey2] = "%$keyword%";
$params[$searchkey3] = "%$keyword%";
$params[$searchkey4] = "%$keyword%";
$params[$searchkey5] = "%$keyword%";
$params[$searchkey6] = "%$keyword%";
$params[$searchkey7] = "%$keyword%";
}
}
if (!empty($additionalwhere)) {
$wheres[] = $additionalwhere;
$params = array_merge($params, $additionalparams);
}
$from = implode("\n", $joins);
if ($wheres) {
$where = 'WHERE ' . implode(' AND ', $wheres);
} else {
$where = '';
}
return array($select, $from, $where, $params);
}
/**
* Returns the total number of participants for a given course.
*
* @param int $courseid The course id
* @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
* @param int $accesssince The time since last access, 0 means any time
* @param int $roleid The role id, 0 means all roles
* @param int $enrolid The applied filter for the user enrolment ID.
* @param int $status The applied filter for the user's enrolment status.
* @param string|array $search The search that was performed, empty means perform no search
* @param string $additionalwhere Any additional SQL to add to where
* @param array $additionalparams The additional params
* @return int
*/
function user_get_total_participants($courseid, $groupid = 0, $accesssince = 0, $roleid = 0, $enrolid = 0, $statusid = -1,
$search = '', $additionalwhere = '', $additionalparams = array()) {
global $DB;
list($select, $from, $where, $params) = user_get_participants_sql($courseid, $groupid, $accesssince, $roleid, $enrolid,
$statusid, $search, $additionalwhere, $additionalparams);
return $DB->count_records_sql("SELECT COUNT(u.id) $from $where", $params);
}
/**
* Returns the participants for a given course.
*
* @param int $courseid The course id
* @param int $groupid The groupid, 0 means all groups and USERSWITHOUTGROUP no group
* @param int $accesssince The time since last access
* @param int $roleid The role id
* @param int $enrolid The applied filter for the user enrolment ID.
* @param int $status The applied filter for the user's enrolment status.
* @param string $search The search that was performed
* @param string $additionalwhere Any additional SQL to add to where
* @param array $additionalparams The additional params
* @param string $sort The SQL sort
* @param int $limitfrom return a subset of records, starting at this point (optional).
* @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
* @return moodle_recordset
*/
function user_get_participants($courseid, $groupid = 0, $accesssince, $roleid, $enrolid = 0, $statusid, $search,
$additionalwhere = '', $additionalparams = array(), $sort = '', $limitfrom = 0, $limitnum = 0) {
global $DB;
list($select, $from, $where, $params) = user_get_participants_sql($courseid, $groupid, $accesssince, $roleid, $enrolid,
$statusid, $search, $additionalwhere, $additionalparams);
return $DB->get_recordset_sql("$select $from $where $sort", $params, $limitfrom, $limitnum);
}
/**
* Returns SQL that can be used to limit a query to a period where the user last accessed / did not access a course.
*

View File

@ -110,6 +110,7 @@ class core_user_renderer extends plugin_renderer_base {
/**
* Renders the unified filter element for the course participants page.
* @deprecated since Moodle 3.9 MDL-68612 - Please use participants_filter() instead.
*
* @param stdClass $course The course object.
* @param context $context The context object.
@ -120,6 +121,8 @@ class core_user_renderer extends plugin_renderer_base {
public function unified_filter($course, $context, $filtersapplied, $baseurl = null) {
global $CFG, $DB, $USER;
debugging('core_user_renderer->unified_filter() is deprecated. Please use participants_filter() instead.', DEBUG_DEVELOPER);
require_once($CFG->dirroot . '/enrol/locallib.php');
require_once($CFG->dirroot . '/lib/grouplib.php');
$manager = new course_enrolment_manager($this->page, $course);

View File

@ -43,7 +43,10 @@
{{#items}}
<span role="listitem" data-value="{{value}}" aria-selected="true"
class="badge badge-secondary clickable text-wrap text-break line-height-4 mr-2 my-1">
{{label}}<i class="icon fa fa-times pl-2 mr-0"></i>
{{label}}
<button class="btn btn-link text-reset p-0" aria-label='{{#str}}clearfilterselection, core_user, {{label}}{{/str}}'>
<i class="icon fa fa-times pl-2 mr-0"></i>
</button>
</span>
{{/items}}
{{^items}}

View File

@ -29,37 +29,41 @@
"name": "status",
"title": "Status"
}
]
],
"rownumber": 1
}
}}
<div data-filterregion="filter">
<div class="border-radius my-2 p-2 bg-white border d-flex flex-column flex-md-row align-items-md-start">
<div class="d-flex flex-column flex-md-row align-items-md-center">
<label for="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}" class="mr-md-2 mb-md-0">{{#str}}match, core_user{{/str}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="join" id="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}">
<option value="0">{{#str}}none{{/str}}</option>
<option selected=selected value="1">{{#str}}any{{/str}}</option>
<option value="2">{{#str}}all{{/str}}</option>
<fieldset>
<legend class="sr-only">{{#str}}filterrowlegend, core_user, {{rownumber}}{{/str}}</legend>
<div class="border-radius my-2 p-2 bg-white border d-flex flex-column flex-md-row align-items-md-start">
<div class="d-flex flex-column flex-md-row align-items-md-center">
<label for="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}" class="mr-md-2 mb-md-0">{{#str}}match, core_user{{/str}}</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="join" id="core_user-local-participantsfilter-filterrow-jointype-{{uniqid}}">
<option value="0">{{#str}}none{{/str}}</option>
<option selected=selected value="1">{{#str}}any{{/str}}</option>
<option value="2">{{#str}}all{{/str}}</option>
</select>
</div>
<label class="sr-only pt-2" for="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">filtertype</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="type" id="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">
<option value="">{{#str}}selectfiltertype, core_user{{/str}}</option>
{{#filtertypes}}
<option value="{{name}}">{{title}}</option>
{{/filtertypes}}
</select>
<div data-filterregion="value" class="d-md-flex flex-column align-items-start flex-lg-row"></div>
<button data-filteraction="remove" class="ml-auto icon-no-margin icon-size-4 btn text-reset" aria-label="{{#str}}clearfilterrow, core_user{{/str}}">
<i class="icon fa fa-times-circle"></i>
</button>
</div>
<label class="sr-only pt-2" for="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">filtertype</label>
<select class="custom-select mb-1 mb-md-0 mr-md-2" data-filterfield="type" id="core_user-local-participantsfilter-filterrow-filtertype-{{uniqid}}">
<option value="">{{#str}}selectfiltertype, core_user{{/str}}</option>
{{#filtertypes}}
<option value="{{name}}">{{title}}</option>
{{/filtertypes}}
</select>
<div data-filterregion="value" class="d-md-flex flex-column align-items-start flex-lg-row"></div>
<button data-filteraction="remove" class="ml-auto icon-no-margin icon-size-4 btn text-reset" aria-label="{{#str}}clearfilterrow, core_user{{/str}}">
<i class="icon fa fa-times-circle"></i>
</button>
</div>
<div data-filterregion="joinadverb" class="pl-1 text-uppercase font-weight-bold">
<div data-filterverbfor="0">{{#str}}adverbfor_andnot, core_user{{/str}}</div>
<div data-filterverbfor="1">{{#str}}adverbfor_or, core_user{{/str}}</div>
<div data-filterverbfor="2">{{#str}}adverbfor_and, core_user{{/str}}</div>
</div>
<div data-filterregion="joinadverb" class="pl-1 text-uppercase font-weight-bold">
<div data-filterverbfor="0">{{#str}}adverbfor_andnot, core_user{{/str}}</div>
<div data-filterverbfor="1">{{#str}}adverbfor_or, core_user{{/str}}</div>
<div data-filterverbfor="2">{{#str}}adverbfor_and, core_user{{/str}}</div>
</div>
</fieldset>
</div>

View File

@ -16,6 +16,7 @@
}}
{{!
@template core_user/unified_filter
@deprecated since Moodle 3.9 MDL-68612 - please use core_user/participantsfilter instead.
Template for the unified filter element.

View File

@ -6,17 +6,17 @@ Feature: Course participants can be filtered
Background:
Given the following "courses" exist:
| fullname | shortname | groupmode |
| Course 1 | C1 | 1 |
| Course 2 | C2 | 0 |
| Course 3 | C3 | 0 |
| fullname | shortname | groupmode | startdate |
| Course 1 | C1 | 1 | ##5 months ago## |
| Course 2 | C2 | 0 | ##4 months ago## |
| Course 3 | C3 | 0 | ##3 months ago## |
And the following "users" exist:
| username | firstname | lastname | email | idnumber | country | city | maildisplay |
| student1 | Student | 1 | student1@example.com | SID1 | | SCITY1 | 0 |
| student2 | Student | 2 | student2@example.com | SID2 | GB | SCITY2 | 1 |
| student3 | Student | 3 | student3@example.com | SID3 | AU | SCITY3 | 0 |
| student4 | Student | 4 | student4@example.com | SID4 | AT | SCITY4 | 0 |
| teacher1 | Teacher | 1 | teacher1@example.com | TID1 | US | TCITY1 | 0 |
| student4 | Student | 4 | student4@moodle.com | SID4 | AT | SCITY4 | 0 |
| teacher1 | Teacher | 1 | teacher1@example.org | TID1 | US | TCITY1 | 0 |
And the following "course enrolments" exist:
| user | course | role | status | timeend |
| student1 | C1 | student | 0 | |
@ -32,6 +32,13 @@ Feature: Course participants can be filtered
| teacher1 | C1 | editingteacher | 0 | |
| teacher1 | C2 | editingteacher | 0 | |
| teacher1 | C3 | editingteacher | 0 | |
And the following "last access times" exist:
| user | course | lastaccess |
| student1 | C1 | ##yesterday## |
| student1 | C2 | ##2 weeks ago## |
| student2 | C1 | ##4 days ago## |
| student3 | C1 | ##2 weeks ago## |
| student4 | C1 | ##3 weeks ago## |
And the following "groups" exist:
| name | course | idnumber |
| Group 1 | C1 | G1 |
@ -58,12 +65,15 @@ Feature: Course participants can be filtered
And I should see "Teacher 1" in the "participants" "table"
@javascript
Scenario Outline: Filter users for a course
Scenario Outline: Filter users for a course with a single value
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
When I open the autocomplete suggestions list
And I click on "<filter1>" item in the autocomplete list
And I set the field "Match" in the "Filter 1" "fieldset" to "<matchtype>"
And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "<filtervalue>" "list_item"
When I click on "Apply filters" "button"
Then I should see "<expected1>" in the "participants" "table"
And I should see "<expected2>" in the "participants" "table"
And I should see "<expected3>" in the "participants" "table"
@ -72,33 +82,152 @@ Feature: Course participants can be filtered
# Note the 'XX-IGNORE-XX' elements are for when there is less than 2 'not expected' items.
Examples:
| filter1 | expected1 | expected2 | expected3 | notexpected1 | notexpected2 |
| Group: No group | Student 1 | Student 4 | Teacher 1 | Student 2 | Student 3 |
| Group: Group 1 | Student 2 | | | Student 1 | Student 3 |
| Group: Group 2 | Student 2 | Student 3 | | Student 1 | XX-IGNORE-XX |
| Role: Teacher | Teacher 1 | | | Student 1 | Student 2 |
| Status: Active | Teacher 1 | Student 1 | Student 3 | Student 2 | Student 4 |
| Status: Inactive | Student 2 | Student 4 | | Teacher 1 | Student 1 |
| matchtype | filtertype | filtervalue | expected1 | expected2 | expected3 | notexpected1 | notexpected2 |
| Any | Groups | No group | Student 1 | Student 4 | Teacher 1 | Student 2 | Student 3 |
| All | Groups | No group | Student 1 | Student 4 | Teacher 1 | Student 2 | Student 3 |
| None | Groups | No group | Student 2 | Student 3 | | Student 1 | Teacher 1 |
| Any | Role | Student | Student 1 | Student 2 | Student 3 | Teacher 1 | XX-IGNORE-XX |
| All | Role | Student | Student 1 | Student 2 | Student 3 | Teacher 1 | XX-IGNORE-XX |
| None | Role | Student | Teacher 1 | | | Student 1 | Student 2 |
| Any | Status | Active | Student 1 | Student 3 | Teacher 1 | Student 2 | Student 4 |
| All | Status | Active | Student 1 | Student 3 | Teacher 1 | Student 2 | Student 4 |
| None | Status | Active | Student 2 | Student 4 | | Student 1 | Student 3 |
| Any | Inactive for more than | 1 week | Student 3 | Student 4 | | Student 1 | Student 2 |
| All | Inactive for more than | 1 week | Student 3 | Student 4 | | Student 1 | Student 2 |
| None | Inactive for more than | 1 week | Student 1 | Student 2 | Teacher 1 | Student 3 | XX-IGNORE-XX |
@javascript
Scenario Outline: Filter users for a course with multiple values for a single filter
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
And I set the field "Match" in the "Filter 1" "fieldset" to "<matchtype>"
And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "<filtervalue1>" "list_item"
And I click on "<filtervalue2>" "list_item"
When I click on "Apply filters" "button"
Then I should see "<expected1>" in the "participants" "table"
And I should see "<expected2>" in the "participants" "table"
And I should see "<expected3>" in the "participants" "table"
And I should not see "<notexpected1>" in the "participants" "table"
And I should not see "<notexpected2>" in the "participants" "table"
# Note the 'XX-IGNORE-XX' elements are for when there is less than 2 'not expected' items.
Examples:
| matchtype | filtertype | filtervalue1 | filtervalue2 | expected1 | expected2 | expected3 | notexpected1 | notexpected2 |
| Any | Groups | Group 1 | Group 2 | Student 2 | Student 3 | | Student 1 | XX-IGNORE-XX |
| All | Groups | Group 1 | Group 2 | Student 2 | | | Student 1 | Student 3 |
| None | Groups | Group 1 | Group 2 | Student 1 | Teacher 1 | | Student 2 | Student 3 |
@javascript
Scenario Outline: Filter users which are group members in several courses
Given I log in as "teacher1"
And I am on "Course 3" course homepage
And I navigate to course participants
When I open the autocomplete suggestions list
And I click on "<filter1>" item in the autocomplete list
And I set the field "type" in the "Filter 1" "fieldset" to "<filtertype>"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "<filtervalue>" "list_item"
When I click on "Apply filters" "button"
Then I should see "<expected1>" in the "participants" "table"
And I should see "<expected2>" in the "participants" "table"
And I should see "<expected3>" in the "participants" "table"
And I should not see "<notexpected1>" in the "participants" "table"
And I should not see "<notexpected2>" in the "participants" "table"
# Note the 'XX-IGNORE-XX' elements are for when there is less than 2 'not expected' items.
Examples:
| filter1 | expected1 | expected2 | expected3 | notexpected1 | notexpected2 |
| Group: No group | Student 3 | | | Student 1 | Student 2 |
| Group: Group A | Student 1 | Student 2 | | Student 3 | XX-IGNORE-XX |
| Group: Group B | Student 2 | | | Student 1 | Student 3 |
| filtertype | filtervalue | expected1 | expected2 | notexpected1 | notexpected2 |
| Groups | No group | Student 3 | | Student 1 | Student 2 |
| Groups | Group A | Student 1 | Student 2 | Student 3 | XX-IGNORE-XX |
| Groups | Group B | Student 2 | | Student 1 | Student 3 |
@javascript
Scenario: In separate groups mode, a student in a single group can only view and filter by users in their own group
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
# Unsuspend student 2 for to improve coverage of this test.
And I click on "Edit enrolment" "icon" in the "Student 2" "table_row"
And I set the field "Status" to "Active"
And I click on "Save changes" "button"
And I log out
When I log in as "student3"
And I am on "Course 1" course homepage
And I navigate to course participants
# Default view should have groups filter pre-set.
Then I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should see "Group 2" in the "Filter 1" "fieldset"
And I should not see "Group 1" in the "Filter 1" "fieldset"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
# Testing result of removing groups filter row.
And I click on "Remove filter row" "button" in the "Filter 1" "fieldset"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
# Testing result of applying groups filter manually.
And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I should see "Group 2" in the ".form-autocomplete-suggestions" "css_element"
And I should not see "Group 1" in the ".form-autocomplete-suggestions" "css_element"
And I click on "Group 2" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
# Testing result of removing groups filter by clearing all filters.
And I click on "Clear filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
@javascript
Scenario: In separate groups mode, a student in multiple groups can only view and filter by users in their own groups
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
# Unsuspend student 2 for to improve coverage of this test.
And I click on "Edit enrolment" "icon" in the "Student 2" "table_row"
And I set the field "Status" to "Active"
And I click on "Save changes" "button"
And I log out
When I log in as "student2"
And I am on "Course 1" course homepage
And I navigate to course participants
# Default view should have groups filter pre-set.
Then I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should see "Group 1" in the "Filter 1" "fieldset"
And I should see "Group 2" in the "Filter 1" "fieldset"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
# Testing result of removing groups filter row.
And I click on "Remove filter row" "button" in the "Filter 1" "fieldset"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
# Testing result of applying groups filter manually.
And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I should see "Group 1" in the ".form-autocomplete-suggestions" "css_element"
And I should see "Group 2" in the ".form-autocomplete-suggestions" "css_element"
And I click on "Group 1" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
# Testing result of removing groups filter by clearing all filters.
And I click on "Clear filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
@javascript
Scenario: Filter users who have no role in a course
@ -109,8 +238,10 @@ Feature: Course participants can be filtered
And I click on ".form-autocomplete-selection [aria-selected=true]" "css_element"
And I press key "27" in the field "Student 1's role assignments"
And I click on "Save changes" "link"
When I open the autocomplete suggestions list
And I click on "Role: No roles" item in the autocomplete list
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "No roles" "list_item"
When I click on "Apply filters" "button"
Then I should see "Student 1" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
@ -122,44 +253,127 @@ Feature: Course participants can be filtered
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
When I open the autocomplete suggestions list
And I click on "Role: Student" item in the autocomplete list
And I open the autocomplete suggestions list
And I click on "Status: Active" item in the autocomplete list
And I set the field "Match" in the "Filter 1" "fieldset" to "All"
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Student" "list_item"
And I click on "Add condition" "button"
# Set filterset to match all.
And I set the field "Match" to "All"
And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
And I set the field "type" in the "Filter 2" "fieldset" to "Status"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
And I click on "Active" "list_item"
When I click on "Apply filters" "button"
Then I should see "Student 1" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Add more filters.
And I open the autocomplete suggestions list
And I click on "Enrolment methods: Manual enrolments" item in the autocomplete list
And I open the autocomplete suggestions list
And I click on "Group: Group 2" item in the autocomplete list
And I click on "Add condition" "button"
And I set the field "Match" in the "Filter 3" "fieldset" to "Any"
And I set the field "type" in the "Filter 3" "fieldset" to "Enrolment methods"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 3" "fieldset"
And I click on "Manual enrolments" "list_item"
And I click on "Add condition" "button"
And I set the field "Match" in the "Filter 4" "fieldset" to "All"
And I set the field "type" in the "Filter 4" "fieldset" to "Groups"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 4" "fieldset"
And I click on "Group 2" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 3" in the "participants" "table"
But I should not see "Teacher 1" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
# Deselect the active status filter.
And I click on "Status: Active" "text" in the ".form-autocomplete-selection" "css_element"
# Apply Status: Inactive filter.
And I open the autocomplete suggestions list
And I click on "Status: Inactive" item in the autocomplete list
# Change the active status filter to inactive.
And I click on "Remove \"Active\" from filter" "button" in the "Filter 2" "fieldset"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
And I click on "Inactive" "list_item"
And I click on "Apply filters" "button"
Then I should see "Student 2" in the "participants" "table"
But I should not see "Student 4" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Set both statuses (match any).
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
And I click on "Active" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
# Switch to match all.
And I set the field "Match" in the "Filter 2" "fieldset" to "All"
And I click on "Apply filters" "button"
And I should see "Nothing to display"
@javascript
Scenario: Filter by keyword
Scenario: Filter match by one or more keywords and modified match types
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
# Note: This is the literal string "student", not the Role student.
When I set the field "Filters" to "student"
And I press key "13" in the field "Filters"
And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I set the field "Type..." to "1@example"
And I press key "13" in the field "Type..."
When I click on "Apply filters" "button"
Then I should see "Student 1" in the "participants" "table"
And I should see "Teacher 1" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
And I set the field "Match" in the "Filter 1" "fieldset" to "All"
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Teacher 1" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
And I set the field "Match" in the "Filter 1" "fieldset" to "None"
And I click on "Apply filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Add a second keyword filter value
And I set the field "Type..." to "moodle"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Teacher 1" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
And I set the field "Match" in the "Filter 1" "fieldset" to "All"
And I click on "Apply filters" "button"
And I should see "Nothing to display"
@javascript
Scenario: Reorder users without losing filter
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Student" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
When I click on "Surname" "link"
Then I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
@ -167,39 +381,40 @@ Feature: Course participants can be filtered
And I should not see "Teacher 1" in the "participants" "table"
@javascript
Scenario: Reorder users without losing filter
Scenario: Only possible to add filter rows for the number of filters available
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
And I open the autocomplete suggestions list
And I click on "Role: Student" item in the autocomplete list
When I click on "Surname" "link"
Then I should see "Role: Student"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I click on "Add condition" "button"
And I set the field "type" in the "Filter 2" "fieldset" to "Status"
And I click on "Add condition" "button"
And I set the field "type" in the "Filter 3" "fieldset" to "Roles"
And I click on "Add condition" "button"
And I set the field "type" in the "Filter 4" "fieldset" to "Enrolment methods"
And I click on "Add condition" "button"
And I set the field "type" in the "Filter 5" "fieldset" to "Groups"
And I click on "Add condition" "button"
And I set the field "type" in the "Filter 6" "fieldset" to "Inactive for more than"
And the "Add condition" "button" should be disabled
@javascript
Scenario: Rendering filter options for teachers in a course that don't support groups
Given I log in as "teacher1"
And I am on "Course 2" course homepage
And I navigate to course participants
When I open the autocomplete suggestions list
Then I should see "Role:" in the ".form-autocomplete-suggestions" "css_element"
And I should see "Enrolment methods:" in the ".form-autocomplete-suggestions" "css_element"
But I should not see "Group:" in the ".form-autocomplete-suggestions" "css_element"
When I navigate to course participants
Then I should see "Roles" in the "type" "field"
And I should see "Enrolment methods" in the "type" "field"
But I should not see "Groups" in the "type" "field"
@javascript
Scenario: Rendering filter options for students who have limited privileges
Given I log in as "student1"
And I am on "Course 2" course homepage
And I navigate to course participants
When I open the autocomplete suggestions list
Then I should see "Role:" in the ".form-autocomplete-suggestions" "css_element"
But I should not see "Status:" in the ".form-autocomplete-suggestions" "css_element"
And I should not see "Enrolment methods:" in the ".form-autocomplete-suggestions" "css_element"
When I navigate to course participants
Then I should see "Roles" in the "type" "field"
But I should not see "Status" in the "type" "field"
And I should not see "Enrolment methods" in the "type" "field"
@javascript
Scenario: Filter by user identity fields
@ -208,39 +423,45 @@ Feature: Course participants can be filtered
| showuseridentity | idnumber,email,city,country |
And I am on "Course 1" course homepage
And I navigate to course participants
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
# Search by email (only).
When I set the field "Filters" to "student1@example.com"
And I press key "13" in the field "Filters"
And I set the field "Type..." to "student1@example.com"
And I press key "13" in the field "Type..."
When I click on "Apply filters" "button"
Then I should see "Student 1" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Search by idnumber (only).
And I click on "student1@example.com" "text" in the ".form-autocomplete-selection" "css_element"
And I set the field "Filters" to "SID"
And I press key "13" in the field "Filters"
And I click on "Remove \"student1@example.com\" from filter" "button" in the "Filter 1" "fieldset"
And I set the field "Type..." to "SID"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Search by city (only).
And I click on "SID" "text" in the ".form-autocomplete-selection" "css_element"
And I set the field "Filters" to "SCITY"
And I press key "13" in the field "Filters"
And I click on "Remove \"SID\" from filter" "button" in the "Filter 1" "fieldset"
And I set the field "Type..." to "SCITY"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Search by country text (only) - should not match.
And I click on "SCITY" "text" in the ".form-autocomplete-selection" "css_element"
And I set the field "Filters" to "GB"
And I press key "13" in the field "Filters"
And I click on "Remove \"SCITY\" from filter" "button" in the "Filter 1" "fieldset"
And I set the field "Type..." to "GB"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Nothing to display"
# Check no match.
And I click on "GB" "text" in the ".form-autocomplete-selection" "css_element"
And I set the field "Filters" to "NOTHING"
And I press key "13" in the field "Filters"
And I click on "Remove \"GB\" from filter" "button" in the "Filter 1" "fieldset"
And I set the field "Type..." to "NOTHING"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Nothing to display"
@javascript
@ -255,27 +476,137 @@ Feature: Course participants can be filtered
And I am on "Course 1" course homepage
And I navigate to course participants
# Search by email (only) - should only see visible email + own.
When I set the field "Filters" to "@example.com"
And I press key "13" in the field "Filters"
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I set the field "Type..." to "@example."
And I press key "13" in the field "Type..."
When I click on "Apply filters" "button"
Then I should not see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
And I should see "Teacher 1" in the "participants" "table"
# Search for other fields - should only see own results.
And I click on "@example.com" "text" in the ".form-autocomplete-selection" "css_element"
And I set the field "Filters" to "SID"
And I press key "13" in the field "Filters"
And I click on "Remove \"@example.\" from filter" "button" in the "Filter 1" "fieldset"
And I set the field "Type..." to "SID"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Nothing to display"
And I click on "SID" "text" in the ".form-autocomplete-selection" "css_element"
And I set the field "Filters" to "TID"
And I press key "13" in the field "Filters"
And I click on "Remove \"SID\" from filter" "button" in the "Filter 1" "fieldset"
And I set the field "Type..." to "TID"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Teacher 1" in the "participants" "table"
And I set the field "Filters" to "CITY"
And I press key "13" in the field "Filters"
And I should not see "Student 1" in the "participants" "table"
And I click on "Remove \"TID\" from filter" "button" in the "Filter 1" "fieldset"
And I set the field "Type..." to "CITY"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Teacher 1" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
# Check no match.
And I set the field "Filters" to "NOTHING"
And I press key "13" in the field "Filters"
And I click on "Remove \"CITY\" from filter" "button" in the "Filter 1" "fieldset"
And I set the field "Type..." to "NOTHING"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Nothing to display"
@javascript
Scenario: Individual filters can be removed, which will automatically refresh the participants list
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
And I set the field "Match" in the "Filter 1" "fieldset" to "All"
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Student" "list_item"
And I click on "Add condition" "button"
# Set filterset to match all.
And I set the field "Match" to "All"
And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
And I set the field "type" in the "Filter 2" "fieldset" to "Keyword"
And I set the field "Type..." to "@example"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
When I click on "Remove filter row" "button" in the "Filter 1" "fieldset"
Then I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Teacher 1" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
@javascript
Scenario: All filters can be cleared at once
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
And I set the field "Match" in the "Filter 1" "fieldset" to "All"
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Student" "list_item"
And I click on "Add condition" "button"
# Set filterset to match all.
And I set the field "Match" to "All"
And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
And I set the field "type" in the "Filter 2" "fieldset" to "Keyword"
And I set the field "Type..." to "@example"
And I press key "13" in the field "Type..."
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
When I click on "Clear filters" "button"
Then I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should see "Teacher 1" in the "participants" "table"
@javascript
Scenario: Filterset match type is reset when reducing to a single filter
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I set the field "Type..." to "@example.com"
And I press key "13" in the field "Type..."
And I click on "Add condition" "button"
# Set filterset to match none.
And I set the field "Match" to "None"
And I set the field "Match" in the "Filter 2" "fieldset" to "All"
And I set the field "type" in the "Filter 2" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
And I click on "Student" "list_item"
# Match none of student role and @example.com keyword.
And I click on "Apply filters" "button"
And I should see "Teacher 1" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I should not see "Student 3" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
When I click on "Remove filter row" "button" in the "Filter 2" "fieldset"
# Filterset match type and role filter are removed, leaving keyword filter only.
Then I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should not see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
And I click on "Add condition" "button"
# Re-add a second filter and ensure the default (any) filterset match type is set.
And I set the field "Match" in the "Filter 2" "fieldset" to "All"
And I set the field "type" in the "Filter 2" "fieldset" to "Role"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
And I click on "Student" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"

View File

@ -78,30 +78,43 @@ Feature: Course participants can be filtered to display all the users
| student3 | G2 |
@javascript
Scenario: Show all filtered users for a course
Scenario: Show all users in a course that match a single filter value
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
When I open the autocomplete suggestions list
And I click on "Role: Student" item in the autocomplete list
And I set the field "Match" in the "Filter 1" "fieldset" to "All"
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Student" "list_item"
When I click on "Apply filters" "button"
Then I should see "24 participants found"
And I should see "Show all 24"
And I should not see "Show 20 per page"
And I should not see "of the following"
And I click on "Show all 24" "link"
Then I should see "Role: Student"
And I should see "24 participants found"
And I should see "Show 20 per page"
And I should not see "Show all 24"
@javascript
Scenario: Apply more than one filter and show all users
Scenario: Apply one value for more than one filter and show all matching users
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to course participants
When I open the autocomplete suggestions list
And I click on "Role: Student" item in the autocomplete list
And I open the autocomplete suggestions list
And I click on "Status: Active" item in the autocomplete list
And I click on "Add condition" "button"
And I set the field "Match" to "All"
And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Student" "list_item"
And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
And I set the field "type" in the "Filter 2" "fieldset" to "Status"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 2" "fieldset"
And I click on "Active" "list_item"
When I click on "Apply filters" "button"
And I click on "Show all 23" "link"
Then I should see "Role: Student"
And I should see "Status: Active"
And I should see "23 participants found"
Then I should see "23 participants found"
And I should see "Show 20 per page"
And I should see "of the following"
And I should see "Student 1"
And I should not see "Student 24"
And I should see "Show 20 per page"
And I should not see "Show all 23"

View File

@ -59,9 +59,10 @@ Feature: View course participants groups
Then I should see "Group A"
And I should see "Student 1x"
And I should see "Student 2x"
And I open the autocomplete suggestions list
And I click on "Group: Group B" item in the autocomplete list
And I should see "Group B"
And I set the field "type" in the "Filter 1" "fieldset" to "Groups"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I click on "Group B" "list_item"
And I click on "Apply filters" "button"
And I should see "Student 3x"
And I should see "Student 4x"

View File

@ -2008,6 +2008,407 @@ class participants_search_test extends advanced_testcase {
return $finaltests;
}
/**
* Ensure that the groups filter works as expected when separate groups mode is enabled, with the provided test cases.
*
* @param array $usersdata The list of users to create
* @param array $groupsavailable The names of groups that should be created in the course
* @param array $filtergroups The names of groups to filter by
* @param int $jointype The join type to use when combining filter values
* @param int $count The expected count
* @param array $expectedusers
* @param string $loginusername The user to login as for the tests
* @dataProvider groups_separate_provider
*/
public function test_groups_filter_separate_groups(array $usersdata, array $groupsavailable, array $filtergroups, int $jointype,
int $count, array $expectedusers, string $loginusername): void {
$course = $this->getDataGenerator()->create_course();
$coursecontext = context_course::instance($course->id);
$users = [];
// Enable separate groups mode on the course.
$course->groupmode = SEPARATEGROUPS;
$course->groupmodeforce = true;
update_course($course);
// Prepare data for filtering by users in no groups.
$nogroupsdata = (object) [
'id' => USERSWITHOUTGROUP,
];
// Map group names to group data.
$groupsdata = ['nogroups' => $nogroupsdata];
foreach ($groupsavailable as $groupname) {
$groupinfo = [
'courseid' => $course->id,
'name' => $groupname,
];
$groupsdata[$groupname] = $this->getDataGenerator()->create_group($groupinfo);
}
foreach ($usersdata as $username => $userdata) {
$user = $this->getDataGenerator()->create_user(['username' => $username]);
$this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
if (array_key_exists('groups', $userdata)) {
foreach ($userdata['groups'] as $groupname) {
$userinfo = [
'userid' => $user->id,
'groupid' => (int) $groupsdata[$groupname]->id,
];
$this->getDataGenerator()->create_group_member($userinfo);
}
}
$users[$username] = $user;
if ($username == $loginusername) {
$loginuser = $user;
}
}
// Create a secondary course with users. We should not see these users.
$this->create_course_with_users(1, 1, 1, 1);
// Log in as the user to be tested.
$this->setUser($loginuser);
// Create the basic filter.
$filterset = new participants_filterset();
$filterset->add_filter(new integer_filter('courseid', null, [(int) $course->id]));
// Create the groups filter.
$groupsfilter = new integer_filter('groups');
$filterset->add_filter($groupsfilter);
// Configure the filter.
foreach ($filtergroups as $filtergroupname) {
$groupsfilter->add_filter_value((int) $groupsdata[$filtergroupname]->id);
}
$groupsfilter->set_join_type($jointype);
// Run the search.
$search = new participants_search($course, $coursecontext, $filterset);
// Tests on user in no groups should throw an exception as they are not supported (participants are not visible to them).
if (in_array('exception', $expectedusers)) {
$this->expectException(\coding_exception::class);
$rs = $search->get_participants();
} else {
// All other cases are tested as normal.
$rs = $search->get_participants();
$this->assertInstanceOf(moodle_recordset::class, $rs);
$records = $this->convert_recordset_to_array($rs);
$this->assertCount($count, $records);
$this->assertEquals($count, $search->get_total_participants_count());
foreach ($expectedusers as $expecteduser) {
$this->assertArrayHasKey($users[$expecteduser]->id, $records);
}
}
}
/**
* Data provider for groups filter tests.
*
* @return array
*/
public function groups_separate_provider(): array {
$tests = [
'Users in different groups with separate groups mode enabled' => (object) [
'groupsavailable' => [
'groupa',
'groupb',
'groupc',
],
'users' => [
'a' => [
'groups' => ['groupa'],
],
'b' => [
'groups' => ['groupb'],
],
'c' => [
'groups' => ['groupa', 'groupb'],
],
'd' => [
'groups' => [],
],
],
'expect' => [
// Tests for jointype: ANY.
'ANY: No filter, user in one group' => (object) [
'loginuser' => 'a',
'groups' => [],
'jointype' => filter::JOINTYPE_ANY,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'ANY: No filter, user in multiple groups' => (object) [
'loginuser' => 'c',
'groups' => [],
'jointype' => filter::JOINTYPE_ANY,
'count' => 3,
'expectedusers' => [
'a',
'b',
'c',
],
],
'ANY: No filter, user in no groups' => (object) [
'loginuser' => 'd',
'groups' => [],
'jointype' => filter::JOINTYPE_ANY,
'count' => 0,
'expectedusers' => ['exception'],
],
'ANY: Filter on a single group, user in one group' => (object) [
'loginuser' => 'a',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_ANY,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'ANY: Filter on a single group, user in multple groups' => (object) [
'loginuser' => 'c',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_ANY,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'ANY: Filter on a single group, user in no groups' => (object) [
'loginuser' => 'd',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_ANY,
'count' => 0,
'expectedusers' => ['exception'],
],
'ANY: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
'loginuser' => 'a',
'groups' => ['groupa', 'groupb'],
'jointype' => filter::JOINTYPE_ANY,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'ANY: Filter on multiple groups, user in multiple groups' => (object) [
'loginuser' => 'c',
'groups' => ['groupa', 'groupb'],
'jointype' => filter::JOINTYPE_ANY,
'count' => 3,
'expectedusers' => [
'a',
'b',
'c',
],
],
'ANY: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
'loginuser' => 'c',
'groups' => ['groupa', 'groupb', 'nogroups'],
'jointype' => filter::JOINTYPE_ANY,
'count' => 3,
'expectedusers' => [
'a',
'b',
'c',
],
],
// Tests for jointype: ALL.
'ALL: No filter, user in one group' => (object) [
'loginuser' => 'a',
'groups' => [],
'jointype' => filter::JOINTYPE_ALL,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'ALL: No filter, user in multiple groups' => (object) [
'loginuser' => 'c',
'groups' => [],
'jointype' => filter::JOINTYPE_ALL,
'count' => 3,
'expectedusers' => [
'a',
'b',
'c',
],
],
'ALL: No filter, user in no groups' => (object) [
'loginuser' => 'd',
'groups' => [],
'jointype' => filter::JOINTYPE_ALL,
'count' => 0,
'expectedusers' => ['exception'],
],
'ALL: Filter on a single group, user in one group' => (object) [
'loginuser' => 'a',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_ALL,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'ALL: Filter on a single group, user in multple groups' => (object) [
'loginuser' => 'c',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_ALL,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'ALL: Filter on a single group, user in no groups' => (object) [
'loginuser' => 'd',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_ALL,
'count' => 0,
'expectedusers' => ['exception'],
],
'ALL: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
'loginuser' => 'a',
'groups' => ['groupa', 'groupb'],
'jointype' => filter::JOINTYPE_ALL,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'ALL: Filter on multiple groups, user in multiple groups' => (object) [
'loginuser' => 'c',
'groups' => ['groupa', 'groupb'],
'jointype' => filter::JOINTYPE_ALL,
'count' => 1,
'expectedusers' => [
'c',
],
],
'ALL: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
'loginuser' => 'c',
'groups' => ['groupa', 'groupb', 'nogroups'],
'jointype' => filter::JOINTYPE_ALL,
'count' => 1,
'expectedusers' => [
'c',
],
],
// Tests for jointype: NONE.
'NONE: No filter, user in one group' => (object) [
'loginuser' => 'a',
'groups' => [],
'jointype' => filter::JOINTYPE_NONE,
'count' => 2,
'expectedusers' => [
'a',
'c',
],
],
'NONE: No filter, user in multiple groups' => (object) [
'loginuser' => 'c',
'groups' => [],
'jointype' => filter::JOINTYPE_NONE,
'count' => 3,
'expectedusers' => [
'a',
'b',
'c',
],
],
'NONE: No filter, user in no groups' => (object) [
'loginuser' => 'd',
'groups' => [],
'jointype' => filter::JOINTYPE_NONE,
'count' => 0,
'expectedusers' => ['exception'],
],
'NONE: Filter on a single group, user in one group' => (object) [
'loginuser' => 'a',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_NONE,
'count' => 0,
'expectedusers' => [],
],
'NONE: Filter on a single group, user in multple groups' => (object) [
'loginuser' => 'c',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_NONE,
'count' => 1,
'expectedusers' => [
'b',
],
],
'NONE: Filter on a single group, user in no groups' => (object) [
'loginuser' => 'd',
'groups' => ['groupa'],
'jointype' => filter::JOINTYPE_NONE,
'count' => 0,
'expectedusers' => ['exception'],
],
'NONE: Filter on multiple groups, user in one group (ignore invalid groups)' => (object) [
'loginuser' => 'a',
'groups' => ['groupa', 'groupb'],
'jointype' => filter::JOINTYPE_NONE,
'count' => 0,
'expectedusers' => [],
],
'NONE: Filter on multiple groups, user in multiple groups' => (object) [
'loginuser' => 'c',
'groups' => ['groupa', 'groupb'],
'jointype' => filter::JOINTYPE_NONE,
'count' => 0,
'expectedusers' => [],
],
'NONE: Filter on multiple groups or no groups, user in multiple groups (ignore no groups)' => (object) [
'loginuser' => 'c',
'groups' => ['groupa', 'groupb', 'nogroups'],
'jointype' => filter::JOINTYPE_NONE,
'count' => 0,
'expectedusers' => [],
],
],
],
];
$finaltests = [];
foreach ($tests as $testname => $testdata) {
foreach ($testdata->expect as $expectname => $expectdata) {
$finaltests["{$testname} => {$expectname}"] = [
'users' => $testdata->users,
'groupsavailable' => $testdata->groupsavailable,
'filtergroups' => $expectdata->groups,
'jointype' => $expectdata->jointype,
'count' => $expectdata->count,
'expectedusers' => $expectdata->expectedusers,
'loginusername' => $expectdata->loginuser,
];
}
}
return $finaltests;
}
/**
* Ensure that the last access filter works as expected with the provided test cases.
*

View File

@ -852,192 +852,4 @@ class core_userliblib_testcase extends advanced_testcase {
self::assertSame('5', $got['timezone']);
self::assertSame('0', $got['mailformat']);
}
/**
* Test returning the total number of participants.
*/
public function test_user_get_total_participants() {
global $DB;
$this->resetAfterTest();
// Create a course.
$course = self::getDataGenerator()->create_course();
// Create a teacher.
$teacher = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
// Create a bunch of students.
$student1 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
$student2 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
$student3 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
// Create a group.
$group = self::getDataGenerator()->create_group(array('courseid' => $course->id));
// Enrol the students.
self::getDataGenerator()->enrol_user($student1->id, $course->id);
self::getDataGenerator()->enrol_user($student2->id, $course->id);
self::getDataGenerator()->enrol_user($student3->id, $course->id);
// Enrol the teacher.
$roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
self::getDataGenerator()->enrol_user($teacher->id, $course->id, $roleids['editingteacher']);
// Add the teacher and two of the students to the group.
groups_add_member($group->id, $teacher->id);
groups_add_member($group->id, $student1->id);
groups_add_member($group->id, $student2->id);
// Set it so the teacher and two of the students have not accessed the courses within the last day,
// but only one of the students is in the group.
$accesssince = time() - DAYSECS;
$lastaccess = new stdClass();
$lastaccess->userid = $teacher->id;
$lastaccess->courseid = $course->id;
$lastaccess->timeaccess = time() - DAYSECS;
$DB->insert_record('user_lastaccess', $lastaccess);
$lastaccess->userid = $student1->id;
$DB->insert_record('user_lastaccess', $lastaccess);
$lastaccess->userid = $student3->id;
$DB->insert_record('user_lastaccess', $lastaccess);
// Now, when we perform the following search we should only return 2 users. Student who belong to
// the group and have the name 'searchforthis' and have not accessed the course in the last day.
$count = user_get_total_participants($course->id, $group->id, $accesssince + 1, $roleids['student'], 0, -1,
'searchforthis');
$this->assertEquals(2, $count);
}
/**
* Test returning the number of participants on the front page.
*/
public function test_user_get_total_participants_on_front_page() {
$this->resetAfterTest();
// Set it so that only 3 users have not accessed the site within the last day (including one which has never accessed it).
$accesssince = time() - DAYSECS;
// Create a bunch of users.
$user1 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis', 'lastaccess' => $accesssince]);
$user2 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis', 'lastaccess' => $accesssince]);
$user3 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis', 'lastaccess' => time()]);
$user4 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
// Create a group.
$group = self::getDataGenerator()->create_group(array('courseid' => SITEID));
// Add 3 of the users to a group.
groups_add_member($group->id, $user1->id);
groups_add_member($group->id, $user2->id);
groups_add_member($group->id, $user3->id);
// Now, when we perform the following search we should only return 2 users. Users who belong to
// the group and have the name 'searchforthis' and have not accessed the site in the last day.
$count = user_get_total_participants(SITEID, $group->id, $accesssince + 1, 0, 0, -1, 'searchforthis');
$this->assertEquals(2, $count);
}
/**
* Test returning the participants.
*/
public function test_user_get_participants() {
global $DB;
$this->resetAfterTest();
// Create a course.
$course = self::getDataGenerator()->create_course();
// Create a teacher.
$teacher = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
// Create a bunch of students.
$student1 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
$student2 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
$student3 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
// Create a group.
$group = self::getDataGenerator()->create_group(array('courseid' => $course->id));
// Enrol the students.
self::getDataGenerator()->enrol_user($student1->id, $course->id);
self::getDataGenerator()->enrol_user($student2->id, $course->id);
self::getDataGenerator()->enrol_user($student3->id, $course->id);
// Enrol the teacher.
$roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
self::getDataGenerator()->enrol_user($teacher->id, $course->id, $roleids['editingteacher']);
// Add the teacher and two of the students to the group.
groups_add_member($group->id, $teacher->id);
groups_add_member($group->id, $student1->id);
groups_add_member($group->id, $student2->id);
// Set it so the teacher and two of the students have not accessed the course within the last day, but only one of
// the students is in the group (student 3 has never accessed the course).
$accesssince = time() - DAYSECS;
$lastaccess = new stdClass();
$lastaccess->userid = $teacher->id;
$lastaccess->courseid = $course->id;
$lastaccess->timeaccess = time() - DAYSECS;
$DB->insert_record('user_lastaccess', $lastaccess);
$lastaccess->userid = $student1->id;
$DB->insert_record('user_lastaccess', $lastaccess);
$lastaccess->userid = $student2->id;
$lastaccess->timeaccess = time();
$DB->insert_record('user_lastaccess', $lastaccess);
// Now, when we perform the following search we should only return 1 user. A student who belongs to
// the group and has the name 'searchforthis' and has not accessed the course in the last day.
$userset = user_get_participants($course->id, $group->id, $accesssince + 1, $roleids['student'], 0, -1, 'searchforthis');
$this->assertEquals($student1->id, $userset->current()->id);
$this->assertEquals(1, iterator_count($userset));
// Search for users without any group.
$userset = user_get_participants($course->id, USERSWITHOUTGROUP, 0, $roleids['student'], 0, -1, '');
$this->assertEquals($student3->id, $userset->current()->id);
$this->assertEquals(1, iterator_count($userset));
}
/**
* Test returning the participants on the front page.
*/
public function test_user_get_participants_on_front_page() {
$this->resetAfterTest();
// Set it so that only 3 users have not accessed the site within the last day (user 4 has never accessed the site).
$accesssince = time() - DAYSECS;
// Create a bunch of users.
$user1 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis', 'lastaccess' => $accesssince]);
$user2 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis', 'lastaccess' => $accesssince]);
$user3 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis', 'lastaccess' => time()]);
$user4 = self::getDataGenerator()->create_user(['firstname' => 'searchforthis']);
// Create a group.
$group = self::getDataGenerator()->create_group(array('courseid' => SITEID));
// Add 3 of the users to a group.
groups_add_member($group->id, $user1->id);
groups_add_member($group->id, $user2->id);
groups_add_member($group->id, $user3->id);
// Now, when we perform the following search we should only return 2 users. Users who belong to
// the group and have the name 'searchforthis' and have not accessed the site in the last day.
$userset = user_get_participants(SITEID, $group->id, $accesssince + 1, 0, 0, -1, 'searchforthis', '', array(),
'ORDER BY id ASC');
$this->assertEquals($user1->id, $userset->current()->id);
$userset->next();
$this->assertEquals($user2->id, $userset->current()->id);
}
}

View File

@ -1,5 +1,17 @@
This files describes API changes for code that uses the user API.
=== 3.9 ===
* The unified filter has been replaced by the participants filter. The following have therefore been deprecated:
* Library functions:
* user_get_participants_sql
* user_get_total_participants
* user_get_participants
* Unified filter renderer (core_user_renderer::unified_filter)
* Unified filter renderable (\core_user\output\unified_filter)
* Unified filter JavaScript (core_user/unified_filter.js and core_user/unified_filter_datasource.js)
* Unified filter template (unified_filter.mustache)
=== 3.6 ===
* The following functions have been finally deprecated and can not be used anymore: