diff --git a/contentbank/classes/content.php b/contentbank/classes/content.php
index 27975cb61f4..13996a9875d 100644
--- a/contentbank/classes/content.php
+++ b/contentbank/classes/content.php
@@ -100,7 +100,13 @@ abstract class content {
}
$this->content->usermodified = $USER->id;
$this->content->timemodified = time();
- return $DB->update_record('contentbank_content', $this->content);
+ $result = $DB->update_record('contentbank_content', $this->content);
+ if ($result) {
+ // Trigger an event for updating this content.
+ $event = contentbank_content_updated::create_from_record($this->content);
+ $event->trigger();
+ }
+ return $result;
}
/**
diff --git a/contentbank/classes/contentbank.php b/contentbank/classes/contentbank.php
index 0a21353a974..35499ce6514 100644
--- a/contentbank/classes/contentbank.php
+++ b/contentbank/classes/contentbank.php
@@ -24,6 +24,8 @@
namespace core_contentbank;
+use stored_file;
+
/**
* Content bank class
*
@@ -202,4 +204,32 @@ class contentbank {
return $contents;
}
+
+ /**
+ * Create content from a file information.
+ *
+ * @param \context $context Context where to upload the file and content.
+ * @param int $userid Id of the user uploading the file.
+ * @param stored_file $file The file to get information from
+ * @return content
+ */
+ public function create_content_from_file(\context $context, int $userid, stored_file $file): ?content {
+ global $USER;
+ if (empty($userid)) {
+ $userid = $USER->id;
+ }
+ // Get the contenttype to manage given file's extension.
+ $filename = $file->get_filename();
+ $extension = $this->get_extension($filename);
+ $plugin = $this->get_extension_supporter($extension, $context);
+ $classname = '\\contenttype_'.$plugin.'\\contenttype';
+ $record = new \stdClass();
+ $record->name = $filename;
+ $record->usercreated = $userid;
+ $contentype = new $classname($context);
+ $content = $contentype->create_content($record);
+ $event = \core\event\contentbank_content_uploaded::create_from_record($content->get_content());
+ $event->trigger();
+ return $content;
+ }
}
diff --git a/contentbank/classes/contenttype.php b/contentbank/classes/contenttype.php
index 05922b7e01c..7f7fbf59694 100644
--- a/contentbank/classes/contenttype.php
+++ b/contentbank/classes/contenttype.php
@@ -24,6 +24,9 @@
namespace core_contentbank;
+use core\event\contentbank_content_created;
+use core\event\contentbank_content_deleted;
+use core\event\contentbank_content_viewed;
use moodle_url;
/**
@@ -71,10 +74,15 @@ abstract class contenttype {
$entry->usermodified = $entry->usercreated;
$entry->timemodified = $entry->timecreated;
$entry->configdata = $record->configdata ?? '';
+ $entry->instanceid = $record->instanceid ?? 0;
$entry->id = $DB->insert_record('contentbank_content', $entry);
if ($entry->id) {
$classname = '\\'.$entry->contenttype.'\\content';
- return new $classname($entry);
+ $content = new $classname($entry);
+ // Trigger an event for creating the content.
+ $event = contentbank_content_created::create_from_record($content->get_content());
+ $event->trigger();
+ return $content;
}
return null;
}
@@ -95,7 +103,23 @@ abstract class contenttype {
}
// Delete the contentbank DB entry.
- return $DB->delete_records('contentbank_content', ['id' => $content->get_id()]);
+ $result = $DB->delete_records('contentbank_content', ['id' => $content->get_id()]);
+ if ($result) {
+ // Trigger an event for deleting this content.
+ $record = $content->get_content();
+ $event = contentbank_content_deleted::create([
+ 'objectid' => $content->get_id(),
+ 'relateduserid' => $record->usercreated,
+ 'context' => \context::instance_by_id($record->contextid),
+ 'other' => [
+ 'contenttype' => $content->get_content_type(),
+ 'name' => $content->get_name()
+ ]
+ ]);
+ $event->add_record_snapshot('contentbank_content', $record);
+ $event->trigger();
+ }
+ return $result;
}
/**
@@ -149,6 +173,10 @@ abstract class contenttype {
* @return string HTML code to include in view.php.
*/
public function get_view_content(\stdClass $record): string {
+ // Trigger an event for viewing this content.
+ $event = contentbank_content_viewed::create_from_record($record);
+ $event->trigger();
+
// Main contenttype class can visualize the content, but plugins could overwrite visualization.
return '';
}
diff --git a/contentbank/contenttype/h5p/classes/contenttype.php b/contentbank/contenttype/h5p/classes/contenttype.php
index f28bee8837b..806205d4f63 100644
--- a/contentbank/contenttype/h5p/classes/contenttype.php
+++ b/contentbank/contenttype/h5p/classes/contenttype.php
@@ -24,6 +24,7 @@
namespace contenttype_h5p;
+use core\event\contentbank_content_viewed;
use stdClass;
use html_writer;
@@ -45,7 +46,9 @@ class contenttype extends \core_contentbank\contenttype {
public function delete_content(\core_contentbank\content $content): bool {
// Delete the H5P content.
$factory = new \core_h5p\factory();
- \core_h5p\api::delete_content_from_pluginfile_url($content->get_file_url(), $factory);
+ if (!empty($content->get_file_url())) {
+ \core_h5p\api::delete_content_from_pluginfile_url($content->get_file_url(), $factory);
+ }
// Delete the content from the content_bank.
return parent::delete_content($content);
@@ -58,6 +61,10 @@ class contenttype extends \core_contentbank\contenttype {
* @return string HTML code to include in view.php.
*/
public function get_view_content(\stdClass $record): string {
+ // Trigger an event for viewing this content.
+ $event = contentbank_content_viewed::create_from_record($record);
+ $event->trigger();
+
$content = new content($record);
$fileurl = $content->get_file_url();
$html = html_writer::tag('h2', $content->get_name());
diff --git a/contentbank/tests/behat/events.feature b/contentbank/tests/behat/events.feature
new file mode 100644
index 00000000000..c52019903e4
--- /dev/null
+++ b/contentbank/tests/behat/events.feature
@@ -0,0 +1,73 @@
+@core @core_contentbank @_file_upload @_switch_iframe @javascript
+Feature: Confirm content bank events are triggered
+ In order to log content bank actions
+ As an admin
+ I need to be able to check triggered events
+
+ Background:
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "contentbank content" exist:
+ | course | contenttype | user | contentname |
+ | C1 | contenttype_h5p | admin | Existing |
+ And I log in as "admin"
+ And I follow "Manage private files..."
+ And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
+ And I click on "Save changes" "button"
+ And I am on "Course 1" course homepage with editing mode on
+ And I add the "Navigation" block if not present
+
+ Scenario: Content created and uploaded events when uploading a content file
+ Given I navigate to "Reports > Live logs" in site administration
+ And I should not see "Content uploaded"
+ And I should not see "Content created"
+ And I am on "Course 1" course homepage
+ And I expand "Site pages" node
+ And I click on "Content bank" "link"
+ When I click on "Upload" "link"
+ And I click on "Choose a file..." "button"
+ And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
+ And I click on "filltheblanks.h5p" "link"
+ And I click on "Select this file" "button"
+ And I click on "Save changes" "button"
+ And I navigate to "Reports > Live logs" in site administration
+ Then I should see "Content uploaded"
+ And I should see "Content created"
+
+ Scenario: Content viewed event
+ Given I navigate to "Reports > Live logs" in site administration
+ And I should not see "Content viewed"
+ And I am on "Course 1" course homepage
+ And I expand "Site pages" node
+ And I click on "Content bank" "link"
+ When I click on "Existing" "link"
+ And I navigate to "Reports > Live logs" in site administration
+ Then I should see "Content viewed"
+
+ Scenario: Content deleted event
+ Given I navigate to "Reports > Live logs" in site administration
+ And I should not see "Content deleted"
+ And I am on "Course 1" course homepage
+ And I expand "Site pages" node
+ And I click on "Content bank" "link"
+ And I click on "Existing" "link"
+ And I open the action menu in "region-main-settings-menu" "region"
+ When I choose "Delete" in the open action menu
+ And I click on "Delete" "button" in the "Delete content" "dialogue"
+ And I navigate to "Reports > Live logs" in site administration
+ Then I should see "Content deleted"
+
+ Scenario: Content updated event when renaming
+ Given I navigate to "Reports > Live logs" in site administration
+ And I should not see "Content updated"
+ And I am on "Course 1" course homepage
+ And I expand "Site pages" node
+ And I click on "Content bank" "link"
+ And I click on "Existing" "link"
+ And I open the action menu in "region-main-settings-menu" "region"
+ When I choose "Rename" in the open action menu
+ And I set the field "Content name" to "New name"
+ And I click on "Rename" "button"
+ And I navigate to "Reports > Live logs" in site administration
+ Then I should see "Content updated"
\ No newline at end of file
diff --git a/contentbank/tests/contentbank_test.php b/contentbank/tests/contentbank_test.php
index 824f7a51a6b..e42045643ab 100644
--- a/contentbank/tests/contentbank_test.php
+++ b/contentbank/tests/contentbank_test.php
@@ -181,11 +181,11 @@ class core_contentbank_testcase extends advanced_testcase {
*
* @dataProvider search_contents_provider
* @param string $search String to search.
- * @param int $contextid Contextid to search.
+ * @param string $where Context where to search.
* @param int $expectedresult Expected result.
* @param array $contexts List of contexts where to create content.
*/
- public function test_search_contents(?string $search, int $contextid, int $expectedresult, array $contexts = []): void {
+ public function test_search_contents(?string $search, string $where, int $expectedresult, array $contexts = []): void {
global $DB;
$this->resetAfterTest();
@@ -195,11 +195,26 @@ class core_contentbank_testcase extends advanced_testcase {
$manager = $this->getDataGenerator()->create_user();
$this->getDataGenerator()->role_assign($managerroleid, $manager->id);
+ // Create a category and a course.
+ $coursecat = $this->getDataGenerator()->create_category();
+ $course = $this->getDataGenerator()->create_course();
+ $existingcontexts = [];
+ $existingcontexts['system'] = \context_system::instance();
+ $existingcontexts['category'] = \context_coursecat::instance($coursecat->id);
+ $existingcontexts['course'] = \context_course::instance($course->id);
+
+ if (empty($where)) {
+ $contextid = 0;
+ } else {
+ $contextid = $existingcontexts[$where]->id;
+ }
+
// Add some content to the content bank.
$generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
foreach ($contexts as $context) {
+ $contextinstance = $existingcontexts[$context];
$records = $generator->generate_contentbank_data('contenttype_h5p', 3,
- $manager->id, $context, false);
+ $manager->id, $contextinstance, false);
}
// Search for some content.
@@ -220,98 +235,126 @@ class core_contentbank_testcase extends advanced_testcase {
* @return array
*/
public function search_contents_provider(): array {
- // Create a category and a course.
- $systemcontext = \context_system::instance();
- $coursecat = $this->getDataGenerator()->create_category();
- $course = $this->getDataGenerator()->create_course();
- $coursecatcontext = \context_coursecat::instance($coursecat->id);
- $coursecontext = \context_course::instance($course->id);
return [
'Search all content in all contexts' => [
null,
- 0,
+ '',
9,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in all contexts for existing string in all contents' => [
'content',
- 0,
+ '',
9,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in all contexts for unexisting string in all contents' => [
'chocolate',
+ '',
0,
- 0,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in all contexts for existing string in some contents' => [
'1',
- 0,
+ '',
3,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in all contexts for existing string in some contents (create only 1 context)' => [
'1',
- 0,
+ '',
1,
- [$systemcontext]
+ ['system']
],
'Search in system context for existing string in all contents' => [
'content',
- $systemcontext->id,
+ 'system',
3,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in category context for unexisting string in all contents' => [
'chocolate',
- $coursecatcontext->id,
+ 'category',
0,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in course context for existing string in some contents' => [
'1',
- $coursecontext->id,
+ 'course',
1,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in system context' => [
null,
- $systemcontext->id,
+ 'system',
3,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in course context with existing content' => [
null,
- $coursecontext->id,
+ 'course',
3,
- [$systemcontext, $coursecatcontext, $coursecontext]
+ ['system', 'category', 'course']
],
'Search in course context without existing content' => [
null,
- $coursecontext->id,
+ 'course',
0,
- [$systemcontext, $coursecatcontext]
+ ['system', 'category']
],
'Search in an empty contentbank' => [
null,
- 0,
+ '',
0,
[]
],
'Search in a context in an empty contentbank' => [
null,
- $systemcontext->id,
+ 'system',
0,
[]
],
'Search for a string in an empty contentbank' => [
'content',
- 0,
+ '',
0,
[]
],
];
}
+
+ /**
+ * Test create_content_from_file function.
+ *
+ * @covers ::create_content_from_file
+ */
+ public function test_create_content_from_file() {
+ global $USER;
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $systemcontext = \context_system::instance();
+ $name = 'dummy_h5p.h5p';
+
+ // Create a dummy H5P file.
+ $dummyh5p = array(
+ 'contextid' => $systemcontext->id,
+ 'component' => 'contentbank',
+ 'filearea' => 'public',
+ 'itemid' => 1,
+ 'filepath' => '/',
+ 'filename' => $name,
+ 'userid' => $USER->id
+ );
+ $fs = get_file_storage();
+ $dummyh5pfile = $fs->create_file_from_string($dummyh5p, 'Dummy H5Pcontent');
+
+ $cb = new \core_contentbank\contentbank();
+ $content = $cb->create_content_from_file($systemcontext, $USER->id, $dummyh5pfile);
+
+ $this->assertEquals('contenttype_h5p', $content->get_content_type());
+ $this->assertInstanceOf('\\contenttype_h5p\\content', $content);
+ $this->assertEquals($name, $content->get_name());
+ }
}
diff --git a/contentbank/upload.php b/contentbank/upload.php
index 48b651e84b6..0a88a6928ee 100644
--- a/contentbank/upload.php
+++ b/contentbank/upload.php
@@ -72,25 +72,14 @@ if ($mform->is_cancelled()) {
redirect($returnurl);
} else if ($formdata = $mform->get_data()) {
require_sesskey();
-
- // Get the file and the contenttype to manage given file's extension.
- $usercontext = context_user::instance($USER->id);
+ // Get the file and create the content based on it.
+ $usercontext = \context_user::instance($USER->id);
$fs = get_file_storage();
$files = $fs->get_area_files($usercontext->id, 'user', 'draft', $formdata->file, 'itemid, filepath, filename', false);
-
if (!empty($files)) {
$file = reset($files);
- $filename = $file->get_filename();
- $extension = $cb->get_extension($filename);
- $plugin = $cb->get_extension_supporter($extension, $context);
- $classname = '\\contenttype_'.$plugin.'\\contenttype';
- $record = new stdClass();
- $record->name = $filename;
- if (class_exists($classname)) {
- $contentype = new $classname($context);
- $content = $contentype->create_content($record);
- file_save_draft_area_files($formdata->file, $contextid, 'contentbank', 'public', $content->get_id());
- }
+ $content = $cb->create_content_from_file($context, $USER->id, $file);
+ file_save_draft_area_files($formdata->file, $contextid, 'contentbank', 'public', $content->get_id());
}
redirect($returnurl);
}
diff --git a/lang/en/contentbank.php b/lang/en/contentbank.php
index 1d3b885f488..06958d6feca 100644
--- a/lang/en/contentbank.php
+++ b/lang/en/contentbank.php
@@ -28,6 +28,11 @@ $string['contentname'] = 'Content name';
$string['contentnotdeleted'] = 'An error was encountered while trying to delete the content.';
$string['contentnotrenamed'] = 'An error was encountered while trying to rename the content.';
$string['contentrenamed'] = 'The content has been renamed.';
+$string['eventcontentcreated'] = 'Content created';
+$string['eventcontentdeleted'] = 'Content deleted';
+$string['eventcontentupdated'] = 'Content updated';
+$string['eventcontentuploaded'] = 'Content uploaded';
+$string['eventcontentviewed'] = 'Content viewed';
$string['deletecontent'] = 'Delete content';
$string['deletecontentconfirm'] = 'Are you sure you want to delete the content \'{$a->name}\' and all associated files? This action cannot be undone.';
$string['file'] = 'Upload content';
diff --git a/lib/classes/event/contentbank_content_created.php b/lib/classes/event/contentbank_content_created.php
new file mode 100644
index 00000000000..bf651c7de02
--- /dev/null
+++ b/lib/classes/event/contentbank_content_created.php
@@ -0,0 +1,137 @@
+.
+
+/**
+ * Contentbank content created event.
+ *
+ * @package core
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Content bank content created class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ * - string contenttype: the contenttype of the content.
+ * - string name: the name of the content.
+ * }
+ *
+ * @package core
+ * @since Moodle 3.9
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contentbank_content_created extends base {
+
+ /**
+ * Initialise the event data.
+ */
+ protected function init() {
+ $this->data['objecttable'] = 'contentbank_content';
+ $this->data['crud'] = 'c';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Creates an event from content bank content object
+ *
+ * @since Moodle 3.9
+ * @param \stdClass $record Data to create the event
+ * @return contentbank_content_created
+ */
+ public static function create_from_record(\stdClass $record) {
+ $event = self::create([
+ 'objectid' => $record->id,
+ 'relateduserid' => $record->usercreated,
+ 'context' => \context::instance_by_id($record->contextid),
+ 'other' => [
+ 'contenttype' => $record->contenttype,
+ 'name' => $record->name
+ ]
+ ]);
+ return $event;
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventcontentcreated', 'core_contentbank');
+ }
+
+ /**
+ * Returns non-localised description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' created the content with id '$this->objectid'.";
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ * @return void
+ */
+ protected function validate_data() {
+ parent::validate_data();
+
+ if (!isset($this->other['contenttype'])) {
+ throw new \coding_exception('The \'contenttype\' value must be set in other.');
+ }
+
+ if (!isset($this->other['name'])) {
+ throw new \coding_exception('The \'name\' value must be set in other.');
+ }
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ $url = new \moodle_url('/contentbank/view.php');
+ $url->param('id', $this->objectid);
+ return $url;
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return array
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'contentbank_content', 'restore' => 'contentbank_content');
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return bool
+ */
+ public static function get_other_mapping() {
+ // No mapping required.
+ return false;
+ }
+}
diff --git a/lib/classes/event/contentbank_content_deleted.php b/lib/classes/event/contentbank_content_deleted.php
new file mode 100644
index 00000000000..6021d5a178b
--- /dev/null
+++ b/lib/classes/event/contentbank_content_deleted.php
@@ -0,0 +1,126 @@
+.
+
+/**
+ * Contentbank content deleted event.
+ *
+ * @package core
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Content bank content deleted class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ * - string contenttype: the contenttype of the content.
+ * - string name: the name of the content.
+ * }
+ *
+ * @package core
+ * @since Moodle 3.9
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contentbank_content_deleted extends base {
+
+ /**
+ * Initialise the event data.
+ */
+ protected function init() {
+ $this->data['objecttable'] = 'contentbank_content';
+ $this->data['crud'] = 'd';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Creates an event from content bank content object
+ *
+ * @since Moodle 3.9
+ * @param \stdClass $record Data to create the event
+ * @return contentbank_content_deleted
+ */
+ public static function create_from_record(\stdClass $record) {
+ $event = self::create([
+ 'objectid' => $record->id,
+ 'relateduserid' => $record->usercreated,
+ 'context' => \context::instance_by_id($record->contextid),
+ 'other' => [
+ 'contenttype' => $record->contenttype,
+ 'name' => $record->name
+ ]
+ ]);
+ return $event;
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventcontentdeleted', 'core_contentbank');
+ }
+
+ /**
+ * Returns non-localised description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' deleted the content with id '$this->objectid'.";
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ * @return void
+ */
+ protected function validate_data() {
+ parent::validate_data();
+
+ if (!isset($this->other['contenttype'])) {
+ throw new \coding_exception('The \'contenttype\' value must be set in other.');
+ }
+
+ if (!isset($this->other['name'])) {
+ throw new \coding_exception('The \'name\' value must be set in other.');
+ }
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return array
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'contentbank_content', 'restore' => 'contentbank_content');
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return bool
+ */
+ public static function get_other_mapping() {
+ // No mapping required.
+ return false;
+ }
+}
diff --git a/lib/classes/event/contentbank_content_updated.php b/lib/classes/event/contentbank_content_updated.php
new file mode 100644
index 00000000000..74a3420ead3
--- /dev/null
+++ b/lib/classes/event/contentbank_content_updated.php
@@ -0,0 +1,137 @@
+.
+
+/**
+ * Contentbank content uploaded event.
+ *
+ * @package core
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Content bank content updated class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ * - string contenttype: the contenttype of the content.
+ * - string name: the name of the content.
+ * }
+ *
+ * @package core
+ * @since Moodle 3.9
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contentbank_content_updated extends base {
+
+ /**
+ * Initialise the event data.
+ */
+ protected function init() {
+ $this->data['objecttable'] = 'contentbank_content';
+ $this->data['crud'] = 'u';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Creates an event from content bank content object
+ *
+ * @since Moodle 3.9
+ * @param \stdClass $record Data to create the event
+ * @return contentbank_content_updated
+ */
+ public static function create_from_record(\stdClass $record) {
+ $event = self::create([
+ 'objectid' => $record->id,
+ 'relateduserid' => $record->usercreated,
+ 'context' => \context::instance_by_id($record->contextid),
+ 'other' => [
+ 'contenttype' => $record->contenttype,
+ 'name' => $record->name
+ ]
+ ]);
+ return $event;
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventcontentupdated', 'core_contentbank');
+ }
+
+ /**
+ * Returns non-localised description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' updated the content with id '$this->objectid'.";
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ * @return void
+ */
+ protected function validate_data() {
+ parent::validate_data();
+
+ if (!isset($this->other['contenttype'])) {
+ throw new \coding_exception('The \'contenttype\' value must be set in other.');
+ }
+
+ if (!isset($this->other['name'])) {
+ throw new \coding_exception('The \'name\' value must be set in other.');
+ }
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ $url = new \moodle_url('/contentbank/view.php');
+ $url->param('id', $this->objectid);
+ return $url;
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return array
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'contentbank_content', 'restore' => 'contentbank_content');
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return bool
+ */
+ public static function get_other_mapping() {
+ // No mapping required.
+ return false;
+ }
+}
diff --git a/lib/classes/event/contentbank_content_uploaded.php b/lib/classes/event/contentbank_content_uploaded.php
new file mode 100644
index 00000000000..1080407e32e
--- /dev/null
+++ b/lib/classes/event/contentbank_content_uploaded.php
@@ -0,0 +1,137 @@
+.
+
+/**
+ * Contentbank content uploaded event.
+ *
+ * @package core
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Content bank content uploaded class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ * - string contenttype: the contenttype of the content.
+ * - string name: the name of the content.
+ * }
+ *
+ * @package core
+ * @since Moodle 3.9
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contentbank_content_uploaded extends base {
+
+ /**
+ * Initialise the event data.
+ */
+ protected function init() {
+ $this->data['objecttable'] = 'contentbank_content';
+ $this->data['crud'] = 'c';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Creates an event from content bank content object
+ *
+ * @since Moodle 3.9
+ * @param \stdClass $record Data to create the event
+ * @return contentbank_content_uploaded
+ */
+ public static function create_from_record(\stdClass $record) {
+ $event = self::create([
+ 'objectid' => $record->id,
+ 'relateduserid' => $record->usercreated,
+ 'context' => \context::instance_by_id($record->contextid),
+ 'other' => [
+ 'contenttype' => $record->contenttype,
+ 'name' => $record->name
+ ]
+ ]);
+ return $event;
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventcontentuploaded', 'core_contentbank');
+ }
+
+ /**
+ * Returns non-localised description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' uploaded the content with id '$this->objectid'.";
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ * @return void
+ */
+ protected function validate_data() {
+ parent::validate_data();
+
+ if (!isset($this->other['contenttype'])) {
+ throw new \coding_exception('The \'contenttype\' value must be set in other.');
+ }
+
+ if (!isset($this->other['name'])) {
+ throw new \coding_exception('The \'name\' value must be set in other.');
+ }
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ $url = new \moodle_url('/contentbank/view.php');
+ $url->param('id', $this->objectid);
+ return $url;
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return array
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'contentbank_content', 'restore' => 'contentbank_content');
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return bool
+ */
+ public static function get_other_mapping() {
+ // No mapping required.
+ return false;
+ }
+}
diff --git a/lib/classes/event/contentbank_content_viewed.php b/lib/classes/event/contentbank_content_viewed.php
new file mode 100644
index 00000000000..cd8b4d2dd4c
--- /dev/null
+++ b/lib/classes/event/contentbank_content_viewed.php
@@ -0,0 +1,137 @@
+.
+
+/**
+ * Contentbank content viewed event.
+ *
+ * @package core
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Content bank content updated class.
+ *
+ * @property-read array $other {
+ * Extra information about event.
+ * - string contenttype: the contenttype of the content.
+ * - string name: the name of the content.
+ * }
+ *
+ * @package core
+ * @since Moodle 3.9
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class contentbank_content_viewed extends base {
+
+ /**
+ * Initialise the event data.
+ */
+ protected function init() {
+ $this->data['objecttable'] = 'contentbank_content';
+ $this->data['crud'] = 'r';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Creates an event from content bank content object
+ *
+ * @since Moodle 3.9
+ * @param \stdClass $record Data to create the event
+ * @return contentbank_content_viewed
+ */
+ public static function create_from_record(\stdClass $record) {
+ $event = self::create([
+ 'objectid' => $record->id,
+ 'relateduserid' => $record->usercreated,
+ 'context' => \context::instance_by_id($record->contextid),
+ 'other' => [
+ 'contenttype' => $record->contenttype,
+ 'name' => $record->name
+ ]
+ ]);
+ return $event;
+ }
+
+ /**
+ * Returns localised general event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventcontentviewed', 'core_contentbank');
+ }
+
+ /**
+ * Returns non-localised description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '$this->userid' viewed the content with id '$this->objectid'.";
+ }
+
+ /**
+ * Custom validation.
+ *
+ * @throws \coding_exception
+ * @return void
+ */
+ protected function validate_data() {
+ parent::validate_data();
+
+ if (!isset($this->other['contenttype'])) {
+ throw new \coding_exception('The \'contenttype\' value must be set in other.');
+ }
+
+ if (!isset($this->other['name'])) {
+ throw new \coding_exception('The \'name\' value must be set in other.');
+ }
+ }
+
+ /**
+ * Returns relevant URL.
+ *
+ * @return \moodle_url
+ */
+ public function get_url() {
+ $url = new \moodle_url('/contentbank/view.php');
+ $url->param('id', $this->objectid);
+ return $url;
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return array
+ */
+ public static function get_objectid_mapping() {
+ return array('db' => 'contentbank_content', 'restore' => 'contentbank_content');
+ }
+
+ /**
+ * Used for mapping events on restore
+ *
+ * @return bool
+ */
+ public static function get_other_mapping() {
+ // No mapping required.
+ return false;
+ }
+}
diff --git a/lib/tests/event/contentbank_content_created_test.php b/lib/tests/event/contentbank_content_created_test.php
new file mode 100644
index 00000000000..0e381b9e6d3
--- /dev/null
+++ b/lib/tests/event/contentbank_content_created_test.php
@@ -0,0 +1,77 @@
+.
+
+/**
+ * Content bank created event tests.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Test for content bank created event.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core\event\contentbank_content_created
+ */
+class contentbank_content_created_testcase extends \advanced_testcase {
+
+ /**
+ * Setup to ensure that fixtures are loaded.
+ */
+ public static function setUpBeforeClass() {
+ global $CFG;
+
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+ }
+
+ /**
+ * Test the content created event.
+ *
+ * @covers ::create_from_record
+ */
+ public function test_content_created() {
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Save the system context.
+ $systemcontext = \context_system::instance();
+
+ // Trigger and capture the event when creating a content.
+ $sink = $this->redirectEvents();
+
+ // Create a content bank content.
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+ $contents = $generator->generate_contentbank_data('contenttype_testable', 1);
+ $content = array_shift($contents);
+
+ $events = $sink->get_events();
+ $event = reset($events);
+
+ // Check that the event data is valid.
+ $this->assertInstanceOf('\core\event\contentbank_content_created', $event);
+ $this->assertEquals($systemcontext, $event->get_context());
+ }
+}
diff --git a/lib/tests/event/contentbank_content_deleted_test.php b/lib/tests/event/contentbank_content_deleted_test.php
new file mode 100644
index 00000000000..20c7517d846
--- /dev/null
+++ b/lib/tests/event/contentbank_content_deleted_test.php
@@ -0,0 +1,83 @@
+.
+
+/**
+ * Content bank deleted event tests.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Test for content bank deleted event.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core\event\contentbank_content_deleted
+ */
+class contentbank_content_deleted_testcase extends \advanced_testcase {
+
+ /**
+ * Setup to ensure that fixtures are loaded.
+ */
+ public static function setUpBeforeClass() {
+ global $CFG;
+
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+ }
+
+ /**
+ * Test the content deleted event
+ *
+ * @covers ::create_from_record
+ */
+ public function test_content_deleted() {
+ global $DB;
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Save the system context.
+ $systemcontext = \context_system::instance();
+
+ // Create a content bank content.
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+ $contents = $generator->generate_contentbank_data('contenttype_testable', 3);
+ $content = array_shift($contents);
+ $this->assertEquals(3, $DB->count_records('contentbank_content'));
+
+ $classname = '\\contenttype_testable\\contenttype';
+ $contentype = new $classname($systemcontext);
+
+ // Trigger and capture the event for deleting a content.
+ $sink = $this->redirectEvents();
+ $contentype->delete_content($content);
+ $events = $sink->get_events();
+ $event = reset($events);
+
+ // Check that the content was deleted and the event data is valid.
+ $this->assertEquals(2, $DB->count_records('contentbank_content'));
+ $this->assertInstanceOf('\core\event\contentbank_content_deleted', $event);
+ $this->assertEquals(\context_system::instance(), $event->get_context());
+ }
+}
diff --git a/lib/tests/event/contentbank_content_updated_test.php b/lib/tests/event/contentbank_content_updated_test.php
new file mode 100644
index 00000000000..b6aeae790d3
--- /dev/null
+++ b/lib/tests/event/contentbank_content_updated_test.php
@@ -0,0 +1,82 @@
+.
+
+/**
+ * Content bank updated event tests.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Test for content bank updated event.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core\event\contentbank_content_updated
+ */
+class contentbank_content_updated_testcase extends \advanced_testcase {
+
+ /**
+ * Setup to ensure that fixtures are loaded.
+ */
+ public static function setUpBeforeClass() {
+ global $CFG;
+
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+ }
+
+ /**
+ * Test the content updated event.
+ *
+ * @covers ::create_from_record
+ */
+ public function test_content_updated() {
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Save the system context.
+ $systemcontext = \context_system::instance();
+
+ // Create a content bank content.
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+ $contents = $generator->generate_contentbank_data('contenttype_testable', 1);
+ $content = array_shift($contents);
+
+ // Store the name before we change it.
+ $oldname = $content->get_name();
+
+ // Trigger and capture the event when renaming a content.
+ $sink = $this->redirectEvents();
+
+ $newname = "New name";
+ $content->set_name($newname);
+ $events = $sink->get_events();
+ $event = reset($events);
+
+ // Check that the event data is valid.
+ $this->assertInstanceOf('\core\event\contentbank_content_updated', $event);
+ $this->assertEquals($systemcontext, $event->get_context());
+ }
+}
diff --git a/lib/tests/event/contentbank_content_uploaded_test.php b/lib/tests/event/contentbank_content_uploaded_test.php
new file mode 100644
index 00000000000..93e6d7d045d
--- /dev/null
+++ b/lib/tests/event/contentbank_content_uploaded_test.php
@@ -0,0 +1,94 @@
+.
+
+/**
+ * Content bank uploaded event tests.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+use core_contentbank\contentbank;
+
+/**
+ * Test for content bank uploaded event.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core\event\contentbank_content_uploaded
+ */
+class contentbank_content_uploaded_testcase extends \advanced_testcase {
+
+ /**
+ * Setup to ensure that fixtures are loaded.
+ */
+ public static function setUpBeforeClass() {
+ global $CFG;
+
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+ }
+
+ /**
+ * Test the content created event.
+ *
+ * @covers ::create_from_record
+ */
+ public function test_content_created() {
+ global $USER;
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+ $systemcontext = \context_system::instance();
+
+ // Create a dummy H5P file.
+ $dummyh5p = array(
+ 'contextid' => $systemcontext->id,
+ 'component' => 'contentbank',
+ 'filearea' => 'public',
+ 'itemid' => 1,
+ 'filepath' => '/',
+ 'filename' => 'dummy_h5p.h5p'
+ );
+ $fs = get_file_storage();
+ $dummyh5pfile = $fs->create_file_from_string($dummyh5p, 'Dummy H5Pcontent');
+
+ // Trigger and capture the event when creating content from a file.
+ $sink = $this->redirectEvents();
+ $cb = new contentbank();
+ $cb->create_content_from_file($systemcontext, $USER->id, $dummyh5pfile);
+
+ // Both uploaded and created events are raised.
+ $events = $sink->get_events();
+ $this->assertCount(2, $events);
+
+ // First the created content event has been raised.
+ $event = array_shift($events);
+ $this->assertInstanceOf('\core\event\contentbank_content_created', $event);
+ $this->assertEquals($systemcontext, $event->get_context());
+
+ // Second the uploaded content event has been raised.
+ $event = array_pop($events);
+ $this->assertInstanceOf('\core\event\contentbank_content_uploaded', $event);
+ $this->assertEquals($systemcontext, $event->get_context());
+ }
+}
diff --git a/lib/tests/event/contentbank_content_viewed_test.php b/lib/tests/event/contentbank_content_viewed_test.php
new file mode 100644
index 00000000000..acc7e53d521
--- /dev/null
+++ b/lib/tests/event/contentbank_content_viewed_test.php
@@ -0,0 +1,79 @@
+.
+
+/**
+ * Content bank viewed event tests.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Test for content bank viewed event.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Amaia Anabitarte
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core\event\contentbank_content_viewed
+ */
+class contentbank_content_viewed_testcase extends \advanced_testcase {
+
+ /**
+ * Setup to ensure that fixtures are loaded.
+ */
+ public static function setUpBeforeClass() {
+ global $CFG;
+
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+ }
+
+ /**
+ * Test the content updated event.
+ *
+ * @covers ::create_from_record
+ */
+ public function test_content_updated() {
+
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Save the system context.
+ $systemcontext = \context_system::instance();
+
+ // Create a content bank content.
+ $generator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+ $contents = $generator->generate_contentbank_data('contenttype_testable', 1);
+ $content = array_shift($contents);
+
+ // Trigger and capture the content viewed event.
+ $sink = $this->redirectEvents();
+ $eventtotrigger = \core\event\contentbank_content_viewed::create_from_record($content->get_content());
+ $eventtotrigger->trigger();
+
+ $events = $sink->get_events();
+ $event = reset($events);
+
+ // Check that the event data is valid.
+ $this->assertInstanceOf('\core\event\contentbank_content_viewed', $event);
+ $this->assertEquals($systemcontext, $event->get_context());
+ }
+}