Merge branch 'MDL-65326-master' of https://github.com/dmitriim/moodle
authorJake Dallimore <jake@moodle.com>
Wed, 29 May 2019 01:27:17 +0000 (09:27 +0800)
committerJake Dallimore <jake@moodle.com>
Wed, 29 May 2019 01:27:17 +0000 (09:27 +0800)
36 files changed:
admin/roles/classes/permissions_table.php
admin/roles/permissions.php
admin/settings/courses.php
admin/settings/subsystems.php
analytics/tests/analysis_test.php
badges/renderer.php
calendar/classes/external/event_exporter_base.php
calendar/templates/event_details.mustache
course/tests/courselib_test.php
lib/amd/build/permissionmanager.min.js
lib/amd/src/permissionmanager.js
lib/behat/classes/partial_named_selector.php
lib/outputlib.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/permissionmanager.feature [new file with mode: 0644]
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_state_manager.min.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_state_manager.js
message/classes/helper.php
message/templates/message_drawer_view_conversation_body.mustache
message/tests/behat/delete_messages.feature [new file with mode: 0644]
message/tests/behat/favourite_conversations.feature
message/tests/behat/group_conversation.feature
message/tests/behat/message_delete_conversation.feature
message/tests/behat/message_drawer_manage_contacts.feature
message/tests/behat/message_manage_preferences.feature
message/tests/behat/message_send_messages.feature
message/tests/behat/self_conversation.feature
message/tests/behat/unread_messages.feature
mod/data/db/upgrade.php
mod/data/lib.php
mod/data/version.php
question/type/missingtype/tests/missingtype_test.php

index 80e4b20..b877a70 100644 (file)
@@ -96,7 +96,7 @@ class core_role_permissions_table extends core_role_capability_table_base {
                                   "linkclass" => "preventlink", "adminurl" => $adminurl->out(), "icon" => "", "iconalt" => "");
                 if (isset($overridableroles[$id]) and ($allowoverrides or ($allowsafeoverrides and is_safe_capability($capability)))) {
                     $templatecontext['icon'] = 't/delete';
-                    $templatecontext['iconalt'] = get_string('delete');
+                    $templatecontext['iconalt'] = get_string('deletexrole', 'core_role', $name);
                 }
                 $neededroles[$id] = $renderer->render_from_template('core/permissionmanager_role', $templatecontext);
             }
@@ -109,7 +109,7 @@ class core_role_permissions_table extends core_role_capability_table_base {
                                 "icon" => "", "iconalt" => "");
                 if (isset($overridableroles[$id]) and prohibit_is_removable($id, $context, $capability->name)) {
                     $templatecontext['icon'] = 't/delete';
-                    $templatecontext['iconalt'] = get_string('delete');
+                    $templatecontext['iconalt'] = get_string('deletexrole', 'core_role', $name);
                 }
                 $forbiddenroles[$id] = $renderer->render_from_template('core/permissionmanager_role', $templatecontext);
             }
index 89668dc..bc54948 100644 (file)
@@ -213,7 +213,7 @@ $arguments = array('contextid' => $contextid,
 $PAGE->requires->strings_for_js(
                                 array('roleprohibitinfo', 'roleprohibitheader', 'roleallowinfo', 'roleallowheader',
                                     'confirmunassigntitle', 'confirmroleunprohibit', 'confirmroleprevent', 'confirmunassignyes',
-                                    'confirmunassignno'), 'core_role');
+                                    'confirmunassignno', 'deletexrole'), 'core_role');
 $PAGE->requires->js_call_amd('core/permissionmanager', 'initialize', array($arguments));
 $table = new core_role_permissions_table($context, $contextname, $allowoverrides, $allowsafeoverrides, $overridableroles);
 echo $OUTPUT->box_start('generalbox capbox');
index 99892d6..ace90d3 100644 (file)
@@ -459,27 +459,31 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $ADMIN->add('backups', $temp);
 
     // Create a page for asynchronous backup and restore configuration and defaults.
-    if (!empty($CFG->enableasyncbackup)) {  // Only add settings if async mode is enable at site level.
-        $temp = new admin_settingpage('asyncgeneralsettings', new lang_string('asyncgeneralsettings', 'backup'));
-
-        $temp->add(new admin_setting_configcheckbox(
-                'backup/backup_async_message_users',
-                new lang_string('asyncemailenable', 'backup'),
-                new lang_string('asyncemailenabledetail', 'backup'), 0));
-
-        $temp->add(new admin_setting_configtext(
-                'backup/backup_async_message_subject',
-                new lang_string('asyncmessagesubject', 'backup'),
-                new lang_string('asyncmessagesubjectdetail', 'backup'),
-                new lang_string('asyncmessagesubjectdefault', 'backup')));
-
-        $temp->add(new admin_setting_confightmleditor(
-                'backup/backup_async_message',
-                new lang_string('asyncmessagebody', 'backup'),
-                new lang_string('asyncmessagebodydetail', 'backup'),
-                new lang_string('asyncmessagebodydefault', 'backup')));
-
-        $ADMIN->add('backups', $temp);
-    }
+    $temp = new admin_settingpage('asyncgeneralsettings', new lang_string('asyncgeneralsettings', 'backup'));
+
+    $temp->add(new admin_setting_configcheckbox('enableasyncbackup', new lang_string('enableasyncbackup', 'backup'),
+            new lang_string('enableasyncbackup_help', 'backup'), 0, 1, 0));
+
+    $temp->add(new admin_setting_configcheckbox(
+            'backup/backup_async_message_users',
+            new lang_string('asyncemailenable', 'backup'),
+            new lang_string('asyncemailenabledetail', 'backup'), 0));
+    $temp->hide_if('backup/backup_async_message_users', 'enableasyncbackup');
+
+    $temp->add(new admin_setting_configtext(
+            'backup/backup_async_message_subject',
+            new lang_string('asyncmessagesubject', 'backup'),
+            new lang_string('asyncmessagesubjectdetail', 'backup'),
+            new lang_string('asyncmessagesubjectdefault', 'backup')));
+    $temp->hide_if('backup/backup_async_message_subject', 'backup/backup_async_message_users');
+
+    $temp->add(new admin_setting_confightmleditor(
+            'backup/backup_async_message',
+            new lang_string('asyncmessagebody', 'backup'),
+            new lang_string('asyncmessagebodydetail', 'backup'),
+            new lang_string('asyncmessagebodydefault', 'backup')));
+    $temp->hide_if('backup/backup_async_message', 'backup/backup_async_message_users');
+
+    $ADMIN->add('backups', $temp);
 
 }
index 0b62749..e5c660b 100644 (file)
@@ -51,7 +51,4 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $optionalsubsystems->add(new admin_setting_configcheckbox('enablecoursepublishing',
         new lang_string('enablecoursepublishing', 'hub'), new lang_string('enablecoursepublishing_help', 'hub'), 0));
-
-    $optionalsubsystems->add(new admin_setting_configcheckbox('enableasyncbackup', new lang_string('enableasyncbackup', 'backup'),
-        new lang_string('enableasyncbackup_help', 'backup'), 0, 1, 0));
 }
