diff --git a/availability/classes/info.php b/availability/classes/info.php index 1f31f1b30cd..fc00ad8d5cc 100644 --- a/availability/classes/info.php +++ b/availability/classes/info.php @@ -590,6 +590,36 @@ abstract class info { return $result; } + /** + * Obtains SQL that returns a list of enrolled users that has been filtered + * by the conditions applied in the availability API, similar to calling + * get_enrolled_users and then filter_user_list. As for filter_user_list, + * this ONLY filteres out users with conditions that are marked as applying + * to user lists. For example, group conditions are included but date + * conditions are not included. + * + * The returned SQL is a query that returns a list of user IDs. It does not + * include brackets, so you neeed to add these to make it into a subquery. + * You would normally use it in an SQL phrase like "WHERE u.id IN ($sql)". + * + * The function returns an array with '' and an empty array, if there are + * no restrictions on users from these conditions. + * + * The SQL will be complex and may be slow. It uses named parameters (sorry, + * I know they are annoying, but it was unavoidable here). + * + * @param bool $onlyactive True if including only active enrolments + * @return array Array of SQL code (may be empty) and params + */ + public function get_user_list_sql($onlyactive) { + global $CFG; + if (is_null($this->availability) || !$CFG->enableavailability) { + return array('', array()); + } + $tree = $this->get_availability_tree(); + return $tree->get_user_list_sql(false, $this, $onlyactive); + } + /** * Formats the $cm->availableinfo string for display. This includes * filling in the names of any course-modules that might be mentioned. diff --git a/availability/classes/info_module.php b/availability/classes/info_module.php index 1bff351737b..952aa7b8474 100644 --- a/availability/classes/info_module.php +++ b/availability/classes/info_module.php @@ -109,6 +109,30 @@ class info_module extends info { return parent::filter_user_list($filtered); } + public function get_user_list_sql($onlyactive = true) { + global $CFG, $DB; + if (!$CFG->enableavailability) { + return array('', array()); + } + + // Get query for section (if any) and module. + $section = $this->cm->get_modinfo()->get_section_info( + $this->cm->sectionnum, MUST_EXIST); + $sectioninfo = new info_section($section); + $sectionresult = $sectioninfo->get_user_list_sql($onlyactive); + $moduleresult = parent::get_user_list_sql($onlyactive); + + if (!$sectionresult[0]) { + return $moduleresult; + } + if (!$moduleresult[0]) { + return $sectionresult; + } + + return array('(' . $sectionresult[0] . ') INTERSECT (' . $moduleresult[0] . ')', + array_merge($sectionresult[1], $moduleresult[1])); + } + /** * Checks if an activity is visible to the given user. * diff --git a/availability/classes/tree.php b/availability/classes/tree.php index 823c556f117..49cfc80c882 100644 --- a/availability/classes/tree.php +++ b/availability/classes/tree.php @@ -350,6 +350,52 @@ class tree extends tree_node { } } + public function get_user_list_sql($not, info $info, $onlyactive) { + // Get logic flags from operator. + list($innernot, $andoperator) = $this->get_logic_flags($not); + + // Loop through all valid children, getting SQL for each. + $childresults = array(); + foreach ($this->children as $index => $child) { + if (!$child->is_applied_to_user_lists()) { + continue; + } + $childresult = $child->get_user_list_sql($innernot, $info, $onlyactive); + if ($childresult[0]) { + $childresults[] = $childresult; + } else if (!$andoperator) { + // When using OR operator, if any part doesn't have restrictions, + // then nor does the whole thing. + return array('', array()); + } + } + + // If there are no conditions, return null. + if (!$childresults) { + return array('', array()); + } + // If there is a single condition, return it. + if (count($childresults) === 1) { + return $childresults[0]; + } + + // Combine results using INTERSECT or UNION. + $outsql = null; + $outparams = null; + foreach ($childresults as $childresult) { + if (!$outsql) { + $outsql = '(' . $childresult[0] . ')'; + $outparams = $childresult[1]; + } else { + $outsql .= $andoperator ? ' INTERSECT (' : ' UNION ('; + $outsql .= $childresult[0]; + $outsql .= ')'; + $outparams = array_merge($outparams, $childresult[1]); + } + } + return array($outsql, $outparams); + } + public function is_available_for_all($not = false) { // Get logic flags. list($innernot, $andoperator) = $this->get_logic_flags($not); diff --git a/availability/classes/tree_node.php b/availability/classes/tree_node.php index 22dc48dbffd..54b42cd5676 100644 --- a/availability/classes/tree_node.php +++ b/availability/classes/tree_node.php @@ -159,4 +159,60 @@ abstract class tree_node { throw new \coding_exception('Not implemented (do not call unless '. 'is_applied_to_user_lists is true)'); } + + /** + * Obtains SQL that returns a list of enrolled users that has been filtered + * by the conditions applied in the availability API, similar to calling + * get_enrolled_users and then filter_user_list. As for filter_user_list, + * this ONLY filteres out users with conditions that are marked as applying + * to user lists. For example, group conditions are included but date + * conditions are not included. + * + * The returned SQL is a query that returns a list of user IDs. It does not + * include brackets, so you neeed to add these to make it into a subquery. + * You would normally use it in an SQL phrase like "WHERE u.id IN ($sql)". + * + * The SQL will be complex and may be slow. It uses named parameters (sorry, + * I know they are annoying, but it was unavoidable here). + * + * If there are no conditions, the returned result is array('', array()). + * + * @param bool $not True if this condition is applying in negative mode + * @param \core_availability\info $info Item we're checking + * @param bool $onlyactive If true, only returns active enrolments + * @return array Array with two elements: SQL subquery and parameters array + * @throws \coding_exception If called on a condition that doesn't apply to user lists + */ + public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { + if (!$this->is_applied_to_user_lists()) { + throw new \coding_exception('Not implemented (do not call unless '. + 'is_applied_to_user_lists is true)'); + } + + // Handle situation where plugin does not implement this, by returning a + // default (all enrolled users). This ensures compatibility with 2.7 + // plugins and behaviour. Plugins should be updated to support this + // new function (if they return true to is_applied_to_user_lists). + debugging('Availability plugins that return true to is_applied_to_user_lists ' . + 'should also now implement get_user_list_sql: ' . get_class($this), + DEBUG_DEVELOPER); + return get_enrolled_sql($info->get_context(), '', 0, $onlyactive); + } + + /** + * Utility function for generating SQL parameters (because we can't use ? + * parameters because get_enrolled_sql has infected us with horrible named + * parameters). + * + * @param array $params Params array (value will be added to this array) + * @param string|int $value Value + * @return SQL code for the parameter, e.g. ':pr1234' + */ + protected static function unique_sql_parameter(array &$params, $value) { + static $count = 1; + $unique = 'usp' . $count; + $params[$unique] = $value; + $count++; + return ':' . $unique; + } } diff --git a/availability/condition/group/classes/condition.php b/availability/condition/group/classes/condition.php index bd4a1451f7e..39050603269 100644 --- a/availability/condition/group/classes/condition.php +++ b/availability/condition/group/classes/condition.php @@ -225,4 +225,40 @@ class condition extends \core_availability\condition { public static function get_json($groupid = 0) { return (object)array('type' => 'group', 'id' => (int)$groupid); } + + public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { + global $DB; + + // Get enrolled users with access all groups. These always are allowed. + list($aagsql, $aagparams) = get_enrolled_sql( + $info->get_context(), 'moodle/site:accessallgroups', 0, $onlyactive); + + // Get all enrolled users. + list ($enrolsql, $enrolparams) = + get_enrolled_sql($info->get_context(), '', 0, $onlyactive); + + // Condition for specified or any group. + $matchparams = array(); + if ($this->groupid) { + $matchsql = "SELECT 1 + FROM {groups_members} gm + WHERE gm.userid = userids.id + AND gm.groupid = " . + self::unique_sql_parameter($matchparams, $this->groupid); + } else { + $matchsql = "SELECT 1 + FROM {groups_members} gm + JOIN {groups} g ON g.id = gm.groupid + WHERE gm.userid = userids.id + AND g.courseid = " . + self::unique_sql_parameter($matchparams, $info->get_course()->id); + } + + // Overall query combines all this. + $condition = $not ? 'NOT' : ''; + $sql = "SELECT userids.id + FROM ($enrolsql) userids + WHERE (userids.id IN ($aagsql)) OR $condition EXISTS ($matchsql)"; + return array($sql, array_merge($enrolparams, $aagparams, $matchparams)); + } } diff --git a/availability/condition/group/tests/condition_test.php b/availability/condition/group/tests/condition_test.php index 061840ef36b..da242943276 100644 --- a/availability/condition/group/tests/condition_test.php +++ b/availability/condition/group/tests/condition_test.php @@ -164,7 +164,8 @@ class availability_group_condition_testcase extends advanced_testcase { } /** - * Tests the filter_users (bulk checking) function. + * Tests the filter_users (bulk checking) function. Also tests the SQL + * variant get_user_list_sql. */ public function test_filter_users() { global $DB; @@ -205,21 +206,48 @@ class availability_group_condition_testcase extends advanced_testcase { $cond = new condition((object)array()); $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker)); ksort($result); - $this->assertEquals(array($teacher->id, $students[1]->id, $students[2]->id), $result); + $expected = array($teacher->id, $students[1]->id, $students[2]->id); + $this->assertEquals($expected, $result); + + // Test it with get_user_list_sql. + list ($sql, $params) = $cond->get_user_list_sql(false, $info, true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); // Test NOT version (note that teacher can still access because AAG works // both ways). $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker)); ksort($result); - $this->assertEquals(array($teacher->id, $students[0]->id), $result); + $expected = array($teacher->id, $students[0]->id); + $this->assertEquals($expected, $result); + + // Test with get_user_list_sql. + list ($sql, $params) = $cond->get_user_list_sql(true, $info, true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); // Test specific group. $cond = new condition((object)array('id' => (int)$group1->id)); $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker)); ksort($result); - $this->assertEquals(array($teacher->id, $students[1]->id), $result); + $expected = array($teacher->id, $students[1]->id); + $this->assertEquals($expected, $result); + + list ($sql, $params) = $cond->get_user_list_sql(false, $info, true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); + $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker)); ksort($result); - $this->assertEquals(array($teacher->id, $students[0]->id, $students[2]->id), $result); + $expected = array($teacher->id, $students[0]->id, $students[2]->id); + $this->assertEquals($expected, $result); + + list ($sql, $params) = $cond->get_user_list_sql(true, $info, true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); } } diff --git a/availability/condition/grouping/classes/condition.php b/availability/condition/grouping/classes/condition.php index 9e37772c74f..a538704b38f 100644 --- a/availability/condition/grouping/classes/condition.php +++ b/availability/condition/grouping/classes/condition.php @@ -258,4 +258,32 @@ class condition extends \core_availability\condition { } return $result; } + + public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { + global $DB; + + // Get enrolled users with access all groups. These always are allowed. + list($aagsql, $aagparams) = get_enrolled_sql( + $info->get_context(), 'moodle/site:accessallgroups', 0, $onlyactive); + + // Get all enrolled users. + list ($enrolsql, $enrolparams) = + get_enrolled_sql($info->get_context(), '', 0, $onlyactive); + + // Condition for specified or any group. + $matchparams = array(); + $matchsql = "SELECT 1 + FROM {groups_members} gm + JOIN {groupings_groups} gg ON gg.groupid = gm.groupid + WHERE gm.userid = userids.id + AND gg.groupingid = " . + self::unique_sql_parameter($matchparams, $this->get_grouping_id($info)); + + // Overall query combines all this. + $condition = $not ? 'NOT' : ''; + $sql = "SELECT userids.id + FROM ($enrolsql) userids + WHERE (userids.id IN ($aagsql)) OR $condition EXISTS ($matchsql)"; + return array($sql, array_merge($enrolparams, $aagparams, $matchparams)); + } } diff --git a/availability/condition/grouping/tests/condition_test.php b/availability/condition/grouping/tests/condition_test.php index d9d48b49c60..4ffd76f4420 100644 --- a/availability/condition/grouping/tests/condition_test.php +++ b/availability/condition/grouping/tests/condition_test.php @@ -206,7 +206,8 @@ class availability_grouping_condition_testcase extends advanced_testcase { } /** - * Tests the filter_users (bulk checking) function. + * Tests the filter_users (bulk checking) function. Also tests the SQL + * variant get_user_list_sql. */ public function test_filter_users() { global $DB, $CFG; @@ -258,16 +259,39 @@ class availability_grouping_condition_testcase extends advanced_testcase { $cond = new condition((object)array('id' => (int)$grouping1->id)); $result = array_keys($cond->filter_user_list($allusers, false, $info, $checker)); ksort($result); - $this->assertEquals(array($teacher->id, $students[1]->id), $result); + $expected = array($teacher->id, $students[1]->id); + $this->assertEquals($expected, $result); + + // Test it with get_user_list_sql. + list ($sql, $params) = $cond->get_user_list_sql(false, $info, true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); + + // NOT test. $result = array_keys($cond->filter_user_list($allusers, true, $info, $checker)); ksort($result); - $this->assertEquals(array($teacher->id, $students[0]->id, $students[2]->id), $result); + $expected = array($teacher->id, $students[0]->id, $students[2]->id); + $this->assertEquals($expected, $result); + + // NOT with get_user_list_sql. + list ($sql, $params) = $cond->get_user_list_sql(true, $info, true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); // Test course-module grouping. $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($page->cmid); $info = new \core_availability\info_module($cm); $result = array_keys($info->filter_user_list($allusers, $course)); - $this->assertEquals(array($teacher->id, $students[2]->id), $result); + $expected = array($teacher->id, $students[2]->id); + $this->assertEquals($expected, $result); + + // With get_user_list_sql. + list ($sql, $params) = $info->get_user_list_sql(true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); } } diff --git a/availability/condition/profile/classes/condition.php b/availability/condition/profile/classes/condition.php index d5ca5ed286d..0007e8751fc 100644 --- a/availability/condition/profile/classes/condition.php +++ b/availability/condition/profile/classes/condition.php @@ -461,4 +461,104 @@ class condition extends \core_availability\condition { } return $result; } + + /** + * Gets SQL to match a field against this condition. The second copy of the + * field is in case you're using variables for the field so that it needs + * to be two different ones. + * + * @param string $field Field name + * @param string $field2 Second copy of field name (default same). + * @return array Array of SQL and parameters + */ + private function get_condition_sql($field, $field2 = null) { + global $DB; + if (is_null($field2)) { + $field2 = $field; + } + + $params = array(); + switch($this->operator) { + case self::OP_CONTAINS: + $sql = $DB->sql_like($field, self::unique_sql_parameter( + $params, '%' . $this->value . '%')); + break; + case self::OP_DOES_NOT_CONTAIN: + if (empty($this->value)) { + // The 'does not contain nothing' expression matches everyone. + return null; + } + $sql = $DB->sql_like($field, self::unique_sql_parameter( + $params, '%' . $this->value . '%'), true, true, true); + break; + case self::OP_IS_EQUAL_TO: + $sql = $field . ' = ' . self::unique_sql_parameter( + $params, $this->value); + break; + case self::OP_STARTS_WITH: + $sql = $DB->sql_like($field, self::unique_sql_parameter( + $params, $this->value . '%')); + break; + case self::OP_ENDS_WITH: + $sql = $DB->sql_like($field, self::unique_sql_parameter( + $params, '%' . $this->value)); + break; + case self::OP_IS_EMPTY: + // Mimic PHP empty() behaviour for strings, '0' or ''. + $sql = '(' . $field . " IN ('0', '') OR $field2 IS NULL)"; + break; + case self::OP_IS_NOT_EMPTY: + $sql = '(' . $field . " NOT IN ('0', '') AND $field2 IS NOT NULL)"; + break; + } + return array($sql, $params); + } + + public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { + global $DB; + + // Build suitable SQL depending on custom or standard field. + if ($this->customfield) { + $customfields = self::get_custom_profile_fields(); + if (!array_key_exists($this->customfield, $customfields)) { + // If the field isn't found, nobody matches. + return array('SELECT id FROM {user} WHERE 0 = 1', array()); + } + $customfield = $customfields[$this->customfield]; + + $mainparams = array(); + $tablesql = "LEFT JOIN {user_info_data} uid ON uid.fieldid = " . + self::unique_sql_parameter($mainparams, $customfield->id) . + " AND uid.userid = userids.id"; + list ($condition, $conditionparams) = $this->get_condition_sql('uid.data'); + $mainparams = array_merge($mainparams, $conditionparams); + + // If default is true, then allow that too. + if ($this->is_field_condition_met( + $this->operator, $customfield->defaultdata, $this->value)) { + $where = "((uid.data IS NOT NULL AND $condition) OR (uid.data IS NULL))"; + } else { + $where = "(uid.data IS NOT NULL AND $condition)"; + } + } else { + $tablesql = "JOIN {user} u ON u.id = userids.id"; + list ($where, $mainparams) = $this->get_condition_sql( + 'u.' . $this->standardfield); + } + + // Handle NOT. + if ($not) { + $where = 'NOT (' . $where . ')'; + } + + // Get enrolled user SQL and combine with this query. + list ($enrolsql, $enrolparams) = + get_enrolled_sql($info->get_context(), '', 0, $onlyactive); + $sql = "SELECT userids.id + FROM ($enrolsql) userids + $tablesql + WHERE $where"; + $params = array_merge($enrolparams, $mainparams); + return array($sql, $params); + } } diff --git a/availability/condition/profile/tests/condition_test.php b/availability/condition/profile/tests/condition_test.php index 8b2619adc74..df9e0856485 100644 --- a/availability/condition/profile/tests/condition_test.php +++ b/availability/condition/profile/tests/condition_test.php @@ -40,6 +40,11 @@ class availability_profile_condition_testcase extends advanced_testcase { /** @var array Array of user IDs for whome we already set the profile field */ protected $setusers = array(); + /** @var condition Current condition */ + private $cond; + /** @var \core_availability\info Current info */ + private $info; + public function setUp() { global $DB, $CFG; @@ -345,21 +350,25 @@ class availability_profile_condition_testcase extends advanced_testcase { * * @param int $userid User id * @param string|null $value Field value or null to clear + * @param int $fieldid Field id or 0 to use default one */ - protected function set_field($userid, $value) { + protected function set_field($userid, $value, $fieldid = 0) { global $DB, $USER; + if (!$fieldid) { + $fieldid = $this->profilefield->id; + } $alreadyset = array_key_exists($userid, $this->setusers); if (is_null($value)) { $DB->delete_records('user_info_data', - array('userid' => $userid, 'fieldid' => $this->profilefield->id)); + array('userid' => $userid, 'fieldid' => $fieldid)); unset($this->setusers[$userid]); } else if ($alreadyset) { $DB->set_field('user_info_data', 'data', $value, - array('userid' => $userid, 'fieldid' => $this->profilefield->id)); + array('userid' => $userid, 'fieldid' => $fieldid)); } else { $DB->insert_record('user_info_data', array('userid' => $userid, - 'fieldid' => $this->profilefield->id, 'data' => $value)); + 'fieldid' => $fieldid, 'data' => $value)); $this->setusers[$userid] = true; } } @@ -441,4 +450,101 @@ class availability_profile_condition_testcase extends advanced_testcase { ksort($result); $this->assertEquals(array($student3->id), $result); } + + /** + * Tests getting user list SQL. This is a different test from the above because + * there is some additional code in this function so more variants need testing. + */ + public function test_get_user_list_sql() { + global $DB, $CFG; + $this->resetAfterTest(); + $CFG->enableavailability = true; + + // Erase static cache before test. + condition::wipe_static_cache(); + + // For testing, make another info field with default value. + $DB->insert_record('user_info_field', array( + 'shortname' => 'tonguestyle', 'name' => 'Tongue style', 'categoryid' => 1, + 'datatype' => 'text', 'defaultdata' => 'Slimy')); + $otherprofilefield = $DB->get_record('user_info_field', + array('shortname' => 'tonguestyle')); + + // Make a test course and some users. + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $student1 = $generator->create_user(array('institution' => 'Unseen University')); + $student2 = $generator->create_user(array('institution' => 'Hogwarts')); + $student3 = $generator->create_user(array('institution' => 'Unseen University')); + $student4 = $generator->create_user(array('institution' => '0')); + $allusers = array(); + foreach (array($student1, $student2, $student3, $student4) as $student) { + $generator->enrol_user($student->id, $course->id); + $allusers[$student->id] = $student; + } + $this->set_field($student1->id, 'poison dart'); + $this->set_field($student2->id, 'poison dart'); + $this->set_field($student3->id, 'Rough', $otherprofilefield->id); + $this->info = new \core_availability\mock_info($course); + + // Test standard field condition (positive). + $this->cond = new condition((object)array('sf' => 'institution', + 'op' => condition::OP_CONTAINS, 'v' => 'Univ')); + $this->assert_user_list_sql_results(array($student1->id, $student3->id)); + + // Now try it negative. + $this->assert_user_list_sql_results(array($student2->id, $student4->id), true); + + // Try all the other condition types. + $this->cond = new condition((object)array('sf' => 'institution', + 'op' => condition::OP_DOES_NOT_CONTAIN, 'v' => 's')); + $this->assert_user_list_sql_results(array($student4->id)); + $this->cond = new condition((object)array('sf' => 'institution', + 'op' => condition::OP_IS_EQUAL_TO, 'v' => 'Hogwarts')); + $this->assert_user_list_sql_results(array($student2->id)); + $this->cond = new condition((object)array('sf' => 'institution', + 'op' => condition::OP_STARTS_WITH, 'v' => 'U')); + $this->assert_user_list_sql_results(array($student1->id, $student3->id)); + $this->cond = new condition((object)array('sf' => 'institution', + 'op' => condition::OP_ENDS_WITH, 'v' => 'rts')); + $this->assert_user_list_sql_results(array($student2->id)); + $this->cond = new condition((object)array('sf' => 'institution', + 'op' => condition::OP_IS_EMPTY)); + $this->assert_user_list_sql_results(array($student4->id)); + $this->cond = new condition((object)array('sf' => 'institution', + 'op' => condition::OP_IS_NOT_EMPTY)); + $this->assert_user_list_sql_results(array($student1->id, $student2->id, $student3->id)); + + // Try with a custom field condition that doesn't have a default. + $this->cond = new condition((object)array('cf' => 'frogtype', + 'op' => condition::OP_CONTAINS, 'v' => 'poison')); + $this->assert_user_list_sql_results(array($student1->id, $student2->id)); + $this->cond = new condition((object)array('cf' => 'frogtype', + 'op' => condition::OP_IS_EMPTY)); + $this->assert_user_list_sql_results(array($student3->id, $student4->id)); + + // Try with one that does have a default. + $this->cond = new condition((object)array('cf' => 'tonguestyle', + 'op' => condition::OP_STARTS_WITH, 'v' => 'Sli')); + $this->assert_user_list_sql_results(array($student1->id, $student2->id, + $student4->id)); + $this->cond = new condition((object)array('cf' => 'tonguestyle', + 'op' => condition::OP_IS_EMPTY)); + $this->assert_user_list_sql_results(array()); + } + + /** + * Convenience function. Gets the user list SQL and runs it, then checks + * results. + * + * @param array $expected Array of expected user ids + * @param bool $not True if using NOT condition + */ + private function assert_user_list_sql_results(array $expected, $not = false) { + global $DB; + list ($sql, $params) = $this->cond->get_user_list_sql($not, $this->info, true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); + } } diff --git a/availability/tests/fixtures/mock_condition.php b/availability/tests/fixtures/mock_condition.php index 3294a5bdc3b..5575b37f9db 100644 --- a/availability/tests/fixtures/mock_condition.php +++ b/availability/tests/fixtures/mock_condition.php @@ -122,4 +122,18 @@ class condition extends \core_availability\condition { } return $result; } + + public function get_user_list_sql($not, \core_availability\info $info, $onlyactive) { + global $DB; + // The data for this condition is not really stored in the database, + // so we return SQL that contains the hard-coded user list. + list ($enrolsql, $enrolparams) = + get_enrolled_sql($info->get_context(), '', 0, $onlyactive); + $condition = $not ? 'NOT' : ''; + list ($matchsql, $matchparams) = $DB->get_in_or_equal($this->filter, SQL_PARAMS_NAMED); + $sql = "SELECT userids.id + FROM ($enrolsql) userids + WHERE $condition (userids.id $matchsql)"; + return array($sql, array_merge($enrolparams, $matchparams)); + } } diff --git a/availability/tests/info_test.php b/availability/tests/info_test.php index 0ee25ee32fb..d198df834a0 100644 --- a/availability/tests/info_test.php +++ b/availability/tests/info_test.php @@ -391,9 +391,9 @@ class info_testcase extends \advanced_testcase { } /** - * Tests the filter_users() function. + * Tests the filter_user_list() and get_user_list_sql() functions. */ - public function test_filter_users() { + public function test_filter_user_list() { global $CFG, $DB; require_once($CFG->dirroot . '/course/lib.php'); $this->resetAfterTest(); @@ -408,6 +408,10 @@ class info_testcase extends \advanced_testcase { $u2 = $generator->create_user(); $u3 = $generator->create_user(); $allusers = array($u1->id => $u1, $u2->id => $u2, $u3->id => $u3); + $generator->enrol_user($u1->id, $course->id); + $generator->enrol_user($u2->id, $course->id); + $generator->enrol_user($u3->id, $course->id); + $pagegen = $generator->get_plugin_generator('mod_page'); $page = $pagegen->create_instance(array('course' => $course)); $page2 = $pagegen->create_instance(array('course' => $course, @@ -425,6 +429,7 @@ class info_testcase extends \advanced_testcase { $info = new info_module($modinfo->get_cm($page->cmid)); $this->assertEquals(array($u1->id, $u2->id, $u3->id), array_keys($info->filter_user_list($allusers))); + $this->assertEquals(array('', array()), $info->get_user_list_sql(true)); // Set an availability restriction in database for section 1. // For the section we set it so it doesn't support filters; for the @@ -440,28 +445,43 @@ class info_testcase extends \advanced_testcase { // Now it should work (for the module). $info = new info_module($modinfo->get_cm($page->cmid)); - $this->assertEquals(array($u3->id), + $expected = array($u3->id); + $this->assertEquals($expected, array_keys($info->filter_user_list($allusers))); + list ($sql, $params) = $info->get_user_list_sql(); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); $info = new info_section($modinfo->get_section_info(1)); $this->assertEquals(array($u1->id, $u2->id, $u3->id), array_keys($info->filter_user_list($allusers))); + $this->assertEquals(array('', array()), $info->get_user_list_sql(true)); // With availability disabled, module returns full list too. $CFG->enableavailability = false; $info = new info_module($modinfo->get_cm($page->cmid)); $this->assertEquals(array($u1->id, $u2->id, $u3->id), array_keys($info->filter_user_list($allusers))); + $this->assertEquals(array('', array()), $info->get_user_list_sql(true)); // Check the other section... $CFG->enableavailability = true; $info = new info_section($modinfo->get_section_info(2)); - $this->assertEquals(array($u1->id, $u2->id), - array_keys($info->filter_user_list($allusers))); + $expected = array($u1->id, $u2->id); + $this->assertEquals($expected, array_keys($info->filter_user_list($allusers))); + list ($sql, $params) = $info->get_user_list_sql(true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); // And the module in that section - which has combined the section and // module restrictions. $info = new info_module($modinfo->get_cm($page2->cmid)); - $this->assertEquals(array($u2->id), - array_keys($info->filter_user_list($allusers))); + $expected = array($u2->id); + $this->assertEquals($expected, array_keys($info->filter_user_list($allusers))); + list ($sql, $params) = $info->get_user_list_sql(true); + $result = $DB->get_fieldset_sql($sql, $params); + sort($result); + $this->assertEquals($expected, $result); } } diff --git a/availability/upgrade.txt b/availability/upgrade.txt new file mode 100644 index 00000000000..1d2dc4622b6 --- /dev/null +++ b/availability/upgrade.txt @@ -0,0 +1,17 @@ +This files describes API changes in /availability/*. + +The information here is intended only for developers. + +=== 2.8 === + +* There is a new API function in the info_module/info_section objects (and + related functions in internal API): get_user_list_sql. This returns SQL code + that does roughly the same as filter_user_list to return a list of users who + should be shown as having access to the module or section. + +* Any third-party availability plugins which return true to + is_applied_to_user_lists (and therefore previously implemented + filter_user_list) should now also implement get_user_list_sql. If not + implemented, a debugging warning will occur when anybody calls + get_user_list_sql if the affected plugin is in use, and that user list will + not be filtered by the plugin.