From d53dd31f0522656df8a6119070c0d6514fb12b9c Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 6 May 2020 18:39:55 +0800 Subject: [PATCH 01/13] MDL-68612 user: Remove unified filter from participants page --- user/index.php | 188 +++++++++---------------------------------------- 1 file changed, 34 insertions(+), 154 deletions(-) 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 = ''; From d85315ee8c3207177a6303e007e8e890bd731290 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 27 May 2020 18:49:18 +0800 Subject: [PATCH 02/13] MDL-68612 user: Update participants group filtering to enforce groups This is required to ensure regardless of user applied filters, only members of groups visible to the user are ever fetched. This also includes a fix to remove the groups filter option where no groups mode is applied. --- user/classes/output/participants_filter.php | 3 +- user/classes/table/participants_search.php | 52 ++- user/tests/table/participants_search_test.php | 401 ++++++++++++++++++ 3 files changed, 452 insertions(+), 4 deletions(-) diff --git a/user/classes/output/participants_filter.php b/user/classes/output/participants_filter.php index c98fccb472d..0ab5ed8f335 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; } 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/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. * From 9e791ff7f5b8bf5fbc472a973c9cd7dde3b0be15 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 20 May 2020 17:32:34 +0800 Subject: [PATCH 03/13] MDL-68612 lib: Reverting filter/filterset default join types to match UI These had temporarily been set to ALL for consistency while the unified filter remained in use. Now that is being replaced with the new UI, it can be returned to its intended default. --- lib/table/classes/local/filter/filter.php | 7 ++----- lib/table/classes/local/filter/filterset.php | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) 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; From 624635fb0253b33f0032e6fc66eccf1e6ddf6616 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 6 May 2020 18:31:36 +0800 Subject: [PATCH 04/13] MDL-68612 core: Unified filter deprecations - language string --- lang/en/deprecated.txt | 1 + lang/en/moodle.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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'; From 9e8aa94b884edd9b4758b2879529422c8b5b9402 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 6 May 2020 18:32:50 +0800 Subject: [PATCH 05/13] MDL-68612 user: Unified filter deprecations - JavaScript --- user/amd/build/unified_filter.min.js.map | 2 +- user/amd/build/unified_filter_datasource.min.js.map | 2 +- user/amd/src/unified_filter.js | 3 +++ user/amd/src/unified_filter_datasource.js | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) 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":"AAuBAA,OAAM,4BAAC,CAAC,QAAD,CAAW,wBAAX,CAAqC,UAArC,CAAiD,mBAAjD,CAAD,CACE,SAASC,CAAT,CAAYC,CAAZ,CAA0BC,CAA1B,CAA+BC,CAA/B,CAA6C,IAQ7CC,CAAAA,CAAS,CAAG,CACZC,eAAe,CAAE,kBADL,CARiC,CAkB7CC,CAAI,CAAG,QAAPA,CAAAA,IAAO,EAAW,CAYlBC,CAAC,CAACC,IAAF,CAAOC,UAAP,CAAkB,2BAAlB,EACAP,CAAG,CAACQ,WAAJ,CAZiB,CACb,CACIC,GAAG,CAAE,uBADT,CAEIC,SAAS,CAAE,QAFf,CADa,CAKb,CACID,GAAG,CAAE,kBADT,CAEIC,SAAS,CAAE,QAFf,CALa,CAYjB,EAA4BC,IAA5B,CAAiC,SAASC,CAAT,CAAsB,IAC/CC,CAAAA,CAAW,CAAGD,CAAW,CAAC,CAAD,CADsB,CAE/CE,CAAiB,CAAGF,CAAW,CAAC,CAAD,CAFgB,CAGnDb,CAAY,CAACgB,OAAb,CAAqBb,CAAS,CAACC,eAA/B,IAAsD,qCAAtD,CAA6FU,CAA7F,OACiBC,CADjB,KAECE,IAFD,CAEM,UAAW,CACbX,CAAC,CAACC,IAAF,CAAOW,WAAP,CAAmB,2BAAnB,CAGH,CAND,EAOCC,IAPD,CAOMjB,CAAY,CAACkB,SAPnB,CAQH,CAXD,EAWGD,IAXH,CAWQjB,CAAY,CAACkB,SAXrB,EAaA,GAAIC,CAAAA,CAAI,CAAGtB,CAAC,CAACI,CAAS,CAACC,eAAX,CAAD,CAA6BkB,GAA7B,EAAX,CACAvB,CAAC,CAACI,CAAS,CAACC,eAAX,CAAD,CAA6BmB,EAA7B,CAAgC,QAAhC,CAA0C,UAAW,IAC7CC,CAAAA,CAAO,CAAGzB,CAAC,CAAC,IAAD,CAAD,CAAQuB,GAAR,EADmC,CAE7CG,CAAa,CAAG,EAF6B,CAG7CC,CAAW,CAAG,EAH+B,CAI7CC,CAAsB,GAJuB,CAMjD5B,CAAC,CAAC6B,IAAF,CAAOJ,CAAP,CAAgB,SAASK,CAAT,CAAgBC,CAAhB,CAA2B,CACvC,GAAIC,CAAAA,CAAY,CAAGD,CAAS,CAACE,KAAV,CAAgB,GAAhB,CAAqB,CAArB,CAAnB,CACA,GAA4B,CAAxB,GAAAD,CAAY,CAACE,MAAjB,CAA+B,CAC3BP,CAAW,CAACQ,IAAZ,CAAiBJ,CAAjB,EACA,QACH,CALsC,GAOnCK,CAAAA,CAAQ,CAAGJ,CAAY,CAAC,CAAD,CAPY,CAQnCK,CAAM,CAAGL,CAAY,CAAC,CAAD,CARc,CAevC,GAAuC,WAAnC,QAAON,CAAAA,CAAa,CAACU,CAAD,CAAxB,CAAoD,CAChDR,CAAsB,GACzB,CAEDF,CAAa,CAACU,CAAD,CAAb,CAA0BC,CAA1B,CACA,QACH,CArBD,EAwBA,GAAIT,CAAJ,CAA4B,CAExB,GAAIU,CAAAA,CAAa,CAAG,EAApB,CACA,IAAK,GAAIF,CAAAA,CAAT,GAAqBV,CAAAA,CAArB,CAAoC,CAChCY,CAAa,CAACH,IAAd,CAAmBC,CAAQ,CAAG,GAAX,CAAiBV,CAAa,CAACU,CAAD,CAAjD,CACH,CACDE,CAAa,CAAGA,CAAa,CAACC,MAAd,CAAqBZ,CAArB,CAAhB,CACA3B,CAAC,CAAC,IAAD,CAAD,CAAQuB,GAAR,CAAYe,CAAZ,CACH,CAGD,GAAIhB,CAAI,CAACkB,IAAL,CAAU,GAAV,GAAkBf,CAAO,CAACe,IAAR,CAAa,GAAb,CAAtB,CAAyC,CACrC,KAAKC,IAAL,CAAUC,MAAV,EACH,CACJ,CA5CD,CA6CH,CA1FgD,CAkG7CC,CAAO,CAAG,QAAVA,CAAAA,OAAU,EAAW,CACrB,MAAO3C,CAAAA,CAAC,CAACI,CAAS,CAACC,eAAX,CAAD,CAA6BuC,OAA7B,CAAqC,MAArC,CACV,CApGgD,CAsGjD,MAAmD,CAM/CtC,IAAI,CAAE,eAAW,CACbA,CAAI,EACP,CAR8C,CAgB/CqC,OAAO,CAAE,kBAAW,CAChB,MAAOA,CAAAA,CAAO,EACjB,CAlB8C,CAoBtD,CA3HK,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 * 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/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. * From f6263081939f617633d5441ba753c657ec9bb1ba Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 6 May 2020 18:33:19 +0800 Subject: [PATCH 06/13] MDL-68612 user: Unified filter deprecations - mustache template --- user/templates/unified_filter.mustache | 1 + 1 file changed, 1 insertion(+) 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. From 2396e3156f7f86e7c5b280124d22496fe5d9b7a5 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 6 May 2020 18:34:59 +0800 Subject: [PATCH 07/13] MDL-68612 user: Unified filter deprecations - related library functions --- lib/deprecatedlib.php | 264 ++++++++++++++++++++++++++++++++++++ user/lib.php | 249 ---------------------------------- user/tests/userlib_test.php | 188 ------------------------- 3 files changed, 264 insertions(+), 437 deletions(-) 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/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/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); - } } From 03cb6064eae89ba308046e446e1e9d883eb3d553 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 6 May 2020 18:39:22 +0800 Subject: [PATCH 08/13] MDL-68612 user: Unified filter deprecations - renderer and renderable --- user/classes/output/unified_filter.php | 7 +++++++ user/renderer.php | 3 +++ user/upgrade.txt | 12 ++++++++++++ 3 files changed, 22 insertions(+) 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/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/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: From 3d60881d5d7e18a549dcf39f19c122f28336e7d3 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Mon, 18 May 2020 16:07:05 +0800 Subject: [PATCH 09/13] MDL-68612 user: Participants filter row accessibility improvements More clearly defining each filter row and its ability to remove filter selections for screen readers. --- lang/en/user.php | 2 + .../local/participantsfilter/selectors.min.js | 2 +- .../participantsfilter/selectors.min.js.map | 2 +- user/amd/build/participantsfilter.min.js | 2 +- user/amd/build/participantsfilter.min.js.map | 2 +- .../src/local/participantsfilter/selectors.js | 1 + user/amd/src/participantsfilter.js | 46 ++++++++++++-- user/classes/output/participants_filter.php | 1 + .../autocomplete_selection_items.mustache | 5 +- .../participantsfilter/filterrow.mustache | 60 ++++++++++--------- 10 files changed, 86 insertions(+), 37 deletions(-) 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/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..0269fe3efc5 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 t="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 p(a)||o(a)||n(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,b){if(!a)return;if("string"==typeof a)return q(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 q(a,b)}function o(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function p(a){if(Array.isArray(a))return q(a)}function q(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","dataset","Filter","GenericFilter","filterTypeClass","typeField","disabled","getFilterObject","name","removeOrReplaceFilterRow","filterCount","replaceFilterRow","removeFilterRow","removeFilterObject","remove","updateTableFromFilter","getAvailableFilterLegends","filterLegends","index","innerText","rowNum","replaceNode","filterName","tearDown","removeAllFilters","filters","options","option","value","classList","add","addRowButton","actions","addRow","all","setAttribute","removeAttribute","filtermatch","join","DynamicTable","setFilters","getTableFromId","tableRegion","Object","values","map","filterValue","jointype","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","legendStrings","addEventListener","e","target","closest","preventDefault","applyFilters","resetFilters","filterverb"],"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 */\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 = async 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 // 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 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 /**\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"],"file":"participantsfilter.min.js"} \ No newline at end of file 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..087beb0c261 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); @@ -157,7 +159,7 @@ export const init = participantsRegionId => { * * @param {HTMLElement} filterRow */ - const removeFilterRow = filterRow => { + const removeFilterRow = async filterRow => { // Remove the filter object. removeFilterObject(filterRow.dataset.filterType); @@ -169,19 +171,28 @@ export const init = participantsRegionId => { // 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); @@ -302,6 +313,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)) { diff --git a/user/classes/output/participants_filter.php b/user/classes/output/participants_filter.php index 0ab5ed8f335..443c5a280e2 100644 --- a/user/classes/output/participants_filter.php +++ b/user/classes/output/participants_filter.php @@ -350,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/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}}
+
+
From c36f37dfb4bdf7520ba4a213b47343537c94d644 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Thu, 21 May 2020 22:27:46 +0800 Subject: [PATCH 10/13] MDL-68612 user: Fixed delete participants filter row execution order This ensures that when deleting the penultimate filter, the filterset join type is reset before the filters are re-applied. --- user/amd/build/participantsfilter.min.js | 2 +- user/amd/build/participantsfilter.min.js.map | 2 +- user/amd/src/participantsfilter.js | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/user/amd/build/participantsfilter.min.js b/user/amd/build/participantsfilter.min.js index 0269fe3efc5..8e225bf9a4d 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/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 t="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 p(a)||o(a)||n(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,b){if(!a)return;if("string"==typeof a)return q(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 q(a,b)}function o(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function p(a){if(Array.isArray(a))return q(a)}function q(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 * 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 */\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 = async 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 // 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 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 /**\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"],"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","dataset","Filter","GenericFilter","filterTypeClass","typeField","disabled","getFilterObject","name","removeOrReplaceFilterRow","filterCount","replaceFilterRow","removeFilterRow","removeFilterObject","remove","updateTableFromFilter","getAvailableFilterLegends","filterLegends","index","innerText","rowNum","replaceNode","filterName","tearDown","removeAllFilters","filters","options","option","value","classList","add","addRowButton","actions","addRow","all","setAttribute","removeAttribute","filtermatch","join","DynamicTable","setFilters","getTableFromId","tableRegion","Object","values","map","filterValue","jointype","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","legendStrings","addEventListener","e","target","closest","preventDefault","applyFilters","resetFilters","filterverb"],"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 */\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 = 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 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 /**\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"],"file":"participantsfilter.min.js"} \ No newline at end of file diff --git a/user/amd/src/participantsfilter.js b/user/amd/src/participantsfilter.js index 087beb0c261..2214b8f8bca 100644 --- a/user/amd/src/participantsfilter.js +++ b/user/amd/src/participantsfilter.js @@ -166,12 +166,12 @@ export const init = participantsRegionId => { // Remove the actual filter HTML. filterRow.remove(); - // Refresh the table. - updateTableFromFilter(); - // Update the list of available filter types. updateFiltersOptions(); + // Refresh the table. + updateTableFromFilter(); + // Update filter fieldset legends. const filterLegends = await getAvailableFilterLegends(); From 67bf30a549dc34cacf1e8f6fa727c80640d1481b Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Wed, 20 May 2020 14:01:05 +0800 Subject: [PATCH 11/13] MDL-68612 user: Created behat step to set user course lastaccess data --- lib/behat/classes/behat_core_generator.php | 103 ++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) 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); + } + } } From 5187e7c51530d0856ce69e47e90c3dd1d1d18121 Mon Sep 17 00:00:00 2001 From: Michael Hawkins Date: Mon, 18 May 2020 19:15:52 +0800 Subject: [PATCH 12/13] MDL-68612 core: Behat tests updated to support new participants filter Existing tests have been updated, rewritten and/or expanded to support the new participants filter. --- course/tests/behat/rename_roles.feature | 16 +- group/tests/behat/create_groups.feature | 13 +- group/tests/behat/group_description.feature | 41 +- user/tests/behat/filter_participants.feature | 507 +++++++++++++++--- .../behat/filter_participants_showall.feature | 41 +- .../behat/view_participants_groups.feature | 7 +- 6 files changed, 496 insertions(+), 129 deletions(-) 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/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" From 084c955e49027578dbb6c31bfc2073d0e74875bf Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Fri, 22 May 2020 18:59:41 +0800 Subject: [PATCH 13/13] MDL-68612 user: Set the initial filter on page load --- lib/table/amd/build/dynamic.min.js | 2 +- lib/table/amd/build/dynamic.min.js.map | 2 +- lib/table/amd/src/dynamic.js | 56 +++++++++++++ .../local/participantsfilter/filter.min.js | 2 +- .../participantsfilter/filter.min.js.map | 2 +- .../filtertypes/keyword.min.js | 2 +- .../filtertypes/keyword.min.js.map | 2 +- user/amd/build/participantsfilter.min.js | 2 +- user/amd/build/participantsfilter.min.js.map | 2 +- .../src/local/participantsfilter/filter.js | 24 +++++- .../participantsfilter/filtertypes/keyword.js | 4 - user/amd/src/participantsfilter.js | 82 +++++++++++++++++-- 12 files changed, 160 insertions(+), 22 deletions(-) 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":"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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 * 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/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/participantsfilter.min.js b/user/amd/build/participantsfilter.min.js index 8e225bf9a4d..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/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 t="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 p(a)||o(a)||n(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,b){if(!a)return;if("string"==typeof a)return q(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 q(a,b)}function o(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function p(a){if(Array.isArray(a))return q(a)}function q(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 * 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 */\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 = 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 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 /**\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"],"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":"8nBAwBA,OACA,OACA,OAEA,OACA,OACA,O,g0EAOO,GAAMA,CAAAA,CAAI,CAAG,SAAAC,CAAoB,CAAI,IAElCC,CAAAA,CAAS,CAAGC,QAAQ,CAACC,aAAT,YAA2BH,CAA3B,EAFsB,CAKlCI,CAAa,CAAG,CAClBC,QAAQ,CAAE,GAAIC,UAAJ,CAAiB,UAAjB,CAA6BL,CAA7B,CADQ,CALkB,CAclCM,CAAe,CAAG,iBAAMN,CAAAA,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBC,OAApB,CAA4BC,UAApD,CAAN,CAdgB,CAqBlCC,CAAY,CAAG,UAAM,CACvB,GAAMC,CAAAA,CAAM,CAAG,EAAIN,CAAe,GAAGO,gBAAlB,CAAmCN,UAAUO,MAAV,CAAiBC,MAApD,EAA4DC,MAA/E,CACA,MAAOC,WAAUC,gBAAV,CAA2B,8CAA3B,CAA2E,CAAC,UAAaN,CAAd,CAA3E,EACNO,IADM,CACD,WAAgB,IAAdC,CAAAA,CAAc,GAAdA,IAAc,CAARC,CAAQ,GAARA,EAAQ,CACZC,CAAe,CAAGL,UAAUM,kBAAV,CAA6BjB,CAAe,EAA5C,CAAgDc,CAAhD,CAAsDC,CAAtD,CADN,CAGlB,MAAOC,CAAAA,CACV,CALM,EAMNH,IANM,CAMD,SAAAK,CAAS,CAAI,CAKf,GAAMC,CAAAA,CAAQ,CAAGzB,CAAS,CAACE,aAAV,CAAwBK,UAAUmB,IAAV,CAAeD,QAAvC,CAAjB,CAEAD,CAAS,CAACG,OAAV,CAAkB,SAAAC,CAAW,CAAI,CAC7B,GAAMC,CAAAA,CAAe,CAAGD,CAAW,CAAC1B,aAAZ,CAA0BK,UAAUO,MAAV,CAAiBgB,MAAjB,CAAwBC,IAAlD,CAAxB,CAEA,GAAIF,CAAJ,CAAqB,CACjBA,CAAe,CAACG,SAAhB,CAA4BP,CAAQ,CAACO,SACxC,CACJ,CAND,EAQA,MAAOR,CAAAA,CACV,CAtBM,EAuBNL,IAvBM,CAuBD,SAAAK,CAAS,CAAI,CACfS,CAAoB,GAEpB,MAAOT,CAAAA,CACV,CA3BM,EA4BNU,KA5BM,CA4BAC,UAAaC,SA5Bb,CA6BV,CApDuC,CA4DlCC,CAAmB,CAAG,SAAAC,CAAU,CAAI,CACtC,GAAMC,CAAAA,CAAc,CAAGvC,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBC,OAApB,CAA4B+B,UAApD,CAAvB,CAEA,MAAOD,CAAAA,CAAc,CAACrC,aAAf,CAA6BK,UAAUmB,IAAV,CAAeI,MAAf,CAAsBW,MAAtB,CAA6BH,CAA7B,CAA7B,CACV,CAhEuC,CA0ElCI,CAAS,4CAAG,WAAMlB,CAAN,CAAiBc,CAAjB,CAA6BK,CAA7B,6FAEdnB,CAAS,CAACoB,OAAV,CAAkBN,UAAlB,CAA+BA,CAA/B,CAEMC,CAJQ,CAISF,CAAmB,CAACC,CAAD,CAJ5B,CAOVO,CAPU,CAODC,SAPC,KAQVP,CAAc,CAACK,OAAf,CAAuBG,eARb,+GASYR,CAAc,CAACK,OAAf,CAAuBG,eATnC,mMASYR,CAAc,CAACK,OAAf,CAAuBG,eATnC,sBASYR,CAAc,CAACK,OAAf,CAAuBG,eATnC,UASVF,CATU,eAWd1C,CAAa,CAACmC,CAAD,CAAb,CAA4B,GAAIO,CAAAA,CAAJ,CAAWP,CAAX,CAAuBtC,CAAvB,CAAkC2C,CAAlC,CAA5B,CAGMK,CAdQ,CAcIxB,CAAS,CAACtB,aAAV,CAAwBK,UAAUO,MAAV,CAAiBgB,MAAjB,CAAwBC,IAAhD,CAdJ,CAediB,CAAS,CAACC,KAAV,CAAkBX,CAAlB,CACAU,CAAS,CAACE,QAAV,CAAqB,UAArB,CAGAjB,CAAoB,GAnBN,yBAqBP9B,CAAa,CAACmC,CAAD,CArBN,2CAAH,uDA1EyB,CAwGlCa,CAAe,CAAG,SAAAC,CAAI,CAAI,CAC5B,MAAOjD,CAAAA,CAAa,CAACiD,CAAD,CACvB,CA1GuC,CAkHlCC,CAAwB,CAAG,SAAA7B,CAAS,CAAI,CAC1C,GAAM8B,CAAAA,CAAW,CAAGhD,CAAe,GAAGO,gBAAlB,CAAmCN,UAAUO,MAAV,CAAiBC,MAApD,EAA4DC,MAAhF,CAEA,GAAoB,CAAhB,GAAAsC,CAAJ,CAAuB,CACnBC,CAAgB,CAAC/B,CAAD,CACnB,CAFD,IAEO,CACHgC,CAAe,CAAChC,CAAD,CAClB,CACJ,CA1HuC,CAiIlCgC,CAAe,4CAAG,WAAMhC,CAAN,yFAEpBiC,CAAkB,CAACjC,CAAS,CAACoB,OAAV,CAAkBN,UAAnB,CAAlB,CAGAd,CAAS,CAACkC,MAAV,GAGAzB,CAAoB,GAGpB0B,CAAqB,GAXD,eAcQC,CAAAA,CAAyB,EAdjC,QAcdC,CAdc,QAgBpBvD,CAAe,GAAGO,gBAAlB,CAAmCN,UAAUO,MAAV,CAAiBC,MAApD,EAA4DY,OAA5D,CAAoE,SAACH,CAAD,CAAYsC,CAAZ,CAAsB,CACtFtC,CAAS,CAACtB,aAAV,CAAwB,QAAxB,EAAkC6D,SAAlC,CAA8CF,CAAa,CAACC,CAAD,CAC9D,CAFD,EAhBoB,wCAAH,uDAjImB,CA8JlCP,CAAgB,CAAG,SAAC/B,CAAD,CAA2B,IAAfwC,CAAAA,CAAe,wDAAN,CAAM,CAEhDP,CAAkB,CAACjC,CAAS,CAACoB,OAAV,CAAkBN,UAAnB,CAAlB,CAEA,MAAOrB,WAAUC,gBAAV,CAA2B,8CAA3B,CAA2E,CAAC,UAAa8C,CAAd,CAA3E,EACN7C,IADM,CACD,WAAgB,IAAdC,CAAAA,CAAc,GAAdA,IAAc,CAARC,CAAQ,GAARA,EAAQ,CACZC,CAAe,CAAGL,UAAUgD,WAAV,CAAsBzC,CAAtB,CAAiCJ,CAAjC,CAAuCC,CAAvC,CADN,CAGlB,MAAOC,CAAAA,CACV,CALM,EAMNH,IANM,CAMD,SAAAK,CAAS,CAAI,CAKf,GAAMC,CAAAA,CAAQ,CAAGzB,CAAS,CAACE,aAAV,CAAwBK,UAAUmB,IAAV,CAAeD,QAAvC,CAAjB,CAEAD,CAAS,CAACG,OAAV,CAAkB,SAAAC,CAAW,CAAI,CAC7B,GAAMC,CAAAA,CAAe,CAAGD,CAAW,CAAC1B,aAAZ,CAA0BK,UAAUO,MAAV,CAAiBgB,MAAjB,CAAwBC,IAAlD,CAAxB,CAEA,GAAIF,CAAJ,CAAqB,CACjBA,CAAe,CAACG,SAAhB,CAA4BP,CAAQ,CAACO,SACxC,CACJ,CAND,EAQA,MAAOR,CAAAA,CACV,CAtBM,EAuBNL,IAvBM,CAuBD,SAAAK,CAAS,CAAI,CACfS,CAAoB,GAEpB,MAAOT,CAAAA,CACV,CA3BM,EA4BNL,IA5BM,CA4BD,SAAAK,CAAS,CAAI,CAEfmC,CAAqB,GAErB,MAAOnC,CAAAA,CACV,CAjCM,EAkCNU,KAlCM,CAkCAC,UAAaC,SAlCb,CAmCV,CArMuC,CA4MlCqB,CAAkB,CAAG,SAAAS,CAAU,CAAI,CACrC,GAAIA,CAAJ,CAAgB,CACZ,GAAMpD,CAAAA,CAAM,CAAGqC,CAAe,CAACe,CAAD,CAA9B,CACA,GAAIpD,CAAJ,CAAY,CACRA,CAAM,CAACqD,QAAP,GAGA,MAAOhE,CAAAA,CAAa,CAAC+D,CAAD,CACvB,CACJ,CACJ,CAtNuC,CA6NlCE,CAAgB,CAAG,UAAM,CAC3B,GAAMC,CAAAA,CAAO,CAAG/D,CAAe,GAAGO,gBAAlB,CAAmCN,UAAUO,MAAV,CAAiBC,MAApD,CAAhB,CACAsD,CAAO,CAAC1C,OAAR,CAAgB,SAAAH,CAAS,QAAI6B,CAAAA,CAAwB,CAAC7B,CAAD,CAA5B,CAAzB,EAGA,MAAOmC,CAAAA,CAAqB,EAC/B,CAnOuC,CAwOlCW,CAAkB,CAAG,UAAM,CAC7B,GAAMD,CAAAA,CAAO,CAAG/D,CAAe,GAAGO,gBAAlB,CAAmCN,UAAUO,MAAV,CAAiBC,MAApD,CAAhB,CACAsD,CAAO,CAAC1C,OAAR,CAAgB,SAAAH,CAAS,CAAI,CACzB,GAAMc,CAAAA,CAAU,CAAGd,CAAS,CAACtB,aAAV,CAAwBK,UAAUO,MAAV,CAAiBgB,MAAjB,CAAwBC,IAAhD,CAAnB,CACA,GAAI,CAACO,CAAU,CAACW,KAAhB,CAAuB,CACnBI,CAAwB,CAAC7B,CAAD,CAC3B,CACJ,CALD,CAMH,CAhPuC,CAqPlCS,CAAoB,CAAG,UAAM,CAC/B,GAAMoC,CAAAA,CAAO,CAAG/D,CAAe,GAAGO,gBAAlB,CAAmCN,UAAUO,MAAV,CAAiBC,MAApD,CAAhB,CACAsD,CAAO,CAAC1C,OAAR,CAAgB,SAAAH,CAAS,CAAI,CACzB,GAAM+C,CAAAA,CAAO,CAAG/C,CAAS,CAACX,gBAAV,CAA2BN,UAAUO,MAAV,CAAiBgB,MAAjB,CAAwBC,IAAxB,CAA+B,SAA1D,CAAhB,CACAwC,CAAO,CAAC5C,OAAR,CAAgB,SAAA6C,CAAM,CAAI,CACtB,GAAIA,CAAM,CAACvB,KAAP,GAAiBzB,CAAS,CAACoB,OAAV,CAAkBN,UAAvC,CAAmD,CAC/CkC,CAAM,CAACC,SAAP,CAAiBf,MAAjB,CAAwB,QAAxB,EACAc,CAAM,CAACtB,QAAP,GACH,CAHD,IAGO,IAAI/C,CAAa,CAACqE,CAAM,CAACvB,KAAR,CAAjB,CAAiC,CACpCuB,CAAM,CAACC,SAAP,CAAiBC,GAAjB,CAAqB,QAArB,EACAF,CAAM,CAACtB,QAAP,GACH,CAHM,IAGA,CACHsB,CAAM,CAACC,SAAP,CAAiBf,MAAjB,CAAwB,QAAxB,EACAc,CAAM,CAACtB,QAAP,GACH,CACJ,CAXD,CAYH,CAdD,EAF+B,GAoBzByB,CAAAA,CAAY,CAAG3E,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBoE,OAApB,CAA4BC,MAApD,CApBU,CAqBzBtC,CAAc,CAAGvC,CAAS,CAACa,gBAAV,CAA2BN,UAAUmB,IAAV,CAAeI,MAAf,CAAsBgD,GAAjD,CArBQ,CAsB/B,GAAIvC,CAAc,CAACvB,MAAf,EAAyBqD,CAAO,CAACrD,MAArC,CAA6C,CACzC2D,CAAY,CAACI,YAAb,CAA0B,UAA1B,CAAsC,UAAtC,CACH,CAFD,IAEO,CACHJ,CAAY,CAACK,eAAb,CAA6B,UAA7B,CACH,CAED,GAAuB,CAAnB,GAAAX,CAAO,CAACrD,MAAZ,CAA0B,CACtBhB,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBC,OAApB,CAA4BwE,WAApD,EAAiER,SAAjE,CAA2EC,GAA3E,CAA+E,QAA/E,EACA1E,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBsB,MAApB,CAA2BoD,IAAnD,EAAyDjC,KAAzD,CAAiE,CACpE,CAHD,IAGO,CACHjD,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBC,OAApB,CAA4BwE,WAApD,EAAiER,SAAjE,CAA2Ef,MAA3E,CAAkF,QAAlF,CACH,CACJ,CAvRuC,CAgSlCyB,CAAmB,CAAG,SAAAC,CAAM,CAAI,CAClC,GAAMC,CAAAA,CAAY,CAAGC,MAAM,CAACC,OAAP,CAAeH,CAAM,CAACf,OAAtB,CAArB,CAEA,GAAI,CAACgB,CAAY,CAACrE,MAAlB,CAA0B,CAEtB,MACH,CAGDhB,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBsB,MAApB,CAA2BoD,IAAnD,EAAyDjC,KAAzD,CAAiEmC,CAAM,CAACI,QAAxE,CAEA,GAAMC,CAAAA,CAAc,CAAGJ,CAAY,CAACK,GAAb,CAAiB,WAA8B,cAA5BpD,CAA4B,MAAhBqD,CAAgB,MAClE,GAAmB,UAAf,GAAArD,CAAJ,CAA+B,CAE3B,MAAOsD,CAAAA,OAAO,CAACC,OAAR,EACV,CAED,GAAMC,CAAAA,CAAY,CAAGH,CAAU,CAACI,MAAhC,CAEA,GAAI,CAACD,CAAY,CAAC9E,MAAlB,CAA0B,CAGtB,MAAO4E,CAAAA,OAAO,CAACC,OAAR,EACV,CAED,MAAOlF,CAAAA,CAAY,GAAGQ,IAAf,CAAoB,yBAAEK,CAAF,YAAiBkB,CAAAA,CAAS,CAAClB,CAAD,CAAYc,CAAZ,CAAwBwD,CAAxB,CAA1B,CAApB,CACV,CAfsB,CAAvB,CAiBAF,OAAO,CAACd,GAAR,CAAYW,CAAZ,EAA4BtE,IAA5B,CAAiC,UAAM,CACnC,MAAOmD,CAAAA,CAAkB,EAC5B,CAFD,EAGCnD,IAHD,CAGMc,CAHN,EAICd,IAJD,CAIMwC,CAJN,EAKCzB,KALD,EAMH,CAlUuC,CAyUlCyB,CAAqB,CAAG,UAAM,CAChC,MAAOqC,CAAAA,CAAY,CAACC,UAAb,CACHD,CAAY,CAACE,cAAb,CAA4BlG,CAAS,CAAC4C,OAAV,CAAkBuD,WAA9C,CADG,CAEH,CACI9B,OAAO,CAAEiB,MAAM,CAACS,MAAP,CAAc5F,CAAd,EAA6BuF,GAA7B,CAAiC,SAAA5E,CAAM,QAAIA,CAAAA,CAAM,CAACsF,WAAX,CAAvC,CADb,CAEIZ,QAAQ,CAAExF,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBsB,MAApB,CAA2BoD,IAAnD,EAAyDjC,KAFvE,CAFG,CAOV,CAjVuC,CAwVlCW,CAAyB,4CAAG,wGACxByC,CADwB,CACXpG,QAAQ,CAACC,aAAT,CAAuBK,UAAUmB,IAAV,CAAe4E,cAAtC,EAAsDtF,MAAtD,CAA+D,CADpD,CAE1BuF,CAF0B,CAEf,EAFe,CAI9B,EAAIC,KAAK,CAACH,CAAD,CAAT,EAAuB1E,OAAvB,CAA+B,SAAC8E,CAAD,CAAIC,CAAJ,CAAiB,CAC5CH,CAAQ,CAACI,IAAT,CAAc,CACV,IAAO,iBADG,CAEV,UAAa,WAFH,CAIV,MAASD,CAAQ,CAAG,CAJV,CAAd,CAMH,CAPD,EAJ8B,eAaF,kBAAWH,CAAX,EAC3BpF,IAD2B,CACtB,SAAAyF,CAAc,CAAI,CACpB,MAAOA,CAAAA,CACV,CAH2B,EAI3B1E,KAJ2B,CAIrBC,UAAaC,SAJQ,CAbE,QAaxByE,CAbwB,iCAmBvBA,CAnBuB,0CAAH,uDAxVS,CA+WxC7G,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBO,MAA5C,EAAoD+F,gBAApD,CAAqE,OAArE,CAA8E,SAAAC,CAAC,CAAI,CAC/E,GAAIA,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB1G,UAAUC,SAAV,CAAoBoE,OAApB,CAA4BC,MAA7C,CAAJ,CAA0D,CACtDkC,CAAC,CAACG,cAAF,GAEAvG,CAAY,EACf,CAED,GAAIoG,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB1G,UAAUC,SAAV,CAAoBoE,OAApB,CAA4BuC,YAA7C,CAAJ,CAAgE,CAC5DJ,CAAC,CAACG,cAAF,GAEAvD,CAAqB,EACxB,CAED,GAAIoD,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB1G,UAAUC,SAAV,CAAoBoE,OAApB,CAA4BwC,YAA7C,CAAJ,CAAgE,CAC5DL,CAAC,CAACG,cAAF,GAEA9C,CAAgB,EACnB,CACJ,CAlBD,EAqBApE,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBC,OAApB,CAA4BC,UAApD,EAAgEoG,gBAAhE,CAAiF,OAAjF,CAA0F,SAAAC,CAAC,CAAI,CAC3F,GAAIA,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB1G,UAAUO,MAAV,CAAiB8D,OAAjB,CAAyBlB,MAA1C,CAAJ,CAAuD,CACnDqD,CAAC,CAACG,cAAF,GAEA7D,CAAwB,CAAC0D,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB1G,UAAUO,MAAV,CAAiBC,MAAlC,CAAD,CAC3B,CACJ,CAND,EASAf,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBC,OAApB,CAA4BC,UAApD,EAAgEoG,gBAAhE,CAAiF,QAAjF,CAA2F,SAAAC,CAAC,CAAI,CAC5F,GAAM/D,CAAAA,CAAS,CAAG+D,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB1G,UAAUO,MAAV,CAAiBgB,MAAjB,CAAwBC,IAAzC,CAAlB,CACA,GAAIiB,CAAS,EAAIA,CAAS,CAACC,KAA3B,CAAkC,CAC9B,GAAMnC,CAAAA,CAAM,CAAGiG,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiB1G,UAAUO,MAAV,CAAiBC,MAAlC,CAAf,CAEA2B,CAAS,CAAC5B,CAAD,CAASkC,CAAS,CAACC,KAAnB,CACZ,CACJ,CAPD,EASAjD,CAAS,CAACE,aAAV,CAAwBK,UAAUC,SAAV,CAAoBsB,MAApB,CAA2BoD,IAAnD,EAAyD4B,gBAAzD,CAA0E,QAA1E,CAAoF,SAAAC,CAAC,CAAI,CACrF/G,CAAS,CAAC4C,OAAV,CAAkByE,UAAlB,CAA+BN,CAAC,CAACC,MAAF,CAAS/D,KAC3C,CAFD,EAtZwC,GA0ZlCqE,CAAAA,CAAS,CAAGtB,CAAY,CAACE,cAAb,CAA4BlG,CAAS,CAAC4C,OAAV,CAAkBuD,WAA9C,CA1ZsB,CA2ZlCoB,CAAc,CAAGvB,CAAY,CAACwB,UAAb,CAAwBF,CAAxB,CA3ZiB,CA4ZxC,GAAIC,CAAJ,CAAoB,CAEhBpC,CAAmB,CAACoC,CAAD,CACtB,CACJ,CAhaM,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 * 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/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/participantsfilter.js b/user/amd/src/participantsfilter.js index 2214b8f8bca..718e5ab79dd 100644 --- a/user/amd/src/participantsfilter.js +++ b/user/amd/src/participantsfilter.js @@ -106,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; @@ -118,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]; }; /** @@ -248,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); + } + }); }; /** @@ -298,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. * @@ -383,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); + } };