index 0f5f3e6..3ceb40e 100644 (file)
@@ -65,7 +65,7 @@ class analytics_analysis_testcase extends advanced_testcase {
         $this->assertEquals($afewsecsago, $firstanalyses[$modelid . '_' . $course1->id]);
         $this->assertEquals($afewsecsago + 1, $firstanalyses[$modelid . '_' . $course2->id]);
 
-        // The cached elements gets refreshed.
+        // The cached elements get refreshed.
         $this->insert_used($modelid, $course1->id, 'prediction', $earliest);
         $firstanalyses = \core_analytics\analysis::fill_firstanalyses_cache($modelid, $course1->id);
         $this->assertCount(1, $firstanalyses);
@@ -78,7 +78,7 @@ class analytics_analysis_testcase extends advanced_testcase {
 
         // The generated ranges should start from the cached firstanalysis value, which is $earliest.
         $ranges = $seconds->get_all_ranges();
-        $this->assertCount(7, $ranges);
+        $this->assertGreaterThanOrEqual(7, count($ranges));
         $firstrange = reset($ranges);
         $this->assertEquals($earliest, $firstrange['time']);
     }
index 7e86ba2..74ce368 100644 (file)
@@ -420,12 +420,14 @@ class core_badges_renderer extends plugin_renderer_base {
 
         $output .= $this->output->heading(get_string('issuancedetails', 'badges'), 3);
         $dl = array();
-        $issued['issuedOn'] = !preg_match( '~^[1-9][0-9]*$~', $issued['issuedOn'] ) ?
-            strtotime($issued['issuedOn']) : $issued['issuedOn'];
+        if (!is_numeric($issued['issuedOn'])) {
+            $issued['issuedOn'] = strtotime($issued['issuedOn']);
+        }
         $dl[get_string('dateawarded', 'badges')] = userdate($issued['issuedOn']);
         if (isset($issued['expires'])) {
-            $issued['expires'] = !preg_match( '~^[1-9][0-9]*$~', $issued['expires'] ) ?
-                strtotime($issued['expires']) : $issued['expires'];
+            if (!is_numeric($issued['expires'])) {
+                $issued['expires'] = strtotime($issued['expires']);
+            }
             if ($issued['expires'] < $now) {
                 $dl[get_string('expirydate', 'badges')] = userdate($issued['expires']) . get_string('warnexpired', 'badges');
 
@@ -508,7 +510,7 @@ class core_badges_renderer extends plugin_renderer_base {
         }
         $output .= html_writer::empty_tag('img', array('src' => $issued->image, 'width' => '100'));
         if (isset($assertion->expires)) {
-            $expiration = !strtotime($assertion->expires) ? s($assertion->expires) : strtotime($assertion->expires);
+            $expiration = is_numeric($assertion->expires) ? $assertion->expires : strtotime($assertion->expires);
             if ($expiration < $today) {
                 $output .= $this->output->pix_icon('i/expired',
                         get_string('expireddate', 'badges', userdate($expiration)),
@@ -564,7 +566,7 @@ class core_badges_renderer extends plugin_renderer_base {
 
         $dl = array();
         if (isset($assertion->issued_on)) {
-            $issuedate = !strtotime($assertion->issued_on) ? s($assertion->issued_on) : strtotime($assertion->issued_on);
+            $issuedate = is_numeric($assertion->issued_on) ? $assertion->issued_on : strtotime($assertion->issued_on);
             $dl[get_string('dateawarded', 'badges')] = userdate($issuedate);
         }
         if (isset($assertion->expires)) {
index 08d14ac..f761f70 100644 (file)
@@ -275,7 +275,9 @@ class event_exporter_base extends exporter {
         }
         $timesort = $event->get_times()->get_sort_time()->getTimestamp();
         $iconexporter = new event_icon_exporter($event, ['context' => $context]);
-        $values['normalisedeventtypetext'] = get_string('type' . $values['normalisedeventtype'], 'calendar');
+        $identifier = 'type' . $values['normalisedeventtype'];
+        $stringexists = get_string_manager()->string_exists($identifier, 'calendar');
+        $values['normalisedeventtypetext'] = $stringexists ? get_string($identifier, 'calendar') : '';
 
         $values['icon'] = $iconexporter->export($output);
 
index 86fcda4..1a1516f 100644 (file)
     <div class="col-xs-1">{{#pix}} i/calendareventtime, core, {{#str}} when, core_calendar {{/str}} {{/pix}}</div>
     <div class="col-xs-11">{{{formattedtime}}}</div>
 </div>
-<div class="row mt-1">
-    <div class="col-xs-1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
-    <div class="col-xs-11">{{normalisedeventtypetext}}</div>
-</div>
+{{#normalisedeventtypetext}}
+    <div class="row mt-1">
+        <div class="col-xs-1">{{#pix}} i/calendar, core, {{#str}} eventtype, core_calendar {{/str}} {{/pix}}</div>
+        <div class="col-xs-11">{{normalisedeventtypetext}}</div>
+    </div>
+{{/normalisedeventtypetext}}
 {{#description}}
     <div class="row mt-1">
         <div class="col-xs-1">{{#pix}} i/calendareventdescription, core, {{#str}} description {{/str}} {{/pix}}</div>
index 26197f0..8cd7d25 100644 (file)
@@ -5021,6 +5021,7 @@ class core_course_courselib_testcase extends advanced_testcase {
      * Test the course_get_recent_courses function.
      */
     public function test_course_get_recent_courses() {
+        global $DB;
 
         $this->resetAfterTest();
         $generator = $this->getDataGenerator();
@@ -5043,9 +5044,15 @@ class core_course_courselib_testcase extends advanced_testcase {
         // No course accessed.
         $this->assertCount(0, $result);
 
+        $time = time();
         foreach ($courses as $course) {
             $context = context_course::instance($course->id);
             course_view($context);
+            $DB->set_field('user_lastaccess', 'timeaccess', $time, [
+                'userid' => $student->id,
+                'courseid' => $course->id,
+                ]);
+            $time++;
         }
 
         // Every course accessed.
@@ -5056,10 +5063,10 @@ class core_course_courselib_testcase extends advanced_testcase {
         $result = course_get_recent_courses($student->id, 2);
         $this->assertCount(2, $result);
 
-        // Every course accessed, with limit and offset. Should return only the last created course ($course[2]).
+        // Every course accessed, with limit and offset should return the first course.
         $result = course_get_recent_courses($student->id, 3, 2);
         $this->assertCount(1, $result);
-        $this->assertArrayHasKey($courses[2]->id, $result);
+        $this->assertArrayHasKey($courses[0]->id, $result);
 
         // Every course accessed, order by shortname DESC. The last create course ($course[2]) should have the greater shortname.
         $result = course_get_recent_courses($student->id, 0, 0, 'shortname DESC');
index 4a6c31b..0ed75d1 100644 (file)
Binary files a/lib/amd/build/permissionmanager.min.js and b/lib/amd/build/permissionmanager.min.js differ
index dcd8712..0772104 100644 (file)
@@ -105,12 +105,14 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
                         templatedata.linkclass = 'preventlink';
                         templatedata.action = 'prevent';
                         templatedata.icon = 't/delete';
+                        templatedata.iconalt = M.util.get_string('deletexrole', 'core_role', overideableroles[roleid]);
                         break;
                     case 'prohibit':
                         templatedata.spanclass = 'forbidden';
                         templatedata.linkclass = 'unprohibitlink';
                         templatedata.action = 'unprohibit';
                         templatedata.icon = 't/delete';
+                        templatedata.iconalt = M.util.get_string('deletexrole', 'core_role', overideableroles[roleid]);
                         break;
                     case 'prevent':
                         row.find('a[data-role-id="' + roleid + '"]').first().closest('.allowed').remove();
@@ -155,10 +157,11 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
     var handleAddRole = function(e) {
         e.preventDefault();
 
+        var link = $(e.currentTarget);
+
         // TODO: MDL-57778 Convert to core/modal.
-        Y.use('moodle-core-notification-dialogue', function() {
-            $('body').one('rolesloaded', function() {
-                var link = $(e.currentTarget);
+        $('body').one('rolesloaded', function() {
+            Y.use('moodle-core-notification-dialogue', function() {
                 var action = link.data('action');
                 var row = link.closest('tr.rolecap');
                 var confirmationDetails = {
@@ -201,7 +204,7 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
                 .done(function(content) {
                     panel.set('bodyContent', content);
                     panel.show();
-                    $('div.role_buttons').delegate('input', 'click', function(e) {
+                    $('div.role_buttons').on('click', 'input', function(e) {
                         var roleid = $(e.currentTarget).data('role-id');
                         changePermissions(row, roleid, action);
                     });
@@ -222,8 +225,8 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
      */
     var handleRemoveRole = function(e) {
         e.preventDefault();
+        var link = $(e.currentTarget);
         $('body').one('rolesloaded', function() {
-            var link = $(e.currentTarget);
             var action = link.data('action');
             var roleid = link.data('role-id');
             var row = link.closest('tr.rolecap');
@@ -256,8 +259,8 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates', 'core/yu
             contextname = args.contextname;
             adminurl = args.adminurl;
             var body = $('body');
-            body.delegate(SELECTORS.ADDROLE, 'click', handleAddRole);
-            body.delegate(SELECTORS.REMOVEROLE, 'click', handleRemoveRole);
+            body.on('click', SELECTORS.ADDROLE, handleAddRole);
+            body.on('click', SELECTORS.REMOVEROLE, handleRemoveRole);
         }
     };
 });
index b9233ef..b632895 100644 (file)
@@ -95,6 +95,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'group_message_member' => 'group_message_member',
         'group_message_tab' => 'group_message_tab',
         'group_message_list_area' => 'group_message_list_area',
+        'group_message_message_content' => 'group_message_message_content',
         'icon' => 'icon',
         'link' => 'link',
         'link_or_button' => 'link_or_button',
@@ -174,6 +175,9 @@ XPATH
 XPATH
     , 'group_message_list_area' => <<<XPATH
         .//*[@data-region='message-drawer']//*[contains(@data-region, concat('view-overview-', %locator%))]
+XPATH
+    , 'group_message_message_content' => <<<XPATH
+        .//*[@data-region='message-drawer']//*[@data-region='message' and @data-message-id and contains(., %locator%)]
 XPATH
         , 'icon' => <<<XPATH
 .//*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') and ( contains(normalize-space(@title), %locator%))]
index a62786a..5a2c831 100644 (file)
@@ -739,7 +739,11 @@ class theme_config {
         $this->name     = $config->name;
         $this->dir      = $config->dir;
 
-        $baseconfig = $config;
+        if ($this->name != self::DEFAULT_THEME) {
+            $baseconfig = self::find_theme_config(self::DEFAULT_THEME, $this->settings);
+        } else {
+            $baseconfig = $config;
+        }
 
         $configurable = array(
             'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'usefallback',
index ce1584b..e87f6c4 100644 (file)
@@ -208,6 +208,11 @@ class behat_data_generators extends behat_base {
             'required' => array('user', 'contact'),
             'switchids' => array('user' => 'userid', 'contact' => 'contactid')
         ),
+        'group messages' => array(
+            'datagenerator' => 'group_messages',
+            'required' => array('user', 'group', 'message'),
+            'switchids' => array('user' => 'userid', 'group' => 'groupid')
+        ),
         'language customisations' => array(
             'datagenerator' => 'customlang',
             'required' => array('component', 'stringid', 'value'),
@@ -956,6 +961,10 @@ class behat_data_generators extends behat_base {
      * @return void
      */
     protected function process_private_messages(array $data) {
+        if (empty($data['format'])) {
+            $data['format'] = 'FORMAT_PLAIN';
+        }
+
         if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) {
             $conversation = \core_message\api::create_conversation(
                 \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
@@ -963,7 +972,48 @@ class behat_data_generators extends behat_base {
             );
             $conversationid = $conversation->id;
         }
-        \core_message\api::send_message_to_conversation($data['userid'], $conversationid, $data['message'], FORMAT_PLAIN);
+        \core_message\api::send_message_to_conversation(
+            $data['userid'],
+            $conversationid,
+            $data['message'],
+            constant($data['format'])
+        );
+    }
+
+    /**
+     * Send a new message from user to a group conversation
+     *
+     * @param array $data
+     * @return void
+     */
+    protected function process_group_messages(array $data) {
+        global $DB;
+
+        if (empty($data['format'])) {
+            $data['format'] = 'FORMAT_PLAIN';
+        }
+
+        $group = $DB->get_record('groups', ['id' => $data['groupid']]);
+        $coursecontext = context_course::instance($group->courseid);
+        if (!$conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $data['groupid'],
+            $coursecontext->id)) {
+            $members = $DB->get_records_menu('groups_members', ['groupid' => $data['groupid']], '', 'userid, id');
+            $conversation = \core_message\api::create_conversation(
+                \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+                array_keys($members),
+                $group->name,
+                \core_message\api::MESSAGE_CONVERSATION_ENABLED,
+                'core_group',
+                'groups',
+                $group->id,
+                $coursecontext->id);
+        }
+        \core_message\api::send_message_to_conversation(
+            $data['userid'],
+            $conversation->id,
+            $data['message'],
+            constant($data['format'])
+        );
     }
 
     /**
diff --git a/lib/tests/behat/permissionmanager.feature b/lib/tests/behat/permissionmanager.feature
new file mode 100644 (file)
index 0000000..bd96363
--- /dev/null
@@ -0,0 +1,51 @@
+@core @javascript
+Feature: Override permissions on a context
+  In order to extend and restrict moodle features
+  As an admin or a teacher
+  I need to allow/deny the existing capabilities at different levels
+
+  Background:
+    Given the following "users" exist:
+      | username  | firstname | lastname | email          |
+      | teacher1  | Teacher   | 1        | t1@example.com |
+    And the following "courses" exist:
+      | fullname  | shortname |
+      | Course 1  | C1        |
+    And the following "course enrolments" exist:
+      | user      | course | role           |
+      | teacher1  | C1     | editingteacher |
+
+  Scenario: Default system capabilities modification
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Permissions" in current page administration
+    When I click on "Allow" "icon" in the "mod/forum:addnews" "table_row"
+    And I press "Student"
+    Then "Add announcementsmod/forum:addnews" row "Roles with permission" column of "permissions" table should contain "Student"
+    When I reload the page
+    And I click on "Delete Student role" "link" in the "mod/forum:addnews" "table_row"
+    And I click on "Remove" "button" in the "Confirm role change" "dialogue"
+    Then "Add announcementsmod/forum:addnews" row "Roles with permission" column of "permissions" table should not contain "Student"
+    When I reload the page
+    And I click on "Prohibit" "icon" in the "mod/forum:addnews" "table_row"
+    And I press "Student"
+    Then "Add announcementsmod/forum:addnews" row "Prohibited" column of "permissions" table should contain "Student"
+
+  Scenario: Module capabilities overrides
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name  | Forum 1 |
+    And I follow "Forum 1"
+    And I navigate to "Permissions" in current page administration
+    When I click on "Allow" "icon" in the "mod/forum:addnews" "table_row"
+    And I press "Student"
+    Then "Add announcementsmod/forum:addnews" row "Roles with permission" column of "permissions" table should contain "Student"
+    When I reload the page
+    And I click on "Delete Student role" "link" in the "mod/forum:addnews" "table_row"
+    And I click on "Remove" "button" in the "Confirm role change" "dialogue"
+    Then "Add announcementsmod/forum:addnews" row "Roles with permission" column of "permissions" table should not contain "Student"
+    When I reload the page
+    And I click on "Prohibit" "icon" in the "mod/forum:addnews" "table_row"
+    And I press "Student"
+    Then "Add announcementsmod/forum:addnews" row "Prohibited" column of "permissions" table should contain "Student"
index 67374e6..972fcc3 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation.min.js and b/message/amd/build/message_drawer_view_conversation.min.js differ
index 2abab80..130466d 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_constants.min.js and b/message/amd/build/message_drawer_view_conversation_constants.min.js differ
index baa15c2..759a316 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_state_manager.min.js and b/message/amd/build/message_drawer_view_conversation_state_manager.min.js differ
index e2ca7e5..7ff0805 100644 (file)
@@ -97,11 +97,13 @@ function(
     var loadedAllMessages = false;
     var messagesOffset = 0;
     var newMessagesPollTimer = null;
+    var isRendering = false;
+    var renderBuffer = [];
     // If the UI is currently resetting.
     var isResetting = true;
     // If the UI is currently sending a message.
     var isSendingMessage = false;
-    // This is the render function which will be generated when this module is
+    // These functions which will be generated when this module is
     // first called. See generateRenderFunction for details.
     var render = null;
     // The list of renderers that have been registered to render
@@ -110,7 +112,7 @@ function(
 
     var NEWEST_FIRST = Constants.NEWEST_MESSAGES_FIRST;
     var LOAD_MESSAGE_LIMIT = Constants.LOAD_MESSAGE_LIMIT;
-    var INITIAL_NEW_MESSAGE_POLL_TIMEOUT = Constants.INITIAL_NEW_MESSAGE_POLL_TIMEOUT;
+    var MILLISECONDS_IN_SEC = Constants.MILLISECONDS_IN_SEC;
     var SELECTORS = Constants.SELECTORS;
     var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES;
 
@@ -279,10 +281,9 @@ function(
         var conversationType = loggedInUserId == otherUserId ? CONVERSATION_TYPES.SELF : CONVERSATION_TYPES.PRIVATE;
         var newState = StateManager.setLoadingMembers(viewState, true);
         newState = StateManager.setLoadingMessages(newState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.getMemberInfo(loggedInUserId, [otherUserId], true, true);
-            })
+        render(newState);
+
+        return Repository.getMemberInfo(loggedInUserId, [otherUserId], true, true)
             .then(function(profiles) {
                 if (profiles.length) {
                     return profiles[0];
@@ -301,10 +302,8 @@ function(
                 newState = StateManager.setType(newState, conversationType);
                 newState = StateManager.setImageUrl(newState, profile.profileimageurl);
                 newState = StateManager.setTotalMemberCount(newState, members.length);
-                return render(newState)
-                    .then(function() {
-                        return profile;
-                    });
+                render(newState);
+                return profile;
             })
             .catch(function(error) {
                 var newState = StateManager.setLoadingMembers(viewState, false);
@@ -374,20 +373,19 @@ function(
         var loggedInUserId = loggedInUserProfile.id;
         var newState = StateManager.setLoadingMembers(viewState, true);
         newState = StateManager.setLoadingMessages(newState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.getConversation(
-                    loggedInUserId,
-                    conversationId,
-                    true,
-                    true,
-                    0,
-                    0,
-                    messageLimit + 1,
-                    messageOffset,
-                    newestFirst
-                );
-            })
+        render(newState);
+
+        return Repository.getConversation(
+            loggedInUserId,
+            conversationId,
+            true,
+            true,
+            0,
+            0,
+            messageLimit + 1,
+            messageOffset,
+            newestFirst
+        )
             .then(function(conversation) {
                 if (conversation.messages.length > messageLimit) {
                     conversation.messages = conversation.messages.slice(1);
@@ -450,37 +448,30 @@ function(
             conversation.members = conversation.members.concat([loggedInUserProfile]);
         }
 
+        var messageCount = conversation.messages.length;
+        var hasLoadedEnoughMessages = messageCount >= messageLimit;
         var newState = updateStateFromConversation(conversation, loggedInUserProfile.id);
         newState = StateManager.setLoadingMembers(newState, false);
-        newState = StateManager.setLoadingMessages(newState, true);
-        var messageCount = conversation.messages.length;
-        return render(newState)
-            .then(function() {
-                if (messageCount < messageLimit) {
+        newState = StateManager.setLoadingMessages(newState, !hasLoadedEnoughMessages);
+        var renderPromise = render(newState);
+
+        return renderPromise.then(function() {
+                if (!hasLoadedEnoughMessages) {
                     // We haven't got enough messages so let's load some more.
-                    return loadMessages(conversation.id, messageLimit, messageCount, newestFirst, [])
-                        .then(function(result) {
-                            // Give the list of messages to the next handler.
-                            return result.messages;
-                        });
+                    return loadMessages(conversation.id, messageLimit, messageCount, newestFirst, []);
                 } else {
                     // We've got enough messages. No need to load any more for now.
-                    var newState = StateManager.setLoadingMessages(viewState, false);
-                    return render(newState)
-                        .then(function() {
-                            // Give the list of messages to the next handler.
-                            return conversation.messages;
-                        });
+                    return {messages: conversation.messages};
                 }
             })
-            .then(function(messages) {
+            .then(function() {
+                var messages = viewState.messages;
                 // Update the offset to reflect the number of messages we've loaded.
                 setMessagesOffset(messages.length);
+                markConversationAsRead(viewState.id);
+
                 return messages;
             })
-            .then(function() {
-                return markConversationAsRead(conversation.id);
-            })
             .catch(Notification.exception);
     };
 
@@ -627,14 +618,12 @@ function(
      * Tell the statemanager there is request to block a user and run the renderer
      * to show the block user dialogue.
      *
-     * @param  {Number} userId User id.
-     * @return {Promise} Renderer promise.
+     * @param {Number} userId User id.
      */
     var requestBlockUser = function(userId) {
-        return cancelRequest(userId).then(function() {
-            var newState = StateManager.addPendingBlockUsersById(viewState, [userId]);
-            return render(newState);
-        });
+        cancelRequest(userId);
+        var newState = StateManager.addPendingBlockUsersById(viewState, [userId]);
+        render(newState);
     };
 
     /**
@@ -646,10 +635,9 @@ function(
      */
     var blockUser = function(userId) {
         var newState = StateManager.setLoadingConfirmAction(viewState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.blockUser(viewState.loggedInUserId, userId);
-            })
+        render(newState);
+
+        return Repository.blockUser(viewState.loggedInUserId, userId)
             .then(function(profile) {
                 var newState = StateManager.addMembers(viewState, [profile]);
                 newState = StateManager.removePendingBlockUsersById(newState, [userId]);
@@ -663,14 +651,12 @@ function(
      * Tell the statemanager there is a request to unblock a user and run the renderer
      * to show the unblock user dialogue.
      *
-     * @param  {Number} userId User id of user to unblock.
-     * @return {Promise} Renderer promise.
+     * @param {Number} userId User id of user to unblock.
      */
     var requestUnblockUser = function(userId) {
-        return cancelRequest(userId).then(function() {
-            var newState = StateManager.addPendingUnblockUsersById(viewState, [userId]);
-            return render(newState);
-        });
+        cancelRequest(userId);
+        var newState = StateManager.addPendingUnblockUsersById(viewState, [userId]);
+        render(newState);
     };
 
     /**
@@ -682,10 +668,9 @@ function(
      */
     var unblockUser = function(userId) {
         var newState = StateManager.setLoadingConfirmAction(viewState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.unblockUser(viewState.loggedInUserId, userId);
-            })
+        render(newState);
+
+        return Repository.unblockUser(viewState.loggedInUserId, userId)
             .then(function(profile) {
                 var newState = StateManager.addMembers(viewState, [profile]);
                 newState = StateManager.removePendingUnblockUsersById(newState, [userId]);
@@ -699,14 +684,12 @@ function(
      * Tell the statemanager there is a request to remove a user from the contact list
      * and run the renderer to show the remove user from contacts dialogue.
      *
-     * @param  {Number} userId User id of user to remove from contacts.
-     * @return {Promise} Renderer promise.
+     * @param {Number} userId User id of user to remove from contacts.
      */
     var requestRemoveContact = function(userId) {
-        return cancelRequest(userId).then(function() {
-            var newState = StateManager.addPendingRemoveContactsById(viewState, [userId]);
-            return render(newState);
-        });
+        cancelRequest(userId);
+        var newState = StateManager.addPendingRemoveContactsById(viewState, [userId]);
+        render(newState);
     };
 
     /**
@@ -718,10 +701,9 @@ function(
      */
     var removeContact = function(userId) {
         var newState = StateManager.setLoadingConfirmAction(viewState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.deleteContacts(viewState.loggedInUserId, [userId]);
-            })
+        render(newState);
+
+        return Repository.deleteContacts(viewState.loggedInUserId, [userId])
             .then(function(profiles) {
                 var newState = StateManager.addMembers(viewState, profiles);
                 newState = StateManager.removePendingRemoveContactsById(newState, [userId]);
@@ -735,14 +717,12 @@ function(
      * Tell the statemanager there is a request to add a user to the contact list
      * and run the renderer to show the add user to contacts dialogue.
      *
-     * @param  {Number} userId User id of user to add to contacts.
-     * @return {Promise} Renderer promise.
+     * @param {Number} userId User id of user to add to contacts.
      */
     var requestAddContact = function(userId) {
-        return cancelRequest(userId).then(function() {
-            var newState = StateManager.addPendingAddContactsById(viewState, [userId]);
-            return render(newState);
-        });
+        cancelRequest(userId);
+        var newState = StateManager.addPendingAddContactsById(viewState, [userId]);
+        render(newState);
     };
 
     /**
@@ -754,10 +734,9 @@ function(
      */
     var addContact = function(userId) {
         var newState = StateManager.setLoadingConfirmAction(viewState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.createContactRequest(viewState.loggedInUserId, userId);
-            })
+        render(newState);
+
+        return Repository.createContactRequest(viewState.loggedInUserId, userId)
             .then(function(response) {
                 if (!response.request) {
                     throw new Error(response.warnings[0].message);
@@ -865,15 +844,13 @@ function(
      * Tell the statemanager there is a request to delete the selected messages
      * and run the renderer to show confirm delete messages dialogue.
      *
-     * @param  {Number} userId User id.
-     * @return {Promise} Renderer promise.
+     * @param {Number} userId User id.
      */
     var requestDeleteSelectedMessages = function(userId) {
         var selectedMessageIds = viewState.selectedMessageIds;
-        return cancelRequest(userId).then(function() {
-            var newState = StateManager.addPendingDeleteMessagesById(viewState, selectedMessageIds);
-            return render(newState);
-        });
+        cancelRequest(userId);
+        var newState = StateManager.addPendingDeleteMessagesById(viewState, selectedMessageIds);
+        render(newState);
     };
 
     /**
@@ -885,15 +862,18 @@ function(
     var deleteSelectedMessages = function() {
         var messageIds = viewState.pendingDeleteMessageIds;
         var newState = StateManager.setLoadingConfirmAction(viewState, true);
-        return render(newState)
-            .then(function() {
-                if (newState.deleteMessagesForAllUsers) {
-                    return Repository.deleteMessagesForAllUsers(viewState.loggedInUserId, messageIds);
-                }
 
-                return Repository.deleteMessages(viewState.loggedInUserId, messageIds);
-            })
-            .then(function() {
+        render(newState);
+
+        var deleteMessagesPromise = null;
+
+        if (newState.deleteMessagesForAllUsers) {
+            deleteMessagesPromise = Repository.deleteMessagesForAllUsers(viewState.loggedInUserId, messageIds);
+        } else {
+            deleteMessagesPromise = Repository.deleteMessages(viewState.loggedInUserId, messageIds);
+        }
+
+        return deleteMessagesPromise.then(function() {
                 var newState = StateManager.removeMessagesById(viewState, messageIds);
                 newState = StateManager.removePendingDeleteMessagesById(newState, messageIds);
                 newState = StateManager.removeSelectedMessagesById(newState, messageIds);
@@ -918,14 +898,12 @@ function(
      * Tell the statemanager there is a request to delete a conversation
      * and run the renderer to show confirm delete conversation dialogue.
      *
-     * @param  {Number} userId User id of other user.
-     * @return {Promise} Renderer promise.
+     * @param {Number} userId User id of other user.
      */
     var requestDeleteConversation = function(userId) {
-        return cancelRequest(userId).then(function() {
-            var newState = StateManager.setPendingDeleteConversation(viewState, true);
-            return render(newState);
-        });
+        cancelRequest(userId);
+        var newState = StateManager.setPendingDeleteConversation(viewState, true);
+        render(newState);
     };
 
     /**
@@ -936,10 +914,9 @@ function(
      */
     var deleteConversation = function() {
         var newState = StateManager.setLoadingConfirmAction(viewState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.deleteConversation(viewState.loggedInUserId, viewState.id);
-            })
+        render(newState);
+
+        return Repository.deleteConversation(viewState.loggedInUserId, viewState.id)
             .then(function() {
                 var newState = StateManager.removeMessages(viewState, viewState.messages);
                 newState = StateManager.removeSelectedMessagesById(newState, viewState.selectedMessageIds);
@@ -955,7 +932,6 @@ function(
      * Tell the statemanager to cancel all pending actions.
      *
      * @param  {Number} userId User id.
-     * @return {Promise} Renderer promise.
      */
     var cancelRequest = function(userId) {
         var pendingDeleteMessageIds = viewState.pendingDeleteMessageIds;
@@ -966,7 +942,7 @@ function(
         newState = StateManager.removePendingDeleteMessagesById(newState, pendingDeleteMessageIds);
         newState = StateManager.setPendingDeleteConversation(newState, false);
         newState = StateManager.setDeleteMessagesForAllUsers(newState, false);
-        return render(newState);
+        render(newState);
     };
 
     /**
@@ -984,10 +960,9 @@ function(
         });
         var request = requests[0];
         var newState = StateManager.setLoadingConfirmAction(viewState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.acceptContactRequest(userId, loggedInUserId);
-            })
+        render(newState);
+
+        return Repository.acceptContactRequest(userId, loggedInUserId)
             .then(function(profile) {
                 var newState = StateManager.removeContactRequests(viewState, [request]);
                 newState = StateManager.addMembers(viewState, [profile]);
@@ -1016,10 +991,9 @@ function(
         });
         var request = requests[0];
         var newState = StateManager.setLoadingConfirmAction(viewState, true);
-        return render(newState)
-            .then(function() {
-                return Repository.declineContactRequest(userId, loggedInUserId);
-            })
+        render(newState);
+
+        return Repository.declineContactRequest(userId, loggedInUserId)
             .then(function(profile) {
                 var newState = StateManager.removeContactRequests(viewState, [request]);
                 newState = StateManager.addMembers(viewState, [profile]);
@@ -1044,24 +1018,26 @@ function(
         isSendingMessage = true;
         var newState = StateManager.setSendingMessage(viewState, true);
         var newConversationId = null;
-        var newCanDeleteMessagesForAllUsers = false;
-        return render(newState)
-            .then(function() {
-                if (!conversationId && (viewState.type != CONVERSATION_TYPES.PUBLIC)) {
-                    // If it's a new private conversation then we need to use the old
-                    // web service function to create the conversation.
-                    var otherUserId = getOtherUserId();
-                    return Repository.sendMessageToUser(otherUserId, text)
-                        .then(function(message) {
-                            newConversationId = parseInt(message.conversationid, 10);
-                            newCanDeleteMessagesForAllUsers = message.candeletemessagesforallusers;
-                            return message;
-                        });
-                } else {
-                    return Repository.sendMessageToConversation(conversationId, text);
-                }
-            })
-            .then(function(message) {
+
+        render(newState);
+
+        var sendMessagePromise = null;
+        var newCanDeleteMessagesForAllUsers = null;
+        if (!conversationId && (viewState.type != CONVERSATION_TYPES.PUBLIC)) {
+            // If it's a new private conversation then we need to use the old
+            // web service function to create the conversation.
+            var otherUserId = getOtherUserId();
+            sendMessagePromise = Repository.sendMessageToUser(otherUserId, text)
+                .then(function(message) {
+                    newConversationId = parseInt(message.conversationid, 10);
+                    newCanDeleteMessagesForAllUsers = message.candeletemessagesforallusers;
+                    return message;
+                });
+        } else {
+            sendMessagePromise = Repository.sendMessageToConversation(conversationId, text);
+        }
+
+        sendMessagePromise.then(function(message) {
                 var newState = StateManager.addMessages(viewState, [message]);
                 newState = StateManager.setSendingMessage(newState, false);
                 var conversation = formatConversationForEvent(newState);
@@ -1076,12 +1052,10 @@ function(
                     newState = StateManager.setCanDeleteMessagesForAllUsers(newState, newCanDeleteMessagesForAllUsers);
                 }
 
-                return render(newState)
-                    .then(function() {
-                        isSendingMessage = false;
-                        PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);
-                        return;
-                    });
+                render(newState);
+                isSendingMessage = false;
+                PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);
+                return;
             })
             .catch(function(error) {
                 isSendingMessage = false;
@@ -1095,7 +1069,6 @@ function(
      * Toggle the selected messages update the statemanager and render the result.
      *
      * @param  {Number} messageId The id of the message to be toggled
-     * @return {Promise} Renderer promise.
      */
     var toggleSelectMessage = function(messageId) {
         var newState = viewState;
@@ -1106,7 +1079,7 @@ function(
             newState = StateManager.addSelectedMessagesById(viewState, [messageId]);
         }
 
-        return render(newState);
+        render(newState);
     };
 
     /**
@@ -1115,10 +1088,45 @@ function(
      * @return {Promise} Renderer promise.
      */
     var cancelEditMode = function() {
-        return cancelRequest(getOtherUserId())
+        cancelRequest(getOtherUserId());
+        var newState = StateManager.removeSelectedMessagesById(viewState, viewState.selectedMessageIds);
+        render(newState);
+    };
+
+    /**
+     * Process the patches in the render buffer one at a time in order until the
+     * buffer is empty.
+     *
+     * @param {Object} header The conversation header container element.
+     * @param {Object} body The conversation body container element.
+     * @param {Object} footer The conversation footer container element.
+     */
+    var processRenderBuffer = function(header, body, footer) {
+        if (isRendering) {
+            return;
+        }
+
+        if (!renderBuffer.length) {
+            return;
+        }
+
+        isRendering = true;
+        var renderable = renderBuffer.shift();
+        var renderPromises = renderers.map(function(renderFunc) {
+            return renderFunc(renderable.patch);
+        });
+
+        $.when.apply(null, renderPromises)
             .then(function() {
-                var newState = StateManager.removeSelectedMessagesById(viewState, viewState.selectedMessageIds);
-                return render(newState);
+                isRendering = false;
+                renderable.deferred.resolve(true);
+                // Keep processing the buffer until it's empty.
+                processRenderBuffer(header, body, footer);
+            })
+            .catch(function(error) {
+                isRendering = false;
+                renderable.deferred.reject(error);
+                Notification.exception(error);
             });
     };
 
@@ -1148,25 +1156,39 @@ function(
 
         return function(newState) {
             var patch = Patcher.buildPatch(viewState, newState);
+            var deferred = $.Deferred();
+
+            // Check if the patch has any data. Ignore empty patches.
+            if (Object.keys(patch).length) {
+                // Add the patch to the render buffer which gets processed in order.
+                renderBuffer.push({
+                    patch: patch,
+                    deferred: deferred
+                });
+            } else {
+                deferred.resolve(true);
+            }
             // This is a great place to add in some console logging if you need
             // to debug something. You can log the current state, the next state,
             // and the generated patch and see exactly what will be updated.
-            var renderPromises = renderers.map(function(renderFunc) {
-                return renderFunc(patch);
-            });
-            return $.when.apply(null, renderPromises)
-                .then(function() {
-                    viewState = newState;
-                    if (newState.id) {
-                        // Only cache created conversations.
-                        stateCache[newState.id] = {
-                            state: newState,
-                            messagesOffset: getMessagesOffset(),
-                            loadedAllMessages: hasLoadedAllMessages()
-                        };
-                    }
-                    return;
-                });
+
+            // Optimistically update the state. We're going to assume that the rendering
+            // will always succeed. The rendering is asynchronous (annoyingly) so it's buffered
+            // but it'll reach eventual consistency with the current state.
+            viewState = newState;
+            if (newState.id) {
+                // Only cache created conversations.
+                stateCache[newState.id] = {
+                    state: newState,
+                    messagesOffset: getMessagesOffset(),
+                    loadedAllMessages: hasLoadedAllMessages()
+                };
+            }
+
+            // Start processing the buffer.
+            processRenderBuffer(header, body, footer);
+
+            return deferred.promise();
         };
     };
 
@@ -1179,12 +1201,9 @@ function(
     var generateConfirmActionHandler = function(actionCallback) {
         return function(e, data) {
             if (!viewState.loadingConfirmAction) {
-                actionCallback(getOtherUserId())
-                    .catch(function(error) {
-                        var newState = StateManager.setLoadingConfirmAction(viewState, false);
-                        render(newState);
-                        Notification.exception(error);
-                    });
+                actionCallback(getOtherUserId());
+                var newState = StateManager.setLoadingConfirmAction(viewState, false);
+                render(newState);
             }
             data.originalEvent.preventDefault();
         };
@@ -1232,7 +1251,7 @@ function(
         var element = target.closest(SELECTORS.MESSAGE);
         var messageId = parseInt(element.attr('data-message-id'), 10);
 
-        toggleSelectMessage(messageId).catch(Notification.exception);
+        toggleSelectMessage(messageId);
 
         data.originalEvent.preventDefault();
     };
@@ -1244,7 +1263,7 @@ function(
      * @param {Object} data Data for this event.
      */
     var handleCancelEditMode = function(e, data) {
-        cancelEditMode().catch(Notification.exception);
+        cancelEditMode();
         data.originalEvent.preventDefault();
     };
 
@@ -1413,10 +1432,9 @@ function(
             if (!isResetting && !isLoadingMoreMessages && !hasLoadedAllMessages() && hasMembers) {
                 isLoadingMoreMessages = true;
                 var newState = StateManager.setLoadingMessages(viewState, true);
-                render(newState)
-                    .then(function() {
-                        return loadMessages(viewState.id, LOAD_MESSAGE_LIMIT, getMessagesOffset(), NEWEST_FIRST, []);
-                    })
+                render(newState);
+
+                loadMessages(viewState.id, LOAD_MESSAGE_LIMIT, getMessagesOffset(), NEWEST_FIRST, [])
                     .then(function() {
                         isLoadingMoreMessages = false;
                         setMessagesOffset(getMessagesOffset() + LOAD_MESSAGE_LIMIT);
@@ -1477,13 +1495,12 @@ function(
 
         newMessagesPollTimer = new BackOffTimer(
             getLoadNewMessagesCallback(conversationId, NEWEST_FIRST),
-            function(time) {
-                if (!time) {
-                    return INITIAL_NEW_MESSAGE_POLL_TIMEOUT;
-                }
-
-                return time * 2;
-            }
+            BackOffTimer.getIncrementalCallback(
+                viewState.messagePollMin * MILLISECONDS_IN_SEC,
+                MILLISECONDS_IN_SEC,
+                viewState.messagePollMax * MILLISECONDS_IN_SEC,
+                viewState.messagePollAfterMax * MILLISECONDS_IN_SEC
+            )
         );
 
         newMessagesPollTimer.start();
@@ -1495,12 +1512,28 @@ function(
      * @param  {Object} body Conversation body container element.
      * @param  {Number|null} conversationId The conversation id.
      * @param  {Object} loggedInUserProfile The logged in user's profile.
-     * @return {Promise} Renderer promise.
      */
     var resetState = function(body, conversationId, loggedInUserProfile) {
+        // Reset all of the states back to the beginning if we're loading a new
+        // conversation.
+        isResetting = true;
+        isRendering = false;
+        renderBuffer = [];
+        isSendingMessage = false;
+
         var loggedInUserId = loggedInUserProfile.id;
         var midnight = parseInt(body.attr('data-midnight'), 10);
-        var initialState = StateManager.buildInitialState(midnight, loggedInUserId, conversationId);
+        var messagePollMin = parseInt(body.attr('data-message-poll-min'), 10);
+        var messagePollMax = parseInt(body.attr('data-message-poll-max'), 10);
+        var messagePollAfterMax = parseInt(body.attr('data-message-poll-after-max'), 10);
+        var initialState = StateManager.buildInitialState(
+            midnight,
+            loggedInUserId,
+            conversationId,
+            messagePollMin,
+            messagePollMax,
+            messagePollAfterMax
+        );
 
         if (!viewState) {
             viewState = initialState;
@@ -1510,7 +1543,7 @@ function(
             newMessagesPollTimer.stop();
         }
 
-        return render(initialState);
+        render(initialState);
     };
 
     /**
@@ -1524,32 +1557,34 @@ function(
     var resetNoConversation = function(body, loggedInUserProfile, otherUserId) {
         // Always reset the state back to the initial state so that the
         // state manager and patcher can work correctly.
-        return resetState(body, null, loggedInUserProfile)
-            .then(function() {
-                if (loggedInUserProfile.id != otherUserId) {
-                    // Private conversation between two different users.
-                    return Repository.getConversationBetweenUsers(
-                        loggedInUserProfile.id,
-                        otherUserId,
-                        true,
-                        true,
-                        0,
-                        0,
-                        LOAD_MESSAGE_LIMIT,
-                        0,
-                        NEWEST_FIRST
-                    );
-                } else {
-                    // Self conversation.
-                    return Repository.getSelfConversation(
-                        loggedInUserProfile.id,
-                        LOAD_MESSAGE_LIMIT,
-                        0,
-                        NEWEST_FIRST
-                    );
-                }
-            })
-            .then(function(conversation) {
+        resetState(body, null, loggedInUserProfile);
+
+        var resetNoConversationPromise = null;
+
+        if (loggedInUserProfile.id != otherUserId) {
+            // Private conversation between two different users.
+            resetNoConversationPromise = Repository.getConversationBetweenUsers(
+                loggedInUserProfile.id,
+                otherUserId,
+                true,
+                true,
+                0,
+                0,
+                LOAD_MESSAGE_LIMIT,
+                0,
+                NEWEST_FIRST
+            );
+        } else {
+            // Self conversation.
+            resetNoConversationPromise = Repository.getSelfConversation(
+                loggedInUserProfile.id,
+                LOAD_MESSAGE_LIMIT,
+                0,
+                NEWEST_FIRST
+            );
+        }
+
+        return resetNoConversationPromise.then(function(conversation) {
                 // Looks like we have a conversation after all! Let's use that.
                 return resetByConversation(body, conversation, loggedInUserProfile);
             })
@@ -1575,31 +1610,32 @@ function(
 
         // Always reset the state back to the initial state so that the
         // state manager and patcher can work correctly.
-        return resetState(body, conversationId, loggedInUserProfile)
-            .then(function() {
-                if (cache) {
-                    // We've seen this conversation before so there is no need to
-                    // send any network requests.
-                    var newState = cache.state;
-                    // Reset some loading states just in case they were left weirdly.
-                    newState = StateManager.setLoadingMessages(newState, false);
-                    newState = StateManager.setLoadingMembers(newState, false);
-                    setMessagesOffset(cache.messagesOffset);
-                    setLoadedAllMessages(cache.loadedAllMessages);
-                    return render(newState);
-                } else {
-                    return loadNewConversation(
-                        conversationId,
-                        loggedInUserProfile,
-                        LOAD_MESSAGE_LIMIT,
-                        0,
-                        NEWEST_FIRST
-                    );
-                }
-            })
-            .then(function() {
-                return resetMessagePollTimer(conversationId);
-            });
+        resetState(body, conversationId, loggedInUserProfile);
+
+        var promise = $.Deferred().resolve({}).promise();
+        if (cache) {
+            // We've seen this conversation before so there is no need to
+            // send any network requests.
+            var newState = cache.state;
+            // Reset some loading states just in case they were left weirdly.
+            newState = StateManager.setLoadingMessages(newState, false);
+            newState = StateManager.setLoadingMembers(newState, false);
+            setMessagesOffset(cache.messagesOffset);
+            setLoadedAllMessages(cache.loadedAllMessages);
+            render(newState);
+        } else {
+            promise = loadNewConversation(
+                conversationId,
+                loggedInUserProfile,
+                LOAD_MESSAGE_LIMIT,
+                0,
+                NEWEST_FIRST
+            );
+        }
+
+        return promise.then(function() {
+            return resetMessagePollTimer(conversationId);
+        });
     };
 
     /**
@@ -1618,30 +1654,31 @@ function(
 
         // Always reset the state back to the initial state so that the
         // state manager and patcher can work correctly.
-        return resetState(body, conversation.id, loggedInUserProfile)
-            .then(function() {
-                if (cache) {
-                    // We've seen this conversation before so there is no need to
-                    // send any network requests.
-                    var newState = cache.state;
-                    // Reset some loading states just in case they were left weirdly.
-                    newState = StateManager.setLoadingMessages(newState, false);
-                    newState = StateManager.setLoadingMembers(newState, false);
-                    setMessagesOffset(cache.messagesOffset);
-                    setLoadedAllMessages(cache.loadedAllMessages);
-                    return render(newState);
-                } else {
-                    return loadExistingConversation(
-                        conversation,
-                        loggedInUserProfile,
-                        LOAD_MESSAGE_LIMIT,
-                        NEWEST_FIRST
-                    );
-                }
-            })
-            .then(function() {
-                return resetMessagePollTimer(conversation.id);
-            });
+        resetState(body, conversation.id, loggedInUserProfile);
+
+        var promise = $.Deferred().resolve({}).promise();
+        if (cache) {
+            // We've seen this conversation before so there is no need to
+            // send any network requests.
+            var newState = cache.state;
+            // Reset some loading states just in case they were left weirdly.
+            newState = StateManager.setLoadingMessages(newState, false);
+            newState = StateManager.setLoadingMembers(newState, false);
+            setMessagesOffset(cache.messagesOffset);
+            setLoadedAllMessages(cache.loadedAllMessages);
+            render(newState);
+        } else {
+            promise = loadExistingConversation(
+                conversation,
+                loggedInUserProfile,
+                LOAD_MESSAGE_LIMIT,
+                NEWEST_FIRST
+            );
+        }
+
+        return promise.then(function() {
+            return resetMessagePollTimer(conversation.id);
+        });
     };
 
     /**
@@ -1702,9 +1739,6 @@ function(
         }
 
         if (isNewConversation) {
-            // Reset all of the states back to the beginning if we're loading a new
-            // conversation.
-            isResetting = true;
             var renderPromise = null;
             var loggedInUserProfile = getLoggedInUserProfile(body);
             if (conversation) {
index 00a3bbb..8dd66ef 100644 (file)
@@ -110,6 +110,6 @@ define([], function() {
         CONVERSATION_TYPES: CONVERSATION_TYPES,
         NEWEST_MESSAGES_FIRST: true,
         LOAD_MESSAGE_LIMIT: 100,
-        INITIAL_NEW_MESSAGE_POLL_TIMEOUT: 1000
+        MILLISECONDS_IN_SEC: 1000
     };
 });
index 3f67cb7..5ed8ecc 100644 (file)
@@ -101,13 +101,26 @@ define(['jquery'], function($) {
      * @param  {Number} midnight Midnight time.
      * @param  {Number} loggedInUserId The logged in user id.
      * @param  {Number} id The conversation id.
+     * @param  {Number} messagePollMin The message poll start timeout in seconds.
+     * @param  {Number} messagePollMax The message poll max timeout limit in seconds.
+     * @param  {Number} messagePollAfterMax The message poll frequency in seconds to reset to after max limit is reached.
      * @return {Object} Initial state.
      */
-    var buildInitialState = function(midnight, loggedInUserId, id) {
+    var buildInitialState = function(
+        midnight,
+        loggedInUserId,
+        id,
+        messagePollMin,
+        messagePollMax,
+        messagePollAfterMax
+    ) {
         return {
             midnight: midnight,
             loggedInUserId: loggedInUserId,
             id: id,
+            messagePollMin: messagePollMin,
+            messagePollMax: messagePollMax,
+            messagePollAfterMax: messagePollAfterMax,
             name: null,
             subname: null,
             type: null,
index aa061c2..5136ce8 100644 (file)
@@ -26,6 +26,8 @@ namespace core_message;
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->dirroot . '/message/lib.php');
+
 /**
  * Helper class for the message area.
  *
@@ -741,6 +743,12 @@ class helper {
                 'id' => $USER->id,
                 'midnight' => usergetmidnight(time())
             ],
+            // The starting timeout value for message polling.
+            'messagepollmin' => $CFG->messagingminpoll ?? MESSAGE_DEFAULT_MIN_POLL_IN_SECONDS,
+            // The maximum value that message polling timeout can reach.
+            'messagepollmax' => $CFG->messagingmaxpoll ?? MESSAGE_DEFAULT_MAX_POLL_IN_SECONDS,
+            // The timeout to reset back to after the max polling time has been reached.
+            'messagepollaftermax' => $CFG->messagingtimeoutpoll ?? MESSAGE_DEFAULT_TIMEOUT_POLL_IN_SECONDS,
             'contacts' => [
                 'sectioncontacts' => [
                     'placeholders' => array_fill(0, $contactscount > 50 ? 50 : $contactscount, true)
index 463f89a..8f87320 100644 (file)
@@ -40,6 +40,9 @@
     data-region="view-conversation"
     data-user-id="{{loggedinuser.id}}"
     data-midnight="{{loggedinuser.midnight}}"
+    data-message-poll-min="{{messagepollmin}}"
+    data-message-poll-max="{{messagepollmax}}"
+    data-message-poll-after-max="{{messagepollaftermax}}"
     style="overflow-y: auto; overflow-x: hidden"
 >
     <div class="position-relative h-100" data-region="content-container" style="overflow-y: auto; overflow-x: hidden">
diff --git a/message/tests/behat/delete_messages.feature b/message/tests/behat/delete_messages.feature
new file mode 100644 (file)
index 0000000..9de5bc3
--- /dev/null
@@ -0,0 +1,269 @@
+@core @core_message @javascript
+Feature: Delete messages from conversations
+  In order to manage a course group in a course
+  As a user
+  I need to be able to delete messages from conversations
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1        | 0        | 1         |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role |
+      | student1 | C1     | student |
+      | student2 | C1     | student |
+    And the following "groups" exist:
+      | name    | course | idnumber | enablemessaging |
+      | Group 1 | C1     | G1       | 1               |
+    And the following "group members" exist:
+      | user     | group |
+      | student1 | G1 |
+      | student2 | G1 |
+    And the following "group messages" exist:
+      | user     | group  | message                   |
+      | student1 | G1     | Hi!                       |
+      | student2 | G1     | How are you?              |
+      | student1 | G1     | Can somebody help me?     |
+    And the following "private messages" exist:
+      | user     | contact  | message       |
+      | student1 | student2 | Hi!           |
+      | student2 | student1 | Hello!        |
+      | student1 | student2 | Are you free? |
+    And the following config values are set as admin:
+      | messaging        | 1 |
+      | messagingminpoll | 1 |
+
+  Scenario: Delete a message sent by the user from a group conversation
+    Given I log in as "student1"
+    And I open messaging
+    And "Group 1" "group_message" should exist
+    And I select "Group 1" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I click on "How are you?" "group_message_message_content"
+    And I click on "Can somebody help me?" "group_message_message_content"
+    And I should see "3" in the "[data-region='message-selected-court']" "css_element"
+#   Clicking to unselect
+    And I click on "How are you?" "group_message_message_content"
+    And I click on "Can somebody help me?" "group_message_message_content"
+    And I should see "1" in the "[data-region='message-selected-court']" "css_element"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Deleting, so messages should not be there
+    And I should see "Delete"
+    And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
+    Then I should not see "Delete"
+    And I should not see "Hi!"
+    And I should see "##today##j F##" in the "Group 1" "group_message_conversation"
+    And I should see "How are you?" in the "Group 1" "group_message_conversation"
+    And I should see "Can somebody help me?" in the "Group 1" "group_message_conversation"
+    And I should not see "Messages selected"
+
+  Scenario: Delete two messages from a group conversation; one sent by another user.
+    Given I log in as "student1"
+    And I open messaging
+    And "Group 1" "group_message" should exist
+    And I select "Group 1" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I should see "1" in the "[data-region='message-selected-court']" "css_element"
+    And I click on "How are you?" "group_message_message_content"
+    And I should see "2" in the "[data-region='message-selected-court']" "css_element"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Deleting, so messages should not be there
+    And I should see "Delete"
+    And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
+    Then I should not see "Delete"
+    And I should not see "Hi!"
+    And I should see "##today##j F##" in the "Group 1" "group_message_conversation"
+    And I should not see "How are you?" in the "Group 1" "group_message_conversation"
+    And I should see "Can somebody help me?" in the "Group 1" "group_message_conversation"
+    And I should not see "Messages selected"
+#   Check messages were not deleted for other users
+    And I log out
+    And I log in as "student2"
+    And I open messaging
+    And I select "Group 1" conversation in messaging
+    And I should see "Hi!"
+    And I should see "How are you?"
+    And I should see "Can somebody help me?"
+
+  Scenario: Cancel deleting two messages from a group conversation
+    Given I log in as "student1"
+    And I open messaging
+    And "Group 1" "group_message" should exist
+    And I select "Group 1" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I click on "How are you?" "group_message_message_content"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Canceling deletion, so messages should be there
+    And I should see "Cancel"
+    And I click on "//button[@data-action='cancel-confirm']" "xpath_element"
+    Then I should not see "Cancel"
+    And I should see "Hi!"
+    And I should see "How are you?" in the "Group 1" "group_message_conversation"
+    And I should see "2" in the "[data-region='message-selected-court']" "css_element"
+
+  Scenario: Delete a message sent by the user from a private conversation
+    Given I log in as "student1"
+    And I open messaging
+    And I should see "Private"
+    And I open the "Private" conversations list
+    And I should see "Student 2"
+    And I select "Student 2" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I should see "1" in the "[data-region='message-selected-court']" "css_element"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Deleting, so messages should not be there
+    And I should see "Delete"
+    And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
+    Then I should not see "Delete"
+    And I should not see "Hi!"
+    And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+    And I should see "Hello!" in the "Student 2" "group_message_conversation"
+    And I should see "Are you free?" in the "Student 2" "group_message_conversation"
+    And I should not see "Messages selected"
+
+  Scenario: Delete two messages from a private conversation; one sent by another user
+    Given I log in as "student1"
+    And I open messaging
+    And I should see "Private"
+    And I open the "Private" conversations list
+    And I should see "Student 2"
+    And I select "Student 2" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I should see "1" in the "[data-region='message-selected-court']" "css_element"
+    And I click on "Hello!" "group_message_message_content"
+    And I should see "2" in the "[data-region='message-selected-court']" "css_element"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Deleting, so messages should not be there
+    And I should see "Delete"
+    And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
+    Then I should not see "Delete"
+    And I should not see "Hi!"
+    And I should not see "Hello!" in the "Student 2" "group_message_conversation"
+    And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+    And I should see "Are you free?" in the "Student 2" "group_message_conversation"
+    And I should not see "Messages selected"
+#   Check messages were not deleted for the other user
+    And I log out
+    And I log in as "student2"
+    And I open messaging
+    And I open the "Private" conversations list
+    And I select "Student 1" conversation in messaging
+    And I should see "Hi!"
+    And I should see "Hello!"
+    And I should see "Are you free?"
+
+  Scenario: Cancel deleting two messages from a private conversation
+    Given I log in as "student1"
+    And I open messaging
+    And I should see "Private"
+    And I open the "Private" conversations list
+    And I should see "Student 2"
+    And I select "Student 2" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I click on "Hello!" "group_message_message_content"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Canceling deletion, so messages should be there
+    And I should see "Cancel"
+    And I click on "//button[@data-action='cancel-confirm']" "xpath_element"
+    Then I should not see "Cancel"
+    And I should see "Hi!"
+    And I should see "Hello!" in the "Student 2" "group_message_conversation"
+    And I should see "2" in the "[data-region='message-selected-court']" "css_element"
+
+  Scenario: Delete a message sent by the user from a favorite conversation
+    Given the following "favourite conversations" exist:
+      | user     | contact  |
+      | student1 | student2 |
+    And I log in as "student1"
+    And I open messaging
+    And I should see "Student 2"
+    And I select "Student 2" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I should see "1" in the "[data-region='message-selected-court']" "css_element"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Deleting, so messages should not be there
+    And I should see "Delete"
+    And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
+    Then I should not see "Delete"
+    And I should not see "Hi!"
+    And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+    And I should see "Hello!" in the "Student 2" "group_message_conversation"
+    And I should not see "Messages selected"
+
+  Scenario: Delete two messages from a favourite conversation; one sent by another user
+    Given the following "favourite conversations" exist:
+      | user     | contact  |
+      | student1 | student2 |
+    And I log in as "student1"
+    And I open messaging
+    And I should see "Student 2"
+    And I select "Student 2" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I should see "1" in the "[data-region='message-selected-court']" "css_element"
+    And I click on "Hello!" "group_message_message_content"
+    And I should see "2" in the "[data-region='message-selected-court']" "css_element"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Deleting, so messages should not be there
+    And I should see "Delete"
+    And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
+    Then I should not see "Delete"
+    And I should not see "Hi!"
+    And I should not see "Hello!" in the "Student 2" "group_message_conversation"
+    And I should see "##today##j F##" in the "Student 2" "group_message_conversation"
+    And I should see "Are you free?" in the "Student 2" "group_message_conversation"
+    And I should not see "Messages selected"
+
+  Scenario: Cancel deleting two messages from a favourite conversation
+    Given the following "favourite conversations" exist:
+      | user     | contact  |
+      | student1 | student2 |
+    And I log in as "student1"
+    And I open messaging
+    And I should see "Student 2"
+    And I select "Student 2" conversation in messaging
+    And I click on "Hi!" "group_message_message_content"
+    And I click on "Hello!" "group_message_message_content"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+#   Canceling deletion, so messages should be there
+    And I should see "Cancel"
+    And I click on "//button[@data-action='cancel-confirm']" "xpath_element"
+    Then I should not see "Cancel"
+    And I should see "Hi!"
+    And I should see "Hello!" in the "Student 2" "group_message_conversation"
+    And I should see "2" in the "[data-region='message-selected-court']" "css_element"
+
+  Scenario: Check an empty favourite conversation is still favourite
+    Given the following "favourite conversations" exist:
+      | user     | contact  |
+      | student1 | student2 |
+    And I log in as "student1"
+    And I open messaging
+    And I should see "Student 2"
+    And I select "Student 2" conversation in the "favourites" conversations list
+    And I click on "Hi!" "group_message_message_content"
+    And I click on "Hello!" "group_message_message_content"
+    And I click on "Are you free?" "group_message_message_content"
+    And "Delete selected messages" "button" should exist
+    When I click on "Delete selected messages" "button"
+    And I should see "Delete"
+    And I click on "//button[@data-action='confirm-delete-selected-messages']" "xpath_element"
+    And I go back in "view-conversation" message drawer
+    Then I should not see "Student 2" in the "//*[@data-region='message-drawer']//div[@data-region='view-overview-favourites']" "xpath_element"
+    And I send "Hi!" message to "Student 2" user
+    And I go back in "view-conversation" message drawer
+    And I go back in "view-search" message drawer
+    And I open the "Starred" conversations list
+    And I should see "Student 2" in the "//*[@data-region='message-drawer']//div[@data-region='view-overview-favourites']" "xpath_element"
index 4f8b25c..842b55e 100644 (file)
@@ -24,7 +24,8 @@ Feature: Star and unstar conversations
       | student1 | G1 |
       | student2 | G1 |
     And the following config values are set as admin:
-      | messaging | 1 |
+      | messaging        | 1 |
+      | messagingminpoll | 1 |
 
   Scenario: Star a group conversation
     Given I log in as "student1"
index 105164e..e990727 100644 (file)
@@ -39,6 +39,9 @@ Feature: Create conversations for course's groups
       | teacher1 | G2 |
       | teacher1 | G3 |
       | student0 | G3 |
+    And the following config values are set as admin:
+      | messaging        | 1 |
+      | messagingminpoll | 1 |
 
   Scenario: Group conversations are restricted to members
     Given I log in as "teacher1"
index 02bfe52..183c07a 100644 (file)
@@ -19,6 +19,7 @@ Feature: Message delete conversations
     And the following config values are set as admin:
       | messaging         | 1 |
       | messagingallusers | 1 |
+      | messagingminpoll  | 1 |
     And the following "private messages" exist:
       | user     | contact  | message               |
       | student1 | student2 | Hi!                   |
index 4c57223..1b8c3b0 100644 (file)
@@ -24,8 +24,9 @@ Feature: Manage contacts
       | user     | contact |
       | student1 | student2 |
     And the following config values are set as admin:
-      | messaging | 1 |
+      | messaging         | 1 |
       | messagingallusers | 1 |
+      | messagingminpoll  | 1 |
 
   Scenario: Send a 'contact request' to someone to add a contact
     Given I log in as "student1"
index f8c304a..65c013e 100644 (file)
@@ -33,8 +33,9 @@ Feature: Manage preferences
       | user     | contact |
       | student1 | student2 |
     And the following config values are set as admin:
-      | messaging | 1 |
+      | messaging         | 1 |
       | messagingallusers | 1 |
+      | messagingminpoll  | 1 |
 
   # Recipient has 'My contacts only' set.
   Scenario: Allow sending a message when you are a contact
index 1596073..45f1a93 100644 (file)
@@ -24,7 +24,8 @@ Feature: Message send messages
       | student1 | G1 |
       | student2 | G1 |
     And the following config values are set as admin:
-      | messaging | 1 |
+      | messaging        | 1 |
+      | messagingminpoll | 1 |
 
   Scenario: Send a message to a group conversation
     Given I log in as "student1"
index 14f6a54..5063b3c 100644 (file)
@@ -9,7 +9,8 @@ Feature: Self conversation
       | username | firstname | lastname | email                |
       | student1 | Student   | 1        | student1@example.com |
     And the following config values are set as admin:
-      | messaging | 1 |
+      | messaging        | 1 |
+      | messagingminpoll | 1 |
 
   Scenario: Self conversation exists
     Given I log in as "student1"
index 07b47f5..97f5ffa 100644 (file)
@@ -24,7 +24,8 @@ Feature: Unread messages
       | student1 | NG |
       | student2 | NG |
     And the following config values are set as admin:
-      | messaging | 1 |
+      | messaging        | 1 |
+      | messagingminpoll | 1 |
 
   Scenario: Unread messages for group conversation
     Given I log in as "student1"
index 0ecbeff..a38a100 100644 (file)
@@ -56,5 +56,22 @@ function xmldb_data_upgrade($oldversion) {
     // Automatically generated Moodle v3.7.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2019052001) {
+
+        $columns = $DB->get_columns('data');
+
+        $oldclass = "mod-data-default-template ##approvalstatus##";
+        $newclass = "mod-data-default-template ##approvalstatusclass##";
+
+        // Update existing classes.
+        $DB->replace_all_text('data', $columns['singletemplate'], $oldclass, $newclass);
+        $DB->replace_all_text('data', $columns['listtemplate'], $oldclass, $newclass);
+        $DB->replace_all_text('data', $columns['addtemplate'], $oldclass, $newclass);
+        $DB->replace_all_text('data', $columns['rsstemplate'], $oldclass, $newclass);
+        $DB->replace_all_text('data', $columns['asearchtemplate'], $oldclass, $newclass);
+
+        // Data savepoint reached.
+        upgrade_mod_savepoint(true, 2019052001, 'data');
+    }
     return true;
 }
index 37519ce..9b3d93d 100644 (file)
@@ -608,7 +608,7 @@ function data_generate_default_template(&$data, $template, $recordid=0, $form=fa
     if ($fields = $DB->get_records('data_fields', array('dataid'=>$data->id), 'id')) {
 
         $table = new html_table();
-        $table->attributes['class'] = 'mod-data-default-template ##approvalstatus##';
+        $table->attributes['class'] = 'mod-data-default-template ##approvalstatusclass##';
         $table->colclasses = array('template-field', 'template-token');
         $table->data = array();
         foreach ($fields as $field) {
@@ -1507,12 +1507,16 @@ function data_print_template($template, $records, $data, $search='', $page=0, $r
         }
 
         $patterns[] = '##approvalstatus##';
+        $patterns[] = '##approvalstatusclass##';
         if (!$data->approval) {
             $replacement[] = '';
+            $replacement[] = '';
         } else if ($record->approved) {
             $replacement[] = get_string('approved', 'data');
+            $replacement[] = 'approved';
         } else {
             $replacement[] = get_string('notapproved', 'data');
+            $replacement[] = 'notapproved';
         }
 
         $patterns[]='##comments##';
index d3bcc4f..d2b5d3f 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2019052000;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2019052001;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2019051100;       // Requires this Moodle version
 $plugin->component = 'mod_data';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index cc2a922..95c9437 100644 (file)
@@ -30,7 +30,7 @@ global $CFG;
 require_once(__DIR__ . '/../../../engine/tests/helpers.php');
 require_once(__DIR__ . '/../../../behaviour/deferredfeedback/behaviour.php');
 require_once(__DIR__ . '/../question.php');
-
+require_once($CFG->dirroot . '/question/type/missingtype/questiontype.php');
 
 /**
  * Unit tests for the 'missing' question type.