From a92bbd867d44de1f1c16697dd92b424b4965331d Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Wed, 9 May 2018 10:57:13 +0800 Subject: [PATCH 1/2] MDL-61994 mod_glossary: implement privacy providers --- mod/glossary/classes/privacy/provider.php | 332 ++++++++++++++++++++++ mod/glossary/lang/en/glossary.php | 12 + 2 files changed, 344 insertions(+) create mode 100644 mod/glossary/classes/privacy/provider.php diff --git a/mod/glossary/classes/privacy/provider.php b/mod/glossary/classes/privacy/provider.php new file mode 100644 index 00000000000..4659e720a9a --- /dev/null +++ b/mod/glossary/classes/privacy/provider.php @@ -0,0 +1,332 @@ +. + +/** + * Privacy Subsystem implementation for mod_glossary. + * + * @package mod_glossary + * @copyright 2018 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_glossary\privacy; +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\deletion_criteria; +use core_privacy\local\request\helper; +use core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); +/** + * Implementation of the privacy subsystem plugin provider for the glossary activity module. + * + * @copyright 2018 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + // This plugin stores personal data. + \core_privacy\local\metadata\provider, + // This plugin is a core_user_data_provider. + \core_privacy\local\request\plugin\provider { + + /** + * Return the fields which contain personal data. + * + * @param collection $items a reference to the collection to use to store the metadata. + * @return collection the updated collection of metadata items. + */ + public static function get_metadata(collection $items) : collection { + $items->add_database_table( + 'glossary_entries', + [ + 'glossaryid' => 'privacy:metadata:glossary_entries:glossaryid', + 'userid' => 'privacy:metadata:glossary_entries:userid', + 'concept' => 'privacy:metadata:glossary_entries:concept', + 'definition' => 'privacy:metadata:glossary_entries:definition', + 'attachment' => 'privacy:metadata:glossary_entries:attachment', + 'timemodified' => 'privacy:metadata:glossary_entries:timemodified', + ], + 'privacy:metadata:glossary_entries' + ); + + $items->add_subsystem_link('core_files', [], 'privacy:metadata:core_files'); + $items->add_subsystem_link('core_comment', [], 'privacy:metadata:core_comments'); + $items->add_subsystem_link('core_tag', [], 'privacy:metadata:core_tag'); + $items->add_subsystem_link('core_ratings', [], 'privacy:metadata:core_ratings'); + return $items; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid the userid. + * @return contextlist the list of contexts containing user info for the user. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $ratingquery = \core_rating\privacy\provider::get_sql_join('r', 'mod_glossary', 'entry', 'ge.id', $userid); + + $sql = "SELECT c.id + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {glossary} g ON g.id = cm.instance + INNER JOIN {glossary_entries} ge ON ge.glossaryid = g.id + LEFT JOIN {comments} com ON com.commentarea =:commentarea AND com.itemid = ge.id + {$ratingquery->join} + WHERE ge.userid = :glossaryentryuserid OR com.userid = :commentuserid OR {$ratingquery->userwhere}"; + $params = [ + 'contextlevel' => CONTEXT_MODULE, + 'modname' => 'glossary', + 'commentarea' => 'glossary_entry', + 'glossaryentryuserid' => $userid, + 'commentuserid' => $userid, + ] + $ratingquery->params; + + $contextlist = new contextlist(); + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Export personal data for the given approved_contextlist. + * + * User and context information is contained within the contextlist. + * + * @param approved_contextlist $contextlist a list of contexts approved for export. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $user = $contextlist->get_user(); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + $sql = "SELECT ge.id as entryid, + cm.id AS cmid, + ge.userid, + ge.concept, + ge.definition, + ge.definitionformat, + ge.attachment, + ge.timecreated, + ge.timemodified + FROM {glossary_entries} ge + JOIN {glossary} g ON ge.glossaryid = g.id + JOIN {course_modules} cm ON g.id = cm.instance + JOIN {context} c ON cm.id = c.instanceid + WHERE c.id {$contextsql} + AND ge.userid = :userid + OR EXISTS (SELECT 1 FROM {comments} com WHERE com.commentarea = :commentarea AND com.itemid = ge.id + AND com.userid = :commentuserid) + OR EXISTS (SELECT 1 FROM {rating} r WHERE r.contextid = c.id AND r.itemid = ge.id + AND r.component = :ratingcomponent + AND r.ratingarea = :ratingarea + AND r.userid = :ratinguserid) + ORDER BY ge.id, cm.id"; + $params = [ + 'userid' => $user->id, + 'commentarea' => 'glossary_entry', + 'commentuserid' => $user->id, + 'ratingcomponent' => 'mod_glossary', + 'ratingarea' => 'entry', + 'ratinguserid' => $user->id + ] + $contextparams; + $glossaryentries = $DB->get_recordset_sql($sql, $params); + + // Reference to the glossary activity seen in the last iteration of the loop. By comparing this with the + // current record, and because we know the results are ordered, we know when we've moved to the entries + // for a new glossary activity and therefore when we can export the complete data for the last activity. + $lastcmid = null; + + $glossarydata = []; + foreach ($glossaryentries as $record) { + $concept = format_string($record->concept); + $path = array_merge([get_string('entries', 'mod_glossary'), $concept . " ({$record->entryid})"]); + + // If we've moved to a new glossary, then write the last glossary data and reinit the glossary data array. + if (!is_null($lastcmid)) { + if ($lastcmid != $record->cmid) { + if (!empty($glossarydata)) { + $context = \context_module::instance($lastcmid); + self::export_glossary_data_for_user($glossarydata, $context, [], $user); + $glossarydata = []; + } + } + } + $lastcmid = $record->cmid; + $context = \context_module::instance($lastcmid); + + // Export files added on the glossary entry definition field. + $definition = format_text(writer::with_context($context)->rewrite_pluginfile_urls($path, 'mod_glossary', + 'entry', $record->entryid, $record->definition), $record->definitionformat); + + // Export just the files attached to this user entry. + if ($record->userid == $user->id) { + // Get all files attached to the glossary attachment. + writer::with_context($context)->export_area_files($path, 'mod_glossary', 'entry', $record->entryid); + + // Get all files attached to the glossary attachment. + writer::with_context($context)->export_area_files($path, 'mod_glossary', 'attachment', $record->entryid); + } + + // Export associated comments. + \core_comment\privacy\provider::export_comments($context, 'mod_glossary', 'glossary_entry', + $record->entryid, $path, $record->userid != $user->id); + + // Export associated tags. + \core_tag\privacy\provider::export_item_tags($user->id, $context, $path, 'mod_glossary', 'glossary_entries', + $record->entryid, $record->userid != $user->id); + + // Export associated ratings. + \core_rating\privacy\provider::export_area_ratings($user->id, $context, $path, 'mod_glossary', 'entry', + $record->entryid, $record->userid != $user->id); + + $glossarydata['entries'][] = [ + 'concept' => $record->concept, + 'definition' => $definition, + 'timecreated' => \core_privacy\local\request\transform::datetime($record->timecreated), + 'timemodified' => \core_privacy\local\request\transform::datetime($record->timemodified) + ]; + } + $glossaryentries->close(); + + // The data for the last activity won't have been written yet, so make sure to write it now! + if (!empty($glossarydata)) { + $context = \context_module::instance($lastcmid); + self::export_glossary_data_for_user($glossarydata, $context, [], $user); + } + } + + /** + * Export the supplied personal data for a single glossary activity, along with any generic data or area files. + * + * @param array $glossarydata The personal data to export for the glossary. + * @param \context_module $context The context of the glossary. + * @param array $subcontext The location within the current context that this data belongs. + * @param \stdClass $user the user record + */ + protected static function export_glossary_data_for_user(array $glossarydata, \context_module $context, + array $subcontext, \stdClass $user) { + // Fetch the generic module data for the glossary. + $contextdata = helper::get_context_data($context, $user); + // Merge with glossary data and write it. + $contextdata = (object)array_merge((array)$contextdata, $glossarydata); + writer::with_context($context)->export_data($subcontext, $contextdata); + // Write generic module intro files. + helper::export_context_files($context, $user); + } + + /** + * Delete all data for all users in the specified context. + * + * @param \context $context the context to delete in. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + if (empty($context)) { + return; + } + + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->record_exists('glossary', ['id' => $context->instanceid]); + $DB->delete_records('glossary_entries', ['glossaryid' => $instanceid]); + + if ($context->contextlevel == CONTEXT_MODULE) { + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->record_exists('glossary', ['id' => $context->instanceid]); + + $entries = $DB->get_records('glossary_entries', ['glossaryid' => $instanceid]); + foreach ($entries as $entry) { + // Delete related entry categories. + $DB->delete_records('glossary_entries_categories', ['entryid' => $entry->id]); + + // Delete related entry aliases. + $DB->delete_records('glossary_alias', ['entryid' => $entry->id]); + } + + // Delete entry and attachment files. + get_file_storage()->delete_area_files($context->id, 'mod_glossary', 'entry'); + get_file_storage()->delete_area_files($context->id, 'mod_glossary', 'attachment'); + + // Delete related ratings. + \core_rating\privacy\provider::delete_ratings($context, 'mod_glossary', 'entry'); + + // Delete comments. + \core_comment\privacy\provider::delete_comments_for_all_users($context, 'mod_glossary', 'glossary_entry'); + + // Delete tags. + \core_tag\privacy\provider::delete_item_tags($context, 'mod_glossary', 'glossary_entries'); + + // Now delete all user related entries. + $DB->delete_records('glossary_entries', ['glossaryid' => $instanceid]); + } + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist a list of contexts approved for deletion. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + if ($context->contextlevel == CONTEXT_MODULE) { + + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->record_exists('glossary', ['id' => $context->instanceid]); + + $entries = $DB->get_records('glossary_entries', ['glossaryid' => $instanceid, 'userid' => $userid]); + foreach ($entries as $entry) { + // Delete related entry categories. + $DB->delete_records('glossary_entries_categories', ['entryid' => $entry->id]); + + // Delete related entry aliases. + $DB->delete_records('glossary_alias', ['entryid' => $entry->id]); + + // Delete tags. + \core_tag\privacy\provider::delete_item_tags($context, 'mod_glossary', 'glossary_entries', $entry->id); + + // Delete entry and attachment files. + get_file_storage()->delete_area_files($context->id, 'mod_glossary', 'entry', $entry->id); + get_file_storage()->delete_area_files($context->id, 'mod_glossary', 'attachment', $entry->id); + + // Delete related ratings. + \core_rating\privacy\provider::delete_ratings($context, 'mod_glossary', 'entry', $entry->id); + } + + // Delete comments. + \core_comment\privacy\provider::delete_comments_for_user($contextlist, 'mod_glossary', 'glossary_entry'); + + // Now delete all user related entries. + $DB->delete_records('glossary_entries', ['glossaryid' => $instanceid, 'userid' => $userid]); + } + } + } +} diff --git a/mod/glossary/lang/en/glossary.php b/mod/glossary/lang/en/glossary.php index 92c07fbb90e..8c6dd456bdb 100644 --- a/mod/glossary/lang/en/glossary.php +++ b/mod/glossary/lang/en/glossary.php @@ -271,6 +271,18 @@ $string['popupformat'] = 'Popup format'; $string['print'] = 'Print'; $string['printerfriendly'] = 'Printer-friendly version'; $string['printviewnotallowed'] = 'Print view isn\'t allowed'; +$string['privacy'] = 'Privacy of results'; +$string['privacy:metadata:core_comments'] = 'Comments made on glossary definitions are stored using core_comment system'; +$string['privacy:metadata:core_files'] = 'Files linked to glossary definitions are stored using the core_files system'; +$string['privacy:metadata:core_ratings'] = 'Ratings added to glossary entries are stored using core_ratings system '; +$string['privacy:metadata:core_tag'] = 'Tags added to glossary definitions are stored using core_tag system '; +$string['privacy:metadata:glossary_entries'] = 'Information about the user\'s entries for a given glossary activity'; +$string['privacy:metadata:glossary_entries:attachment'] = 'The attachment of the entry the user added'; +$string['privacy:metadata:glossary_entries:concept'] = 'The concept of the entry the user added'; +$string['privacy:metadata:glossary_entries:definition'] = 'The definition of the entry the user added'; +$string['privacy:metadata:glossary_entries:glossaryid'] = 'The ID of the glossary activity the user is providing entry for'; +$string['privacy:metadata:glossary_entries:userid'] = 'The ID of the user that is adding this glossary entry'; +$string['privacy:metadata:glossary_entries:timemodified'] = 'The timestamp indicating when the glossary entry was modified by the user'; $string['question'] = 'Question'; $string['rejectedentries'] = 'Rejected entries'; $string['rejectionrpt'] = 'Rejection report'; From fe80a5ab669948b1e61f83a9b7326b003a5d2eb5 Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Wed, 9 May 2018 10:57:42 +0800 Subject: [PATCH 2/2] MDL-61994 mod_glossary: add unit tests for provider --- mod/glossary/tests/privacy_provider_test.php | 310 +++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 mod/glossary/tests/privacy_provider_test.php diff --git a/mod/glossary/tests/privacy_provider_test.php b/mod/glossary/tests/privacy_provider_test.php new file mode 100644 index 00000000000..b5a09c3e688 --- /dev/null +++ b/mod/glossary/tests/privacy_provider_test.php @@ -0,0 +1,310 @@ +. + +/** + * Privacy provider tests. + * + * @package mod_glossary + * @copyright 2018 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\deletion_criteria; +use mod_glossary\privacy\provider; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/comment/lib.php'); + +/** + * Privacy provider tests class. + * + * @package mod_glossary + * @copyright 2018 Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_glossary_privacy_provider_testcase extends \core_privacy\tests\provider_testcase { + /** @var stdClass The student object. */ + protected $student; + + /** @var stdClass The teacher object. */ + protected $teacher; + + /** @var stdClass The glossary object. */ + protected $glossary; + + /** @var stdClass The course object. */ + protected $course; + + /** @var stdClass The plugin generator object. */ + protected $plugingenerator; + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->resetAfterTest(); + + global $DB; + $generator = $this->getDataGenerator(); + $course = $generator->create_course(); + $this->course = $course; + + $this->plugingenerator = $generator->get_plugin_generator('mod_glossary'); + + // The glossary activity the user will answer. + $glossary = $this->plugingenerator->create_instance(['course' => $course->id]); + $this->glossary = $glossary; + + $cm = get_coursemodule_from_instance('glossary', $glossary->id); + $context = context_module::instance($cm->id); + + // Create a student which will add an entry to a glossary. + $student = $generator->create_user(); + $generator->enrol_user($student->id, $course->id, 'student'); + $this->student = $student; + + $teacher = $generator->create_user(); + $generator->enrol_user($teacher->id, $course->id, 'editingteacher'); + $this->teacher = $teacher; + + $this->setUser($student->id); + $ge1 = $this->plugingenerator->create_content($glossary, ['concept' => 'first', 'approved' => 1]); + + // Student create a comment on a glossary entry. + $this->setUser($student); + $comment = $this->get_comment_object($context, $ge1->id); + $comment->add('Hello, it\'s me!'); + + // Attach tags. + core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge1->id, $context, ['Beer', 'Golf']); + } + + /** + * Test for provider::get_metadata(). + */ + public function test_get_metadata() { + $collection = new collection('mod_glossary'); + $newcollection = provider::get_metadata($collection); + $itemcollection = $newcollection->get_collection(); + $this->assertCount(5, $itemcollection); + + $table = reset($itemcollection); + $this->assertEquals('glossary_entries', $table->get_name()); + + $privacyfields = $table->get_privacy_fields(); + $this->assertArrayHasKey('glossaryid', $privacyfields); + $this->assertArrayHasKey('concept', $privacyfields); + $this->assertArrayHasKey('definition', $privacyfields); + $this->assertArrayHasKey('attachment', $privacyfields); + $this->assertArrayHasKey('userid', $privacyfields); + $this->assertArrayHasKey('timemodified', $privacyfields); + + $this->assertEquals('privacy:metadata:glossary_entries', $table->get_summary()); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + $cm = get_coursemodule_from_instance('glossary', $this->glossary->id); + + $contextlist = provider::get_contexts_for_userid($this->student->id); + $this->assertCount(1, $contextlist); + $contextforuser = $contextlist->current(); + $cmcontext = context_module::instance($cm->id); + $this->assertEquals($cmcontext->id, $contextforuser->id); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_for_context() { + $cm = get_coursemodule_from_instance('glossary', $this->glossary->id); + $cmcontext = context_module::instance($cm->id); + + // Export all of the data for the context. + $writer = \core_privacy\local\request\writer::with_context($cmcontext); + $contextlist = new \core_privacy\local\request\approved_contextlist($this->student, 'mod_glossary' , [$cmcontext->id]); + + \mod_glossary\privacy\provider::export_user_data($contextlist); + $this->assertTrue($writer->has_any_data()); + $data = $writer->get_data([]); + + $this->assertEquals('Glossary 1', $data->name); + $this->assertEquals('first', $data->entries[0]['concept']); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + $generator = $this->getDataGenerator(); + $cm = get_coursemodule_from_instance('glossary', $this->glossary->id); + $context = context_module::instance($cm->id); + // Create another student who will add an entry the glossary activity. + $student2 = $generator->create_user(); + $generator->enrol_user($student2->id, $this->course->id, 'student'); + + $this->setUser($student2); + $ge3 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'first', 'approved' => 1]); + $comment = $this->get_comment_object($context, $ge3->id); + $comment->add('User 2 comment'); + + core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context, ['Pizza', 'Noodles']); + + // As a teacher, rate student 2 entry. + $this->setUser($this->teacher); + $rating = $this->get_rating_object($context, $ge3->id); + $rating->update_rating(2); + + // Before deletion, we should have 2 entries. + $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); + $this->assertEquals(2, $count); + + // Delete data based on context. + provider::delete_data_for_all_users_in_context($context); + + // After deletion, the glossary entries for that glossary activity should have been deleted. + $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); + $this->assertEquals(0, $count); + + $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', + 'itemid' => $ge3->id]); + $this->assertEquals(0, $tagcount); + + $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', + 'itemid' => $ge3->id, 'userid' => $student2->id]); + $this->assertEquals(0, $commentcount); + + $ratingcount = $DB->count_records('rating', ['component' => 'mod_glossary', 'ratingarea' => 'entry', + 'itemid' => $ge3->id]); + $this->assertEquals(0, $ratingcount); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + $generator = $this->getDataGenerator(); + + $student2 = $generator->create_user(); + $generator->enrol_user($student2->id, $this->course->id, 'student'); + + $cm1 = get_coursemodule_from_instance('glossary', $this->glossary->id); + $glossary2 = $this->plugingenerator->create_instance(['course' => $this->course->id]); + $cm2 = get_coursemodule_from_instance('glossary', $glossary2->id); + + $ge1 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'first user glossary entry', 'approved' => 1]); + $this->plugingenerator->create_content($glossary2, ['concept' => 'first user second glossary entry', 'approved' => 1]); + + $context1 = context_module::instance($cm1->id); + $context2 = context_module::instance($cm2->id); + core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge1->id, $context1, ['Parmi', 'Sushi']); + + $this->setUser($student2); + $ge3 = $this->plugingenerator->create_content($this->glossary, ['concept' => 'second user glossary entry', + 'approved' => 1]); + + $comment = $this->get_comment_object($context1, $ge3->id); + $comment->add('User 2 comment'); + + core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $ge3->id, $context1, ['Pizza', 'Noodles']); + + // Before deletion, we should have 3 entries, one rating and 2 tag instances. + $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id]); + $this->assertEquals(3, $count); + $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', + 'itemid' => $ge3->id]); + $this->assertEquals(2, $tagcount); + + // Create another student who will add an entry to the first glossary. + $contextlist = new \core_privacy\local\request\approved_contextlist($student2, 'glossary', + [$context1->id, $context2->id]); + provider::delete_data_for_user($contextlist); + + // After deletion, the glossary entry and tags for the second student should have been deleted. + $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id, 'userid' => $student2->id]); + $this->assertEquals(0, $count); + + $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', + 'itemid' => $ge3->id]); + $this->assertEquals(0, $tagcount); + + $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', + 'itemid' => $ge3->id, 'userid' => $student2->id]); + $this->assertEquals(0, $commentcount); + + // Student's 1 entries, comments and tags should not be removed. + $count = $DB->count_records('glossary_entries', ['glossaryid' => $this->glossary->id, + 'userid' => $this->student->id]); + $this->assertEquals(2, $count); + + $tagcount = $DB->count_records('tag_instance', ['component' => 'mod_glossary', 'itemtype' => 'glossary_entries', + 'itemid' => $ge1->id]); + $this->assertEquals(2, $tagcount); + + $commentcount = $DB->count_records('comments', ['component' => 'mod_glossary', 'commentarea' => 'glossary_entry', + 'userid' => $this->student->id]); + $this->assertEquals(1, $commentcount); + } + + /** + * Get the comment area for glossary module. + * + * @param context $context The context. + * @param int $itemid The item ID. + * @return comment + */ + protected function get_comment_object(context $context, $itemid) { + $args = new stdClass(); + + $args->context = $context; + $args->course = get_course(SITEID); + $args->area = 'glossary_entry'; + $args->itemid = $itemid; + $args->component = 'mod_glossary'; + $comment = new comment($args); + $comment->set_post_permission(true); + + return $comment; + } + + /** + * Get the rating area for glossary module. + * + * @param context $context The context. + * @param int $itemid The item ID. + * @return rating object + */ + protected function get_rating_object(context $context, $itemid) { + global $USER; + + $ratingoptions = new stdClass; + $ratingoptions->context = $context; + $ratingoptions->ratingarea = 'entry'; + $ratingoptions->component = 'mod_glossary'; + $ratingoptions->itemid = $itemid; + $ratingoptions->scaleid = 2; + $ratingoptions->userid = $USER->id; + return new rating($ratingoptions); + } +}