diff --git a/contentbank/tests/generator/lib.php b/contentbank/tests/generator/lib.php index 8bb9ae04f56..4d7e0228cae 100644 --- a/contentbank/tests/generator/lib.php +++ b/contentbank/tests/generator/lib.php @@ -45,10 +45,12 @@ class core_contentbank_generator extends \component_generator_base { * @param context $context The context where the content will be created. * @param bool $convert2class Whether the class should return stdClass or plugin instance. * @param string $filepath The filepath of the file associated to the content to create. + * @param string $contentname The name of the content that will be created. * @return array An array with all the records added to the content bank. */ public function generate_contentbank_data(?string $contenttype, int $itemstocreate = 1, int $userid = 0, - ?\context $context = null, bool $convert2class = true, string $filepath = 'contentfile.h5p'): array { + ?\context $context = null, bool $convert2class = true, string $filepath = 'contentfile.h5p', + string $contentname = 'Test content '): array { global $DB, $USER; $records = []; @@ -67,7 +69,8 @@ class core_contentbank_generator extends \component_generator_base { for ($i = 0; $i < $itemstocreate; $i++) { // Create content. $record = new stdClass(); - $record->name = 'Test content ' . $i; + // If only 1 item is being created, do not add a number suffix to the content name. + $record->name = ($itemstocreate === 1) ? $contentname : $contentname . $i; $record->configdata = ''; $record->usercreated = $userid ?? $USER->id; diff --git a/lib/templates/filemanager_default_searchform.mustache b/lib/templates/filemanager_default_searchform.mustache index 1e6b24a2215..2bf592e9a5e 100644 --- a/lib/templates/filemanager_default_searchform.mustache +++ b/lib/templates/filemanager_default_searchform.mustache @@ -23,6 +23,6 @@ {} }} diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index e3830b7dd91..f211d9fc923 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -1898,4 +1898,19 @@ EOF; $this->getSession()); } } + + /** + * Manually press enter key. + * + * @When /^I press enter/ + * @throws DriverException + */ + public function i_manually_press_enter() { + if (!$this->running_javascript()) { + throw new DriverException('Enter press step is not available with Javascript disabled'); + } + + $value = [\WebDriver\Key::ENTER]; + $this->getSession()->getDriver()->getWebDriverSession()->activeElement()->postValue(['value' => $value]); + } } diff --git a/repository/contentbank/classes/contentbank_search.php b/repository/contentbank/classes/contentbank_search.php new file mode 100644 index 00000000000..ace3f81f411 --- /dev/null +++ b/repository/contentbank/classes/contentbank_search.php @@ -0,0 +1,61 @@ +. + +/** + * Utility class for searching of content bank files. + * + * @package repository_contentbank + * @copyright 2020 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace repository_contentbank; + +/** + * Represents the content bank search related functionality. + * + * @package repository_contentbank + * @copyright 2020 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class contentbank_search { + + /** + * Generate and return content nodes for all content bank files that match the search criteria + * and can be viewed/accessed by the user. + * + * @param string $search The search string + * @return array[] The array containing all content file nodes that match the search criteria. Each content node is + * an array with keys: shorttitle, title, datemodified, datecreated, author, license, isref, source, + * icon, thumbnail. + */ + public static function get_search_contents(string $search): array { + $contentbank = new \core_contentbank\contentbank(); + // Return all content bank content that matches the search criteria and can be viewed/accessed by the user. + $contents = $contentbank->search_contents($search); + return array_reduce($contents, function($list, $content) { + $contentcontext = \context::instance_by_id($content->get_content()->contextid); + $browser = \repository_contentbank\helper::get_contentbank_browser($contentcontext); + // If the user can access the content and content node can be created, add the node into the + // search results list. + if ($browser->can_access_content() && + $contentnode = \repository_contentbank\helper::create_contentbank_content_node($content)) { + $list[] = $contentnode; + } + return $list; + }, []); + } +} diff --git a/repository/contentbank/lib.php b/repository/contentbank/lib.php index 3a17689098a..0982faaabbe 100644 --- a/repository/contentbank/lib.php +++ b/repository/contentbank/lib.php @@ -47,7 +47,7 @@ class repository_contentbank extends repository { $ret = []; $ret['dynload'] = true; - $ret['nosearch'] = true; + $ret['nosearch'] = false; $ret['nologin'] = true; // Return the parameters from the encoded path if the encoded path is not empty. @@ -144,4 +144,19 @@ class repository_contentbank extends repository { return false; } + + /** + * Return search results. + * + * @param string $search + * @param int $page + * @return array + */ + public function search($search, $page = 0) { + $ret = []; + $ret['nologin'] = true; + $ret['list'] = \repository_contentbank\contentbank_search::get_search_contents($search); + + return $ret; + } } diff --git a/repository/contentbank/tests/behat/search_content.feature b/repository/contentbank/tests/behat/search_content.feature new file mode 100644 index 00000000000..a975751a81d --- /dev/null +++ b/repository/contentbank/tests/behat/search_content.feature @@ -0,0 +1,121 @@ +@repository @repository_contentbank @javascript +Feature: Search content bank files using the content bank files repository + In order to find the content I need to select in the file picker + As a user + I need to be able to search in the content bank files repository by content name + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | student | Student | 1 | student@example.com | + | teacher | Teacher | 1 | teacher1@example.com | + And the following "categories" exist: + | name | category | idnumber | + | Category1 | 0 | CAT1 | + | Category2 | 0 | CAT2 | + And the following "courses" exist: + | fullname | shortname | category | + | Course1 | C1 | CAT1 | + | Course2 | C2 | CAT2 | + And the following "contentbank content" exist: + | contextlevel | reference | contenttype | user | contentname | filepath | + | Course | C1 | contenttype_h5p | admin | coursecontent1.h5p | /h5p/tests/fixtures/filltheblanks.h5p | + | Course | C2 | contenttype_h5p | admin | coursecontent2.h5p | /h5p/tests/fixtures/find-the-words.h5p | + | Category | CAT1 | contenttype_h5p | admin | categorycontent1.h5p | /h5p/tests/fixtures/ipsums.h5p | + | Category | CAT2 | contenttype_h5p | admin | categorycontent2.h5p | /h5p/tests/fixtures/multiple-choice-2-6.h5p | + | System | | contenttype_h5p | admin | systemcontent.h5p | /h5p/tests/fixtures/greeting-card-887.h5p | + And the following "activities" exist: + | activity | name | intro | introformat | course | idnumber | + | folder | Folder | FolderDesc | 1 | C1 | folder | + And the following "course enrolments" exist: + | user | course | role | + | teacher | C1 | editingteacher | + + Scenario: User can see a search field and reset search button in the content bank files repository + Given I log in as "admin" + And I am on "Course1" course homepage + And I follow "Folder" + And I click on "Edit" "button" + And I click on "Add..." "button" + And I should see "Content bank" in the ".fp-repo-area" "css_element" + When I select "Content bank" repository in file picker + Then "Search repository" "field" should be visible + And "Refresh" "link" should be visible + + Scenario: User can see search results when there is content that matches the search criteria + Given I log in as "admin" + And I am on "Course1" course homepage + And I follow "Folder" + And I click on "Edit" "button" + And I click on "Add..." "button" + And I should see "Content bank" in the ".fp-repo-area" "css_element" + And I select "Content bank" repository in file picker + And I set the field "Search repository" to "content" + When I press enter + Then I should see "5" elements in repository content area + And I should see "systemcontent.h5p" "file" in repository content area + And I should see "categorycontent1.h5p" "file" in repository content area + And I should see "categorycontent2.h5p" "file" in repository content area + And I should see "coursecontent1.h5p" "file" in repository content area + And I should see "coursecontent2.h5p" "file" in repository content area + + Scenario: User can see search results when there is content that matches the search criteria ignoring case sensitivity + Given I log in as "admin" + And I am on "Course1" course homepage + And I follow "Folder" + And I click on "Edit" "button" + And I click on "Add..." "button" + And I should see "Content bank" in the ".fp-repo-area" "css_element" + And I select "Content bank" repository in file picker + And I set the field "Search repository" to "COURSE" + When I press enter + Then I should see "2" elements in repository content area + And I should see "coursecontent1.h5p" "file" in repository content area + And I should see "coursecontent2.h5p" "file" in repository content area + + Scenario: User can not see any search results when there is not a content that matches the search criteria + Given I log in as "admin" + And I am on "Course1" course homepage + And I follow "Folder" + And I click on "Edit" "button" + And I click on "Add..." "button" + And I should see "Content bank" in the ".fp-repo-area" "css_element" + And I select "Content bank" repository in file picker + And I set the field "Search repository" to "somecontent" + When I press enter + Then I should see "0" elements in repository content area + And I should see "No files available" in the ".filepicker .fp-content" "css_element" + + Scenario: User can reset search criteria and see all content displayed prior the search action + Given I log in as "admin" + And I am on "Course1" course homepage + And I follow "Folder" + And I click on "Edit" "button" + And I click on "Add..." "button" + And I should see "Content bank" in the ".fp-repo-area" "css_element" + And I select "Content bank" repository in file picker + And I should see "1" elements in repository content area + And I should see "coursecontent1.h5p" "file" in repository content area + And I set the field "Search repository" to "category" + And I press enter + And I should see "2" elements in repository content area + And I should see "categorycontent1.h5p" "file" in repository content area + And I should see "categorycontent2.h5p" "file" in repository content area + When I click on "Refresh" "link" + Then I should see "1" elements in repository content area + And I should see "coursecontent1.h5p" "file" in repository content area + + Scenario: Editing teacher can see search results when the content is available to him and matches the search criteria + Given I log in as "teacher" + And I am on "Course1" course homepage + And I follow "Folder" + And I click on "Edit" "button" + And I click on "Add..." "button" + And I should see "Content bank" in the ".fp-repo-area" "css_element" + And I select "Content bank" repository in file picker + And I set the field "Search repository" to "content" + When I press enter + Then I should see "3" elements in repository content area + And I should see "coursecontent1.h5p" "file" in repository content area + And I should see "categorycontent1.h5p" "file" in repository content area + And I should see "systemcontent.h5p" "file" in repository content area diff --git a/repository/contentbank/tests/search_test.php b/repository/contentbank/tests/search_test.php new file mode 100644 index 00000000000..84089f411a5 --- /dev/null +++ b/repository/contentbank/tests/search_test.php @@ -0,0 +1,241 @@ +. + +/** + * Content bank repository search unit tests. + * + * @package repository_contentbank + * @copyright 2020 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once("$CFG->dirroot/repository/lib.php"); + +/** + * Tests for the content bank search class. + * + * @package repository_contentbank + * @copyright 2020 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class repository_contentbank_search_testcase extends advanced_testcase { + + /** + * Test get_search_contents() by searching through some existing content using different search terms. + * + * @dataProvider get_search_contents_provider + * @param array $contentnames The array containing the names of the content that needs to be generated + * @param string $search The search string + * @param array $expected The array containing the expected content names that should be returned by the search + */ + public function test_get_search_contents(array $contentnames, string $search, array $expected) { + $this->resetAfterTest(); + + $admin = get_admin(); + $systemcontext = \context_system::instance(); + // Add some content to the content bank. + $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); + foreach ($contentnames as $contentname) { + $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, + $systemcontext, true, 'file.h5p', $contentname); + } + // Log in as admin. + $this->setUser($admin); + // Search for content bank content items which have the search pattern within the name. + $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); + // Get the content name of the each content returned after performing the search. + $actual = array_map(function($searchcontentnode) { + return $searchcontentnode['shorttitle']; + }, $searchcontentnodes); + + $this->assertEquals($expected, $actual, '', 0.0, 10, true); + } + + /** + * Data provider for test_get_search_contents(). + * + * @return array + */ + public function get_search_contents_provider(): array { + return [ + 'Search for existing pattern found within the name of content items' => [ + [ + 'systemcontentfile1', + 'systemcontentfile2', + 'somesystemfile' + ], + 'content', + [ + 'systemcontentfile1', + 'systemcontentfile2' + ] + ], + 'Search for existing pattern found at the beginning of the name of content items' => [ + [ + 'systemcontentfile1', + 'systemcontentfile2', + 'somesystemfile' + ], + 'some', + [ + 'somesystemfile', + ] + ], + 'Search for existing pattern found at the end of the name of content items' => [ + [ + 'systemcontentfile1', + 'systemcontentfile2', + 'somesystemfile' + ], + 'file2', + [ + 'systemcontentfile2', + ] + ], + 'Search for a pattern which does not exist within the name of any content item' => [ + [ + 'systemcontentfile1', + 'somesystemfile' + ], + 'somefile', + [] + ], + 'Case-insensitive search for a pattern which exists within the name of content items' => [ + [ + 'systemcontentfile1', + 'systemcontentfile2', + 'somesystemfile' + ], + 'CONTENT', + [ + 'systemcontentfile1', + 'systemcontentfile2' + ] + ] + ]; + } + + /** + * Test get_search_contents() by searching for content with users that have capability to access/view + * all existing content bank content. By default, admins, managers should be able to view every existing content + * that matches the search criteria. + */ + public function test_get_search_contents_user_can_access_all_content() { + $this->resetAfterTest(true); + + // Create a course in 'Miscellaneous' category by default. + $course = $this->getDataGenerator()->create_course(); + $coursecontext = \context_course::instance($course->id); + // Create a course category without a course. + $category = $this->getDataGenerator()->create_category(); + $categorycontext = \context_coursecat::instance($category->id); + + $admin = get_admin(); + // Add some content to the content bank in different contexts. + $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); + // Add a content bank file in the category context. + $categorycontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, + $categorycontext, true, 'file.h5p', 'categorycontentfile'); + $categorycontent = reset($categorycontents); + // Add a content bank file in the course context. + $coursecontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, + $coursecontext, true, 'file.h5p', 'coursecontentfile'); + $coursecontent = reset($coursecontents); + + // Log in as admin. + $this->setUser($admin); + + // Search for content bank content items which have the pattern 'contentfile' within the name. + $search = 'contentfile'; + $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); + // All content files which name matches the search criteria should be available to the admin user. + // The search should return 2 file nodes. + $this->assertCount(2, $searchcontentnodes); + $expected = [ + \repository_contentbank\helper::create_contentbank_content_node($categorycontent), + \repository_contentbank\helper::create_contentbank_content_node($coursecontent), + ]; + $this->assertEquals($expected, $searchcontentnodes, '', 0.0, 10, true); + } + + /** + * Test get_search_contents() by searching for content with users that have capability to access/view only + * certain existing content bank content. By default, editing teachers should be able to view content that matches + * the search criteria AND is in the courses they are enrolled, course categories of the enrolled courses + * and system content. Other authenticated users should be able to access only the system content. + */ + public function test_get_search_contents_user_can_access_certain_content() { + $this->resetAfterTest(true); + + $systemcontext = \context_system::instance(); + // Create course1. + $course1 = $this->getDataGenerator()->create_course(); + $course1context = \context_course::instance($course1->id); + // Create course2. + $course2 = $this->getDataGenerator()->create_course(); + $course2context = \context_course::instance($course2->id); + + $admin = get_admin(); + // Create and enrol an editing teacher in course1. + $editingteacher = $this->getDataGenerator()->create_and_enrol($course1, 'editingteacher'); + // Create and enrol a teacher in course2. + $teacher = $this->getDataGenerator()->create_and_enrol($course2, 'teacher'); + + // Add some content to the content bank in different contexts. + $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank'); + // Add a content bank file in the system context. + $systemcontents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, + $systemcontext, true, 'file.h5p', 'systemcontentfile'); + $systemcontent = reset($systemcontents); + // Add a content bank file in the course1 context. + $course1contents = $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, + $course1context, true, 'file.h5p', 'coursecontentfile1'); + $course1content = reset($course1contents); + // Add a content bank file in the course2 context. + $generator->generate_contentbank_data('contenttype_h5p', 1, $admin->id, + $course2context, true, 'file.h5p', 'coursecontentfile2'); + + // Log in as an editing teacher. + $this->setUser($editingteacher); + // Search for content bank content items which have the pattern 'contentfile' within the name. + $search = 'contentfile'; + $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); + // The search should return 2 file nodes. The editing teacher does not have access to the content of course2 + // and therefore, the course2 content should be skipped. + $this->assertCount(2, $searchcontentnodes); + $expected = [ + \repository_contentbank\helper::create_contentbank_content_node($systemcontent), + \repository_contentbank\helper::create_contentbank_content_node($course1content), + ]; + $this->assertEquals($expected, $searchcontentnodes, '', 0.0, 10, true); + + // Log in as a teacher. + $this->setUser($teacher); + // Search for content bank content items which have the pattern 'contentfile' within the name. + $search = 'contentfile'; + $searchcontentnodes = \repository_contentbank\contentbank_search::get_search_contents($search); + // The search should return 1 file node. The teacher should only be able to view system content. + $this->assertCount(1, $searchcontentnodes); + $expected = [ + \repository_contentbank\helper::create_contentbank_content_node($systemcontent), + ]; + $this->assertEquals($expected, $searchcontentnodes, '', 0.0, 10, true); + } +} diff --git a/repository/tests/behat/behat_filepicker.php b/repository/tests/behat/behat_filepicker.php index 8ec12d597b4..2a564741490 100644 --- a/repository/tests/behat/behat_filepicker.php +++ b/repository/tests/behat/behat_filepicker.php @@ -28,6 +28,7 @@ require_once(__DIR__ . '/../../../lib/behat/core_behat_file_helper.php'); use Behat\Mink\Exception\ExpectationException as ExpectationException, + Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException, Behat\Gherkin\Node\TableNode as TableNode; /** @@ -342,7 +343,12 @@ class behat_filepicker extends behat_base { "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-content ')]" . "//a[contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]"; - $elements = $this->find_all('xpath', $xpath); + try { + $elements = $this->find_all('xpath', $xpath); + } catch (ElementNotFoundException $e) { + $elements = []; + } + // Make sure the expected number is equal to the actual number of .fp-file elements. if (count($elements) != $expectedcount) { throw new ExpectationException("Found " . count($elements) .