diff --git a/course/tests/behat/rename_roles.feature b/course/tests/behat/rename_roles.feature index 8c8a5e8a5a3..bb5ab23abd7 100644 --- a/course/tests/behat/rename_roles.feature +++ b/course/tests/behat/rename_roles.feature @@ -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" diff --git a/group/tests/behat/create_groups.feature b/group/tests/behat/create_groups.feature index 47866377a10..ef8bc240df9 100644 --- a/group/tests/behat/create_groups.feature +++ b/group/tests/behat/create_groups.feature @@ -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" diff --git a/group/tests/behat/group_description.feature b/group/tests/behat/group_description.feature index 5f4b82e26e6..e63ee057dbe 100644 --- a/group/tests/behat/group_description.feature +++ b/group/tests/behat/group_description.feature @@ -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 diff --git a/lang/en/deprecated.txt b/lang/en/deprecated.txt index 1bb420fea0d..4203ac997e9 100644 --- a/lang/en/deprecated.txt +++ b/lang/en/deprecated.txt @@ -144,3 +144,4 @@ pacific/yap,core_timezones editsettings,core_badges availablelicenses,core_admin managelicenses,core_admin +userfilterplaceholder,core diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 50ea6d2aed1..4f2001731ad 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -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'; diff --git a/lang/en/user.php b/lang/en/user.php index 5eee2cb2b43..ee3c7fdf6d8 100644 --- a/lang/en/user.php +++ b/lang/en/user.php @@ -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:'; diff --git a/lib/behat/classes/behat_core_generator.php b/lib/behat/classes/behat_core_generator.php index a1ae1ace41e..6e2dd2d73b5 100644 --- a/lib/behat/classes/behat_core_generator.php +++ b/lib/behat/classes/behat_core_generator.php @@ -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); + } + } } diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php index faeb9ace2e2..1327d93cfa3 100644 --- a/lib/deprecatedlib.php +++ b/lib/deprecatedlib.php @@ -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); +} diff --git a/lib/table/amd/build/dynamic.min.js b/lib/table/amd/build/dynamic.min.js index f4b2bdc3449..da081b31a79 100644 --- a/lib/table/amd/build/dynamic.min.js +++ b/lib/table/amd/build/dynamic.min.js @@ -1,2 +1,2 @@ -function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("core_table/dynamic",["exports","core_table/local/dynamic/repository","core_table/local/dynamic/selectors","./local/dynamic/events","core/loadingicon"],function(a,b,c,d,e){"use strict";Object.defineProperty(a,"__esModule",{value:!0});Object.defineProperty(a,"Events",{enumerable:!0,get:function get(){return d.default}});a.getTableFromId=a.init=a.showColumn=a.hideColumn=a.setLastInitial=a.setFirstInitial=a.setPageSize=a.setPageNumber=a.setSortOrder=a.setFilters=a.updateTable=a.refreshTableContent=void 0;c=g(c);d=function(a){return a&&a.__esModule?a:{default:a}}(d);function f(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;f=function(){return a};return a}function g(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=f();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var g=d?Object.getOwnPropertyDescriptor(a,e):null;if(g&&(g.get||g.set)){Object.defineProperty(c,e,g)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function h(a){return l(a)||k(a)||j(a)||i()}function i(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function j(a,b){if(!a)return;if("string"==typeof a)return m(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return m(a,b)}function k(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function l(a){if(Array.isArray(a))return m(a)}function m(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);ca.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Module to handle dynamic table features.\n *\n * @module core_table/dynamic\n * @package core_table\n * @copyright 2020 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {fetch as fetchTableData} from 'core_table/local/dynamic/repository';\nimport * as Selectors from 'core_table/local/dynamic/selectors';\nimport Events from './local/dynamic/events';\nimport {addIconToContainer} from 'core/loadingicon';\n\nlet watching = false;\n\n/**\n * Ensure that a table is a dynamic table.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Bool}\n */\nconst checkTableIsDynamic = tableRoot => {\n if (!tableRoot) {\n // The table is not a dynamic table.\n throw new Error(\"The table specified is not a dynamic table and cannot be updated\");\n }\n\n if (!tableRoot.matches(Selectors.main.region)) {\n // The table is not a dynamic table.\n throw new Error(\"The table specified is not a dynamic table and cannot be updated\");\n }\n\n return true;\n};\n\n/**\n * Get the filterset data from a known dynamic table.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Object}\n */\nconst getFiltersetFromTable = tableRoot => {\n return JSON.parse(tableRoot.dataset.tableFilters);\n};\n\n/**\n * Update the specified table based on its current values.\n *\n * @param {HTMLElement} tableRoot\n * @param {Bool} resetContent\n * @returns {Promise}\n */\nexport const refreshTableContent = (tableRoot, resetContent = false) => {\n const filterset = getFiltersetFromTable(tableRoot);\n addIconToContainer(tableRoot);\n\n return fetchTableData(\n tableRoot.dataset.tableComponent,\n tableRoot.dataset.tableHandler,\n tableRoot.dataset.tableUniqueid,\n {\n sortData: JSON.parse(tableRoot.dataset.tableSortData),\n joinType: filterset.jointype,\n filters: filterset.filters,\n firstinitial: tableRoot.dataset.tableFirstInitial,\n lastinitial: tableRoot.dataset.tableLastInitial,\n pageNumber: tableRoot.dataset.tablePageNumber,\n pageSize: tableRoot.dataset.tablePageSize,\n hiddenColumns: JSON.parse(tableRoot.dataset.tableHiddenColumns),\n },\n resetContent,\n )\n .then(data => {\n const placeholder = document.createElement('div');\n placeholder.innerHTML = data.html;\n tableRoot.replaceWith(...placeholder.childNodes);\n\n // Update the tableRoot.\n return getTableFromId(tableRoot.dataset.tableUniqueid);\n }).then(tableRoot => {\n tableRoot.dispatchEvent(new CustomEvent(Events.tableContentRefreshed, {\n bubbles: true,\n }));\n\n return tableRoot;\n });\n};\n\nexport const updateTable = (tableRoot, {\n sortBy = null,\n sortOrder = null,\n filters = null,\n firstInitial = null,\n lastInitial = null,\n pageNumber = null,\n pageSize = null,\n hiddenColumns = null,\n} = {}, refreshContent = true) => {\n checkTableIsDynamic(tableRoot);\n\n // Update sort fields.\n if (sortBy && sortOrder) {\n const sortData = JSON.parse(tableRoot.dataset.tableSortData);\n sortData.unshift({\n sortby: sortBy,\n sortorder: parseInt(sortOrder, 10),\n });\n tableRoot.dataset.tableSortData = JSON.stringify(sortData);\n }\n\n // Update initials.\n if (firstInitial !== null) {\n tableRoot.dataset.tableFirstInitial = firstInitial;\n }\n\n if (lastInitial !== null) {\n tableRoot.dataset.tableLastInitial = lastInitial;\n }\n\n if (pageNumber !== null) {\n tableRoot.dataset.tablePageNumber = pageNumber;\n }\n\n if (pageSize !== null) {\n tableRoot.dataset.tablePageSize = pageSize;\n }\n\n // Update filters.\n if (filters) {\n tableRoot.dataset.tableFilters = JSON.stringify(filters);\n }\n\n // Update hidden columns.\n if (hiddenColumns) {\n tableRoot.dataset.tableHiddenColumns = JSON.stringify(hiddenColumns);\n }\n\n // Refresh.\n if (refreshContent) {\n return refreshTableContent(tableRoot);\n } else {\n return Promise.resolve(tableRoot);\n }\n};\n\n/**\n * Update the specified table using the new filters.\n *\n * @param {HTMLElement} tableRoot\n * @param {Object} filters\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setFilters = (tableRoot, filters, refreshContent = true) =>\n updateTable(tableRoot, {filters}, refreshContent);\n\n/**\n * Update the sort order.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} sortBy\n * @param {Number} sortOrder\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setSortOrder = (tableRoot, sortBy, sortOrder, refreshContent = true) =>\n updateTable(tableRoot, {sortBy, sortOrder}, refreshContent);\n\n/**\n * Set the page number.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} pageNumber\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setPageNumber = (tableRoot, pageNumber, refreshContent = true) =>\n updateTable(tableRoot, {pageNumber}, refreshContent);\n\n/**\n * Set the page size.\n *\n * @param {HTMLElement} tableRoot\n * @param {Number} pageSize\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setPageSize = (tableRoot, pageSize, refreshContent = true) =>\n updateTable(tableRoot, {pageSize, pageNumber: 0}, refreshContent);\n\n/**\n * Update the first initial to show.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} firstInitial\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setFirstInitial = (tableRoot, firstInitial, refreshContent = true) =>\n updateTable(tableRoot, {firstInitial}, refreshContent);\n\n/**\n * Update the last initial to show.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} lastInitial\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setLastInitial = (tableRoot, lastInitial, refreshContent = true) =>\n updateTable(tableRoot, {lastInitial}, refreshContent);\n\n/**\n * Hide a column in the participants table.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} columnToHide\n * @param {Bool} refreshContent\n */\nexport const hideColumn = (tableRoot, columnToHide, refreshContent = true) => {\n const hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);\n hiddenColumns.push(columnToHide);\n\n updateTable(tableRoot, {hiddenColumns}, refreshContent);\n};\n\n/**\n * Make a hidden column visible in the participants table.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} columnToShow\n * @param {Bool} refreshContent\n */\nexport const showColumn = (tableRoot, columnToShow, refreshContent = true) => {\n let hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);\n hiddenColumns = hiddenColumns.filter(columnName => columnName !== columnToShow);\n\n updateTable(tableRoot, {hiddenColumns}, refreshContent);\n};\n\n/**\n * Reset table preferences.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Promise}\n */\nconst resetTablePreferences = tableRoot => refreshTableContent(tableRoot, true);\n\n/**\n * Set up listeners to handle table updates.\n */\nexport const init = () => {\n if (watching) {\n // Already watching.\n return;\n }\n watching = true;\n\n document.addEventListener('click', e => {\n const tableRoot = e.target.closest(Selectors.main.region);\n\n if (!tableRoot) {\n return;\n }\n\n const sortableLink = e.target.closest(Selectors.table.links.sortableColumn);\n if (sortableLink) {\n e.preventDefault();\n\n setSortOrder(tableRoot, sortableLink.dataset.sortby, sortableLink.dataset.sortorder);\n }\n\n const firstInitialLink = e.target.closest(Selectors.initialsBar.links.firstInitial);\n if (firstInitialLink !== null) {\n e.preventDefault();\n\n setFirstInitial(tableRoot, firstInitialLink.dataset.initial);\n }\n\n const lastInitialLink = e.target.closest(Selectors.initialsBar.links.lastInitial);\n if (lastInitialLink !== null) {\n e.preventDefault();\n\n setLastInitial(tableRoot, lastInitialLink.dataset.initial);\n }\n\n const pageItem = e.target.closest(Selectors.paginationBar.links.pageItem);\n if (pageItem) {\n e.preventDefault();\n\n setPageNumber(tableRoot, pageItem.dataset.pageNumber);\n }\n\n const hide = e.target.closest(Selectors.table.links.hide);\n if (hide) {\n e.preventDefault();\n\n hideColumn(tableRoot, hide.dataset.column);\n }\n\n const show = e.target.closest(Selectors.table.links.show);\n if (show) {\n e.preventDefault();\n\n showColumn(tableRoot, show.dataset.column);\n }\n\n const resetTablePreferencesLink = e.target.closest('.resettable a');\n if (resetTablePreferencesLink) {\n e.preventDefault();\n\n resetTablePreferences(tableRoot);\n }\n });\n};\n\n/**\n * Fetch the table via its table region id.\n *\n * @param {String} tableRegionId\n * @returns {HTMLElement}\n */\nexport const getTableFromId = tableRegionId => {\n const tableRoot = document.querySelector(Selectors.main.fromRegionId(tableRegionId));\n\n\n if (!tableRoot) {\n // The table is not a dynamic table.\n throw new Error(\"The table specified is not a dynamic table and cannot be updated\");\n }\n\n return tableRoot;\n};\n\nexport {\n Events\n};\n"],"file":"dynamic.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/dynamic.js"],"names":["watching","checkTableIsDynamic","tableRoot","Error","matches","Selectors","main","region","getFiltersetFromTable","JSON","parse","dataset","tableFilters","refreshTableContent","resetContent","filterset","tableComponent","tableHandler","tableUniqueid","sortData","tableSortData","joinType","jointype","filters","firstinitial","tableFirstInitial","lastinitial","tableLastInitial","pageNumber","tablePageNumber","pageSize","tablePageSize","hiddenColumns","tableHiddenColumns","then","data","placeholder","document","createElement","innerHTML","html","replaceWith","childNodes","getTableFromId","dispatchEvent","CustomEvent","Events","tableContentRefreshed","bubbles","updateTable","sortBy","sortOrder","firstInitial","lastInitial","refreshContent","unshift","sortby","sortorder","parseInt","stringify","Promise","resolve","getTableData","setFilters","getFilters","setSortOrder","setPageNumber","getPageNumber","setPageSize","getPageSize","setFirstInitial","getFirstInitial","setLastInitial","getLastInitial","hideColumn","columnToHide","push","showColumn","columnToShow","filter","columnName","resetTablePreferences","init","addEventListener","e","target","closest","sortableLink","table","links","sortableColumn","preventDefault","firstInitialLink","initialsBar","initial","lastInitialLink","pageItem","paginationBar","hide","column","show","resetTablePreferencesLink","tableRegionId","querySelector","fromRegionId"],"mappings":"o3BAwBA,OACA,uD,0xCAGIA,CAAAA,CAAQ,G,CAQNC,CAAmB,CAAG,SAAAC,CAAS,CAAI,CACrC,GAAI,CAACA,CAAL,CAAgB,CAEZ,KAAM,IAAIC,CAAAA,KAAJ,CAAU,kEAAV,CACT,CAED,GAAI,CAACD,CAAS,CAACE,OAAV,CAAkBC,CAAS,CAACC,IAAV,CAAeC,MAAjC,CAAL,CAA+C,CAE3C,KAAM,IAAIJ,CAAAA,KAAJ,CAAU,kEAAV,CACT,CAED,QACH,C,CAQKK,CAAqB,CAAG,SAAAN,CAAS,CAAI,CACvC,MAAOO,CAAAA,IAAI,CAACC,KAAL,CAAWR,CAAS,CAACS,OAAV,CAAkBC,YAA7B,CACV,C,CASYC,CAAmB,CAAG,SAACX,CAAD,CAAqC,IAAzBY,CAAAA,CAAyB,2DAC9DC,CAAS,CAAGP,CAAqB,CAACN,CAAD,CAD6B,CAEpE,yBAAmBA,CAAnB,EAEA,MAAO,YACHA,CAAS,CAACS,OAAV,CAAkBK,cADf,CAEHd,CAAS,CAACS,OAAV,CAAkBM,YAFf,CAGHf,CAAS,CAACS,OAAV,CAAkBO,aAHf,CAIH,CACIC,QAAQ,CAAEV,IAAI,CAACC,KAAL,CAAWR,CAAS,CAACS,OAAV,CAAkBS,aAA7B,CADd,CAEIC,QAAQ,CAAEN,CAAS,CAACO,QAFxB,CAGIC,OAAO,CAAER,CAAS,CAACQ,OAHvB,CAIIC,YAAY,CAAEtB,CAAS,CAACS,OAAV,CAAkBc,iBAJpC,CAKIC,WAAW,CAAExB,CAAS,CAACS,OAAV,CAAkBgB,gBALnC,CAMIC,UAAU,CAAE1B,CAAS,CAACS,OAAV,CAAkBkB,eANlC,CAOIC,QAAQ,CAAE5B,CAAS,CAACS,OAAV,CAAkBoB,aAPhC,CAQIC,aAAa,CAAEvB,IAAI,CAACC,KAAL,CAAWR,CAAS,CAACS,OAAV,CAAkBsB,kBAA7B,CARnB,CAJG,CAcHnB,CAdG,EAgBNoB,IAhBM,CAgBD,SAAAC,CAAI,CAAI,CACV,GAAMC,CAAAA,CAAW,CAAGC,QAAQ,CAACC,aAAT,CAAuB,KAAvB,CAApB,CACAF,CAAW,CAACG,SAAZ,CAAwBJ,CAAI,CAACK,IAA7B,CACAtC,CAAS,CAACuC,WAAV,OAAAvC,CAAS,GAAgBkC,CAAW,CAACM,UAA5B,EAAT,CAGA,MAAOC,CAAAA,CAAc,CAACzC,CAAS,CAACS,OAAV,CAAkBO,aAAnB,CACxB,CAvBM,EAuBJgB,IAvBI,CAuBC,SAAAhC,CAAS,CAAI,CACjBA,CAAS,CAAC0C,aAAV,CAAwB,GAAIC,CAAAA,WAAJ,CAAgBC,UAAOC,qBAAvB,CAA8C,CAClEC,OAAO,GAD2D,CAA9C,CAAxB,EAIA,MAAO9C,CAAAA,CACV,CA7BM,CA8BV,C,yBAEM,GAAM+C,CAAAA,CAAW,CAAG,SAAC/C,CAAD,CASO,8DAA9B,EAA8B,KAR9BgD,MAQ8B,CAR9BA,CAQ8B,YARrB,IAQqB,OAP9BC,SAO8B,CAP9BA,CAO8B,YAPlB,IAOkB,OAN9B5B,OAM8B,CAN9BA,CAM8B,YANpB,IAMoB,OAL9B6B,YAK8B,CAL9BA,CAK8B,YALf,IAKe,OAJ9BC,WAI8B,CAJ9BA,CAI8B,YAJhB,IAIgB,OAH9BzB,UAG8B,CAH9BA,CAG8B,YAHjB,IAGiB,OAF9BE,QAE8B,CAF9BA,CAE8B,YAFnB,IAEmB,OAD9BE,aAC8B,CAD9BA,CAC8B,YADd,IACc,GAA1BsB,CAA0B,2DAC9BrD,CAAmB,CAACC,CAAD,CAAnB,CAGA,GAAIgD,CAAM,EAAIC,CAAd,CAAyB,CACrB,GAAMhC,CAAAA,CAAQ,CAAGV,IAAI,CAACC,KAAL,CAAWR,CAAS,CAACS,OAAV,CAAkBS,aAA7B,CAAjB,CACAD,CAAQ,CAACoC,OAAT,CAAiB,CACbC,MAAM,CAAEN,CADK,CAEbO,SAAS,CAAEC,QAAQ,CAACP,CAAD,CAAY,EAAZ,CAFN,CAAjB,EAIAjD,CAAS,CAACS,OAAV,CAAkBS,aAAlB,CAAkCX,IAAI,CAACkD,SAAL,CAAexC,CAAf,CACrC,CAGD,GAAqB,IAAjB,GAAAiC,CAAJ,CAA2B,CACvBlD,CAAS,CAACS,OAAV,CAAkBc,iBAAlB,CAAsC2B,CACzC,CAED,GAAoB,IAAhB,GAAAC,CAAJ,CAA0B,CACtBnD,CAAS,CAACS,OAAV,CAAkBgB,gBAAlB,CAAqC0B,CACxC,CAED,GAAmB,IAAf,GAAAzB,CAAJ,CAAyB,CACrB1B,CAAS,CAACS,OAAV,CAAkBkB,eAAlB,CAAoCD,CACvC,CAED,GAAiB,IAAb,GAAAE,CAAJ,CAAuB,CACnB5B,CAAS,CAACS,OAAV,CAAkBoB,aAAlB,CAAkCD,CACrC,CAGD,GAAIP,CAAJ,CAAa,CACTrB,CAAS,CAACS,OAAV,CAAkBC,YAAlB,CAAiCH,IAAI,CAACkD,SAAL,CAAepC,CAAf,CACpC,CAGD,GAAIS,CAAJ,CAAmB,CACf9B,CAAS,CAACS,OAAV,CAAkBsB,kBAAlB,CAAuCxB,IAAI,CAACkD,SAAL,CAAe3B,CAAf,CAC1C,CAGD,GAAIsB,CAAJ,CAAoB,CAChB,MAAOzC,CAAAA,CAAmB,CAACX,CAAD,CAC7B,CAFD,IAEO,CACH,MAAO0D,CAAAA,OAAO,CAACC,OAAR,CAAgB3D,CAAhB,CACV,CACJ,CAvDM,C,mBA+DD4D,CAAAA,CAAY,CAAG,SAAA5D,CAAS,CAAI,CAC9BD,CAAmB,CAACC,CAAD,CAAnB,CAEA,MAAOA,CAAAA,CAAS,CAACS,OACpB,C,cAUyB,QAAboD,CAAAA,UAAa,CAAC7D,CAAD,CAAYqB,CAAZ,KAAqB+B,CAAAA,CAArB,iEACtBL,CAAAA,CAAW,CAAC/C,CAAD,CAAY,CAACqB,OAAO,CAAPA,CAAD,CAAZ,CAAuB+B,CAAvB,CADW,C,cASA,QAAbU,CAAAA,UAAa,CAAA9D,CAAS,CAAI,CACnCD,CAAmB,CAACC,CAAD,CAAnB,CAEA,MAAOM,CAAAA,CAAqB,CAACN,CAAD,CAC/B,C,CAWM,GAAM+D,CAAAA,CAAY,CAAG,SAAC/D,CAAD,CAAYgD,CAAZ,CAAoBC,CAApB,KAA+BG,CAAAA,CAA/B,iEACxBL,CAAAA,CAAW,CAAC/C,CAAD,CAAY,CAACgD,MAAM,CAANA,CAAD,CAASC,SAAS,CAATA,CAAT,CAAZ,CAAiCG,CAAjC,CADa,CAArB,C,iBAWA,GAAMY,CAAAA,CAAa,CAAG,SAAChE,CAAD,CAAY0B,CAAZ,KAAwB0B,CAAAA,CAAxB,iEACzBL,CAAAA,CAAW,CAAC/C,CAAD,CAAY,CAAC0B,UAAU,CAAVA,CAAD,CAAZ,CAA0B0B,CAA1B,CADc,CAAtB,C,kCASsB,QAAhBa,CAAAA,aAAgB,CAAAjE,CAAS,QAAI4D,CAAAA,CAAY,CAAC5D,CAAD,CAAZ,CAAwB2B,eAA5B,C,eAUX,QAAduC,CAAAA,WAAc,CAAClE,CAAD,CAAY4B,CAAZ,KAAsBwB,CAAAA,CAAtB,iEACvBL,CAAAA,CAAW,CAAC/C,CAAD,CAAY,CAAC4B,QAAQ,CAARA,CAAD,CAAWF,UAAU,CAAE,CAAvB,CAAZ,CAAuC0B,CAAvC,CADY,C,eASA,QAAde,CAAAA,WAAc,CAAAnE,CAAS,QAAI4D,CAAAA,CAAY,CAAC5D,CAAD,CAAZ,CAAwB6B,aAA5B,C,CAU7B,GAAMuC,CAAAA,CAAe,CAAG,SAACpE,CAAD,CAAYkD,CAAZ,KAA0BE,CAAAA,CAA1B,iEAC3BL,CAAAA,CAAW,CAAC/C,CAAD,CAAY,CAACkD,YAAY,CAAZA,CAAD,CAAZ,CAA4BE,CAA5B,CADgB,CAAxB,C,sCASwB,QAAlBiB,CAAAA,eAAkB,CAAArE,CAAS,QAAI4D,CAAAA,CAAY,CAAC5D,CAAD,CAAZ,CAAwBuB,iBAA5B,C,CAUjC,GAAM+C,CAAAA,CAAc,CAAG,SAACtE,CAAD,CAAYmD,CAAZ,KAAyBC,CAAAA,CAAzB,iEAC1BL,CAAAA,CAAW,CAAC/C,CAAD,CAAY,CAACmD,WAAW,CAAXA,CAAD,CAAZ,CAA2BC,CAA3B,CADe,CAAvB,C,oCASuB,QAAjBmB,CAAAA,cAAiB,CAAAvE,CAAS,QAAI4D,CAAAA,CAAY,CAAC5D,CAAD,CAAZ,CAAwByB,gBAA5B,C,CAShC,GAAM+C,CAAAA,CAAU,CAAG,SAACxE,CAAD,CAAYyE,CAAZ,CAAoD,IAA1BrB,CAAAA,CAA0B,2DACpEtB,CAAa,CAAGvB,IAAI,CAACC,KAAL,CAAWR,CAAS,CAACS,OAAV,CAAkBsB,kBAA7B,CADoD,CAE1ED,CAAa,CAAC4C,IAAd,CAAmBD,CAAnB,EAEA1B,CAAW,CAAC/C,CAAD,CAAY,CAAC8B,aAAa,CAAbA,CAAD,CAAZ,CAA6BsB,CAA7B,CACd,CALM,C,eAcA,GAAMuB,CAAAA,CAAU,CAAG,SAAC3E,CAAD,CAAY4E,CAAZ,CAAoD,IAA1BxB,CAAAA,CAA0B,2DACtEtB,CAAa,CAAGvB,IAAI,CAACC,KAAL,CAAWR,CAAS,CAACS,OAAV,CAAkBsB,kBAA7B,CADsD,CAE1ED,CAAa,CAAGA,CAAa,CAAC+C,MAAd,CAAqB,SAAAC,CAAU,QAAIA,CAAAA,CAAU,GAAKF,CAAnB,CAA/B,CAAhB,CAEA7B,CAAW,CAAC/C,CAAD,CAAY,CAAC8B,aAAa,CAAbA,CAAD,CAAZ,CAA6BsB,CAA7B,CACd,CALM,C,kBAaD2B,CAAAA,CAAqB,CAAG,SAAA/E,CAAS,QAAIW,CAAAA,CAAmB,CAACX,CAAD,IAAvB,C,CAK1BgF,CAAI,CAAG,UAAM,CACtB,GAAIlF,CAAJ,CAAc,CAEV,MACH,CACDA,CAAQ,GAAR,CAEAqC,QAAQ,CAAC8C,gBAAT,CAA0B,OAA1B,CAAmC,SAAAC,CAAC,CAAI,CACpC,GAAMlF,CAAAA,CAAS,CAAGkF,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBjF,CAAS,CAACC,IAAV,CAAeC,MAAhC,CAAlB,CAEA,GAAI,CAACL,CAAL,CAAgB,CACZ,MACH,CAED,GAAMqF,CAAAA,CAAY,CAAGH,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBjF,CAAS,CAACmF,KAAV,CAAgBC,KAAhB,CAAsBC,cAAvC,CAArB,CACA,GAAIH,CAAJ,CAAkB,CACdH,CAAC,CAACO,cAAF,GAEA1B,CAAY,CAAC/D,CAAD,CAAYqF,CAAY,CAAC5E,OAAb,CAAqB6C,MAAjC,CAAyC+B,CAAY,CAAC5E,OAAb,CAAqB8C,SAA9D,CACf,CAED,GAAMmC,CAAAA,CAAgB,CAAGR,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBjF,CAAS,CAACwF,WAAV,CAAsBJ,KAAtB,CAA4BrC,YAA7C,CAAzB,CACA,GAAyB,IAArB,GAAAwC,CAAJ,CAA+B,CAC3BR,CAAC,CAACO,cAAF,GAEArB,CAAe,CAACpE,CAAD,CAAY0F,CAAgB,CAACjF,OAAjB,CAAyBmF,OAArC,CAClB,CAED,GAAMC,CAAAA,CAAe,CAAGX,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBjF,CAAS,CAACwF,WAAV,CAAsBJ,KAAtB,CAA4BpC,WAA7C,CAAxB,CACA,GAAwB,IAApB,GAAA0C,CAAJ,CAA8B,CAC1BX,CAAC,CAACO,cAAF,GAEAnB,CAAc,CAACtE,CAAD,CAAY6F,CAAe,CAACpF,OAAhB,CAAwBmF,OAApC,CACjB,CAED,GAAME,CAAAA,CAAQ,CAAGZ,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBjF,CAAS,CAAC4F,aAAV,CAAwBR,KAAxB,CAA8BO,QAA/C,CAAjB,CACA,GAAIA,CAAJ,CAAc,CACVZ,CAAC,CAACO,cAAF,GAEAzB,CAAa,CAAChE,CAAD,CAAY8F,CAAQ,CAACrF,OAAT,CAAiBiB,UAA7B,CAChB,CAED,GAAMsE,CAAAA,CAAI,CAAGd,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBjF,CAAS,CAACmF,KAAV,CAAgBC,KAAhB,CAAsBS,IAAvC,CAAb,CACA,GAAIA,CAAJ,CAAU,CACNd,CAAC,CAACO,cAAF,GAEAjB,CAAU,CAACxE,CAAD,CAAYgG,CAAI,CAACvF,OAAL,CAAawF,MAAzB,CACb,CAED,GAAMC,CAAAA,CAAI,CAAGhB,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBjF,CAAS,CAACmF,KAAV,CAAgBC,KAAhB,CAAsBW,IAAvC,CAAb,CACA,GAAIA,CAAJ,CAAU,CACNhB,CAAC,CAACO,cAAF,GAEAd,CAAU,CAAC3E,CAAD,CAAYkG,CAAI,CAACzF,OAAL,CAAawF,MAAzB,CACb,CAED,GAAME,CAAAA,CAAyB,CAAGjB,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB,eAAjB,CAAlC,CACA,GAAIe,CAAJ,CAA+B,CAC3BjB,CAAC,CAACO,cAAF,GAEAV,CAAqB,CAAC/E,CAAD,CACxB,CACJ,CAvDD,CAwDH,C,UAQM,GAAMyC,CAAAA,CAAc,CAAG,SAAA2D,CAAa,CAAI,CAC3C,GAAMpG,CAAAA,CAAS,CAAGmC,QAAQ,CAACkE,aAAT,CAAuBlG,CAAS,CAACC,IAAV,CAAekG,YAAf,CAA4BF,CAA5B,CAAvB,CAAlB,CAGA,GAAI,CAACpG,CAAL,CAAgB,CAEZ,KAAM,IAAIC,CAAAA,KAAJ,CAAU,kEAAV,CACT,CAED,MAAOD,CAAAA,CACV,CAVM,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 .\n\n/**\n * Module to handle dynamic table features.\n *\n * @module core_table/dynamic\n * @package core_table\n * @copyright 2020 Simey Lameze \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {fetch as fetchTableData} from 'core_table/local/dynamic/repository';\nimport * as Selectors from 'core_table/local/dynamic/selectors';\nimport Events from './local/dynamic/events';\nimport {addIconToContainer} from 'core/loadingicon';\n\nlet watching = false;\n\n/**\n * Ensure that a table is a dynamic table.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Bool}\n */\nconst checkTableIsDynamic = tableRoot => {\n if (!tableRoot) {\n // The table is not a dynamic table.\n throw new Error(\"The table specified is not a dynamic table and cannot be updated\");\n }\n\n if (!tableRoot.matches(Selectors.main.region)) {\n // The table is not a dynamic table.\n throw new Error(\"The table specified is not a dynamic table and cannot be updated\");\n }\n\n return true;\n};\n\n/**\n * Get the filterset data from a known dynamic table.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Object}\n */\nconst getFiltersetFromTable = tableRoot => {\n return JSON.parse(tableRoot.dataset.tableFilters);\n};\n\n/**\n * Update the specified table based on its current values.\n *\n * @param {HTMLElement} tableRoot\n * @param {Bool} resetContent\n * @returns {Promise}\n */\nexport const refreshTableContent = (tableRoot, resetContent = false) => {\n const filterset = getFiltersetFromTable(tableRoot);\n addIconToContainer(tableRoot);\n\n return fetchTableData(\n tableRoot.dataset.tableComponent,\n tableRoot.dataset.tableHandler,\n tableRoot.dataset.tableUniqueid,\n {\n sortData: JSON.parse(tableRoot.dataset.tableSortData),\n joinType: filterset.jointype,\n filters: filterset.filters,\n firstinitial: tableRoot.dataset.tableFirstInitial,\n lastinitial: tableRoot.dataset.tableLastInitial,\n pageNumber: tableRoot.dataset.tablePageNumber,\n pageSize: tableRoot.dataset.tablePageSize,\n hiddenColumns: JSON.parse(tableRoot.dataset.tableHiddenColumns),\n },\n resetContent,\n )\n .then(data => {\n const placeholder = document.createElement('div');\n placeholder.innerHTML = data.html;\n tableRoot.replaceWith(...placeholder.childNodes);\n\n // Update the tableRoot.\n return getTableFromId(tableRoot.dataset.tableUniqueid);\n }).then(tableRoot => {\n tableRoot.dispatchEvent(new CustomEvent(Events.tableContentRefreshed, {\n bubbles: true,\n }));\n\n return tableRoot;\n });\n};\n\nexport const updateTable = (tableRoot, {\n sortBy = null,\n sortOrder = null,\n filters = null,\n firstInitial = null,\n lastInitial = null,\n pageNumber = null,\n pageSize = null,\n hiddenColumns = null,\n} = {}, refreshContent = true) => {\n checkTableIsDynamic(tableRoot);\n\n // Update sort fields.\n if (sortBy && sortOrder) {\n const sortData = JSON.parse(tableRoot.dataset.tableSortData);\n sortData.unshift({\n sortby: sortBy,\n sortorder: parseInt(sortOrder, 10),\n });\n tableRoot.dataset.tableSortData = JSON.stringify(sortData);\n }\n\n // Update initials.\n if (firstInitial !== null) {\n tableRoot.dataset.tableFirstInitial = firstInitial;\n }\n\n if (lastInitial !== null) {\n tableRoot.dataset.tableLastInitial = lastInitial;\n }\n\n if (pageNumber !== null) {\n tableRoot.dataset.tablePageNumber = pageNumber;\n }\n\n if (pageSize !== null) {\n tableRoot.dataset.tablePageSize = pageSize;\n }\n\n // Update filters.\n if (filters) {\n tableRoot.dataset.tableFilters = JSON.stringify(filters);\n }\n\n // Update hidden columns.\n if (hiddenColumns) {\n tableRoot.dataset.tableHiddenColumns = JSON.stringify(hiddenColumns);\n }\n\n // Refresh.\n if (refreshContent) {\n return refreshTableContent(tableRoot);\n } else {\n return Promise.resolve(tableRoot);\n }\n};\n\n/**\n * Get the table dataset for the specified tableRoot, ensuring that the provided table is a dynamic table.\n *\n * @param {HTMLElement} tableRoot\n * @returns {DOMStringMap}\n */\nconst getTableData = tableRoot => {\n checkTableIsDynamic(tableRoot);\n\n return tableRoot.dataset;\n};\n\n/**\n * Update the specified table using the new filters.\n *\n * @param {HTMLElement} tableRoot\n * @param {Object} filters\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setFilters = (tableRoot, filters, refreshContent = true) =>\n updateTable(tableRoot, {filters}, refreshContent);\n\n/**\n * Get the filter data for the specified table.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Object}\n */\nexport const getFilters = tableRoot => {\n checkTableIsDynamic(tableRoot);\n\n return getFiltersetFromTable(tableRoot);\n};\n\n/**\n * Update the sort order.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} sortBy\n * @param {Number} sortOrder\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setSortOrder = (tableRoot, sortBy, sortOrder, refreshContent = true) =>\n updateTable(tableRoot, {sortBy, sortOrder}, refreshContent);\n\n/**\n * Set the page number.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} pageNumber\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setPageNumber = (tableRoot, pageNumber, refreshContent = true) =>\n updateTable(tableRoot, {pageNumber}, refreshContent);\n\n/**\n * Get the current page number.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Number}\n */\nexport const getPageNumber = tableRoot => getTableData(tableRoot).tablePageNumber;\n\n/**\n * Set the page size.\n *\n * @param {HTMLElement} tableRoot\n * @param {Number} pageSize\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setPageSize = (tableRoot, pageSize, refreshContent = true) =>\n updateTable(tableRoot, {pageSize, pageNumber: 0}, refreshContent);\n\n/**\n * Get the current page size.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Number}\n */\nexport const getPageSize = tableRoot => getTableData(tableRoot).tablePageSize;\n\n/**\n * Update the first initial to show.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} firstInitial\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setFirstInitial = (tableRoot, firstInitial, refreshContent = true) =>\n updateTable(tableRoot, {firstInitial}, refreshContent);\n\n/**\n * Get the current first initial filter.\n *\n * @param {HTMLElement} tableRoot\n * @returns {String}\n */\nexport const getFirstInitial = tableRoot => getTableData(tableRoot).tableFirstInitial;\n\n/**\n * Update the last initial to show.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} lastInitial\n * @param {Bool} refreshContent\n * @returns {Promise}\n */\nexport const setLastInitial = (tableRoot, lastInitial, refreshContent = true) =>\n updateTable(tableRoot, {lastInitial}, refreshContent);\n\n/**\n * Get the current last initial filter.\n *\n * @param {HTMLElement} tableRoot\n * @returns {String}\n */\nexport const getLastInitial = tableRoot => getTableData(tableRoot).tableLastInitial;\n\n/**\n * Hide a column in the participants table.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} columnToHide\n * @param {Bool} refreshContent\n */\nexport const hideColumn = (tableRoot, columnToHide, refreshContent = true) => {\n const hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);\n hiddenColumns.push(columnToHide);\n\n updateTable(tableRoot, {hiddenColumns}, refreshContent);\n};\n\n/**\n * Make a hidden column visible in the participants table.\n *\n * @param {HTMLElement} tableRoot\n * @param {String} columnToShow\n * @param {Bool} refreshContent\n */\nexport const showColumn = (tableRoot, columnToShow, refreshContent = true) => {\n let hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);\n hiddenColumns = hiddenColumns.filter(columnName => columnName !== columnToShow);\n\n updateTable(tableRoot, {hiddenColumns}, refreshContent);\n};\n\n/**\n * Reset table preferences.\n *\n * @param {HTMLElement} tableRoot\n * @returns {Promise}\n */\nconst resetTablePreferences = tableRoot => refreshTableContent(tableRoot, true);\n\n/**\n * Set up listeners to handle table updates.\n */\nexport const init = () => {\n if (watching) {\n // Already watching.\n return;\n }\n watching = true;\n\n document.addEventListener('click', e => {\n const tableRoot = e.target.closest(Selectors.main.region);\n\n if (!tableRoot) {\n return;\n }\n\n const sortableLink = e.target.closest(Selectors.table.links.sortableColumn);\n if (sortableLink) {\n e.preventDefault();\n\n setSortOrder(tableRoot, sortableLink.dataset.sortby, sortableLink.dataset.sortorder);\n }\n\n const firstInitialLink = e.target.closest(Selectors.initialsBar.links.firstInitial);\n if (firstInitialLink !== null) {\n e.preventDefault();\n\n setFirstInitial(tableRoot, firstInitialLink.dataset.initial);\n }\n\n const lastInitialLink = e.target.closest(Selectors.initialsBar.links.lastInitial);\n if (lastInitialLink !== null) {\n e.preventDefault();\n\n setLastInitial(tableRoot, lastInitialLink.dataset.initial);\n }\n\n const pageItem = e.target.closest(Selectors.paginationBar.links.pageItem);\n if (pageItem) {\n e.preventDefault();\n\n setPageNumber(tableRoot, pageItem.dataset.pageNumber);\n }\n\n const hide = e.target.closest(Selectors.table.links.hide);\n if (hide) {\n e.preventDefault();\n\n hideColumn(tableRoot, hide.dataset.column);\n }\n\n const show = e.target.closest(Selectors.table.links.show);\n if (show) {\n e.preventDefault();\n\n showColumn(tableRoot, show.dataset.column);\n }\n\n const resetTablePreferencesLink = e.target.closest('.resettable a');\n if (resetTablePreferencesLink) {\n e.preventDefault();\n\n resetTablePreferences(tableRoot);\n }\n });\n};\n\n/**\n * Fetch the table via its table region id.\n *\n * @param {String} tableRegionId\n * @returns {HTMLElement}\n */\nexport const getTableFromId = tableRegionId => {\n const tableRoot = document.querySelector(Selectors.main.fromRegionId(tableRegionId));\n\n\n if (!tableRoot) {\n // The table is not a dynamic table.\n throw new Error(\"The table specified is not a dynamic table and cannot be updated\");\n }\n\n return tableRoot;\n};\n\nexport {\n Events\n};\n"],"file":"dynamic.min.js"} \ No newline at end of file diff --git a/lib/table/amd/src/dynamic.js b/lib/table/amd/src/dynamic.js index 235dd5e49f4..10abe968f04 100644 --- a/lib/table/amd/src/dynamic.js +++ b/lib/table/amd/src/dynamic.js @@ -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. * diff --git a/lib/table/classes/local/filter/filter.php b/lib/table/classes/local/filter/filter.php index e6fc650af03..eda6039d6e3 100644 --- a/lib/table/classes/local/filter/filter.php +++ b/lib/table/classes/local/filter/filter.php @@ -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; diff --git a/lib/table/classes/local/filter/filterset.php b/lib/table/classes/local/filter/filterset.php index d04eaefdec0..0afddeb4557 100644 --- a/lib/table/classes/local/filter/filterset.php +++ b/lib/table/classes/local/filter/filterset.php @@ -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; diff --git a/user/amd/build/local/participantsfilter/filter.min.js b/user/amd/build/local/participantsfilter/filter.min.js index 3ef1d57004c..0f13ff20617 100644 --- a/user/amd/build/local/participantsfilter/filter.min.js +++ b/user/amd/build/local/participantsfilter/filter.min.js @@ -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.\n\n/**\n * Base Filter class for a filter type in the participants filter UI.\n *\n * @module core_user/local/participantsfilter/filter\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Autocomplete from 'core/form-autocomplete';\nimport Selectors from './selectors';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Fetch all checked options in the select.\n *\n * This is a poor-man's polyfill for select.selectedOptions, which is not available in IE11.\n *\n * @param {HTMLSelectElement} select\n * @returns {HTMLOptionElement[]} All selected options\n */\nconst getOptionsForSelect = select => {\n return select.querySelectorAll(':checked');\n};\n\nexport default class {\n\n /**\n * Constructor for a new filter.\n *\n * @param {String} filterType The type of filter that this relates to\n * @param {HTMLElement} rootNode The root node for the participants filterset\n */\n constructor(filterType, rootNode) {\n this.filterType = filterType;\n this.rootNode = rootNode;\n\n this.addValueSelector();\n }\n\n /**\n * Perform any tear-down for this filter type.\n */\n tearDown() {\n // eslint-disable-line no-empty-function\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('placeholdertypeorselect', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return true;\n }\n\n /**\n * Add the value selector to the filter row.\n */\n async addValueSelector() {\n const filterValueNode = this.getFilterValueNode();\n\n // Copy the data in place.\n filterValueNode.innerHTML = this.getSourceDataForFilter().outerHTML;\n\n const dataSource = filterValueNode.querySelector('select');\n\n Autocomplete.enhance(\n // The source select element.\n dataSource,\n\n // Whether to allow 'tags' (custom entries).\n dataSource.dataset.allowCustom == \"1\",\n\n // We do not require AJAX at all as standard.\n null,\n\n // The string to use as a placeholder.\n await this.placeholder,\n\n // Disable case sensitivity on searches.\n false,\n\n // Show suggestions.\n this.showSuggestions,\n\n // Do not override the 'no suggestions' string.\n null,\n\n // Close the suggestions if this is not a multi-select.\n !dataSource.multiple,\n\n // Template overrides.\n {\n items: 'core_user/local/participantsfilter/autocomplete_selection_items',\n layout: 'core_user/local/participantsfilter/autocomplete_layout',\n selection: 'core_user/local/participantsfilter/autocomplete_selection',\n }\n );\n }\n\n /**\n * Get the root node for this filter.\n *\n * @returns {HTMLElement}\n */\n get filterRoot() {\n return this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n }\n\n /**\n * Get the possible data for this filter type.\n *\n * @returns {Array}\n */\n getSourceDataForFilter() {\n const filterDataNode = this.rootNode.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(this.filterType));\n }\n\n /**\n * Get the HTMLElement which contains the value selector.\n *\n * @returns {HTMLElement}\n */\n getFilterValueNode() {\n return this.filterRoot.querySelector(Selectors.filter.regions.values);\n }\n\n /**\n * Get the name of this filter.\n *\n * @returns {String}\n */\n get name() {\n return this.filterType;\n }\n\n /**\n * Get the type of join specified.\n *\n * @returns {Number}\n */\n get jointype() {\n return this.filterRoot.querySelector(Selectors.filter.fields.join).value;\n }\n\n /**\n * Get the list of raw values for this filter type.\n *\n * @returns {Array}\n */\n get rawValues() {\n const filterValueNode = this.getFilterValueNode();\n const filterValueSelect = filterValueNode.querySelector('select');\n\n return Object.values(getOptionsForSelect(filterValueSelect)).map(option => option.value);\n }\n\n /**\n * Get the list of values for this filter type.\n *\n * @returns {Array}\n */\n get values() {\n return this.rawValues.map(option => parseInt(option, 10));\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n };\n }\n}\n"],"file":"filter.min.js"} \ No newline at end of file +{"version":3,"sources":["../../../src/local/participantsfilter/filter.js"],"names":["getOptionsForSelect","select","querySelectorAll","filterType","rootNode","initialValues","addValueSelector","filterValueNode","getFilterValueNode","innerHTML","getSourceDataForFilter","outerHTML","dataSource","querySelector","forEach","filterValue","selectedOption","selected","showSuggestions","document","createElement","value","append","Autocomplete","dataset","allowCustom","placeholder","multiple","items","layout","selection","enhance","filterDataNode","Selectors","filterset","regions","datasource","data","fields","byName","filterRoot","filter","values","join","filterValueSelect","Object","map","option","rawValues","parseInt","name","jointype"],"mappings":"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","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 .\n\n/**\n * Base Filter class for a filter type in the participants filter UI.\n *\n * @module core_user/local/participantsfilter/filter\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Autocomplete from 'core/form-autocomplete';\nimport Selectors from './selectors';\nimport {get_string as getString} from 'core/str';\n\n/**\n * Fetch all checked options in the select.\n *\n * This is a poor-man's polyfill for select.selectedOptions, which is not available in IE11.\n *\n * @param {HTMLSelectElement} select\n * @returns {HTMLOptionElement[]} All selected options\n */\nconst getOptionsForSelect = select => {\n return select.querySelectorAll(':checked');\n};\n\nexport default class {\n\n /**\n * Constructor for a new filter.\n *\n * @param {String} filterType The type of filter that this relates to\n * @param {HTMLElement} rootNode The root node for the participants filterset\n * @param {Array} initialValues The initial values for the selector\n */\n constructor(filterType, rootNode, initialValues) {\n this.filterType = filterType;\n this.rootNode = rootNode;\n\n this.addValueSelector(initialValues);\n }\n\n /**\n * Perform any tear-down for this filter type.\n */\n tearDown() {\n // eslint-disable-line no-empty-function\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('placeholdertypeorselect', 'core_user');\n }\n\n /**\n * Whether to show suggestions in the autocomplete.\n *\n * @return {Boolean}\n */\n get showSuggestions() {\n return true;\n }\n\n /**\n * Add the value selector to the filter row.\n *\n * @param {Array} initialValues\n */\n async addValueSelector(initialValues = []) {\n const filterValueNode = this.getFilterValueNode();\n\n // Copy the data in place.\n filterValueNode.innerHTML = this.getSourceDataForFilter().outerHTML;\n\n const dataSource = filterValueNode.querySelector('select');\n\n // If there are any initial values then attempt to apply them.\n initialValues.forEach(filterValue => {\n let selectedOption = dataSource.querySelector(`option[value=\"${filterValue}\"]`);\n if (selectedOption) {\n selectedOption.selected = true;\n } else if (!this.showSuggestions) {\n selectedOption = document.createElement('option');\n selectedOption.value = filterValue;\n selectedOption.innerHTML = filterValue;\n selectedOption.selected = true;\n\n dataSource.append(selectedOption);\n }\n });\n\n Autocomplete.enhance(\n // The source select element.\n dataSource,\n\n // Whether to allow 'tags' (custom entries).\n dataSource.dataset.allowCustom == \"1\",\n\n // We do not require AJAX at all as standard.\n null,\n\n // The string to use as a placeholder.\n await this.placeholder,\n\n // Disable case sensitivity on searches.\n false,\n\n // Show suggestions.\n this.showSuggestions,\n\n // Do not override the 'no suggestions' string.\n null,\n\n // Close the suggestions if this is not a multi-select.\n !dataSource.multiple,\n\n // Template overrides.\n {\n items: 'core_user/local/participantsfilter/autocomplete_selection_items',\n layout: 'core_user/local/participantsfilter/autocomplete_layout',\n selection: 'core_user/local/participantsfilter/autocomplete_selection',\n }\n );\n }\n\n /**\n * Get the root node for this filter.\n *\n * @returns {HTMLElement}\n */\n get filterRoot() {\n return this.rootNode.querySelector(Selectors.filter.byName(this.filterType));\n }\n\n /**\n * Get the possible data for this filter type.\n *\n * @returns {Array}\n */\n getSourceDataForFilter() {\n const filterDataNode = this.rootNode.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(this.filterType));\n }\n\n /**\n * Get the HTMLElement which contains the value selector.\n *\n * @returns {HTMLElement}\n */\n getFilterValueNode() {\n return this.filterRoot.querySelector(Selectors.filter.regions.values);\n }\n\n /**\n * Get the name of this filter.\n *\n * @returns {String}\n */\n get name() {\n return this.filterType;\n }\n\n /**\n * Get the type of join specified.\n *\n * @returns {Number}\n */\n get jointype() {\n return this.filterRoot.querySelector(Selectors.filter.fields.join).value;\n }\n\n /**\n * Get the list of raw values for this filter type.\n *\n * @returns {Array}\n */\n get rawValues() {\n const filterValueNode = this.getFilterValueNode();\n const filterValueSelect = filterValueNode.querySelector('select');\n\n return Object.values(getOptionsForSelect(filterValueSelect)).map(option => option.value);\n }\n\n /**\n * Get the list of values for this filter type.\n *\n * @returns {Array}\n */\n get values() {\n return this.rawValues.map(option => parseInt(option, 10));\n }\n\n /**\n * Get the composed value for this filter.\n *\n * @returns {Object}\n */\n get filterValue() {\n return {\n name: this.name,\n jointype: this.jointype,\n values: this.values,\n };\n }\n}\n"],"file":"filter.min.js"} \ No newline at end of file diff --git a/user/amd/build/local/participantsfilter/filtertypes/keyword.min.js b/user/amd/build/local/participantsfilter/filtertypes/keyword.min.js index 4e178f847bc..b9b0dda297f 100644 --- a/user/amd/build/local/participantsfilter/filtertypes/keyword.min.js +++ b/user/amd/build/local/participantsfilter/filtertypes/keyword.min.js @@ -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.\n\n/**\n * Keyword filter.\n *\n * @module core_user/local/participantsfilter/filtertypes/keyword\n * @package core_user\n * @copyright 2020 Andrew Nicols \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"} \ No newline at end of file +{"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 .\n\n/**\n * Keyword filter.\n *\n * @module core_user/local/participantsfilter/filtertypes/keyword\n * @package core_user\n * @copyright 2020 Andrew Nicols \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"} \ No newline at end of file diff --git a/user/amd/build/local/participantsfilter/selectors.min.js b/user/amd/build/local/participantsfilter/selectors.min.js index 21be6651f9b..69b63a7f064 100644 --- a/user/amd/build/local/participantsfilter/selectors.min.js +++ b/user/amd/build/local/participantsfilter/selectors.min.js @@ -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 diff --git a/user/amd/build/local/participantsfilter/selectors.min.js.map b/user/amd/build/local/participantsfilter/selectors.min.js.map index 023c1a6efd1..15c6dd79917 100644 --- a/user/amd/build/local/participantsfilter/selectors.min.js.map +++ b/user/amd/build/local/participantsfilter/selectors.min.js.map @@ -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 .\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 \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"} \ No newline at end of file +{"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 .\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 \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"} \ No newline at end of file diff --git a/user/amd/build/participantsfilter.min.js b/user/amd/build/participantsfilter.min.js index bbb20c6b68e..1176f2b8013 100644 --- a/user/amd/build/participantsfilter.min.js +++ b/user/amd/build/participantsfilter.min.js @@ -1,2 +1,2 @@ -function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("core_user/participantsfilter",["exports","./local/participantsfilter/filtertypes/courseid","core_table/dynamic","./local/participantsfilter/filter","core/notification","./local/participantsfilter/selectors","core/templates"],function(a,b,c,d,e,f,g){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=j(b);c=i(c);d=j(d);e=j(e);f=j(f);g=j(g);var m="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function h(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;h=function(){return a};return a}function i(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=h();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function j(a){return a&&a.__esModule?a:{default:a}}function k(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 l(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){var h=a.apply(b,c);function f(a){k(h,d,e,f,g,"next",a)}function g(a){k(h,d,e,f,g,"throw",a)}f(void 0)})}}var n=function(a){var h=document.querySelector("#".concat(a)),i={courseid:new b.default("courseid",h)},j=function(){return h.querySelector(f.default.filterset.regions.filterlist)},k=function(){return g.default.renderForPromise("core_user/local/participantsfilter/filterrow",{}).then(function(a){var b=a.html,c=a.js,d=g.default.appendNodeContents(j(),b,c);return d}).then(function(a){var b=h.querySelector(f.default.data.typeList);a.forEach(function(a){var c=a.querySelector(f.default.filter.fields.type);if(c){c.innerHTML=b.innerHTML}});return a}).then(function(a){v();return a}).catch(e.default.exception)},n=function(a){var b=h.querySelector(f.default.filterset.regions.datasource);return b.querySelector(f.default.data.fields.byName(a))},o=function(){var a=l(regeneratorRuntime.mark(function a(b,c){var e,g,j;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:b.dataset.filterType=c;e=n(c);g=d.default;if(!e.dataset.filterTypeClass){a.next=7;break}a.next=6;return"function"==typeof m.define&&m.define.amd?new Promise(function(a,b){m.require([e.dataset.filterTypeClass],a,b)}):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&m.require&&"component"===m.require.loader?Promise.resolve(require((e.dataset.filterTypeClass))):Promise.resolve(m[e.dataset.filterTypeClass]);case 6:g=a.sent;case 7:i[c]=new g(c,h);j=b.querySelector(f.default.filter.fields.type);j.disabled="disabled";v();case 11:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}(),p=function(a){return i[a]},q=function(a){var b=j().querySelectorAll(f.default.filter.region).length;if(1===b){s(a)}else{r(a)}},r=function(a){t(a.dataset.filterType);a.remove();w();v()},s=function(a){t(a.dataset.filterType);return g.default.renderForPromise("core_user/local/participantsfilter/filterrow",{}).then(function(b){var c=b.html,d=b.js,e=g.default.replaceNode(a,c,d);return e}).then(function(a){var b=h.querySelector(f.default.data.typeList);a.forEach(function(a){var c=a.querySelector(f.default.filter.fields.type);if(c){c.innerHTML=b.innerHTML}});return a}).then(function(a){v();return a}).then(function(a){w();return a}).catch(e.default.exception)},t=function(a){if(a){var b=p(a);if(b){b.tearDown();delete i[a]}}},u=function(){var a=l(regeneratorRuntime.mark(function a(){var b;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:b=j().querySelectorAll(f.default.filter.region);b.forEach(function(a){q(a)});w();case 3:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}(),v=function(){var a=j().querySelectorAll(f.default.filter.region);a.forEach(function(a){var b=a.querySelectorAll(f.default.filter.fields.type+" option");b.forEach(function(b){if(b.value===a.dataset.filterType){b.classList.remove("hidden");b.disabled=!1}else if(i[b.value]){b.classList.add("hidden");b.disabled=!0}else{b.classList.remove("hidden");b.disabled=!1}})});var b=h.querySelector(f.default.filterset.actions.addRow),c=h.querySelectorAll(f.default.data.fields.all);if(c.length<=a.length){b.setAttribute("disabled","disabled")}else{b.removeAttribute("disabled")}if(1===a.length){h.querySelector(f.default.filterset.regions.filtermatch).classList.add("hidden");h.querySelector(f.default.filterset.fields.join).value=1}else{h.querySelector(f.default.filterset.regions.filtermatch).classList.remove("hidden")}},w=function(){return c.setFilters(c.getTableFromId(h.dataset.tableRegion),{filters:Object.values(i).map(function(a){return a.filterValue}),jointype:h.querySelector(f.default.filterset.fields.join).value})};h.querySelector(f.default.filterset.region).addEventListener("click",function(a){if(a.target.closest(f.default.filterset.actions.addRow)){a.preventDefault();k()}if(a.target.closest(f.default.filterset.actions.applyFilters)){a.preventDefault();w()}if(a.target.closest(f.default.filterset.actions.resetFilters)){a.preventDefault();u()}});h.querySelector(f.default.filterset.regions.filterlist).addEventListener("click",function(a){if(a.target.closest(f.default.filter.actions.remove)){a.preventDefault();q(a.target.closest(f.default.filter.region))}});h.querySelector(f.default.filterset.regions.filterlist).addEventListener("change",function(a){var b=a.target.closest(f.default.filter.fields.type);if(b&&b.value){var c=a.target.closest(f.default.filter.region);o(c,b.value)}});h.querySelector(f.default.filterset.fields.join).addEventListener("change",function(a){h.dataset.filterverb=a.target.value})};a.init=n}); +function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("core_user/participantsfilter",["exports","./local/participantsfilter/filtertypes/courseid","core_table/dynamic","./local/participantsfilter/filter","core/str","core/notification","./local/participantsfilter/selectors","core/templates"],function(a,b,c,d,e,f,g,h){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=k(b);c=j(c);d=k(d);f=k(f);g=k(g);h=k(h);var x="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function i(){if("function"!=typeof WeakMap)return null;var a=new WeakMap;i=function(){return a};return a}function j(a){if(a&&a.__esModule){return a}if(null===a||"object"!==_typeof(a)&&"function"!=typeof a){return{default:a}}var b=i();if(b&&b.has(a)){return b.get(a)}var c={},d=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var e in a){if(Object.prototype.hasOwnProperty.call(a,e)){var f=d?Object.getOwnPropertyDescriptor(a,e):null;if(f&&(f.get||f.set)){Object.defineProperty(c,e,f)}else{c[e]=a[e]}}}c.default=a;if(b){b.set(a,c)}return c}function k(a){return a&&a.__esModule?a:{default:a}}function l(a){return o(a)||n(a)||r(a)||m()}function m(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function n(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function o(a){if(Array.isArray(a))return s(a)}function p(a,b){return u(a)||t(a,b)||r(a,b)||q()}function q(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function r(a,b){if(!a)return;if("string"==typeof a)return s(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);if("Object"===c&&a.constructor)c=a.constructor.name;if("Map"===c||"Set"===c)return Array.from(c);if("Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c))return s(a,b)}function s(a,b){if(null==b||b>a.length)b=a.length;for(var c=0,d=Array(b);c.\n\n/**\n * Participants filter managemnet.\n *\n * @module core_user/participants_filter\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CourseFilter from './local/participantsfilter/filtertypes/courseid';\nimport * as DynamicTable from 'core_table/dynamic';\nimport GenericFilter from './local/participantsfilter/filter';\nimport Notification from 'core/notification';\nimport Selectors from './local/participantsfilter/selectors';\nimport Templates from 'core/templates';\n\n/**\n * Initialise the participants filter on the element with the given id.\n *\n * @param {String} participantsRegionId\n */\nexport const init = participantsRegionId => {\n // Keep a reference to the filterset.\n const filterSet = document.querySelector(`#${participantsRegionId}`);\n\n // Keep a reference to all of the active filters.\n const activeFilters = {\n courseid: new CourseFilter('courseid', filterSet),\n };\n\n /**\n * Get the filter list region.\n *\n * @return {HTMLElement}\n */\n const getFilterRegion = () => filterSet.querySelector(Selectors.filterset.regions.filterlist);\n\n /**\n * Add an unselected filter row.\n *\n * @return {Promise}\n */\n const addFilterRow = () => {\n return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {})\n .then(({html, js}) => {\n const newContentNodes = Templates.appendNodeContents(getFilterRegion(), html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n updateFiltersOptions();\n\n return filterRow;\n })\n .catch(Notification.exception);\n };\n\n /**\n * Get the filter data source node fro the specified filter type.\n *\n * @param {String} filterType\n * @return {HTMLElement}\n */\n const getFilterDataSource = filterType => {\n const filterDataNode = filterSet.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));\n };\n\n /**\n * Add a filter to the list of active filters, performing any necessary setup.\n *\n * @param {HTMLElement} filterRow\n * @param {String} filterType\n */\n const addFilter = async(filterRow, filterType) => {\n // Name the filter on the filter row.\n filterRow.dataset.filterType = filterType;\n\n const filterDataNode = getFilterDataSource(filterType);\n\n // Instantiate the Filter class.\n let Filter = GenericFilter;\n if (filterDataNode.dataset.filterTypeClass) {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n }\n activeFilters[filterType] = new Filter(filterType, filterSet);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.disabled = 'disabled';\n\n // Update the list of available filter types.\n updateFiltersOptions();\n };\n\n /**\n * Get the registered filter class for the named filter.\n *\n * @param {String} name\n * @return {Object} See the Filter class.\n */\n const getFilterObject = name => {\n return activeFilters[name];\n };\n\n /**\n * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,\n * that it is replaced instead of being removed.\n *\n * @param {HTMLElement} filterRow\n */\n const removeOrReplaceFilterRow = filterRow => {\n const filterCount = getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n\n if (filterCount === 1) {\n replaceFilterRow(filterRow);\n } else {\n removeFilterRow(filterRow);\n }\n };\n\n /**\n * Remove the specified filter row and associated class.\n *\n * @param {HTMLElement} filterRow\n */\n const removeFilterRow = filterRow => {\n // Remove the filter object.\n removeFilterObject(filterRow.dataset.filterType);\n\n // Remove the actual filter HTML.\n filterRow.remove();\n\n // Refresh the table.\n updateTableFromFilter();\n\n // Update the list of available filter types.\n updateFiltersOptions();\n };\n\n /**\n * Replace the specified filter row with a new one.\n *\n * @param {HTMLElement} filterRow\n * @return {Promise}\n */\n const replaceFilterRow = filterRow => {\n // Remove the filter object.\n removeFilterObject(filterRow.dataset.filterType);\n\n return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {})\n .then(({html, js}) => {\n const newContentNodes = Templates.replaceNode(filterRow, html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n updateFiltersOptions();\n\n return filterRow;\n })\n .then(filterRow => {\n // Refresh the table.\n updateTableFromFilter();\n\n return filterRow;\n })\n .catch(Notification.exception);\n };\n\n /**\n * Remove the Filter Object from the register.\n *\n * @param {string} filterName The name of the filter to be removed\n */\n const removeFilterObject = filterName => {\n if (filterName) {\n const filter = getFilterObject(filterName);\n if (filter) {\n filter.tearDown();\n\n // Remove from the list of active filters.\n delete activeFilters[filterName];\n }\n }\n };\n\n /**\n * Remove all filters.\n */\n const removeAllFilters = async() => {\n const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach((filterRow) => {\n removeOrReplaceFilterRow(filterRow);\n });\n\n // Refresh the table.\n updateTableFromFilter();\n };\n\n /**\n * Update the list of filter types to filter out those already selected.\n */\n const updateFiltersOptions = () => {\n const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');\n options.forEach(option => {\n if (option.value === filterRow.dataset.filterType) {\n option.classList.remove('hidden');\n option.disabled = false;\n } else if (activeFilters[option.value]) {\n option.classList.add('hidden');\n option.disabled = true;\n } else {\n option.classList.remove('hidden');\n option.disabled = false;\n }\n });\n });\n\n // Configure the state of the \"Add row\" button.\n // This button is disabled when there is a filter row available for each condition.\n const addRowButton = filterSet.querySelector(Selectors.filterset.actions.addRow);\n const filterDataNode = filterSet.querySelectorAll(Selectors.data.fields.all);\n if (filterDataNode.length <= filters.length) {\n addRowButton.setAttribute('disabled', 'disabled');\n } else {\n addRowButton.removeAttribute('disabled');\n }\n\n if (filters.length === 1) {\n filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');\n filterSet.querySelector(Selectors.filterset.fields.join).value = 1;\n } else {\n filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');\n }\n };\n\n /**\n * Update the Dynamic table based upon the current filter.\n *\n * @return {Promise}\n */\n const updateTableFromFilter = () => {\n return DynamicTable.setFilters(\n DynamicTable.getTableFromId(filterSet.dataset.tableRegion),\n {\n filters: Object.values(activeFilters).map(filter => filter.filterValue),\n jointype: filterSet.querySelector(Selectors.filterset.fields.join).value,\n }\n );\n };\n\n // Add listeners for the main actions.\n filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {\n if (e.target.closest(Selectors.filterset.actions.addRow)) {\n e.preventDefault();\n\n addFilterRow();\n }\n\n if (e.target.closest(Selectors.filterset.actions.applyFilters)) {\n e.preventDefault();\n\n updateTableFromFilter();\n }\n\n if (e.target.closest(Selectors.filterset.actions.resetFilters)) {\n e.preventDefault();\n\n removeAllFilters();\n }\n });\n\n // Add the listener to remove a single filter.\n filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {\n if (e.target.closest(Selectors.filter.actions.remove)) {\n e.preventDefault();\n\n removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region));\n }\n });\n\n // Add listeners for the filter type selection.\n filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('change', e => {\n const typeField = e.target.closest(Selectors.filter.fields.type);\n if (typeField && typeField.value) {\n const filter = e.target.closest(Selectors.filter.region);\n\n addFilter(filter, typeField.value);\n }\n });\n\n filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {\n filterSet.dataset.filterverb = e.target.value;\n });\n};\n"],"file":"participantsfilter.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/participantsfilter.js"],"names":["init","participantsRegionId","filterSet","document","querySelector","activeFilters","courseid","CourseFilter","getFilterRegion","Selectors","filterset","regions","filterlist","addFilterRow","rownum","querySelectorAll","filter","region","length","Templates","renderForPromise","then","html","js","newContentNodes","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","fields","type","innerHTML","updateFiltersOptions","catch","Notification","exception","getFilterDataSource","filterType","filterDataNode","datasource","byName","addFilter","initialFilterValues","dataset","Filter","GenericFilter","filterTypeClass","typeField","value","disabled","getFilterObject","name","removeOrReplaceFilterRow","filterCount","replaceFilterRow","removeFilterRow","removeFilterObject","remove","updateTableFromFilter","getAvailableFilterLegends","filterLegends","index","innerText","rowNum","replaceNode","filterName","tearDown","removeAllFilters","filters","removeEmptyFilters","options","option","classList","add","addRowButton","actions","addRow","all","setAttribute","removeAttribute","filtermatch","join","setFilterFromConfig","config","filterConfig","Object","entries","jointype","filterPromises","map","filterData","Promise","resolve","filterValues","values","DynamicTable","setFilters","getTableFromId","tableRegion","filterValue","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","legendStrings","addEventListener","e","target","closest","preventDefault","applyFilters","resetFilters","filterverb","tableRoot","initialFilters","getFilters"],"mappings":"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","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 .\n\n/**\n * Participants filter managemnet.\n *\n * @module core_user/participants_filter\n * @package core_user\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CourseFilter from './local/participantsfilter/filtertypes/courseid';\nimport * as DynamicTable from 'core_table/dynamic';\nimport GenericFilter from './local/participantsfilter/filter';\nimport {get_strings as getStrings} from 'core/str';\nimport Notification from 'core/notification';\nimport Selectors from './local/participantsfilter/selectors';\nimport Templates from 'core/templates';\n\n/**\n * Initialise the participants filter on the element with the given id.\n *\n * @param {String} participantsRegionId\n */\nexport const init = participantsRegionId => {\n // Keep a reference to the filterset.\n const filterSet = document.querySelector(`#${participantsRegionId}`);\n\n // Keep a reference to all of the active filters.\n const activeFilters = {\n courseid: new CourseFilter('courseid', filterSet),\n };\n\n /**\n * Get the filter list region.\n *\n * @return {HTMLElement}\n */\n const getFilterRegion = () => filterSet.querySelector(Selectors.filterset.regions.filterlist);\n\n /**\n * Add an unselected filter row.\n *\n * @return {Promise}\n */\n const addFilterRow = () => {\n const rownum = 1 + getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {\"rownumber\": rownum})\n .then(({html, js}) => {\n const newContentNodes = Templates.appendNodeContents(getFilterRegion(), html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n updateFiltersOptions();\n\n return filterRow;\n })\n .catch(Notification.exception);\n };\n\n /**\n * Get the filter data source node fro the specified filter type.\n *\n * @param {String} filterType\n * @return {HTMLElement}\n */\n const getFilterDataSource = filterType => {\n const filterDataNode = filterSet.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));\n };\n\n /**\n * Add a filter to the list of active filters, performing any necessary setup.\n *\n * @param {HTMLElement} filterRow\n * @param {String} filterType\n * @param {Array} initialFilterValues The initially selected values for the filter\n * @returns {Filter}\n */\n const addFilter = async(filterRow, filterType, initialFilterValues) => {\n // Name the filter on the filter row.\n filterRow.dataset.filterType = filterType;\n\n const filterDataNode = getFilterDataSource(filterType);\n\n // Instantiate the Filter class.\n let Filter = GenericFilter;\n if (filterDataNode.dataset.filterTypeClass) {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n }\n activeFilters[filterType] = new Filter(filterType, filterSet, initialFilterValues);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.value = filterType;\n typeField.disabled = 'disabled';\n\n // Update the list of available filter types.\n updateFiltersOptions();\n\n return activeFilters[filterType];\n };\n\n /**\n * Get the registered filter class for the named filter.\n *\n * @param {String} name\n * @return {Object} See the Filter class.\n */\n const getFilterObject = name => {\n return activeFilters[name];\n };\n\n /**\n * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,\n * that it is replaced instead of being removed.\n *\n * @param {HTMLElement} filterRow\n */\n const removeOrReplaceFilterRow = filterRow => {\n const filterCount = getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n\n if (filterCount === 1) {\n replaceFilterRow(filterRow);\n } else {\n removeFilterRow(filterRow);\n }\n };\n\n /**\n * Remove the specified filter row and associated class.\n *\n * @param {HTMLElement} filterRow\n */\n const removeFilterRow = async filterRow => {\n // Remove the filter object.\n removeFilterObject(filterRow.dataset.filterType);\n\n // Remove the actual filter HTML.\n filterRow.remove();\n\n // Update the list of available filter types.\n updateFiltersOptions();\n\n // Refresh the table.\n updateTableFromFilter();\n\n // Update filter fieldset legends.\n const filterLegends = await getAvailableFilterLegends();\n\n getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {\n filterRow.querySelector('legend').innerText = filterLegends[index];\n });\n\n };\n\n /**\n * Replace the specified filter row with a new one.\n *\n * @param {HTMLElement} filterRow\n * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).\n * @return {Promise}\n */\n const replaceFilterRow = (filterRow, rowNum = 1) => {\n // Remove the filter object.\n removeFilterObject(filterRow.dataset.filterType);\n\n return Templates.renderForPromise('core_user/local/participantsfilter/filterrow', {\"rownumber\": rowNum})\n .then(({html, js}) => {\n const newContentNodes = Templates.replaceNode(filterRow, html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n updateFiltersOptions();\n\n return filterRow;\n })\n .then(filterRow => {\n // Refresh the table.\n updateTableFromFilter();\n\n return filterRow;\n })\n .catch(Notification.exception);\n };\n\n /**\n * Remove the Filter Object from the register.\n *\n * @param {string} filterName The name of the filter to be removed\n */\n const removeFilterObject = filterName => {\n if (filterName) {\n const filter = getFilterObject(filterName);\n if (filter) {\n filter.tearDown();\n\n // Remove from the list of active filters.\n delete activeFilters[filterName];\n }\n }\n };\n\n /**\n * Remove all filters.\n *\n * @returns {Promise}\n */\n const removeAllFilters = () => {\n const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => removeOrReplaceFilterRow(filterRow));\n\n // Refresh the table.\n return updateTableFromFilter();\n };\n\n /**\n * Remove any empty filters.\n */\n const removeEmptyFilters = () => {\n const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n if (!filterType.value) {\n removeOrReplaceFilterRow(filterRow);\n }\n });\n };\n\n /**\n * Update the list of filter types to filter out those already selected.\n */\n const updateFiltersOptions = () => {\n const filters = getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');\n options.forEach(option => {\n if (option.value === filterRow.dataset.filterType) {\n option.classList.remove('hidden');\n option.disabled = false;\n } else if (activeFilters[option.value]) {\n option.classList.add('hidden');\n option.disabled = true;\n } else {\n option.classList.remove('hidden');\n option.disabled = false;\n }\n });\n });\n\n // Configure the state of the \"Add row\" button.\n // This button is disabled when there is a filter row available for each condition.\n const addRowButton = filterSet.querySelector(Selectors.filterset.actions.addRow);\n const filterDataNode = filterSet.querySelectorAll(Selectors.data.fields.all);\n if (filterDataNode.length <= filters.length) {\n addRowButton.setAttribute('disabled', 'disabled');\n } else {\n addRowButton.removeAttribute('disabled');\n }\n\n if (filters.length === 1) {\n filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');\n filterSet.querySelector(Selectors.filterset.fields.join).value = 1;\n } else {\n filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');\n }\n };\n\n /**\n * Set the current filter options based on a provided configuration.\n *\n * @param {Object} config\n * @param {Number} config.jointype\n * @param {Object} config.filters\n */\n const setFilterFromConfig = config => {\n const filterConfig = Object.entries(config.filters);\n\n if (!filterConfig.length) {\n // There are no filters to set from.\n return;\n }\n\n // Set the main join type.\n filterSet.querySelector(Selectors.filterset.fields.join).value = config.jointype;\n\n const filterPromises = filterConfig.map(([filterType, filterData]) => {\n if (filterType === 'courseid') {\n // The courseid is a special case.\n return Promise.resolve();\n }\n\n const filterValues = filterData.values;\n\n if (!filterValues.length) {\n // There are no values for this filter.\n // Skip it.\n return Promise.resolve();\n }\n\n return addFilterRow().then(([filterRow]) => addFilter(filterRow, filterType, filterValues));\n });\n\n Promise.all(filterPromises).then(() => {\n return removeEmptyFilters();\n })\n .then(updateFiltersOptions)\n .then(updateTableFromFilter)\n .catch();\n };\n\n /**\n * Update the Dynamic table based upon the current filter.\n *\n * @return {Promise}\n */\n const updateTableFromFilter = () => {\n return DynamicTable.setFilters(\n DynamicTable.getTableFromId(filterSet.dataset.tableRegion),\n {\n filters: Object.values(activeFilters).map(filter => filter.filterValue),\n jointype: filterSet.querySelector(Selectors.filterset.fields.join).value,\n }\n );\n };\n\n /**\n * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.\n *\n * @return {array}\n */\n const getAvailableFilterLegends = async() => {\n const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;\n let requests = [];\n\n [...Array(maxFilters)].forEach((_, rowIndex) => {\n requests.push({\n \"key\": \"filterrowlegend\",\n \"component\": \"core_user\",\n // Add 1 since rows begin at 1 (index begins at zero).\n \"param\": rowIndex + 1\n });\n });\n\n const legendStrings = await getStrings(requests)\n .then(fetchedStrings => {\n return fetchedStrings;\n })\n .catch(Notification.exception);\n\n return legendStrings;\n };\n\n // Add listeners for the main actions.\n filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {\n if (e.target.closest(Selectors.filterset.actions.addRow)) {\n e.preventDefault();\n\n addFilterRow();\n }\n\n if (e.target.closest(Selectors.filterset.actions.applyFilters)) {\n e.preventDefault();\n\n updateTableFromFilter();\n }\n\n if (e.target.closest(Selectors.filterset.actions.resetFilters)) {\n e.preventDefault();\n\n removeAllFilters();\n }\n });\n\n // Add the listener to remove a single filter.\n filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {\n if (e.target.closest(Selectors.filter.actions.remove)) {\n e.preventDefault();\n\n removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region));\n }\n });\n\n // Add listeners for the filter type selection.\n filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('change', e => {\n const typeField = e.target.closest(Selectors.filter.fields.type);\n if (typeField && typeField.value) {\n const filter = e.target.closest(Selectors.filter.region);\n\n addFilter(filter, typeField.value);\n }\n });\n\n filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {\n filterSet.dataset.filterverb = e.target.value;\n });\n\n const tableRoot = DynamicTable.getTableFromId(filterSet.dataset.tableRegion);\n const initialFilters = DynamicTable.getFilters(tableRoot);\n if (initialFilters) {\n // Apply the initial filter configuration.\n setFilterFromConfig(initialFilters);\n }\n};\n"],"file":"participantsfilter.min.js"} \ No newline at end of file diff --git a/user/amd/build/unified_filter.min.js.map b/user/amd/build/unified_filter.min.js.map index fee4c4fbba9..2eef9fc96fa 100644 --- a/user/amd/build/unified_filter.min.js.map +++ b/user/amd/build/unified_filter.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/unified_filter.js"],"names":["define","$","Autocomplete","Str","Notification","SELECTORS","UNIFIED_FILTERS","init","M","util","js_pending","get_strings","key","component","done","langstrings","placeholder","noSelectionString","enhance","then","js_complete","fail","exception","last","val","on","current","listoffilters","textfilters","updatedselectedfilters","each","index","catoption","catandoption","split","length","push","category","option","updatefilters","concat","join","form","submit","getForm","closest"],"mappings":"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","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 .\n\n/**\n * Unified filter page JS module for the course participants page.\n *\n * @module core_user/unified_filter\n * @package core_user\n * @copyright 2017 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'],\n function($, Autocomplete, Str, Notification) {\n\n /**\n * Selectors.\n *\n * @access private\n * @type {{UNIFIED_FILTERS: string}}\n */\n var SELECTORS = {\n UNIFIED_FILTERS: '#unified-filters'\n };\n\n /**\n * Init function.\n *\n * @method init\n * @private\n */\n var init = function() {\n var stringkeys = [\n {\n key: 'userfilterplaceholder',\n component: 'moodle'\n },\n {\n key: 'nofiltersapplied',\n component: 'moodle'\n }\n ];\n\n M.util.js_pending('unified_filter_datasource');\n Str.get_strings(stringkeys).done(function(langstrings) {\n var placeholder = langstrings[0];\n var noSelectionString = langstrings[1];\n Autocomplete.enhance(SELECTORS.UNIFIED_FILTERS, true, 'core_user/unified_filter_datasource', placeholder,\n false, true, noSelectionString, true)\n .then(function() {\n M.util.js_complete('unified_filter_datasource');\n\n return;\n })\n .fail(Notification.exception);\n }).fail(Notification.exception);\n\n var last = $(SELECTORS.UNIFIED_FILTERS).val();\n $(SELECTORS.UNIFIED_FILTERS).on('change', function() {\n var current = $(this).val();\n var listoffilters = [];\n var textfilters = [];\n var updatedselectedfilters = false;\n\n $.each(current, function(index, catoption) {\n var catandoption = catoption.split(':', 2);\n if (catandoption.length !== 2) {\n textfilters.push(catoption);\n return true; // Text search filter.\n }\n\n var category = catandoption[0];\n var option = catandoption[1];\n\n // The last option (eg. 'Teacher') out of a category (eg. 'Role') in this loop is the one that was last\n // selected, so we want to use that if there are multiple options from the same category. Eg. The user\n // may have chosen to filter by the 'Student' role, then wanted to filter by the 'Teacher' role - the\n // last option in the category to be selected (in this case 'Teacher') will come last, so will overwrite\n // 'Student' (after this if). We want to let the JS know that the filters have been updated.\n if (typeof listoffilters[category] !== 'undefined') {\n updatedselectedfilters = true;\n }\n\n listoffilters[category] = option;\n return true;\n });\n\n // Check if we have something to remove from the list of filters.\n if (updatedselectedfilters) {\n // Go through and put the list into something we can use to update the list of filters.\n var updatefilters = [];\n for (var category in listoffilters) {\n updatefilters.push(category + \":\" + listoffilters[category]);\n }\n updatefilters = updatefilters.concat(textfilters);\n $(this).val(updatefilters);\n }\n\n // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.\n if (last.join(',') != current.join(',')) {\n this.form.submit();\n }\n });\n };\n\n /**\n * Return the unified user filter form.\n *\n * @method getForm\n * @return {DOMElement}\n */\n var getForm = function() {\n return $(SELECTORS.UNIFIED_FILTERS).closest('form');\n };\n\n return /** @alias module:core/form-autocomplete */ {\n /**\n * Initialise the unified user filter.\n *\n * @method init\n */\n init: function() {\n init();\n },\n\n /**\n * Return the unified user filter form.\n *\n * @method getForm\n * @return {DOMElement}\n */\n getForm: function() {\n return getForm();\n }\n };\n});\n"],"file":"unified_filter.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/unified_filter.js"],"names":["define","$","Autocomplete","Str","Notification","SELECTORS","UNIFIED_FILTERS","init","M","util","js_pending","get_strings","key","component","done","langstrings","placeholder","noSelectionString","enhance","then","js_complete","fail","exception","last","val","on","current","listoffilters","textfilters","updatedselectedfilters","each","index","catoption","catandoption","split","length","push","category","option","updatefilters","concat","join","form","submit","getForm","closest"],"mappings":"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","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 .\n\n/**\n * Unified filter page JS module for the course participants page.\n *\n * @deprecated since Moodle 3.9 MDL-68612 - user unified filter replaced by participants filter.\n * @module core_user/unified_filter\n * @package core_user\n * @copyright 2017 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/form-autocomplete', 'core/str', 'core/notification'],\n function($, Autocomplete, Str, Notification) {\n\n /**\n * Selectors.\n *\n * @access private\n * @type {{UNIFIED_FILTERS: string}}\n */\n var SELECTORS = {\n UNIFIED_FILTERS: '#unified-filters'\n };\n\n /**\n * Init function.\n *\n * @deprecated since Moodle 3.9 MDL-68612 - user unified filter replaced by participants filter.\n * @method init\n * @private\n */\n var init = function() {\n var stringkeys = [\n {\n key: 'userfilterplaceholder',\n component: 'moodle'\n },\n {\n key: 'nofiltersapplied',\n component: 'moodle'\n }\n ];\n\n M.util.js_pending('unified_filter_datasource');\n Str.get_strings(stringkeys).done(function(langstrings) {\n var placeholder = langstrings[0];\n var noSelectionString = langstrings[1];\n Autocomplete.enhance(SELECTORS.UNIFIED_FILTERS, true, 'core_user/unified_filter_datasource', placeholder,\n false, true, noSelectionString, true)\n .then(function() {\n M.util.js_complete('unified_filter_datasource');\n\n return;\n })\n .fail(Notification.exception);\n }).fail(Notification.exception);\n\n var last = $(SELECTORS.UNIFIED_FILTERS).val();\n $(SELECTORS.UNIFIED_FILTERS).on('change', function() {\n var current = $(this).val();\n var listoffilters = [];\n var textfilters = [];\n var updatedselectedfilters = false;\n\n $.each(current, function(index, catoption) {\n var catandoption = catoption.split(':', 2);\n if (catandoption.length !== 2) {\n textfilters.push(catoption);\n return true; // Text search filter.\n }\n\n var category = catandoption[0];\n var option = catandoption[1];\n\n // The last option (eg. 'Teacher') out of a category (eg. 'Role') in this loop is the one that was last\n // selected, so we want to use that if there are multiple options from the same category. Eg. The user\n // may have chosen to filter by the 'Student' role, then wanted to filter by the 'Teacher' role - the\n // last option in the category to be selected (in this case 'Teacher') will come last, so will overwrite\n // 'Student' (after this if). We want to let the JS know that the filters have been updated.\n if (typeof listoffilters[category] !== 'undefined') {\n updatedselectedfilters = true;\n }\n\n listoffilters[category] = option;\n return true;\n });\n\n // Check if we have something to remove from the list of filters.\n if (updatedselectedfilters) {\n // Go through and put the list into something we can use to update the list of filters.\n var updatefilters = [];\n for (var category in listoffilters) {\n updatefilters.push(category + \":\" + listoffilters[category]);\n }\n updatefilters = updatefilters.concat(textfilters);\n $(this).val(updatefilters);\n }\n\n // Prevent form from submitting unnecessarily, eg. on blur when no filter is selected.\n if (last.join(',') != current.join(',')) {\n this.form.submit();\n }\n });\n };\n\n /**\n * Return the unified user filter form.\n *\n * @deprecated since Moodle 3.9 MDL-68612 - user unified filter replaced by participants filter.\n * @method getForm\n * @return {DOMElement}\n */\n var getForm = function() {\n return $(SELECTORS.UNIFIED_FILTERS).closest('form');\n };\n\n return /** @alias module:core/form-autocomplete */ {\n /**\n * Initialise the unified user filter.\n *\n * @method init\n */\n init: function() {\n init();\n },\n\n /**\n * Return the unified user filter form.\n *\n * @method getForm\n * @return {DOMElement}\n */\n getForm: function() {\n return getForm();\n }\n };\n});\n"],"file":"unified_filter.min.js"} \ No newline at end of file diff --git a/user/amd/build/unified_filter_datasource.min.js.map b/user/amd/build/unified_filter_datasource.min.js.map index ebe12c40133..3ac811e057e 100644 --- a/user/amd/build/unified_filter_datasource.min.js.map +++ b/user/amd/build/unified_filter_datasource.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/unified_filter_datasource.js"],"names":["define","$","Ajax","Notification","list","selector","query","filteredOptions","el","originalOptions","data","selectedFilters","val","each","index","option","trim","label","toLocaleLowerCase","indexOf","inArray","value","push","deferred","Deferred","resolve","promise","processResults","results","options","transport","callback","then","catch","exception"],"mappings":"AAyBAA,OAAM,uCAAC,CAAC,QAAD,CAAW,WAAX,CAAwB,mBAAxB,CAAD,CAA+C,SAASC,CAAT,CAAYC,CAAZ,CAAkBC,CAAlB,CAAgC,CAEjF,MAAgE,CAQ5DC,IAAI,CAAE,cAASC,CAAT,CAAmBC,CAAnB,CAA0B,IACxBC,CAAAA,CAAe,CAAG,EADM,CAGxBC,CAAE,CAAGP,CAAC,CAACI,CAAD,CAHkB,CAIxBI,CAAe,CAAGR,CAAC,CAACI,CAAD,CAAD,CAAYK,IAAZ,CAAiB,qBAAjB,CAJM,CAKxBC,CAAe,CAAGH,CAAE,CAACI,GAAH,EALM,CAM5BX,CAAC,CAACY,IAAF,CAAOJ,CAAP,CAAwB,SAASK,CAAT,CAAgBC,CAAhB,CAAwB,CAE5C,GAAsB,EAAlB,GAAAd,CAAC,CAACe,IAAF,CAAOV,CAAP,GAAgG,CAAC,CAAzE,GAAAS,CAAM,CAACE,KAAP,CAAaC,iBAAb,GAAiCC,OAAjC,CAAyCb,CAAK,CAACY,iBAAN,EAAzC,CAA5B,CAAwG,CACpG,QACH,CAED,GAA+C,CAAC,CAA5C,CAAAjB,CAAC,CAACmB,OAAF,CAAUL,CAAM,CAACM,KAAjB,CAAwBV,CAAxB,CAAJ,CAAmD,CAC/C,QACH,CAEDJ,CAAe,CAACe,IAAhB,CAAqBP,CAArB,EACA,QACH,CAZD,EAcA,GAAIQ,CAAAA,CAAQ,CAAG,GAAItB,CAAAA,CAAC,CAACuB,QAArB,CACAD,CAAQ,CAACE,OAAT,CAAiBlB,CAAjB,EAEA,MAAOgB,CAAAA,CAAQ,CAACG,OAAT,EACV,CAhC2D,CAyC5DC,cAAc,CAAE,wBAAStB,CAAT,CAAmBuB,CAAnB,CAA4B,CACxC,GAAIC,CAAAA,CAAO,CAAG,EAAd,CACA5B,CAAC,CAACY,IAAF,CAAOe,CAAP,CAAgB,SAASd,CAAT,CAAgBJ,CAAhB,CAAsB,CAClCmB,CAAO,CAACP,IAAR,CAAa,CACTD,KAAK,CAAEX,CAAI,CAACW,KADH,CAETJ,KAAK,CAAEP,CAAI,CAACO,KAFH,CAAb,CAIH,CALD,EAMA,MAAOY,CAAAA,CACV,CAlD2D,CA4D5DC,SAAS,CAAE,mBAASzB,CAAT,CAAmBC,CAAnB,CAA0ByB,CAA1B,CAAoC,CAC3C,KAAK3B,IAAL,CAAUC,CAAV,CAAoBC,CAApB,EAA2B0B,IAA3B,CAAgCD,CAAhC,EAA0CE,KAA1C,CAAgD9B,CAAY,CAAC+B,SAA7D,CACH,CA9D2D,CAiEnE,CAnEK,CAAN","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 .\n\n/**\n * Datasource for the core_user/unified_filter.\n *\n * This module is compatible with core/form-autocomplete.\n *\n * @package core_user\n * @copyright 2017 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {\n\n return /** @alias module:core_user/unified_filter_datasource */ {\n /**\n * List filter options.\n *\n * @param {String} selector The select element selector.\n * @param {String} query The query string.\n * @return {Promise}\n */\n list: function(selector, query) {\n var filteredOptions = [];\n\n var el = $(selector);\n var originalOptions = $(selector).data('originaloptionsjson');\n var selectedFilters = el.val();\n $.each(originalOptions, function(index, option) {\n // Skip option if it does not contain the query string.\n if ($.trim(query) !== '' && option.label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) === -1) {\n return true;\n }\n // Skip filters that have already been selected.\n if ($.inArray(option.value, selectedFilters) > -1) {\n return true;\n }\n\n filteredOptions.push(option);\n return true;\n });\n\n var deferred = new $.Deferred();\n deferred.resolve(filteredOptions);\n\n return deferred.promise();\n },\n\n /**\n * Process the results for auto complete elements.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {Array} results An array or results.\n * @return {Array} New array of results.\n */\n processResults: function(selector, results) {\n var options = [];\n $.each(results, function(index, data) {\n options.push({\n value: data.value,\n label: data.label\n });\n });\n return options;\n },\n\n /**\n * Source of data for Ajax element.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {String} query The query string.\n * @param {Function} callback A callback function receiving an array of results.\n */\n /* eslint-disable promise/no-callback-in-promise */\n transport: function(selector, query, callback) {\n this.list(selector, query).then(callback).catch(Notification.exception);\n }\n };\n\n});\n"],"file":"unified_filter_datasource.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/unified_filter_datasource.js"],"names":["define","$","Ajax","Notification","list","selector","query","filteredOptions","el","originalOptions","data","selectedFilters","val","each","index","option","trim","label","toLocaleLowerCase","indexOf","inArray","value","push","deferred","Deferred","resolve","promise","processResults","results","options","transport","callback","then","catch","exception"],"mappings":"AA0BAA,OAAM,uCAAC,CAAC,QAAD,CAAW,WAAX,CAAwB,mBAAxB,CAAD,CAA+C,SAASC,CAAT,CAAYC,CAAZ,CAAkBC,CAAlB,CAAgC,CAEjF,MAAgE,CAQ5DC,IAAI,CAAE,cAASC,CAAT,CAAmBC,CAAnB,CAA0B,IACxBC,CAAAA,CAAe,CAAG,EADM,CAGxBC,CAAE,CAAGP,CAAC,CAACI,CAAD,CAHkB,CAIxBI,CAAe,CAAGR,CAAC,CAACI,CAAD,CAAD,CAAYK,IAAZ,CAAiB,qBAAjB,CAJM,CAKxBC,CAAe,CAAGH,CAAE,CAACI,GAAH,EALM,CAM5BX,CAAC,CAACY,IAAF,CAAOJ,CAAP,CAAwB,SAASK,CAAT,CAAgBC,CAAhB,CAAwB,CAE5C,GAAsB,EAAlB,GAAAd,CAAC,CAACe,IAAF,CAAOV,CAAP,GAAgG,CAAC,CAAzE,GAAAS,CAAM,CAACE,KAAP,CAAaC,iBAAb,GAAiCC,OAAjC,CAAyCb,CAAK,CAACY,iBAAN,EAAzC,CAA5B,CAAwG,CACpG,QACH,CAED,GAA+C,CAAC,CAA5C,CAAAjB,CAAC,CAACmB,OAAF,CAAUL,CAAM,CAACM,KAAjB,CAAwBV,CAAxB,CAAJ,CAAmD,CAC/C,QACH,CAEDJ,CAAe,CAACe,IAAhB,CAAqBP,CAArB,EACA,QACH,CAZD,EAcA,GAAIQ,CAAAA,CAAQ,CAAG,GAAItB,CAAAA,CAAC,CAACuB,QAArB,CACAD,CAAQ,CAACE,OAAT,CAAiBlB,CAAjB,EAEA,MAAOgB,CAAAA,CAAQ,CAACG,OAAT,EACV,CAhC2D,CAyC5DC,cAAc,CAAE,wBAAStB,CAAT,CAAmBuB,CAAnB,CAA4B,CACxC,GAAIC,CAAAA,CAAO,CAAG,EAAd,CACA5B,CAAC,CAACY,IAAF,CAAOe,CAAP,CAAgB,SAASd,CAAT,CAAgBJ,CAAhB,CAAsB,CAClCmB,CAAO,CAACP,IAAR,CAAa,CACTD,KAAK,CAAEX,CAAI,CAACW,KADH,CAETJ,KAAK,CAAEP,CAAI,CAACO,KAFH,CAAb,CAIH,CALD,EAMA,MAAOY,CAAAA,CACV,CAlD2D,CA4D5DC,SAAS,CAAE,mBAASzB,CAAT,CAAmBC,CAAnB,CAA0ByB,CAA1B,CAAoC,CAC3C,KAAK3B,IAAL,CAAUC,CAAV,CAAoBC,CAApB,EAA2B0B,IAA3B,CAAgCD,CAAhC,EAA0CE,KAA1C,CAAgD9B,CAAY,CAAC+B,SAA7D,CACH,CA9D2D,CAiEnE,CAnEK,CAAN","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 .\n\n/**\n * Datasource for the core_user/unified_filter.\n * @deprecated since Moodle 3.9 MDL-68612 - user unified filter replaced by participants filter.\n *\n * This module is compatible with core/form-autocomplete.\n *\n * @package core_user\n * @copyright 2017 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {\n\n return /** @alias module:core_user/unified_filter_datasource */ {\n /**\n * List filter options.\n *\n * @param {String} selector The select element selector.\n * @param {String} query The query string.\n * @return {Promise}\n */\n list: function(selector, query) {\n var filteredOptions = [];\n\n var el = $(selector);\n var originalOptions = $(selector).data('originaloptionsjson');\n var selectedFilters = el.val();\n $.each(originalOptions, function(index, option) {\n // Skip option if it does not contain the query string.\n if ($.trim(query) !== '' && option.label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) === -1) {\n return true;\n }\n // Skip filters that have already been selected.\n if ($.inArray(option.value, selectedFilters) > -1) {\n return true;\n }\n\n filteredOptions.push(option);\n return true;\n });\n\n var deferred = new $.Deferred();\n deferred.resolve(filteredOptions);\n\n return deferred.promise();\n },\n\n /**\n * Process the results for auto complete elements.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {Array} results An array or results.\n * @return {Array} New array of results.\n */\n processResults: function(selector, results) {\n var options = [];\n $.each(results, function(index, data) {\n options.push({\n value: data.value,\n label: data.label\n });\n });\n return options;\n },\n\n /**\n * Source of data for Ajax element.\n *\n * @param {String} selector The selector of the auto complete element.\n * @param {String} query The query string.\n * @param {Function} callback A callback function receiving an array of results.\n */\n /* eslint-disable promise/no-callback-in-promise */\n transport: function(selector, query, callback) {\n this.list(selector, query).then(callback).catch(Notification.exception);\n }\n };\n\n});\n"],"file":"unified_filter_datasource.min.js"} \ No newline at end of file diff --git a/user/amd/src/local/participantsfilter/filter.js b/user/amd/src/local/participantsfilter/filter.js index 7895ca0cf42..6670c2efe20 100644 --- a/user/amd/src/local/participantsfilter/filter.js +++ b/user/amd/src/local/participantsfilter/filter.js @@ -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, diff --git a/user/amd/src/local/participantsfilter/filtertypes/keyword.js b/user/amd/src/local/participantsfilter/filtertypes/keyword.js index c7b7872e7a7..a1aa4798b72 100644 --- a/user/amd/src/local/participantsfilter/filtertypes/keyword.js +++ b/user/amd/src/local/participantsfilter/filtertypes/keyword.js @@ -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. * diff --git a/user/amd/src/local/participantsfilter/selectors.js b/user/amd/src/local/participantsfilter/selectors.js index 3d0ef29e126..27c94191cc9 100644 --- a/user/amd/src/local/participantsfilter/selectors.js +++ b/user/amd/src/local/participantsfilter/selectors.js @@ -63,5 +63,6 @@ export default { all: `${getFilterRegion('filtertypedata')} [data-field-name]`, }, typeList: getFilterRegion('filtertypelist'), + typeListSelect: `select${getFilterRegion('filtertypelist')}`, }, }; diff --git a/user/amd/src/participantsfilter.js b/user/amd/src/participantsfilter.js index 3dcdb8821d3..718e5ab79dd 100644 --- a/user/amd/src/participantsfilter.js +++ b/user/amd/src/participantsfilter.js @@ -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); + } }; diff --git a/user/amd/src/unified_filter.js b/user/amd/src/unified_filter.js index a18d2fd816c..9248710c539 100644 --- a/user/amd/src/unified_filter.js +++ b/user/amd/src/unified_filter.js @@ -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} */ diff --git a/user/amd/src/unified_filter_datasource.js b/user/amd/src/unified_filter_datasource.js index 60700ac1877..fdbb2064138 100644 --- a/user/amd/src/unified_filter_datasource.js +++ b/user/amd/src/unified_filter_datasource.js @@ -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. * diff --git a/user/classes/output/participants_filter.php b/user/classes/output/participants_filter.php index c98fccb472d..443c5a280e2 100644 --- a/user/classes/output/participants_filter.php +++ b/user/classes/output/participants_filter.php @@ -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; diff --git a/user/classes/output/unified_filter.php b/user/classes/output/unified_filter.php index 849a98664f6..6a05f8635bd 100644 --- a/user/classes/output/unified_filter.php +++ b/user/classes/output/unified_filter.php @@ -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)) { diff --git a/user/classes/table/participants_search.php b/user/classes/table/participants_search.php index d9aaccff2c7..0d944832325 100644 --- a/user/classes/table/participants_search.php +++ b/user/classes/table/participants_search.php @@ -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; diff --git a/user/index.php b/user/index.php index 08169f45e22..29f7949559a 100644 --- a/user/index.php +++ b/user/index.php @@ -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 '
'; -// 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 = ''; diff --git a/user/lib.php b/user/lib.php index 078aa0bacea..0134a2ee0d3 100644 --- a/user/lib.php +++ b/user/lib.php @@ -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. * diff --git a/user/renderer.php b/user/renderer.php index 7be0ce5103d..b85ec06370f 100644 --- a/user/renderer.php +++ b/user/renderer.php @@ -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); diff --git a/user/templates/local/participantsfilter/autocomplete_selection_items.mustache b/user/templates/local/participantsfilter/autocomplete_selection_items.mustache index 9f358b6b2eb..732bb23f9ed 100644 --- a/user/templates/local/participantsfilter/autocomplete_selection_items.mustache +++ b/user/templates/local/participantsfilter/autocomplete_selection_items.mustache @@ -43,7 +43,10 @@ {{#items}} - {{label}} + {{label}} + {{/items}} {{^items}} diff --git a/user/templates/local/participantsfilter/filterrow.mustache b/user/templates/local/participantsfilter/filterrow.mustache index 9d2bdc6ddb6..2ae3c426bd9 100644 --- a/user/templates/local/participantsfilter/filterrow.mustache +++ b/user/templates/local/participantsfilter/filterrow.mustache @@ -29,37 +29,41 @@ "name": "status", "title": "Status" } - ] + ], + "rownumber": 1 } }}
-
-
- - + + + + +
+ + + + +
+ +
- - - - -
- - -
-
-
{{#str}}adverbfor_andnot, core_user{{/str}}
-
{{#str}}adverbfor_or, core_user{{/str}}
-
{{#str}}adverbfor_and, core_user{{/str}}
-
+
+
{{#str}}adverbfor_andnot, core_user{{/str}}
+
{{#str}}adverbfor_or, core_user{{/str}}
+
{{#str}}adverbfor_and, core_user{{/str}}
+
+
diff --git a/user/templates/unified_filter.mustache b/user/templates/unified_filter.mustache index 243f0857429..30e7fe16c11 100644 --- a/user/templates/unified_filter.mustache +++ b/user/templates/unified_filter.mustache @@ -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. diff --git a/user/tests/behat/filter_participants.feature b/user/tests/behat/filter_participants.feature index c716a18a47c..16839b096f2 100644 --- a/user/tests/behat/filter_participants.feature +++ b/user/tests/behat/filter_participants.feature @@ -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 "" item in the autocomplete list + And I set the field "Match" in the "Filter 1" "fieldset" to "" + And I set the field "type" in the "Filter 1" "fieldset" to "" + And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset" + And I click on "" "list_item" + When I click on "Apply filters" "button" Then I should see "" in the "participants" "table" And I should see "" in the "participants" "table" And I should see "" 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 "" + And I set the field "type" in the "Filter 1" "fieldset" to "" + And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset" + And I click on "" "list_item" + And I click on "" "list_item" + When I click on "Apply filters" "button" + Then I should see "" in the "participants" "table" + And I should see "" in the "participants" "table" + And I should see "" in the "participants" "table" + And I should not see "" in the "participants" "table" + And I should not see "" 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 "" item in the autocomplete list + And I set the field "type" in the "Filter 1" "fieldset" to "" + And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset" + And I click on "" "list_item" + When I click on "Apply filters" "button" Then I should see "" in the "participants" "table" And I should see "" in the "participants" "table" - And I should see "" in the "participants" "table" And I should not see "" in the "participants" "table" And I should not see "" 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" diff --git a/user/tests/behat/filter_participants_showall.feature b/user/tests/behat/filter_participants_showall.feature index d50851f2186..997122262f6 100644 --- a/user/tests/behat/filter_participants_showall.feature +++ b/user/tests/behat/filter_participants_showall.feature @@ -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" diff --git a/user/tests/behat/view_participants_groups.feature b/user/tests/behat/view_participants_groups.feature index 9479c5b6607..f15572ec1ff 100644 --- a/user/tests/behat/view_participants_groups.feature +++ b/user/tests/behat/view_participants_groups.feature @@ -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" diff --git a/user/tests/table/participants_search_test.php b/user/tests/table/participants_search_test.php index 3b6fc756294..66ce32c6bc7 100644 --- a/user/tests/table/participants_search_test.php +++ b/user/tests/table/participants_search_test.php @@ -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. * diff --git a/user/tests/userlib_test.php b/user/tests/userlib_test.php index 684e22f2ce4..fcec1fe45db 100644 --- a/user/tests/userlib_test.php +++ b/user/tests/userlib_test.php @@ -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); - } } diff --git a/user/upgrade.txt b/user/upgrade.txt index 56e4dd9ac44..85531920a8d 100644 --- a/user/upgrade.txt +++ b/user/upgrade.txt @@ -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: