diff --git a/blocks/recent_activity/block_recent_activity.php b/blocks/recent_activity/block_recent_activity.php index c58c3596854..ed3ffcbaeb5 100644 --- a/blocks/recent_activity/block_recent_activity.php +++ b/blocks/recent_activity/block_recent_activity.php @@ -125,54 +125,65 @@ class block_recent_activity extends block_base { * 'module' - instance of cm_info (for 'delete mod' it is an object with attributes modname and modfullname) */ protected function get_structural_changes() { - global $DB, $CFG; + global $DB; $course = $this->page->course; + $context = context_course::instance($course->id); + $canviewdeleted = has_capability('block/recent_activity:viewdeletemodule', $context); + $canviewupdated = has_capability('block/recent_activity:viewaddupdatemodule', $context); + if (!$canviewdeleted && !$canviewupdated) { + return; + } + $timestart = $this->get_timestart(); $changelist = array(); - $logs = $DB->get_records_select('log', - "time > ? AND course = ? AND - module = 'course' AND - (action = 'add mod' OR action = 'update mod' OR action = 'delete mod')", - array($timestart, $course->id), "id ASC"); + // The following query will retrieve the latest action for each course module in the specified course. + // Also the query filters out the modules that were created and then deleted during the given interval. + $sql = "SELECT + cmid, MIN(action) AS minaction, MAX(action) AS maxaction, MAX(modname) AS modname + FROM {block_recent_activity} + WHERE timecreated > ? AND courseid = ? + GROUP BY cmid + ORDER BY MAX(timecreated) ASC"; + $params = array($timestart, $course->id); + $logs = $DB->get_records_sql($sql, $params); + if (isset($logs[0])) { + // If special record for this course and cmid=0 is present, migrate logs. + self::migrate_logs($course); + $logs = $DB->get_records_sql($sql, $params); + } if ($logs) { $modinfo = get_fast_modinfo($course); - $newgones = array(); // added and later deleted items - foreach ($logs as $key => $log) { - $info = explode(' ', $log->info); + foreach ($logs as $log) { + // We used aggregate functions since constants CM_CREATED, CM_UPDATED and CM_DELETED have ascending order (0,1,2). + $wasdeleted = ($log->maxaction == block_recent_activity_observer::CM_DELETED); + $wascreated = ($log->minaction == block_recent_activity_observer::CM_CREATED); - if (count($info) != 2) { - debugging("Incorrect log entry info: id = ".$log->id, DEBUG_DEVELOPER); + if ($wasdeleted && $wascreated) { + // Activity was created and deleted within this interval. Do not show it. continue; - } - - $modname = $info[0]; - $instanceid = $info[1]; - - if ($log->action == 'delete mod') { - if (plugin_supports('mod', $modname, FEATURE_NO_VIEW_LINK, false)) { - // we should better call cm_info::has_view() because it can be - // dynamic. But there is no instance of cm_info now + } else if ($wasdeleted && $canviewdeleted) { + if (plugin_supports('mod', $log->modname, FEATURE_NO_VIEW_LINK, false)) { + // Better to call cm_info::has_view() because it can be dynamic. + // But there is no instance of cm_info now. continue; } - // unfortunately we do not know if the mod was visible - if (!array_key_exists($log->info, $newgones)) { - $changelist[$log->info] = array('action' => $log->action, - 'module' => (object)array( - 'modname' => $modname, - 'modfullname' => get_string('modulename', $modname) - )); - } - } else { - if (!isset($modinfo->instances[$modname][$instanceid])) { - if ($log->action == 'add mod') { - // do not display added and later deleted activities - $newgones[$log->info] = true; - } - continue; - } - $cm = $modinfo->instances[$modname][$instanceid]; - if ($cm->has_view() && $cm->uservisible && empty($changelist[$log->info])) { - $changelist[$log->info] = array('action' => $log->action, 'module' => $cm); + // Unfortunately we do not know if the mod was visible. + $modnames = get_module_types_names(); + $changelist[$log->cmid] = array('action' => 'delete mod', + 'module' => (object)array( + 'modname' => $log->modname, + 'modfullname' => $modnames[$log->modname] + )); + + } else if (!$wasdeleted && isset($modinfo->cms[$log->cmid]) && $canviewupdated) { + // Module was either added or updated during this interval and it currently exists. + // If module was both added and updated show only "add" action. + $cm = $modinfo->cms[$log->cmid]; + if ($cm->has_view() && $cm->uservisible) { + $changelist[$log->cmid] = array( + 'action' => $wascreated ? 'add mod' : 'update mod', + 'module' => $cm + ); } } } @@ -216,5 +227,83 @@ class block_recent_activity extends block_base { function applicable_formats() { return array('all' => true, 'my' => false, 'tag' => false); } + + /** + * Remove old entries from table block_recent_activity + */ + public function cron() { + global $DB; + // Those entries will never be displayed as RECENT anyway. + $DB->delete_records_select('block_recent_activity', 'timecreated < ?', + array(time() - COURSE_MAX_RECENT_PERIOD)); + } + + /** + * Migrates entries from table {log} into {block_recent_activity} + * + * We only migrate logs for the courses that actually have recent activity + * block and that are being viewed within COURSE_MAX_RECENT_PERIOD time + * after the upgrade. + * + * The presence of entry in {block_recent_activity} with the cmid=0 indicates + * that the course needs log migration. Those entries were installed in + * db/upgrade.php when the table block_recent_activity was created. + * + * @param stdClass $course + */ + protected static function migrate_logs($course) { + global $DB; + if (!$logstarted = $DB->get_record('block_recent_activity', + array('courseid' => $course->id, 'cmid' => 0), + 'id, timecreated')) { + return; + } + $DB->delete_records('block_recent_activity', array('id' => $logstarted->id)); + try { + $logs = $DB->get_records_select('log', + "time > ? AND time < ? AND course = ? AND + module = 'course' AND + (action = 'add mod' OR action = 'update mod' OR action = 'delete mod')", + array(time()-COURSE_MAX_RECENT_PERIOD, $logstarted->timecreated, $course->id), + 'id ASC', 'id, time, userid, cmid, action, info'); + } catch (Exception $e) { + // Probably table {log} was already removed. + return; + } + if (!$logs) { + return; + } + $modinfo = get_fast_modinfo($course); + $entries = array(); + foreach ($logs as $log) { + $info = explode(' ', $log->info); + if (count($info) != 2) { + continue; + } + $modname = $info[0]; + $instanceid = $info[1]; + $entry = array('courseid' => $course->id, 'userid' => $log->userid, + 'timecreated' => $log->time, 'modname' => $modname); + if ($log->action == 'delete mod') { + if (!$log->cmid) { + continue; + } + $entry['action'] = 2; + $entry['cmid'] = $log->cmid; + } else { + if (!isset($modinfo->instances[$modname][$instanceid])) { + continue; + } + $entry['cmid'] = $modinfo->instances[$modname][$instanceid]->id; + if ($log->action == 'add mod') { + $entry['action'] = 0; + } else { + $entry['action'] = 1; + } + } + $entries[] = $entry; + } + $DB->insert_records('block_recent_activity', $entries); + } } diff --git a/blocks/recent_activity/classes/observer.php b/blocks/recent_activity/classes/observer.php new file mode 100644 index 00000000000..ad55559c125 --- /dev/null +++ b/blocks/recent_activity/classes/observer.php @@ -0,0 +1,73 @@ +. + +/** + * Event observer. + * + * @package block_recent_activity + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Event observer. + * Stores all actions about modules create/update/delete in plugin own's table. + * This allows the block to avoid expensive queries to the log table. + * + * @package block_recent_activity + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class block_recent_activity_observer { + + /** @var int indicates that course module was created */ + const CM_CREATED = 0; + /** @var int indicates that course module was udpated */ + const CM_UPDATED = 1; + /** @var int indicates that course module was deleted */ + const CM_DELETED = 2; + + /** + * Store all actions about modules create/update/delete in own table. + * + * @param \core\event\base $event + */ + public static function store(\core\event\base $event) { + global $DB; + $eventdata = new \stdClass(); + switch ($event->eventname) { + case '\core\event\course_module_created': + $eventdata->action = self::CM_CREATED; + break; + case '\core\event\course_module_updated': + $eventdata->action = self::CM_UPDATED; + break; + case '\core\event\course_module_deleted': + $eventdata->action = self::CM_DELETED; + $eventdata->modname = $event->other['modulename']; + break; + default: + return; + } + $eventdata->timecreated = $event->timecreated; + $eventdata->courseid = $event->courseid; + $eventdata->cmid = $event->objectid; + $eventdata->userid = $event->userid; + $DB->insert_record('block_recent_activity', $eventdata); + } +} diff --git a/blocks/recent_activity/db/access.php b/blocks/recent_activity/db/access.php index 08ed88c5b01..80530bc0c73 100644 --- a/blocks/recent_activity/db/access.php +++ b/blocks/recent_activity/db/access.php @@ -38,4 +38,20 @@ $capabilities = array( 'clonepermissionsfrom' => 'moodle/site:manageblocks' ), + + 'block/recent_activity:viewaddupdatemodule' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ), + + 'block/recent_activity:viewdeletemodule' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ) ); diff --git a/blocks/recent_activity/db/events.php b/blocks/recent_activity/db/events.php new file mode 100644 index 00000000000..0819cdb8272 --- /dev/null +++ b/blocks/recent_activity/db/events.php @@ -0,0 +1,47 @@ +. + +/** + * Event observer. + * + * @package block_recent_activity + * @category event + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$observers = array ( + array ( + 'eventname' => '\core\event\course_module_created', + 'callback' => 'block_recent_activity_observer::store', + 'internal' => false, // This means that we get events only after transaction commit. + 'priority' => 1000, + ), + array ( + 'eventname' => '\core\event\course_module_updated', + 'callback' => 'block_recent_activity_observer::store', + 'internal' => false, // This means that we get events only after transaction commit. + 'priority' => 1000, + ), + array ( + 'eventname' => '\core\event\course_module_deleted', + 'callback' => 'block_recent_activity_observer::store', + 'internal' => false, // This means that we get events only after transaction commit. + 'priority' => 1000, + ), +); diff --git a/blocks/recent_activity/db/install.xml b/blocks/recent_activity/db/install.xml new file mode 100644 index 00000000000..34fc9ff350b --- /dev/null +++ b/blocks/recent_activity/db/install.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/blocks/recent_activity/db/upgrade.php b/blocks/recent_activity/db/upgrade.php new file mode 100644 index 00000000000..3d8b59f01cc --- /dev/null +++ b/blocks/recent_activity/db/upgrade.php @@ -0,0 +1,85 @@ +. + +/** + * This file keeps track of upgrades to the recent activity block + * + * Sometimes, changes between versions involve alterations to database structures + * and other major things that may break installations. + * + * The upgrade function in this file will attempt to perform all the necessary + * actions to upgrade your older installation to the current version. + * + * If there's something it cannot do itself, it will tell you what you need to do. + * + * The commands in here will all be database-neutral, using the methods of + * database_manager class + * + * Please do not forget to use upgrade_set_timeout() + * before any action that may take longer time to finish. + * + * @package block_recent_activity + * @copyright 2014 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Upgrade code for the recent activity block. + * + * @global moodle_database $DB + * @param int $oldversion + * @param object $block + */ +function xmldb_block_recent_activity_upgrade($oldversion, $block) { + global $CFG, $DB; + + $dbman = $DB->get_manager(); // loads ddl manager and xmldb classes + + if ($oldversion < 2014012000) { + + // Define table block_recent_activity to be created. + $table = new xmldb_table('block_recent_activity'); + + // Adding fields to table block_recent_activity. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('cmid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('action', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null); + $table->add_field('modname', XMLDB_TYPE_CHAR, '20', null, null, null, null); + + // Adding keys to table block_recent_activity. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table block_recent_activity. + $table->add_index('coursetime', XMLDB_INDEX_NOTUNIQUE, array('courseid', 'timecreated')); + + // Conditionally launch create table for block_recent_activity. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + // Insert dummy log record for each existing course to notify that their logs need to be migrated. + $DB->execute('INSERT INTO {block_recent_activity} (timecreated, userid, courseid, cmid, action) '. + 'SELECT ?, 0, id, 0, 3 FROM {course}', + array(time())); + } + + // Recent_activity savepoint reached. + upgrade_block_savepoint(true, 2014012000, 'recent_activity'); + } + + return true; +} diff --git a/blocks/recent_activity/lang/en/block_recent_activity.php b/blocks/recent_activity/lang/en/block_recent_activity.php index 75113e1249a..f7d1333978a 100644 --- a/blocks/recent_activity/lang/en/block_recent_activity.php +++ b/blocks/recent_activity/lang/en/block_recent_activity.php @@ -25,3 +25,5 @@ $string['pluginname'] = 'Recent activity'; $string['recent_activity:addinstance'] = 'Add a new recent activity block'; +$string['recent_activity:viewaddupdatemodule'] = 'View added and updated modules in recent activity block'; +$string['recent_activity:viewdeletemodule'] = 'View deleted modules in recent activity block'; diff --git a/blocks/recent_activity/tests/behat/structural_changes.feature b/blocks/recent_activity/tests/behat/structural_changes.feature new file mode 100644 index 00000000000..31911e3a816 --- /dev/null +++ b/blocks/recent_activity/tests/behat/structural_changes.feature @@ -0,0 +1,211 @@ +@block @block_recent_activity +Feature: View structural changes in recent activity block + In order to know when activities were changed + As a user + In need to see the structural changes in recent activity block + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Terry1 | Teacher1 | teacher1@asd.com | + | assistant1 | Terry2 | Teacher2 | teacher2@asd.com | + | student1 | Sam1 | Student1 | student1@asd.com | + | student2 | Sam2 | Student2 | student2@asd.com | + | student3 | Sam3 | Student3 | student3@asd.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | assistant1 | C1 | teacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + And the following "groups" exist: + | name | course | idnumber | + | Group 1 | C1 | G1 | + | Group 2 | C1 | G2 | + And the following "groupings" exist: + | name | course | idnumber | + | Grouping 1 | C1 | GG1 | + | Grouping 2 | C1 | GG2 | + | Grouping 3 | C1 | GG3 | + And the following "group members" exist: + | user | group | + | student1 | G1 | + | student2 | G2 | + | student3 | G1 | + | student3 | G2 | + | assistant1 | G1 | + And the following "grouping groups" exist: + | grouping | group | + | GG1 | G1 | + | GG2 | G2 | + | GG3 | G1 | + | GG3 | G2 | + + Scenario: Check that Added module information is displayed respecting view capability + Given I log in as "admin" + And I set the following administration settings values: + | Enable group members only | 1 | + And I log out + And I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + When I add a "Forum" to section "1" and I fill the form with: + | name | ForumVisibleGroups | + | Description | No description | + | groupmode | Visible groups | + And I add a "Forum" to section "1" and I fill the form with: + | name | ForumSeparateGroups | + | Description | No description | + | groupmode | Separate groups | + And I add a "Forum" to section "1" and I fill the form with: + | name | ForumHidden | + | Description | No description | + | Visible | 0 | + And I add a "Forum" to section "1" and I fill the form with: + | name | ForumNoGroups | + | Description | No description | + | groupmode | No groups | + And I add a "Forum" to section "2" and I fill the form with: + | name | ForumVisibleGroupsG1 | + | Description | No description | + | groupmode | Visible groups | + | Grouping | Grouping 1 | + | Available for group members only | 1 | + And I add a "Forum" to section "2" and I fill the form with: + | name | ForumSeparateGroupsG1 | + | Description | No description | + | groupmode | Separate groups | + | Grouping | Grouping 1 | + | Available for group members only | 1 | + And I add a "Forum" to section "3" and I fill the form with: + | name | ForumVisibleGroupsG2 | + | Description | No description | + | groupmode | Visible groups | + | Grouping | Grouping 2 | + | Available for group members only | 1 | + And I add a "Forum" to section "3" and I fill the form with: + | name | ForumSeparateGroupsG2 | + | Description | No description | + | groupmode | Separate groups | + | Grouping | Grouping 2 | + | Available for group members only | 1 | + Then I should see "ForumVisibleGroups" in the "Recent activity" "block" + And I should see "ForumSeparateGroups" in the "Recent activity" "block" + And I should see "ForumNoGroups" in the "Recent activity" "block" + And I should see "ForumHidden" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + And I log in as "student1" + And I follow "Course 1" + And I should see "ForumVisibleGroups" in the "Recent activity" "block" + And I should see "ForumSeparateGroups" in the "Recent activity" "block" + And I should see "ForumNoGroups" in the "Recent activity" "block" + And I should not see "ForumHidden" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should not see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should not see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + And I log in as "student2" + And I follow "Course 1" + And I should see "ForumVisibleGroups" in the "Recent activity" "block" + And I should see "ForumSeparateGroups" in the "Recent activity" "block" + And I should see "ForumNoGroups" in the "Recent activity" "block" + And I should not see "ForumHidden" in the "Recent activity" "block" + And I should not see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should not see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + And I log in as "student3" + And I follow "Course 1" + And I should see "ForumVisibleGroups" in the "Recent activity" "block" + And I should see "ForumSeparateGroups" in the "Recent activity" "block" + And I should see "ForumNoGroups" in the "Recent activity" "block" + And I should not see "ForumHidden" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + # Teachers have capability to see all groups and hidden activities + And I log in as "assistant1" + And I follow "Course 1" + And I should see "ForumHidden" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG1" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG1" in the "Recent activity" "block" + And I should see "ForumVisibleGroupsG2" in the "Recent activity" "block" + And I should see "ForumSeparateGroupsG2" in the "Recent activity" "block" + And I log out + + Scenario: Updates and deletes in recent activity block + When I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I add a "Forum" to section "1" and I fill the form with: + | name | ForumNew | + | Description | No description | + Then I should see "Added Forum" in the "Recent activity" "block" + And I should see "ForumNew" in the "Recent activity" "block" + And I log out + And I log in as "student1" + And I follow "Course 1" + And I should see "Added Forum" in the "Recent activity" "block" + And I should see "ForumNew" in the "Recent activity" "block" + And I log out + # Update forum as a teacher + And I log in as "teacher1" + And I follow "Course 1" + And I follow "ForumNew" + And I click on "Edit settings" "link" in the "Administration" "block" + And I set the following fields to these values: + | name | ForumUpdated | + And I press "Save and return to course" + And I log out + # Student 1 already saw that forum was created, now he can see that forum was updated + And I log in as "student1" + And I follow "Course 1" + And I should not see "Added Forum" in the "Recent activity" "block" + And I should not see "ForumNew" in the "Recent activity" "block" + And I should see "Updated Forum" in the "Recent activity" "block" + And I should see "ForumUpdated" in the "Recent activity" "block" + And I log out + # Student 2 has bigger interval and he can see one entry that forum was created but with the new name + And I log in as "student2" + And I follow "Course 1" + And I should see "Added Forum" in the "Recent activity" "block" + And I should not see "ForumNew" in the "Recent activity" "block" + And I should not see "Updated Forum" in the "Recent activity" "block" + And I should see "ForumUpdated" in the "Recent activity" "block" + And I log out + # Delete forum as a teacher + And I log in as "teacher1" + And I follow "Course 1" + And I turn editing mode on + And I delete "ForumUpdated" activity + And I log out + # Students 1 and 2 see that forum was deleted + And I log in as "student1" + And I follow "Course 1" + And I should not see "Added Forum" in the "Recent activity" "block" + And I should not see "ForumNew" in the "Recent activity" "block" + And I should not see "Updated Forum" in the "Recent activity" "block" + And I should not see "ForumUpdated" in the "Recent activity" "block" + And I should see "Deleted Forum" in the "Recent activity" "block" + And I log out + # Student 3 never knew that forum was created, so he does not see anything + And I log in as "student3" + And I follow "Course 1" + And I should not see "Added Forum" in the "Recent activity" "block" + And I should not see "ForumNew" in the "Recent activity" "block" + And I should not see "Updated Forum" in the "Recent activity" "block" + And I should not see "ForumUpdated" in the "Recent activity" "block" + And I should not see "Deleted Forum" in the "Recent activity" "block" + And I log out diff --git a/blocks/recent_activity/version.php b/blocks/recent_activity/version.php index 26d485969f4..f3253346caf 100644 --- a/blocks/recent_activity/version.php +++ b/blocks/recent_activity/version.php @@ -24,6 +24,7 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2013110500; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2014012001; // The current plugin version (Date: YYYYMMDDXX) $plugin->requires = 2013110500; // Requires this Moodle version $plugin->component = 'block_recent_activity'; // Full name of the plugin (used for diagnostics) +$plugin->cron = 24*3600; // Cron interval 1 day. \ No newline at end of file