Merge branch 'MDL-56999-master' of git://github.com/damyon/moodle
authorDavid Monllao <davidm@moodle.com>
Thu, 17 Nov 2016 01:31:07 +0000 (09:31 +0800)
committerDavid Monllao <davidm@moodle.com>
Thu, 17 Nov 2016 01:31:07 +0000 (09:31 +0800)
85 files changed:
admin/tool/lp/classes/external/cohort_summary_exporter.php
admin/upgradesettings.php
backup/util/ui/tests/behat/behat_backup.php
composer.json
composer.lock
course/lib.php
course/tests/behat/category_management.feature
enrol/manual/yui/quickenrolment/assets/skins/sam/quickenrolment.css
enrol/manual/yui/quickenrolment/quickenrolment.js
enrol/yui/rolemanager/rolemanager.js
index.php
lang/en/cache.php
lang/en/message.php
lib/ajax/service.php
lib/amd/build/backoff_timer.min.js [new file with mode: 0644]
lib/amd/src/backoff_timer.js [new file with mode: 0644]
lib/classes/upgrade/util.php
lib/coursecatlib.php
lib/db/caches.php
lib/editor/atto/plugins/image/styles.css
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/editor/tinymce/styles.css
lib/messagelib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputfactories.php
lib/outputlib.php
lib/templates/popover_region.mustache
lib/tests/coursecatlib_test.php
lib/tests/upgrade_util_test.php
message/amd/build/message_area.min.js
message/amd/build/message_area_contacts.min.js
message/amd/build/message_area_messages.min.js
message/amd/build/message_area_search.min.js
message/amd/src/message_area.js
message/amd/src/message_area_contacts.js
message/amd/src/message_area_messages.js
message/amd/src/message_area_search.js
message/classes/api.php
message/classes/helper.php
message/classes/output/messagearea/message.php
message/classes/output/messagearea/message_area.php
message/classes/time_last_message_between_users.php [new file with mode: 0644]
message/externallib.php
message/index.php
message/lib.php
message/output/popup/lib.php
message/output/popup/templates/message_popover.mustache
message/templates/message_area.mustache
message/templates/message_area_contacts_area.mustache
message/templates/message_area_messages_area.mustache
message/tests/api_test.php
message/tests/behat/search_messages.feature
message/tests/externallib_test.php
mod/assign/feedback/editpdf/fpdi/fpdi_bridge.php
mod/assign/feedback/editpdf/fpdi/readme_moodle.txt
mod/lti/view.php
mod/quiz/lib.php
mod/wiki/locallib.php
theme/boost/amd/build/form-display-errors.min.js
theme/boost/amd/build/loader.min.js
theme/boost/amd/src/form-display-errors.js
theme/boost/amd/src/loader.js
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/buttons.scss
theme/boost/scss/moodle/calendar.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/expendable.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/icons.scss
theme/boost/scss/moodle/popover-region.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/filemanager_fileselect.mustache
theme/boost/templates/flat_navigation.mustache
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/buttons.less
theme/bootstrapbase/less/moodle/popover_region.less
theme/bootstrapbase/style/moodle.css
theme/clean/style/custom.css
theme/more/style/custom.css
version.php

index a22ac9f..cd00a6f 100644 (file)
@@ -50,7 +50,8 @@ class cohort_summary_exporter extends \core_competency\external\exporter {
             ),
             'idnumber' => array(
                 'type' => PARAM_RAW,        // ID numbers are plain text.
-                'default' => ''
+                'default' => '',
+                'null' => NULL_ALLOWED
             ),
             'visible' => array(
                 'type' => PARAM_BOOL,
index 38a8f06..eecf91a 100644 (file)
@@ -67,7 +67,7 @@ echo '<fieldset>';
 echo '<div class="clearer"><!-- --></div>';
 echo $newsettingshtml;
 echo '</fieldset>';
-echo '<div class="form-buttons"><input class="form-submit" type="submit" value="'.get_string('savechanges','admin').'" /></div>';
+echo '<div class="form-buttons"><input class="form-submit btn btn-primary" type="submit" value="'.get_string('savechanges','admin').'" /></div>';
 echo '</div>';
 echo '</form>';
 
index ef8aada..d4ffb03 100644 (file)
@@ -247,7 +247,7 @@ class behat_backup extends behat_base {
 
         // The first category in the list.
         $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" .
-            "/descendant::div[@class='restore-course-search']" .
+            "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' restore-course-search ')]" .
             "/descendant::input[@type='radio']");
         $radionode->click();
 
index 3f7df7f..bb8421f 100644 (file)
@@ -7,6 +7,6 @@
     "require-dev": {
         "phpunit/phpunit": "5.5.*",
         "phpunit/dbUnit": "1.4.*",
-        "moodlehq/behat-extension": "3.32.3"
+        "moodlehq/behat-extension": "3.32.4"
     }
 }
index 1e231a8..ae338bf 100644 (file)
@@ -4,8 +4,8 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "ec5f1e9d8b8134cf6a4490ba5dfe0082",
-    "content-hash": "583f9a915721de799118a396dc81f177",
+    "hash": "6042e8e9600da0881e3f63b2e79ad742",
+    "content-hash": "59c508468d7f10d1a16b4d24228e7eab",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.32.3",
+            "version": "v3.32.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "8296916088e62f7cdf67b34abeebd419b301caff"
+                "reference": "d363b92f62770acdd8cd878810777f3a61eada4d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/8296916088e62f7cdf67b34abeebd419b301caff",
-                "reference": "8296916088e62f7cdf67b34abeebd419b301caff",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d363b92f62770acdd8cd878810777f3a61eada4d",
+                "reference": "d363b92f62770acdd8cd878810777f3a61eada4d",
                 "shasum": ""
             },
             "require": {
                 "Behat",
                 "moodle"
             ],
-            "time": "2016-10-03 03:33:07"
+            "time": "2016-11-10 23:36:48"
         },
         {
             "name": "myclabs/deep-copy",
index 926c1ae..07616ea 100644 (file)
@@ -3607,6 +3607,7 @@ function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcon
                 JOIN {course} c ON cm.course = c.id
                 JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
                WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
+                AND cm.deletioninprogress = 0
                 AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%";
 
     $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core',
index b90a8f8..5824254 100644 (file)
@@ -348,3 +348,20 @@ Feature: Test category management actions
       | Category ID number | CAT1 |
     When I press "Create category"
     Then I should see "ID number is already used for another category"
+
+  Scenario: Test that is possible to remove an idnumber from a course category
+    Given the following "categories" exist:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+      | Cat 2 | 0 ||
+    And I log in as "admin"
+    And I go to the courses management page
+    And I should see "CAT1" in the "#category-listing" "css_element"
+    When I click on "edit" action for "Cat 1" in management category listing
+    And I set the following fields to these values:
+      | Category name | Category 1 (edited) |
+      | Category ID number ||
+    And I press "Save changes"
+    # Redirect
+    Then I should see "Category 1 (edited)" in the "#category-listing" "css_element"
+    And I should not see "CAT1" in the "#course-listing" "css_element"
index 20076cc..34e12c5 100644 (file)
@@ -37,12 +37,7 @@ Structure of the user enroller panel
 
 .user-enroller-panel {
     width: 400px;
-    background-color: #666;
     position: absolute;
-    top: 10%;
-    left: 10%;
-    border: 1px solid #666;
-    border-width: 0 5px 5px 0;
 }
 
 .user-enroller-panel.hidden {
@@ -50,57 +45,22 @@ Structure of the user enroller panel
 }
 
 .user-enroller-panel .uep-wrap {
-    margin-top: -5px;
-    margin-left: -5px;
-    background-color: #fff;
-    border: 1px solid #999;
     height: inherit;
 }
 
-.user-enroller-panel .uep-header {
-    background-color: #eee;
-    padding: 1px;
-}
-
-.user-enroller-panel .uep-header h2 {
-    margin: 3px 1em 0.5em 1em;
-    font-size: 1em;
+.user-enroller-panel .uep-search-results .user .count {
+    display: none;
 }
 
-.user-enroller-panel .uep-header .close {
-    width: 25px;
-    height: 15px;
-    position: absolute;
-    top: 2px;
-    right: 1em;
-    cursor: pointer;
-    background: url("sprite.png") no-repeat scroll 0 0 transparent;
+.user-enroller-panel .uep-search-results .cohort .count {
+    display: none;
 }
 
 .user-enroller-panel .uep-content {
-    text-align: center;
     position: relative;
     width: 100%;
-    border-top: 1px solid #999;
-    border-bottom: 1px solid #999;
-}
-
-.user-enroller-panel .uep-content .uep-controls {
-    margin: 0;
-    padding: 3px;
-    background-color: #ddd;
-    text-align: left;
-    border-bottom: 1px solid #bbb;
-}
-
-.user-enroller-panel .uep-content .uep-controls label {
-    display: inline;
-    padding-right: 5px;
-}
-
-.user-enroller-panel .uep-content .uep-controls .uep-enrolment-option input {
-    vertical-align: middle;
-    margin-left: 1em;
+    box-sizing: border-box;
+    max-height: 1000px;
 }
 
 .user-enroller-panel .uep-ajax-content {
@@ -108,39 +68,6 @@ Structure of the user enroller panel
     overflow: auto;
 }
 
-.user-enroller-panel .uep-search-results .totalusers {
-    background-color: #eee;
-    padding: 5px;
-    border-bottom: 1px solid #bbb;
-    font-size: 7pt;
-    font-weight: bold;
-}
-
-.user-enroller-panel .uep-search-results .user {
-    width: 100%;
-    text-align: left;
-    font-size: 9pt;
-    border-bottom: 1px solid #ddd;
-    border-top: 1px solid #eee;
-}
-
-.user-enroller-panel .uep-search-results .user.odd {
-    border-bottom: 1px solid #ddd;
-    border-top: 1px solid #eee;
-    background-color: #f9f9f9;
-}
-
-.user-enroller-panel .uep-search-results .user .count {
-    width: 20px;
-    float: left;
-    font-size: 7pt;
-    line-height: 41px;
-    border-right: 1px solid #ddd;
-    background-color: #eee;
-    text-align: right;
-    padding: 2px;
-}
-
 .user-enroller-panel .uep-search-results .user .picture {
     width: 45px;
     float: left;
@@ -148,14 +75,13 @@ Structure of the user enroller panel
 }
 
 .user-enroller-panel .uep-search-results .user .details {
-    width: 250px;
+    width: 180px;
     float: left;
     margin: 3px;
 }
 
 .user-enroller-panel .uep-search-results .user .options {
     padding-right: 7px;
-    font-size: 8pt;
     margin: 3px;
 }
 
@@ -165,56 +91,21 @@ Structure of the user enroller panel
     cursor: pointer;
 }
 
-.user-enroller-panel .uep-search-results .user.enrolled .count {
-    width: 40px;
-    color: #eee;
-}
-
-.user-enroller-panel .uep-search-results .uep-more-results {
-    background-color: #eee;
-    padding: 5px;
-    border-top: 1px solid #bbb;
-}
-
-.user-enroller-panel .uep-search-results .totalcohorts {
-    background-color: #eee;
-    padding: 5px;
-    border-bottom: 1px solid #bbb;
-    font-size: 7pt;
-    font-weight: bold;
-}
-
 .user-enroller-panel .uep-search-results .cohort {
     width: 100%;
     text-align: left;
-    font-size: 9pt;
-    border-bottom: 1px solid #ddd;
-    border-top: 1px solid #eee;
 }
 
 .user-enroller-panel .uep-search-results .cohort .count {
-    width: 20px;
-    float: left;
-    font-size: 7pt;
-    line-height: 27px;
-    border-right: 1px solid #ddd;
-    background-color: #eee;
-    text-align: right;
-    padding: 2px;
+    display: none;
 }
 
 .user-enroller-panel .uep-search-results .cohort .details {
-    width: 250px;
+    width: 180px;
     float: left;
     margin: 5px;
 }
 
-.user-enroller-panel .uep-search-results .cohort .options {
-    padding-right: 7px;
-    font-size: 8pt;
-    margin: 3px;
-}
-
 .user-enroller-panel .uep-search-results .cohort .options .enrol {
     margin: 3px;
     float: right;
@@ -223,16 +114,15 @@ Structure of the user enroller panel
 
 .user-enroller-panel .uep-search-results .cohort.enrolled .count {
     width: 40px;
-    color: #eee;
 }
 
 .user-enroller-panel .uep-loading-lightbox {
     position: absolute;
     width: 100%;
     height: 100%;
+    background-color: #ddd;
     top: 0;
     left: 0;
-    background-color: #fff;
     min-width: 50px;
     min-height: 50px;
 }
@@ -245,12 +135,11 @@ Structure of the user enroller panel
     margin: auto;
     vertical-align: middle;
     margin-top: 125px;
+    display: block;
 }
 
 .user-enroller-panel .uep-footer {
-    padding: 3px;
-    background-color: #ddd;
-    text-align: center;
+    text-align: right;
 }
 
 .user-enroller-panel .uep-search {
@@ -263,7 +152,6 @@ Structure of the user enroller panel
 
 .user-enroller-panel .uep-search input {
     width: 50%;
-    margin: 0 0 10px 5px;
 }
 
 .user-enroller-panel .uep-search input.uep-search-btn {
@@ -285,8 +173,8 @@ Structure of the user enroller panel
 }
 
 .user-enroller-panel .collapsiblearea {
-    border: 1px solid #bbb;
-    background-color: #f6f6f6;
+    border: 1px solid #ddd;
+    padding: 0.5rem;
 }
 
 .user-enroller-panel .collapsiblearea.hidden {
index 012cafb..8f235cc 100644 (file)
@@ -32,7 +32,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
     /** CSS classes for nodes in structure **/
     var CSS = {
         PANEL : 'user-enroller-panel',
-        WRAP : 'uep-wrap',
+        WRAP : 'uep-wrap modal-dialog',
         HEADER : 'uep-header',
         CONTENT : 'uep-content',
         AJAXCONTENT : 'uep-ajax-content',
@@ -90,33 +90,33 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             if (this.get(UEP.DISABLEGRADEHISTORY) != true) {
                 recovergrades = create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.RECOVERGRADES+'"></div>')
                     .append(create('<label class="'+CSS.RECOVERGRADESTITLE+'" for="'+CSS.RECOVERGRADES+'">'+M.util.get_string('recovergrades', 'enrol')+'</label>'))
-                    .append(create('<input type="checkbox" id="'+CSS.RECOVERGRADES+'" name="'+CSS.RECOVERGRADES+'"'+ this.get(UEP.RECOVERGRADESDEFAULT) +' />'))
+                    .append(create('<input type="checkbox" class="m-x-1" id="'+CSS.RECOVERGRADES+'" name="'+CSS.RECOVERGRADES+'"'+ this.get(UEP.RECOVERGRADESDEFAULT) +' />'))
             }
 
             this.set(UEP.BASE, create('<div class="'+CSS.PANEL+' '+CSS.HIDDEN+'"></div>')
-                .append(create('<div class="'+CSS.WRAP+'"></div>')
-                    .append(create('<div class="'+CSS.HEADER+' header"></div>')
-                        .append(create('<div class="'+CSS.CLOSE+'"></div>'))
-                        .append(create('<h2>'+M.util.get_string('enrolusers', 'enrol')+'</h2>')))
-                    .append(create('<div class="'+CSS.CONTENT+'"></div>')
-                        .append(create('<div class="'+CSS.SEARCHCONTROLS+'"></div>')
+                .append(create('<div class="'+CSS.WRAP+' modal show modal-dialog modal-content"></div>')
+                    .append(create('<div class="'+CSS.HEADER+' header modal-header"></div>')
+                        .append(create('<div class="'+CSS.CLOSE+'">&times;</div>'))
+                        .append(create('<h2 class="modal-title">'+M.util.get_string('enrolusers', 'enrol')+'</h2>')))
+                    .append(create('<div class="'+CSS.CONTENT+' modal-body"></div>')
+                        .append(create('<div class="'+CSS.SEARCHCONTROLS+' form-inline"></div>')
                             .append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.ROLE+'"><label for="id_enrol_manual_assignable_roles">'+M.util.get_string('assignroles', 'role')+'</label></div>')
-                                    .append(create('<select id="id_enrol_manual_assignable_roles"><option value="">'+M.util.get_string('none', 'enrol')+'</option></select>'))
+                                    .append(create('<select id="id_enrol_manual_assignable_roles" class="custom-select"><option value="">'+M.util.get_string('none', 'enrol')+'</option></select>'))
                             )
-                            .append(create('<div class="'+CSS.ENTITYSELECTOR+'"></div>'))
+                            .append(create('<div class="'+CSS.ENTITYSELECTOR+' m-y-1"></div>'))
                             .append(create('<div class="'+CSS.SEARCHOPTIONS+'"></div>')
                                 .append(create('<div class="'+CSS.COLLAPSIBLEHEADING+'"><img alt="" />'+M.util.get_string('enrolmentoptions', 'enrol')+'</div>'))
                                 .append(create('<div class="'+CSS.COLLAPSIBLEAREA+' '+CSS.HIDDEN+'"></div>')
                                     .append(recovergrades)
                                     .append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.STARTDATE+'">'+M.util.get_string('startingfrom', 'moodle')+'</div>')
-                                        .append(create('<select></select>')))
+                                        .append(create('<select class="custom-select"></select>')))
                                     .append(create('<div class="'+CSS.ENROLMENTOPTION+' '+CSS.DURATION+'">'+M.util.get_string('enrolperiod', 'enrol')+'</div>')
-                                        .append(create('<select><option value="0" selected="selected">'+M.util.get_string('unlimitedduration', 'enrol')+'</option></select>')))
+                                        .append(create('<select class="custom-select"><option value="0" selected="selected">'+M.util.get_string('unlimitedduration', 'enrol')+'</option></select>')))
                                 )
                             )
                             .append(create('<div class="'+CSS.SEARCH+'"><label for="enrolusersearch" class="accesshide">'+M.util.get_string('usersearch', 'enrol')+'</label></div>')
-                                .append(create('<input type="text" id="enrolusersearch" value="" />'))
-                                .append(create('<input type="button" id="searchbtn" class="'+CSS.SEARCHBTN+'" value="'+M.util.get_string('usersearch', 'enrol')+'" />'))
+                                .append(create('<input class="form-control" type="text" id="enrolusersearch" value="" />'))
+                                .append(create('<input type="button" id="searchbtn" class="'+CSS.SEARCHBTN+' btn btn-secondary m-l-1" value="'+M.util.get_string('usersearch', 'enrol')+'" />'))
                             )
                         )
                         .append(create('<div class="'+CSS.AJAXCONTENT+'"></div>'))
@@ -124,9 +124,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                             .append(create('<img alt="loading" class="'+CSS.LOADINGICON+'" />')
                                 .setAttribute('src', M.util.image_url('i/loading', 'moodle')))
                             .setStyle('opacity', 0.5)))
-                    .append(create('<div class="'+CSS.FOOTER+'"></div>')
+                    .append(create('<div class="'+CSS.FOOTER+' modal-footer"></div>')
                         .append(create('<div class="'+CSS.CLOSEBTN+'"></div>')
-                            .append(create('<input type="button" value="'+M.util.get_string('finishenrollingusers', 'enrol')+'" />'))
+                            .append(create('<input type="button" class="btn btn-primary" value="'+M.util.get_string('finishenrollingusers', 'enrol')+'" />'))
                         )
                     )
                 )
@@ -151,9 +151,9 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
 
             if (this.get(UEP.COHORTSAVAILABLE)) {
                 this.get(UEP.BASE).one('.'+CSS.ENTITYSELECTOR)
-                    .append(create('<input type="radio" id="id_enrol_manual_entity_users" name="enrol_manual_entity" value="users" checked="checked"/>'))
+                    .append(create('<input type="radio" id="id_enrol_manual_entity_users" name="enrol_manual_entity" value="users" class="m-r-1" checked="checked"/>'))
                     .append(create('<label for="id_enrol_manual_entity_users">'+ M.util.get_string('browseusers', 'enrol_manual')+'</label>'))
-                    .append(create('<input type="radio" id="id_enrol_manual_entity_cohorts" name="enrol_manual_entity" value="cohorts"/>'))
+                    .append(create('<input type="radio" id="id_enrol_manual_entity_cohorts" name="enrol_manual_entity" class="m-x-1" value="cohorts"/>'))
                     .append(create('<label for="id_enrol_manual_entity_cohorts">'+M.util.get_string('browsecohorts', 'enrol_manual')+'</label>'));
                 this.get(UEP.BASE).one('#id_enrol_manual_entity_cohorts').on('change', this.search, this);
                 this.get(UEP.BASE).one('#id_enrol_manual_entity_users').on('change', this.search, this);
@@ -397,7 +397,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             }
             var users;
             if (!args.append) {
-                users = create('<div class="'+CSS.USERS+'"></div>');
+                users = create('<div class="'+CSS.USERS+' list-group"></div>');
             } else {
                 users = this.get(UEP.BASE).one('.'+CSS.SEARCHRESULTS+' .'+CSS.USERS);
             }
@@ -405,7 +405,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
             for (var i in result.response.users) {
                 count++;
                 var user = result.response.users[i];
-                users.append(create('<div class="'+CSS.USER+' clearfix" rel="'+user.id+'"></div>')
+                users.append(create('<div class="'+CSS.USER+' clearfix list-group-item list-group-item-action" rel="'+user.id+'"></div>')
                     .addClass((count%2)?CSS.ODD:CSS.EVEN)
                     .append(create('<div class="'+CSS.COUNT+'">'+count+'</div>'))
                     .append(create('<div class="'+CSS.PICTURE+'"></div>')
@@ -414,7 +414,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                         .append(create('<div class="'+CSS.FULLNAME+'">'+user.fullname+'</div>'))
                         .append(create('<div class="'+CSS.EXTRAFIELDS+'">'+user.extrafields+'</div>')))
                     .append(create('<div class="'+CSS.OPTIONS+'"></div>')
-                        .append(create('<input type="button" class="'+CSS.ENROL+'" value="'+M.util.get_string('enrol', 'enrol')+'" />')))
+                        .append(create('<input type="button" class="'+CSS.ENROL+' btn btn-secondary" value="'+M.util.get_string('enrol', 'enrol')+'" />')))
                 );
             }
             this.set(UEP.USERCOUNT, count);
@@ -464,7 +464,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                     .append(create('<div class="'+CSS.DETAILS+'"></div>')
                         .append(create('<div class="'+CSS.COHORTNAME+'">'+cohort.name+'</div>')))
                     .append(create('<div class="'+CSS.OPTIONS+'"></div>')
-                        .append(create('<input type="button" class="' + CSS.ENROL + '" value="' + M.util.get_string('enrolxusers', 'enrol', cohort.cnt) + '" />')))
+                        .append(create('<input type="button" class="' + CSS.ENROL + ' btn btn-secondary" value="' + M.util.get_string('enrolxusers', 'enrol', cohort.cnt) + '" />')))
                 );
             }
             this.set(UEP.COHORTCOUNT, count);
index a75d61a..750ecdb 100644 (file)
@@ -358,12 +358,18 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
         submitevent : null,
         initializer : function() {
             var i, m = this.get(MANIPULATOR);
-            var element = Y.Node.create('<div class="enrolpanel roleassign"><div class="container"><div class="header"><h2>'+M.util.get_string('assignroles', 'role')+'</h2><div class="close"></div></div><div class="content"></div></div></div>');
+            var element = Y.Node.create('<div class="popover popover-bottom"><div class="arrow"></div>' +
+                                        '<div class="header popover-title">' +
+                                        '<div role="button" class="close" aria-label="Close">' +
+                                        '<span aria-hidden="true">&times;</span></div>' +
+                                        '<h3>'+M.util.get_string('assignroles', 'role')+'</h3>' +
+                                        '</div><div class="content popover-content form-inline form-group"></div></div>');
             var content = element.one('.content');
             var roles = m.get(ASSIGNABLEROLES);
             for (i in roles) {
                 var buttonid = 'add_assignable_role_' + roles[i].id;
-                var buttonhtml = '<input type="button" value="' + roles[i].name + '" id="' + buttonid + '" />';
+                var buttonhtml = '<input type="button" class="btn btn-secondary m-r-1" value="' +
+                                 roles[i].name + '" id="' + buttonid + '" />';
                 var button = Y.Node.create(buttonhtml);
                 button.on('click', this.submit, this, roles[i].id);
                 content.append(button);
@@ -392,7 +398,7 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
             } else {
                 this.get('elementNode').setStyle('left', x).setStyle('top', y);
             }
-            this.get('elementNode').addClass('visible');
+            this.get('elementNode').setStyle('display', 'block');
             this.escCloseEvent = Y.on('key', this.hide, document.body, 'down:27', this);
             this.displayed = true;
         },
@@ -409,7 +415,7 @@ YUI.add('moodle-enrol-rolemanager', function(Y) {
             }
             this.roles = [];
             this.user = null;
-            this.get('elementNode').removeClass('visible');
+            this.get('elementNode').setStyle('display', 'none');
             if (this.submitevent) {
                 this.submitevent.detach();
                 this.submitevent = null;
index 7167e9a..0c9e490 100644 (file)
--- a/index.php
+++ b/index.php
@@ -147,7 +147,7 @@ if (!empty($CFG->customfrontpageinclude)) {
         $context = context_course::instance(SITEID);
 
         // If the section name is set we show it.
-        if (!is_null($section->name)) {
+        if (trim($section->name) !== '') {
             echo $OUTPUT->heading(
                 format_string($section->name, true, array('context' => $context)),
                 2,
index 99e3310..b97ae2b 100644 (file)
@@ -52,6 +52,7 @@ $string['cachedef_suspended_userids'] = 'List of suspended users per course';
 $string['cachedef_groupdata'] = 'Course group information';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_langmenu'] = 'List of available languages';
+$string['cachedef_message_time_last_message_between_users'] = 'Time created for most recent message between users';
 $string['cachedef_locking'] = 'Locking';
 $string['cachedef_message_processors_enabled'] = "Message processors enabled status";
 $string['cachedef_navigation_expandcourse'] = 'Navigation expandable courses';
index 404b70a..49bd897 100644 (file)
@@ -68,6 +68,9 @@ $string['messagepreferences'] = 'Message preferences';
 $string['messages'] = 'Messages';
 $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
 $string['newonlymsg'] = 'Show only new';
+$string['newmessage'] = 'New message';
+$string['newmessagesearch'] = 'Select or search for a contact to send a new message.';
+$string['newsearch'] = 'New search';
 $string['noframesjs'] = 'Use more accessible interface';
 $string['nocontacts'] = 'No contacts';
 $string['nomessages'] = 'No messages';
@@ -96,6 +99,8 @@ $string['requiresconfiguration'] = 'Requires configuration';
 $string['searchforuser'] = 'Search for a user';
 $string['searchforuserorcourse'] = 'Search for a user or course';
 $string['searchmessages'] = 'Search messages';
+$string['searchcombined'] = 'Search people and messages';
+$string['seeall'] = 'See all';
 $string['selectmessagestodelete'] = 'Select messages to delete';
 $string['selectnotificationtoview'] = 'Select from the list of notifications on the side to view more details';
 $string['send'] = 'Send';
index 4a0a1db..3298c0e 100644 (file)
@@ -32,6 +32,8 @@ define('AJAX_SCRIPT', true);
 require_once(__DIR__ . '/../../config.php');
 require_once($CFG->libdir . '/externallib.php');
 
+define('PREFERRED_RENDERER_TARGET', RENDERER_TARGET_GENERAL);
+
 $rawjson = file_get_contents('php://input');
 
 $requests = json_decode($rawjson, true);
diff --git a/lib/amd/build/backoff_timer.min.js b/lib/amd/build/backoff_timer.min.js
new file mode 100644 (file)
index 0000000..bb80b45
Binary files /dev/null and b/lib/amd/build/backoff_timer.min.js differ
diff --git a/lib/amd/src/backoff_timer.js b/lib/amd/src/backoff_timer.js
new file mode 100644 (file)
index 0000000..da0fabd
--- /dev/null
@@ -0,0 +1,175 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A timer that will execute a callback with decreasing frequency. Useful for
+ * doing polling on the server without overwhelming it with requests.
+ *
+ * @module     core/backoff_timer
+ * @class      backoff_timer
+ * @package    core
+ * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(function() {
+
+    /**
+     * Constructor for the back off timer.
+     *
+     * @param {function} callback The function to execute after each tick
+     * @param {function} backoffFunction The function to determine what the next timeout value should be
+     */
+    var BackoffTimer = function(callback, backoffFunction) {
+        this.callback = callback;
+        this.backOffFunction = backoffFunction;
+    };
+
+    /**
+     * @type {function} callback The function to execute after each tick
+     */
+    BackoffTimer.prototype.callback = null;
+
+    /**
+     * @type {function} backoffFunction The function to determine what the next timeout value should be
+     */
+    BackoffTimer.prototype.backOffFunction = null;
+
+    /**
+     * @type {int} time The timeout value to use
+     */
+    BackoffTimer.prototype.time = null;
+
+    /**
+     * @type {numeric} timeout The timeout identifier
+     */
+    BackoffTimer.prototype.timeout = null;
+
+    /**
+     * Generate the next timeout in the back off time sequence
+     * for the timer.
+     *
+     * The back off function is called to calculate the next value.
+     * It is given the current value and an array of all previous values.
+     *
+     * @method generateNextTime
+     * @return {int} The new timeout value (in milliseconds)
+     */
+    BackoffTimer.prototype.generateNextTime = function() {
+        var newTime = this.backOffFunction(this.time);
+        this.time = newTime;
+
+        return newTime;
+    };
+
+    /**
+     * Stop the current timer and clear the previous time values
+     *
+     * @method reset
+     * @return {object} this
+     */
+    BackoffTimer.prototype.reset = function() {
+        this.time = null;
+        this.stop();
+
+        return this;
+    };
+
+    /**
+     * Clear the current timeout, if one is set.
+     *
+     * @method stop
+     * @return {object} this
+     */
+    BackoffTimer.prototype.stop = function() {
+        if (this.timeout) {
+            window.clearTimeout(this.timeout);
+            this.timeout = null;
+        }
+
+        return this;
+    };
+
+    /**
+     * Start the current timer by generating the new timeout value and
+     * starting the ticks.
+     *
+     * This function recurses after each tick with a new timeout value
+     * generated each time.
+     *
+     * The callback function is called after each tick.
+     *
+     * @method start
+     * @return {object} this
+     */
+    BackoffTimer.prototype.start = function() {
+        // If we haven't already started.
+        if (!this.timeout) {
+            var time = this.generateNextTime();
+            this.timeout = window.setTimeout(function() {
+                this.callback();
+                // Clear the existing timer.
+                this.stop();
+                // Start the next timer.
+                this.start();
+            }.bind(this), time);
+        }
+
+        return this;
+    };
+
+    /**
+     * Reset the timer and start it again from the initial timeout
+     * values
+     *
+     * @method restart
+     * @return {object} this
+     */
+    BackoffTimer.prototype.restart = function() {
+        return this.reset().start();
+    };
+
+    /**
+     * Returns an incremental function for the timer.
+     *
+     * @param {int} minamount The minimum amount of time we wait before checking
+     * @param {int} incrementamount The amount to increment the timer by
+     * @param {int} maxamount The max amount to ever increment to
+     * @param {int} timeoutamount The timeout to use once we reach the max amount
+     * @return {function}
+     */
+     BackoffTimer.getIncrementalCallback = function(minamount, incrementamount, maxamount, timeoutamount) {
+
+        /**
+         * An incremental function for the timer.
+         *
+         * @param {(int|null)} time The current timeout value or null if none set
+         * @return {int} The new timeout value
+         */
+        return function(time) {
+            if (!time) {
+                return minamount;
+            }
+
+            // Don't go over the max amount.
+            if (time + incrementamount > maxamount) {
+                return timeoutamount;
+            }
+
+            return time + incrementamount;
+        };
+    };
+
+    return BackoffTimer;
+});
index c300901..2c14e98 100644 (file)
@@ -86,7 +86,9 @@ final class util {
      * @return bool
      */
     public static function can_use_tls12(array $curlinfo, $uname) {
-        if ($curlinfo['version_number'] < 467456 || !defined('CURL_SSLVERSION_TLSv1_2')) {
+        // Do not compare the cURL version, e.g. $curlinfo['version_number'], with v7.34.0 (467456):
+        // some Linux distros backport security issues and keep lower version numbers.
+        if (!defined('CURL_SSLVERSION_TLSv1_2')) {
             return false;
         }
 
index 1cd5269..d8ce7b8 100644 (file)
@@ -361,15 +361,13 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
         $newcategory->name = $data->name;
 
         // Validate and set idnumber.
-        if (!empty($data->idnumber)) {
+        if (isset($data->idnumber)) {
             if (core_text::strlen($data->idnumber) > 100) {
                 throw new moodle_exception('idnumbertoolong');
             }
-            if ($DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
+            if (strval($data->idnumber) !== '' && $DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
                 throw new moodle_exception('categoryidnumbertaken');
             }
-        }
-        if (isset($data->idnumber)) {
             $newcategory->idnumber = $data->idnumber;
         }
 
@@ -484,11 +482,11 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
             $newcategory->name = $data->name;
         }
 
-        if (isset($data->idnumber) && $data->idnumber != $this->idnumber) {
+        if (isset($data->idnumber) && $data->idnumber !== $this->idnumber) {
             if (core_text::strlen($data->idnumber) > 100) {
                 throw new moodle_exception('idnumbertoolong');
             }
-            if ($DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
+            if (strval($data->idnumber) !== '' && $DB->record_exists('course_categories', array('idnumber' => $data->idnumber))) {
                 throw new moodle_exception('categoryidnumbertaken');
             }
             $newcategory->idnumber = $data->idnumber;
index b709f06..9a8e9f2 100644 (file)
@@ -301,4 +301,12 @@ $definitions = array(
         'staticacceleration' => true,
         'staticaccelerationsize' => 3
     ),
+
+    // Caches the time of the last message between two users.
+    'message_time_last_message_between_users' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true, // The id of the sender and recipient is used.
+        'simplevalues' => true,
+        'datasource' => '\core_message\time_last_message_between_users',
+    ),
 );
index 80cdcfe..84c0533 100644 (file)
@@ -53,6 +53,7 @@
     max-width: calc(100% - 1em);
 }
 
+/*rtl:begin:ignore*/
 .atto_image_button_left {
     float: left;
     margin: 0 0.5em 0 0;
@@ -64,3 +65,4 @@
     margin: 0 0 0 0.5em;
     max-width: calc(100% - 1em);
 }
+/*rtl:end:ignore*/
\ No newline at end of file
index e0681b5..edab4a1 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index d04e876..b706ced 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 7720c6e..9d68afe 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 546fe20..a28e936 100644 (file)
@@ -170,6 +170,8 @@ Y.extend(Editor, Y.Base, {
             return;
         }
 
+        var extraclasses = this.textarea.getAttribute('class');
+
         this._eventHandles = [];
 
         this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
@@ -178,7 +180,7 @@ Y.extend(Editor, Y.Base, {
                 'role="textbox" ' +
                 'spellcheck="true" ' +
                 'aria-live="off" ' +
-                'class="{{CSS.CONTENT}}" ' +
+                'class="{{CSS.CONTENT}} ' + extraclasses + '" ' +
                 '/>');
         this.editor = Y.Node.create(template({
             elementid: this.get('elementid'),
index b588250..79bbd60 100644 (file)
@@ -18,3 +18,7 @@
         margin-left: -3px;
     }
 }
+
+.mceToolbar td {
+    box-sizing: content-box;
+}
index 34e7c38..5347f17 100644 (file)
@@ -234,6 +234,15 @@ function message_send($eventdata) {
         }
     }
 
+    // Only cache messages, not notifications.
+    if (empty($savemessage->notification)) {
+        // Cache the timecreated value of the last message between these two users.
+        $cache = cache::make('core', 'message_time_last_message_between_users');
+        $key = \core_message\helper::get_last_message_time_created_cache_key($savemessage->useridfrom,
+            $savemessage->useridto);
+        $cache->set($key, $savemessage->timecreated);
+    }
+
     // Store unread message just in case we get a fatal error any time later.
     $savemessage->id = $DB->insert_record('message', $savemessage);
     $eventdata->savedmessageid = $savemessage->id;
index ecff98b..1a7f9e0 100644 (file)
@@ -4438,6 +4438,7 @@ function complete_user_login($user) {
             if ($changeurl = $userauth->change_password_url()) {
                 redirect($changeurl);
             } else {
+                require_once($CFG->dirroot . '/login/lib.php');
                 $SESSION->wantsurl = core_login_get_return_url();
                 redirect($CFG->httpswwwroot.'/login/change_password.php');
             }
index 588bb99..2f6c610 100644 (file)
@@ -2334,9 +2334,6 @@ class global_navigation extends navigation_node {
                 if ($USER->id != $user->id) {
                     $messageargs['user2'] = $user->id;
                 }
-                if ($course->id != $SITE->id) {
-                    $messageargs['viewing'] = MESSAGE_VIEW_COURSE. $course->id;
-                }
                 $url = new moodle_url('/message/index.php', $messageargs);
                 $usernode->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages');
             }
@@ -4640,9 +4637,6 @@ class settings_navigation extends navigation_node {
                 if ($USER->id != $user->id) {
                     $messageargs['user2'] = $user->id;
                 }
-                if ($course->id != $SITE->id) {
-                    $messageargs['viewing'] = MESSAGE_VIEW_COURSE. $course->id;
-                }
                 $url = new moodle_url('/message/index.php', $messageargs);
                 $dashboard->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages');
             }
index 2d7cabf..aa28a9e 100644 (file)
@@ -137,7 +137,9 @@ abstract class renderer_factory_base implements renderer_factory {
             // If the target hasn't been specified we need to guess the defaults.
             // We also override the target with the default if the maintenance target has been provided.
             // This ensures we don't use the maintenance renderer if we are processing a special target.
-            if (CLI_SCRIPT) {
+            if (defined('PREFERRED_RENDERER_TARGET')) {
+                $target = PREFERRED_RENDERER_TARGET;
+            } else if (CLI_SCRIPT) {
                 $target = RENDERER_TARGET_CLI;
             } else if (AJAX_SCRIPT) {
                 $target = RENDERER_TARGET_AJAX;
index dc40d2b..4e5e58e 100644 (file)
@@ -1513,6 +1513,13 @@ class theme_config {
             }
         }
 
+        // Now resolve all theme settings or do any other postprocessing.
+        // This needs to be done before calling core parser, since the parser strips [[settings]] tags.
+        $csspostprocess = $this->csspostprocess;
+        if (function_exists($csspostprocess)) {
+            $css = $csspostprocess($css, $this);
+        }
+
         // Post processing using an object representation of CSS.
         $hastreeprocessor = !empty($this->csstreepostprocessor) && function_exists($this->csstreepostprocessor);
         $needsparsing = $hastreeprocessor || !empty($this->rtlmode);
@@ -1534,12 +1541,6 @@ class theme_config {
             unset($csstree);
         }
 
-        // now resolve all theme settings or do any other postprocessing
-        $csspostprocess = $this->csspostprocess;
-        if (function_exists($csspostprocess)) {
-            $css = $csspostprocess($css, $this);
-        }
-
         return $css;
     }
 
index 17a9701..18bd954 100644 (file)
             </div>
             {{> core/loading }}
         </div>
+        {{$anchor}}
+            <a class="see-all-link"
+                href="{{{urls.seeall}}}">
+                <div class="popover-region-footer-container">
+                    <div class="popover-region-seeall-text">{{#str}} seeall, message {{/str}}</div>
+                </div>
+            </a>
+        {{/anchor}}
     </div>
 </div>
index b219753..10b3cdb 100644 (file)
@@ -128,6 +128,21 @@ class core_coursecatlib_testcase extends advanced_testcase {
         } catch (moodle_exception $e) {
             $this->assertInstanceOf('moodle_exception', $e);
         }
+        // Test that duplicates with an idnumber of 0 cannot be created.
+        coursecat::create(array('name' => 'Cat3', 'idnumber' => '0'));
+        try {
+            coursecat::create(array('name' => 'Cat4', 'idnumber' => '0'));
+            $this->fail('Duplicate idnumber "0" exception expected in coursecat::create');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('moodle_exception', $e);
+        }
+        // Test an update cannot make a duplicate idnumber of 0.
+        try {
+            $cat2->update(array('idnumber' => '0'));
+            $this->fail('Duplicate idnumber "0" exception expected in coursecat::update');
+        } catch (Exception $e) {
+            $this->assertInstanceOf('moodle_exception', $e);
+        }
     }
 
     public function test_visibility() {
index bba4ceb..50aa919 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-// Hack to let tests run on travis..
+// Hack to let tests run on Travis CI.
 defined('CURL_SSLVERSION_TLSv1_2') || define('CURL_SSLVERSION_TLSv1_2', 6);
 
 /**
@@ -36,16 +36,6 @@ defined('CURL_SSLVERSION_TLSv1_2') || define('CURL_SSLVERSION_TLSv1_2', 6);
  */
 class upgrade_util_testcase extends advanced_testcase {
 
-    /**
-     * A cURL version that supports TLS 1.2.
-     */
-    const VALID_CURL_VERSION = 467456;
-
-    /**
-     * A cURL version that does not support TLS 1.2.
-     */
-    const INVALID_CURL_VERSION = 467455;
-
     /**
      * The value of PHP_ZTS when thread safety is enabled.
      */
@@ -132,17 +122,11 @@ class upgrade_util_testcase extends advanced_testcase {
 
         // Set the curl values we are testing to the passed data.
         $curlinfo['ssl_version'] = $sslversion;
-        $curlinfo['version_number'] = self::VALID_CURL_VERSION;
 
         // Set uname to system value if none passed in test case.
         $uname = !empty($uname) ? $uname : php_uname('r');
 
         $this->assertSame($expected, \core\upgrade\util::can_use_tls12($curlinfo, $uname));
-
-        // Now set the curl version to outdated one.
-        $curlinfo['version_number'] = self::INVALID_CURL_VERSION;
-        // Tls12 should never be possible now curl version is bad.
-        $this->assertFalse(\core\upgrade\util::can_use_tls12($curlinfo, $uname));
     }
 
     /**
index 942dc1c..288f2df 100644 (file)
Binary files a/message/amd/build/message_area.min.js and b/message/amd/build/message_area.min.js differ
index 7ff3d01..aafb31b 100644 (file)
Binary files a/message/amd/build/message_area_contacts.min.js and b/message/amd/build/message_area_contacts.min.js differ
index 79d87c8..09a5437 100644 (file)
Binary files a/message/amd/build/message_area_messages.min.js and b/message/amd/build/message_area_messages.min.js differ
index 0a7f3e4..68f4af0 100644 (file)
Binary files a/message/amd/build/message_area_search.min.js and b/message/amd/build/message_area_search.min.js differ
index 681712a..b9482aa 100644 (file)
@@ -29,15 +29,30 @@ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_ar
          * Messagearea class.
          *
          * @param {String} selector The selector for the page region containing the message area.
+         * @param {int} pollmin
+         * @param {int} pollmax
+         * @param {int} polltimeout
          */
-        function Messagearea(selector) {
+        function Messagearea(selector, pollmin, pollmax, polltimeout) {
             this.node = $(selector);
+            this.pollmin = pollmin;
+            this.pollmax = pollmax;
+            this.polltimeout = polltimeout;
             this._init();
         }
 
         /** @type {jQuery} The jQuery node for the page region containing the message area. */
         Messagearea.prototype.node = null;
 
+        /** @type {int} The minimum time to poll for messages. */
+        Messagearea.prototype.pollmin = null;
+
+        /** @type {int} The maximum time to poll for messages. */
+        Messagearea.prototype.pollmax = null;
+
+        /** @type {int} The time used once we have reached the maximum polling time. */
+        Messagearea.prototype.polltimeout = null;
+
         /**
          * Initialise the other objects we require.
          */
@@ -102,6 +117,15 @@ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_ar
             return this.node.data('userid');
         };
 
+        /**
+         * Function to determine if we should be showing contacts initially or messages.
+         *
+         * @return {boolean} True to show contacts first, otherwise show messages.
+         */
+        Messagearea.prototype.showContactsFirst = function() {
+            return !!this.node.data('displaycontacts');
+        };
+
         return Messagearea;
     }
 );
index 237cb65..9af6333 100644 (file)
@@ -149,9 +149,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
             this.messageArea.onDelegateEvent(CustomEvents.events.scrollBottom, SELECTORS.CONTACTS,
                 this._loadContacts.bind(this));
 
-            // Set the number of conversations. We set this to the number of conversations we asked to retrieve not by
-            // the number that was actually retrieved, see MDL-55870.
-            this._numConversationsDisplayed = 20;
+            if (!this.messageArea.showContactsFirst()) {
+                // Set the initial number of conversations to retrieve. Otherwise it will display no conversations.
+                this._numConversationsDisplayed = 20;
+            }
         };
 
         /**
index 51394ca..24d3c8a 100644 (file)
@@ -23,8 +23,9 @@
  */
 define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/custom_interaction_events',
         'core/auto_rows', 'core_message/message_area_actions', 'core/modal_factory', 'core/modal_events',
-        'core/str', 'core_message/message_area_events'],
-    function($, Ajax, Templates, Notification, CustomEvents, AutoRows, Actions, ModalFactory, ModalEvents, Str, Events) {
+        'core/str', 'core_message/message_area_events', 'core/backoff_timer'],
+    function($, Ajax, Templates, Notification, CustomEvents, AutoRows, Actions, ModalFactory,
+             ModalEvents, Str, Events, BackOffTimer) {
 
         /** @type {int} The message area default height. */
         var MESSAGES_AREA_DEFAULT_HEIGHT = 500;
@@ -52,6 +53,9 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
             STARTDELETEMESSAGES: "[data-action='start-delete-messages']"
         };
 
+        /** @type {int} The number of milliseconds in a second. */
+        var MILLISECONDSINSEC = 1000;
+
         /**
          * Messages class.
          *
@@ -77,6 +81,12 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
         /** @type {Modal} the confirmation modal */
         Messages.prototype._confirmationModal = null;
 
+        /** @type {int} the timestamp for the earliest visible message */
+        Messages.prototype._earliestMessageTimestamp = 0;
+
+        /** @type {BackOffTimer} the backoff timer */
+        Messages.prototype._backoffTimer = null;
+
         /** @type {Messagearea} The messaging area object. */
         Messages.prototype.messageArea = null;
 
@@ -137,6 +147,14 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
             if (messages.length) {
                 this._addScrollEventListener(messages.find(SELECTORS.MESSAGE).length);
             }
+
+            // Create a timer to poll the server for new messages.
+            this._backoffTimer = new BackOffTimer(this._loadNewMessages.bind(this),
+                BackOffTimer.getIncrementalCallback(this.messageArea.pollmin * MILLISECONDSINSEC, MILLISECONDSINSEC,
+                    this.messageArea.pollmax * MILLISECONDSINSEC, this.messageArea.polltimeout * MILLISECONDSINSEC));
+
+            // Start the timer.
+            this._backoffTimer.start();
         };
 
         /**
@@ -150,6 +168,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
         Messages.prototype._viewMessages = function(event, userid) {
             // We are viewing another user, or re-loading the panel, so set number of messages displayed to 0.
             this._numMessagesDisplayed = 0;
+            // Stop the existing timer so we can set up the new user's messages.
+            this._backoffTimer.stop();
+            // Reset the earliest timestamp when we change the messages view.
+            this._earliestMessageTimestamp = 0;
 
             // Mark all the messages as read.
             var markMessagesAsRead = Ajax.call([{
@@ -183,6 +205,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
             }).then(function(html, js) {
                 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.MESSAGESAREA), html, js);
                 this._addScrollEventListener(numberreceived);
+                // Restart the poll timer.
+                this._backoffTimer.restart();
             }.bind(this)).fail(Notification.exception);
         };
 
@@ -240,28 +264,131 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
             }.bind(this)).fail(Notification.exception);
         };
 
+        /**
+         * Loads and renders messages newer than the most recently seen messages.
+         *
+         * @return {Promise|boolean} The promise resolved when the messages have been loaded.
+         * @private
+         */
+        Messages.prototype._loadNewMessages = function() {
+            if (this._isLoadingMessages) {
+                return false;
+            }
+
+            // If we have no user id yet then bail early.
+            if (!this._getUserId()) {
+                return false;
+            }
+
+            this._isLoadingMessages = true;
+
+            // Only scroll the message window if the user hasn't scrolled up.
+            var shouldScrollBottom = false;
+            var messages = this.messageArea.find(SELECTORS.MESSAGES);
+            if (messages.length !== 0) {
+                var scrollTop = messages.scrollTop();
+                var innerHeight = messages.innerHeight();
+                var scrollHeight = messages[0].scrollHeight;
+
+                if (scrollTop + innerHeight >= scrollHeight) {
+                    shouldScrollBottom = true;
+                }
+            }
+
+            // Keep track of the number of messages received.
+            var numberreceived = 0;
+            return this._getMessages(this._getUserId(), true).then(function(data) {
+                // Filter out any messages already rendered.
+                var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
+                data.messages = data.messages.filter(function(message) {
+                    var id = "" + message.id + message.isread;
+                    var result = messagesArea.find(SELECTORS.MESSAGE + '[data-id="' + id + '"]');
+                    return !result.length;
+                });
+
+                numberreceived = data.messages.length;
+                // We have the data - lets render the template with it.
+                return Templates.render('core_message/message_area_messages', data);
+            }.bind(this)).then(function(html, js) {
+                // Check if we got something to do.
+                if (numberreceived > 0) {
+                    html = $(html);
+                    // Remove the new block time as it's present above.
+                    html.find(SELECTORS.BLOCKTIME).remove();
+                    // Show the new content.
+                    Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), html, js);
+                    // Scroll the new message into view.
+                    if (shouldScrollBottom) {
+                        this._scrollBottom();
+                    }
+                    // Increment the number of messages displayed.
+                    this._numMessagesDisplayed += numberreceived;
+                    // Reset the poll timer because the user may be active.
+                    this._backoffTimer.restart();
+                }
+            }.bind(this)).always(function() {
+                // Mark that we are no longer busy loading data.
+                this._isLoadingMessages = false;
+            }.bind(this)).fail(Notification.exception);
+        };
+
         /**
          * Handles returning a list of messages to display.
          *
          * @param {int} userid
+         * @param {bool} fromTimestamp Load messages from the earliest known timestamp
          * @return {Promise} The promise resolved when the contact area has been rendered
          * @private
          */
-        Messages.prototype._getMessages = function(userid) {
+        Messages.prototype._getMessages = function(userid, fromTimestamp) {
+            var args = {
+                currentuserid: this.messageArea.getCurrentUserId(),
+                otheruserid: userid,
+                limitfrom: this._numMessagesDisplayed,
+                limitnum: this._numMessagesToRetrieve,
+                newest: true
+            };
+
+            // If we're trying to load new messages since the message UI was
+            // rendered. Used for ajax polling while user is on the message UI.
+            if (fromTimestamp) {
+                args.timefrom = this._earliestMessageTimestamp;
+                // Remove limit and offset. We want all new messages.
+                args.limitfrom = 0;
+                args.limitnum = 0;
+            }
+
             // Call the web service to get our data.
             var promises = Ajax.call([{
                 methodname: 'core_message_data_for_messagearea_messages',
-                args: {
-                    currentuserid: this.messageArea.getCurrentUserId(),
-                    otheruserid: userid,
-                    limitfrom: this._numMessagesDisplayed,
-                    limitnum: this._numMessagesToRetrieve,
-                    newest: true
-                }
+                args: args,
             }]);
 
             // Do stuff when we get data back.
-            return promises[0];
+            return promises[0].then(function(data) {
+                var messages = data.messages;
+
+                // Did we get any new messages?
+                if (messages && messages.length) {
+                    var earliestMessage = messages[messages.length - 1];
+
+                    // If we haven't set the timestamp yet then just use the earliest message.
+                    if (!this._earliestMessageTimestamp) {
+                        // Next request should be for the second after the most recent message we've seen.
+                        this._earliestMessageTimestamp = earliestMessage.timecreated + 1;
+                    // Update our record of the earliest known message for future requests.
+                    } else if (earliestMessage.timecreated < this._earliestMessageTimestamp) {
+                        // Next request should be for the second after the most recent message we've seen.
+                        this._earliestMessageTimestamp = earliestMessage.timecreated + 1;
+                    }
+                }
+
+                return data;
+            }.bind(this)).fail(function(ex) {
+                // Stop the timer if we received an error so that we don't keep spamming the server.
+                this._backoffTimer.stop();
+                Notification.exception(ex);
+            }.bind(this));
         };
 
         /**
index 0ff1f96..eee0989 100644 (file)
@@ -145,7 +145,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
         );
 
         // Set the initial search area.
-        this._searchArea = this._searchAreas.MESSAGES;
+        this._searchArea = (this.messageArea.showContactsFirst()) ? this._searchAreas.USERS : this._searchAreas.MESSAGES;
     };
 
     /**
index eabe7d7..25910da 100644 (file)
@@ -291,11 +291,30 @@ class api {
      * @param int $limitfrom
      * @param int $limitnum
      * @param string $sort
+     * @param int $timefrom the time from the message being sent
+     * @param int $timeto the time up until the message being sent
      * @return array
      */
-    public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0, $sort = 'timecreated ASC') {
+    public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
+        $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
+
+        if (!empty($timefrom)) {
+            // Check the cache to see if we even need to do a DB query.
+            $cache = \cache::make('core', 'message_time_last_message_between_users');
+            $key = helper::get_last_message_time_created_cache_key($otheruserid, $userid);
+            $lastcreated = $cache->get($key);
+
+            // The last known message time is earlier than the one being requested so we can
+            // just return an empty result set rather than having to query the DB.
+            if ($lastcreated && $lastcreated < $timefrom) {
+                return [];
+            }
+        }
+
         $arrmessages = array();
-        if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum, $sort)) {
+        if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum,
+                                             $sort, $timefrom, $timeto)) {
+
             $arrmessages = helper::create_messages($userid, $messages);
         }
 
index 4fe7122..8ae5ea6 100644 (file)
@@ -43,10 +43,12 @@ class helper {
      * @param int $limitfrom
      * @param int $limitnum
      * @param string $sort
+     * @param int $timefrom the time from the message being sent
+     * @param int $timeto the time up until the message being sent
      * @return array of messages
      */
     public static function get_messages($userid, $otheruserid, $timedeleted = 0, $limitfrom = 0, $limitnum = 0,
-                                        $sort = 'timecreated ASC') {
+                                        $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
         global $DB;
 
         $messageid = $DB->sql_concat("'message_'", 'id');
@@ -58,6 +60,7 @@ class helper {
                  WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?)
                     OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?))
                    AND notification = 0
+                   %where%
              UNION ALL
                 SELECT {$messagereadid} AS fakeid, id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat,
                        smallmessage, notification, timecreated, timeread
@@ -65,11 +68,29 @@ class helper {
                  WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?)
                     OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?))
                    AND notification = 0
+                   %where%
               ORDER BY $sort";
-        $params = array($userid, $otheruserid, $timedeleted,
-                        $otheruserid, $userid, $timedeleted,
-                        $userid, $otheruserid, $timedeleted,
-                        $otheruserid, $userid, $timedeleted);
+        $params1 = array($userid, $otheruserid, $timedeleted,
+                         $otheruserid, $userid, $timedeleted);
+
+        $params2 = array($userid, $otheruserid, $timedeleted,
+                         $otheruserid, $userid, $timedeleted);
+        $where = array();
+
+        if (!empty($timefrom)) {
+            $where[] = 'AND timecreated >= ?';
+            $params1[] = $timefrom;
+            $params2[] = $timefrom;
+        }
+
+        if (!empty($timeto)) {
+            $where[] = 'AND timecreated <= ?';
+            $params1[] = $timeto;
+            $params2[] = $timeto;
+        }
+
+        $sql = str_replace('%where%', implode(' ', $where), $sql);
+        $params = array_merge($params1, $params2);
 
         return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
     }
@@ -249,4 +270,17 @@ class helper {
 
         return $params;
     }
+
+    /**
+     * Returns the cache key for the time created value of the last message between two users.
+     *
+     * @param int $userid
+     * @param int $user2id
+     * @return string
+     */
+    public static function get_last_message_time_created_cache_key($userid, $user2id) {
+        $ids = [$userid, $user2id];
+        sort($ids);
+        return implode('_', $ids);
+    }
 }
index 0081349..5fc7e05 100644 (file)
@@ -107,6 +107,7 @@ class message implements templatable, renderable {
             $message->position = 'right';
         }
         $message->timesent = userdate($this->timecreated, get_string('strftimetime'));
+        $message->timecreated = $this->timecreated;
         $message->isread = !empty($this->timeread) ? 1 : 0;
 
         return $message;
index a3dd242..b5cb3bf 100644 (file)
@@ -63,6 +63,26 @@ class message_area implements templatable, renderable {
      */
     public $requestedconversation;
 
+    /**
+     * @var int The minimum time to poll for messages.
+     */
+    public $pollmin;
+
+    /**
+     * @var int The maximum time to poll for messages.
+     */
+    public $pollmax;
+
+    /**
+     * @var int The time used once we have reached the maximum polling time.
+     */
+    public $polltimeout;
+
+    /**
+     * @var bool Are we creating a new message and show the contacts section first?
+     */
+    public $contactsfirst;
+
     /**
      * Constructor.
      *
@@ -71,13 +91,23 @@ class message_area implements templatable, renderable {
      * @param array $contacts
      * @param array|null $messages
      * @param bool $requestedconversation
+     * @param bool $contactsfirst Whether we are viewing the contacts first.
+     * @param int $pollmin
+     * @param int $pollmax
+     * @param int $polltimeout
      */
-    public function __construct($userid, $otheruserid, $contacts, $messages, $requestedconversation) {
+    public function __construct($userid, $otheruserid, $contacts, $messages, $requestedconversation, $contactsfirst, $pollmin,
+            $pollmax, $polltimeout) {
         $this->userid = $userid;
-        $this->otheruserid = $otheruserid;
+        // Setting the other user to null when showing contacts will remove any contact from being selected.
+        $this->otheruserid = (!$contactsfirst) ? $otheruserid : null;
         $this->contacts = $contacts;
         $this->messages = $messages;
         $this->requestedconversation = $requestedconversation;
+        $this->pollmin = $pollmin;
+        $this->pollmax = $pollmax;
+        $this->polltimeout = $polltimeout;
+        $this->contactsfirst = $contactsfirst;
     }
 
     public function export_for_template(\renderer_base $output) {
@@ -85,10 +115,19 @@ class message_area implements templatable, renderable {
         $data->userid = $this->userid;
         $contacts = new contacts($this->otheruserid, $this->contacts);
         $data->contacts = $contacts->export_for_template($output);
-        $messages = new messages($this->userid, $this->otheruserid, $this->messages);
+        if ($this->contactsfirst) {
+            // Don't show any messages if we are creating a new message.
+            $messages = new messages($this->userid, null, array());
+        } else {
+            $messages = new messages($this->userid, $this->otheruserid, $this->messages);
+        }
         $data->messages = $messages->export_for_template($output);
-        $data->isconversation = true;
+        $data->isconversation = ($this->contactsfirst) ? false : true;
         $data->requestedconversation = $this->requestedconversation;
+        $data->pollmin = $this->pollmin;
+        $data->pollmax = $this->pollmax;
+        $data->polltimeout = $this->polltimeout;
+        $data->contactsfirst = $this->contactsfirst;
 
         return $data;
     }
diff --git a/message/classes/time_last_message_between_users.php b/message/classes/time_last_message_between_users.php
new file mode 100644 (file)
index 0000000..750a552
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Cache data source for the time of the last message between users.
+ *
+ * @package    core_message
+ * @category   cache
+ * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_message;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Cache data source for the time of the last message between users.
+ *
+ * @package    core_message
+ * @category   cache
+ * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class time_last_message_between_users implements \cache_data_source {
+
+    /** @var time_last_message_between_users the singleton instance of this class. */
+    protected static $instance = null;
+
+    /**
+     * Returns an instance of the data source class that the cache can use for loading data using the other methods
+     * specified by the cache_data_source interface.
+     *
+     * @param \cache_definition $definition
+     * @return object
+     */
+    public static function get_instance_for_cache(\cache_definition $definition) {
+        if (is_null(self::$instance)) {
+            self::$instance = new time_last_message_between_users();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * Loads the data for the key provided ready formatted for caching.
+     *
+     * @param string|int $key The key to load.
+     * @return mixed What ever data should be returned, or false if it can't be loaded.
+     */
+    public function load_for_cache($key) {
+        list($userid1, $userid2) = explode('_', $key);
+
+        $message = api::get_most_recent_message($userid1, $userid2);
+
+        if ($message) {
+            return $message->timecreated;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Loads several keys for the cache.
+     *
+     * @param array $keys An array of keys each of which will be string|int.
+     * @return array An array of matching data items.
+     */
+    public function load_many_for_cache(array $keys) {
+        $results = [];
+
+        foreach ($keys as $key) {
+            $results[] = $this->load_for_cache($key);
+        }
+
+        return $results;
+    }
+}
index 2ecaf66..a3b4247 100644 (file)
@@ -515,6 +515,7 @@ class core_message_external extends external_api {
                 'blocktime' => new external_value(PARAM_NOTAGS, 'The time to display above the message'),
                 'position' => new external_value(PARAM_ALPHA, 'The position of the text'),
                 'timesent' => new external_value(PARAM_NOTAGS, 'The time the message was sent'),
+                'timecreated' => new external_value(PARAM_INT, 'The timecreated timestamp for the message'),
                 'isread' => new external_value(PARAM_INT, 'Determines if the message was read or not'),
             )
         );
@@ -900,6 +901,8 @@ class core_message_external extends external_api {
                 'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0),
                 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0),
                 'newest' => new external_value(PARAM_BOOL, 'Newest first?', VALUE_DEFAULT, false),
+                'timefrom' => new external_value(PARAM_INT,
+                    'The timestamp from which the messages were created', VALUE_DEFAULT, 0),
             )
         );
     }
@@ -917,7 +920,7 @@ class core_message_external extends external_api {
      * @since 3.2
      */
     public static function data_for_messagearea_messages($currentuserid, $otheruserid, $limitfrom = 0, $limitnum = 0,
-                                                         $newest = false) {
+                                                         $newest = false, $timefrom = 0) {
         global $CFG, $PAGE, $USER;
 
         // Check if messaging is enabled.
@@ -932,7 +935,8 @@ class core_message_external extends external_api {
             'otheruserid' => $otheruserid,
             'limitfrom' => $limitfrom,
             'limitnum' => $limitnum,
-            'newest' => $newest
+            'newest' => $newest,
+            'timefrom' => $timefrom,
         );
         self::validate_parameters(self::data_for_messagearea_messages_parameters(), $params);
         self::validate_context($systemcontext);
@@ -946,7 +950,29 @@ class core_message_external extends external_api {
         } else {
             $sort = 'timecreated ASC';
         }
-        $messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom, $limitnum, $sort);
+
+        // We need to enforce a one second delay on messages to avoid race conditions of current
+        // messages still being sent.
+        //
+        // There is a chance that we could request messages before the current time's
+        // second has elapsed and while other messages are being sent in that same second. In which
+        // case those messages will be lost.
+        //
+        // Instead we ignore the current time in the result set to ensure that second is allowed to finish.
+        if (!empty($timefrom)) {
+            $timeto = time() - 1;
+        } else {
+            $timeto = 0;
+        }
+
+        // No requesting messages from the current time, as stated above.
+        if ($timefrom == time()) {
+            $messages = [];
+        } else {
+            $messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom,
+                                                        $limitnum, $sort, $timefrom, $timeto);
+        }
+
         $messages = new \core_message\output\messagearea\messages($currentuserid, $otheruserid, $messages);
 
         $renderer = $PAGE->get_renderer('core_message');
index 0f834a8..65349b3 100644 (file)
@@ -41,6 +41,7 @@ $id = optional_param('id', 0, PARAM_INT);
 // we are going to accept other URL parameters to figure this out.
 $user1id = optional_param('user1', $USER->id, PARAM_INT);
 $user2id = optional_param('user2', $id, PARAM_INT);
+$contactsfirst = optional_param('contactsfirst', 0, PARAM_INT);
 
 $url = new moodle_url('/message/index.php');
 if ($id) {
@@ -52,6 +53,9 @@ if ($id) {
     if ($user2id) {
         $url->param('user2', $user2id);
     }
+    if ($contactsfirst) {
+        $url->param('contactsfirst', $contactsfirst);
+    }
 }
 $PAGE->set_url($url);
 
@@ -98,7 +102,11 @@ $settings->make_active();
 // Get the renderer and the information we are going to be use.
 $renderer = $PAGE->get_renderer('core_message');
 $requestedconversation = false;
-$conversations = \core_message\api::get_conversations($user1->id, 0, 20);
+if ($contactsfirst) {
+    $conversations = \core_message\api::get_contacts($user1->id, 0, 20);
+} else {
+    $conversations = \core_message\api::get_conversations($user1->id, 0, 20);
+}
 $messages = [];
 if (!$user2realuser) {
     // If there are conversations, but the user has not chosen a particular one, then render the most recent one.
@@ -127,8 +135,11 @@ if (!empty($user2->id)) {
     $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 20, 'timecreated DESC');
 }
 
+$pollmin = !empty($CFG->messagingminpoll) ? $CFG->messagingminpoll : MESSAGE_DEFAULT_MIN_POLL_IN_SECONDS;
+$pollmax = !empty($CFG->messagingmaxpoll) ? $CFG->messagingmaxpoll : MESSAGE_DEFAULT_MAX_POLL_IN_SECONDS;
+$polltimeout = !empty($CFG->messagingtimeoutpoll) ? $CFG->messagingtimeoutpoll : MESSAGE_DEFAULT_TIMEOUT_POLL_IN_SECONDS;
 $messagearea = new \core_message\output\messagearea\message_area($user1->id, $user2->id, $conversations, $messages,
-    $requestedconversation);
+        $requestedconversation, $contactsfirst, $pollmin, $pollmax, $polltimeout);
 
 // Now the page contents.
 echo $OUTPUT->header();
index 34431be..d79fdc7 100644 (file)
 
 require_once($CFG->libdir.'/eventslib.php');
 
-define ('MESSAGE_SHORTLENGTH', 300);
+define('MESSAGE_SHORTLENGTH', 300);
 
-define ('MESSAGE_DISCUSSION_WIDTH',600);
-define ('MESSAGE_DISCUSSION_HEIGHT',500);
-
-define ('MESSAGE_SHORTVIEW_LIMIT', 8);//the maximum number of messages to show on the short message history
-
-define('MESSAGE_HISTORY_SHORT',0);
-define('MESSAGE_HISTORY_ALL',1);
-
-define('MESSAGE_VIEW_UNREAD_MESSAGES','unread');
-define('MESSAGE_VIEW_RECENT_CONVERSATIONS','recentconversations');
-define('MESSAGE_VIEW_RECENT_NOTIFICATIONS','recentnotifications');
-define('MESSAGE_VIEW_CONTACTS','contacts');
-define('MESSAGE_VIEW_BLOCKED','blockedusers');
-define('MESSAGE_VIEW_COURSE','course_');
-define('MESSAGE_VIEW_SEARCH','search');
+define('MESSAGE_HISTORY_ALL', 1);
 
 define('MESSAGE_SEARCH_MAX_RESULTS', 200);
 
-define('MESSAGE_CONTACTS_PER_PAGE',10);
-define('MESSAGE_MAX_COURSE_NAME_LENGTH', 30);
-
-define('MESSAGE_UNREAD', 'unread');
-define('MESSAGE_READ', 'read');
 define('MESSAGE_TYPE_NOTIFICATION', 'notification');
 define('MESSAGE_TYPE_MESSAGE', 'message');
 
@@ -76,6 +57,13 @@ define('MESSAGE_PERMITTED_MASK', 0x0c); // 1100
  */
 define('MESSAGE_DEFAULT_PERMITTED', 'permitted');
 
+/**
+ * Set default values for polling.
+ */
+define('MESSAGE_DEFAULT_MIN_POLL_IN_SECONDS', 10);
+define('MESSAGE_DEFAULT_MAX_POLL_IN_SECONDS', 2 * MINSECS);
+define('MESSAGE_DEFAULT_TIMEOUT_POLL_IN_SECONDS', 5 * MINSECS);
+
 /**
  * Retrieve users blocked by $user1
  *
index 8875855..220515f 100644 (file)
@@ -46,6 +46,8 @@ function message_popup_render_navbar_output(\renderer_base $renderer) {
         $context = [
             'userid' => $USER->id,
             'urls' => [
+                'seeall' => (new moodle_url('/message/index.php'))->out(),
+                'writeamessage' => (new moodle_url('/message/index.php', ['contactsfirst' => 1]))->out(),
                 'preferences' => (new moodle_url('/message/edit.php', ['id' => $USER->id]))->out(),
             ],
         ];
index 8d170f1..666242a 100644 (file)
 
     {{$headertext}}{{#str}} messages, message {{/str}}{{/headertext}}
     {{$headeractions}}
+        <div class="newmessage-link">
+            {{$anchor}}
+                <a href="{{{urls.writeamessage}}}">{{#str}} newmessage, message {{/str}}
+                </a>
+            {{/anchor}}
+        </div>
         {{< core/hover_tooltip }}
             {{$anchor}}
                 <a class="mark-all-read-button"
index 15af327..1a6cbe7 100644 (file)
@@ -14,7 +14,7 @@
     You should have received a copy of the GNU General Public License
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
-<div class="messaging-area-container" data-userid="{{userid}}">
+<div class="messaging-area-container" data-userid="{{userid}}" data-displaycontacts="{{contactsfirst}}">
     <div class="messaging-area {{#requestedconversation}}show-messages{{/requestedconversation}}
             {{^requestedconversation}}hide-messages{{/requestedconversation}}" data-region="messaging-area">
         <div class="contacts-area" data-region="contacts-area" role="tablist">
@@ -32,7 +32,7 @@
 {{#js}}
     require(['core_message/message_area'],
         function(Messagearea) {
-            new Messagearea('.messaging-area-container');
+            new Messagearea('.messaging-area-container', {{pollmin}}, {{pollmax}}, {{polltimeout}});
         }
     );
 {{/js}}
index 204a219..c60b934 100644 (file)
 }}
 <div class="searchtextarea" data-region="search-text-area">
     <label class="accesshide" for="searchtext">{{#str}}search{{/str}}</label>
-    <input data-region="search-box" type="text" id="searchtext" placeholder="{{#str}}searchmessages, message{{/str}}">
+    <input data-region="search-box" type="text" id="searchtext" placeholder="{{#contactsfirst}} {{#str}}searchforuserorcourse, message{{/str}} {{/contactsfirst}} {{^contactsfirst}} {{#str}}searchmessages, message{{/str}} {{/contactsfirst}}">
     <div data-region="search-filter-area" class="searchfilterarea" style="display:none">
         <div data-region="search-filter" class="searchfilter"></div>
         <div data-action="search-filter-delete" class="searchfilterdelete">{{#pix}}t/delete{{/pix}}</div>
     </div>
 </div>
+{{#contactsfirst}}
+<div class="contacts" data-region="contacts" data-region-content="conversations" style="display:none;" role="tabpanel" id="conversations-tab-panel"></div>
+<div class="contacts" data-region="contacts" data-region-content="contacts" role="tabpanel" id="contacts-tab-panel">
+    {{> core_message/message_area_contacts }}
+</div>
+{{/contactsfirst}}
+{{^contactsfirst}}
 <div class="contacts" data-region="contacts" data-region-content="conversations" role="tabpanel" id="conversations-tab-panel">
     {{> core_message/message_area_contacts }}
 </div>
-{{! Hidden divs to load the other tab and search panels via JS when appropriate. }}
 <div class="contacts" data-region="contacts" data-region-content="contacts" style="display:none;" role="tabpanel" id="contacts-tab-panel"></div>
+{{/contactsfirst}}
+{{! Hidden divs to load the other tab and search panels via JS when appropriate. }}
 <div class="contacts searcharea" data-region="search-results-area" style="display:none;"></div>
 <div class="tabs">
-    <div class="tab tabconversations selected" data-action="conversations-view" role="tab" aria-controls="conversations-tab-panel" aria-selected="true" tabindex="0">
+    <div class="tab tabconversations {{^contactsfirst}}selected{{/contactsfirst}} " data-action="conversations-view" role="tab" aria-controls="conversations-tab-panel" aria-selected="{{^contactsfirst}}true{{/contactsfirst}}{{#contactsfirst}}false{{/contactsfirst}}" tabindex="0">
         <div class="tabimage">{{#pix}}t/message, moodle{{/pix}}</div>
         <div>{{#str}}messages, message{{/str}}</div>
     </div>
-    <div class="tab tabcontacts" data-action="contacts-view" role="tab" aria-controls="contacts-tab-panel" aria-selected="false" tabindex="-1">
+    <div class="tab tabcontacts {{#contactsfirst}}selected{{/contactsfirst}}" data-action="contacts-view" role="tab" aria-controls="contacts-tab-panel" aria-selected="{{#contactsfirst}}true{{/contactsfirst}}{{^contactsfirst}}false{{/contactsfirst}}" tabindex="-1">
         <div class="tabimage">{{#pix}}i/cohort, moodle{{/pix}}</div>
         <div>{{#str}}contacts, message{{/str}}</div>
     </div>
index acfa449..d27a1b8 100644 (file)
     </div>
 </div>
 {{/otheruserid}}
+{{#contactsfirst}}
+<div class="messages-header">
+    <div class="name-container">
+        <div class="name">
+            {{#str}}newmessagesearch, message{{/str}}
+        </div>
+    </div>
+</div>
+{{/contactsfirst}}
 <div class="messages" data-region="messages" data-userid="{{otheruserid}}">
     {{> core_message/message_area_messages }}
 </div>
index 67d898e..9c52f86 100644 (file)
@@ -953,4 +953,129 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $status = \core_message\api::is_processor_enabled($name);
         $this->assertEquals(1, $status);
     }
+
+    /**
+     * Test retrieving messages by providing a minimum timecreated value.
+     */
+    public function test_get_messages_time_from_only() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // The person doing the search.
+        $this->setUser($user1);
+
+        // Send some messages back and forth.
+        $time = 1;
+        $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1);
+        $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2);
+        $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3);
+        $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4);
+
+        // Retrieve the messages from $time, which should be all of them.
+        $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time);
+
+        // Confirm the message data is correct.
+        $this->assertEquals(4, count($messages));
+
+        $message1 = $messages[0];
+        $message2 = $messages[1];
+        $message3 = $messages[2];
+        $message4 = $messages[3];
+
+        $this->assertContains('Message 1', $message1->text);
+        $this->assertContains('Message 2', $message2->text);
+        $this->assertContains('Message 3', $message3->text);
+        $this->assertContains('Message 4', $message4->text);
+
+        // Retrieve the messages from $time + 3, which should only be the 2 last messages.
+        $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 3);
+
+        // Confirm the message data is correct.
+        $this->assertEquals(2, count($messages));
+
+        $message1 = $messages[0];
+        $message2 = $messages[1];
+
+        $this->assertContains('Message 3', $message1->text);
+        $this->assertContains('Message 4', $message2->text);
+    }
+
+    /**
+     * Test retrieving messages by providing a maximum timecreated value.
+     */
+    public function test_get_messages_time_to_only() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // The person doing the search.
+        $this->setUser($user1);
+
+        // Send some messages back and forth.
+        $time = 1;
+        $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1);
+        $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2);
+        $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3);
+        $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4);
+
+        // Retrieve the messages up until $time + 4, which should be all of them.
+        $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 4);
+
+        // Confirm the message data is correct.
+        $this->assertEquals(4, count($messages));
+
+        $message1 = $messages[0];
+        $message2 = $messages[1];
+        $message3 = $messages[2];
+        $message4 = $messages[3];
+
+        $this->assertContains('Message 1', $message1->text);
+        $this->assertContains('Message 2', $message2->text);
+        $this->assertContains('Message 3', $message3->text);
+        $this->assertContains('Message 4', $message4->text);
+
+        // Retrieve the messages up until $time + 2, which should be the first two.
+        $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 2);
+
+        // Confirm the message data is correct.
+        $this->assertEquals(2, count($messages));
+
+        $message1 = $messages[0];
+        $message2 = $messages[1];
+
+        $this->assertContains('Message 1', $message1->text);
+        $this->assertContains('Message 2', $message2->text);
+    }
+
+    /**
+     * Test retrieving messages by providing a minimum and maximum timecreated value.
+     */
+    public function test_get_messages_time_from_and_to() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // The person doing the search.
+        $this->setUser($user1);
+
+        // Send some messages back and forth.
+        $time = 1;
+        $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1);
+        $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2);
+        $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3);
+        $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4);
+
+        // Retrieve the messages from $time + 2 up until $time + 3, which should be 2nd and 3rd message.
+        $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 2, $time + 3);
+
+        // Confirm the message data is correct.
+        $this->assertEquals(2, count($messages));
+
+        $message1 = $messages[0];
+        $message2 = $messages[1];
+
+        $this->assertContains('Message 2', $message1->text);
+        $this->assertContains('Message 3', $message2->text);
+    }
 }
index 138caaa..96a2e0d 100644 (file)
@@ -20,12 +20,12 @@ Feature: Search messages
   Scenario: Search for messages
     When I log in as "user1"
     And I follow "Messages" in the user menu
-    And I set the field "Search messages" to "User 2 to User 1"
+    And I set the field "searchtext" to "User 2 to User 1"
     Then I should see "User 2" in the "search-results-area" "message_area_region"
     And I should not see "User 3" in the "search-results-area" "message_area_region"
 
   Scenario: Search for messages no results
     When I log in as "user1"
     And I follow "Messages" in the user menu
-    And I set the field "Search messages" to "No message"
+    And I set the field "searchtext" to "No message"
     Then I should see "No results" in the "search-results-area" "message_area_region"
index f28c50e..3f1e38e 100644 (file)
@@ -1979,6 +1979,46 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertContains('Word.', $message4['text']);
     }
 
+    /**
+     * Tests retrieving messages.
+     */
+    public function test_messagearea_messages_timefrom() {
+        $this->resetAfterTest(true);
+
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // The person asking for the messages.
+        $this->setUser($user1);
+
+        // Send some messages back and forth.
+        $time = time();
+        $this->send_message($user1, $user2, 'Message 1', 0, $time - 4);
+        $this->send_message($user2, $user1, 'Message 2', 0, $time - 3);
+        $this->send_message($user1, $user2, 'Message 3', 0, $time - 2);
+        $this->send_message($user2, $user1, 'Message 4', 0, $time - 1);
+
+        // Retrieve the messages from $time - 3, which should be the 3 most recent messages.
+        $result = core_message_external::data_for_messagearea_messages($user1->id, $user2->id, 0, 0, false, $time - 3);
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_message_external::data_for_messagearea_messages_returns(),
+            $result);
+
+        // Confirm the message data is correct. We shouldn't get 'Message 1' back.
+        $messages = $result['messages'];
+        $this->assertCount(3, $messages);
+
+        $message1 = $messages[0];
+        $message2 = $messages[1];
+        $message3 = $messages[2];
+
+        $this->assertContains('Message 2', $message1['text']);
+        $this->assertContains('Message 3', $message2['text']);
+        $this->assertContains('Message 4', $message3['text']);
+    }
+
     /**
      * Tests retrieving messages as another user.
      */
index b2e43c4..2062185 100644 (file)
  * This way it is possible to use FPDI for both FPDF and TCPDF with one FPDI version.
  */
 
-if (!class_exists('TCPDF', false)) {
-    /**
-     * Class fpdi_bridge
-     */
-    class fpdi_bridge extends FPDF
-    {
-        // empty body
-    }
-
-} else {
-
     /**
      * Class fpdi_bridge
      *
@@ -205,4 +194,3 @@ if (!class_exists('TCPDF', false)) {
             return current(unpack('H*', $str));
         }
     }
-}
\ No newline at end of file
index 7cac3bb..edc01d8 100644 (file)
@@ -19,6 +19,13 @@ Installation
 3) Update mod/assign/feedback/editpdf/fpdi/fpdi_bridge.php (or the replacement file) to extend 'pdf' instead of 'TCPDF'.
 4) Make a note below of any changes made.
 
+2016/11/15
+----------
+
+1) Class not exists check and the empty fpdi_bridge class has been removed from fpdi_bridge.php to fix a behat error.
+
+Updated by Simey Lameze (MDL-55848)
+
 2015/12/04
 ----------
 Updated to FPDI: 1.6.1
index d528b61..cc0c255 100644 (file)
@@ -129,10 +129,7 @@ if ( $launchcontainer == LTI_LAUNCH_CONTAINER_WINDOW ) {
         <script type="text/javascript">
         //<![CDATA[
             YUI().use("node", "event", function(Y) {
-                //Take scrollbars off the outer document to prevent double scroll bar effect
                 var doc = Y.one("body");
-                doc.setStyle("overflow", "hidden");
-
                 var frame = Y.one("#contentframe");
                 var padding = 15; //The bottom of the iframe wasn\'t visible on some themes. Probably because of border widths, etc.
                 var lastHeight;
index 06a1a2f..16b22d1 100644 (file)
@@ -458,7 +458,13 @@ function quiz_user_outline($course, $user, $mod, $quiz) {
     }
 
     $result = new stdClass();
-    $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
+    // If the user can't see hidden grades, don't return that information.
+    $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
+    if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
+        $result->info = get_string('grade') . ': ' . $grade->str_long_grade;
+    } else {
+        $result->info = get_string('grade') . ': ' . get_string('hidden', 'grades');
+    }
 
     // Datesubmitted == time created. dategraded == time modified or time overridden
     // if grade was last modified by the user themselves use date graded. Otherwise use
@@ -491,9 +497,18 @@ function quiz_user_complete($course, $user, $mod, $quiz) {
     $grades = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $user->id);
     if (!empty($grades->items[0]->grades)) {
         $grade = reset($grades->items[0]->grades);
-        echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
-        if ($grade->str_feedback) {
-            echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
+        // If the user can't see hidden grades, don't return that information.
+        $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
+        if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
+            echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
+            if ($grade->str_feedback) {
+                echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
+            }
+        } else {
+            echo $OUTPUT->container(get_string('grade') . ': ' . get_string('hidden', 'grades'));
+            if ($grade->str_feedback) {
+                echo $OUTPUT->container(get_string('feedback').': '.get_string('hidden', 'grades'));
+            }
         }
     }
 
@@ -504,8 +519,19 @@ function quiz_user_complete($course, $user, $mod, $quiz) {
             if ($attempt->state != quiz_attempt::FINISHED) {
                 echo quiz_attempt_state_name($attempt->state);
             } else {
-                echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' .
-                        quiz_format_grade($quiz, $quiz->sumgrades);
+                if (!isset($gitem)) {
+                    if (!empty($grades->items[0]->grades)) {
+                        $gitem = grade_item::fetch(array('id' => $grades->items[0]->id));
+                    } else {
+                        $gitem = new stdClass();
+                        $gitem->hidden = true;
+                    }
+                }
+                if (!$gitem->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
+                    echo quiz_format_grade($quiz, $attempt->sumgrades) . '/' . quiz_format_grade($quiz, $quiz->sumgrades);
+                } else {
+                    echo get_string('hidden', 'grades');
+                }
             }
             echo ' - '.userdate($attempt->timemodified).'<br />';
         }
index 6be0de1..65f5014 100644 (file)
@@ -1771,6 +1771,7 @@ function mod_wiki_get_tagged_pages($tag, $exclusivemode = false, $fromctx = 0, $
                 JOIN {course} c ON cm.course = c.id
                 JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
                WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
+                 AND cm.deletioninprogress = 0
                  AND wp.id %ITEMFILTER% AND c.id %COURSEFILTER%";
 
     $params = array('itemtype' => 'wiki_pages', 'tagid' => $tag->id, 'component' => 'mod_wiki',
index 7440d3a..cb7ed2c 100644 (file)
Binary files a/theme/boost/amd/build/form-display-errors.min.js and b/theme/boost/amd/build/form-display-errors.min.js differ
index 04bc6a9..d3adac7 100644 (file)
Binary files a/theme/boost/amd/build/loader.min.js and b/theme/boost/amd/build/loader.min.js differ
index f2828f5..7405f7a 100644 (file)
@@ -29,6 +29,11 @@ define(['jquery', 'core/event'], function($, Event) {
                 event.preventDefault();
                 var parent = $(element).closest('.form-group');
                 var feedback = parent.find('.form-control-feedback');
+
+                // Sometimes (atto) we have a hidden textarea backed by a real contenteditable div.
+                if (($(element).prop("tagName") == 'TEXTAREA') && parent.find('[contenteditable]')) {
+                    element = parent.find('[contenteditable]');
+                }
                 if (msg !== '') {
                     parent.addClass('has-danger');
                     parent.data('client-validation-error', true);
index 99bce70..cf908f8 100644 (file)
@@ -40,7 +40,10 @@ define(['jquery', './tether'], function(jQuery, Tether) {
             'theme_boost/tooltip',
             'theme_boost/popover'],
             function() {
-        jQuery('[data-toggle="popover"]').popover();
+                jQuery('body').popover({
+                    selector: '[data-toggle="popover"]',
+                    trigger: 'focus'
+                });
     });
 
     return {};
index 598c0e8..95d107c 100644 (file)
     .dropdown-toggle {
         /* So that the caret takes the colour of the icon. */
         color: #999;
-
-        &::after {
-            margin-left: 0;
-        }
     }
 }
 
@@ -54,6 +50,12 @@ $blocks-plus-gutter: $blocks-column-width + $grid-gutter-width;
     border-bottom-left-radius: 0.5rem;
     float: right;
 }
+
+// Required for IE11 to prevent blocks being pushed under the content.
+#region-main {
+    float: left;
+    width: 100%;
+}
 #region-main-settings-menu.has-blocks,
 #region-main.has-blocks {
     display: inline-block;
index cb33500..9f7c1a8 100644 (file)
@@ -46,15 +46,3 @@ p.arrow_button {
     margin: 0 0 10px 5px;
 }
 
-.user-enroller-panel {
-    .uep-search-results {
-        .user,
-        .cohort {
-            .options {
-                .enrol {
-                    @extend .btn-sm;
-                }
-            }
-        }
-    }
-}
index 5067713..4e92465 100644 (file)
@@ -425,6 +425,11 @@ $calendarEventUserColor: #dce7ec; // Pale blue.
     }
 }
 
+/* Display month name above the calendar */
+table.calendartable caption {
+    caption-side: top;
+}
+
 .ical-link {
     font-size: 10px;
     font-weight: bold;
index 18a504e..bfe738f 100644 (file)
@@ -1086,14 +1086,6 @@ tr.flagged-tag a {
         @extend .card;
         @extend .card-block;
     }
-
-    .user-enroller-panel .uep-search-results .user .details {
-        width: 237px;
-    }
-
-    .user-enroller-panel .uep-search-results .cohort .details {
-        width: 237px;
-    }
 }
 
 #page-enrol-users .enrol-users-page-action input {
index 7662edc..4a13d62 100644 (file)
                 text-indent: -31px;
                 padding-left: 31px;
             }
+
+            .dimmed {
+                .activityicon {
+                    opacity: .5;
+                }
+            }
         }
     }
 
index fc97402..bcf5451 100644 (file)
@@ -32,8 +32,6 @@ table#listdirectories {
     @extend .table-sm;
 }
 
-.user-enroller-panel .uep-search-results .users,
-.user-enroller-panel .uep-search-results .cohorts,
 table.grading-report,
 .forumheaderlist,
 .generaltable,
index df3cdcb..9d7f1d0 100644 (file)
     border-bottom: $border-width solid $table-border-color;
 }
 
+.editor_atto_content.form-control {
+    width: 100%;
+}
+
 .mform fieldset.collapsible legend a.fheader {
     padding: 0 5px 0 ($spacer * 1.5);
     margin-left: -($spacer * 1.5);
@@ -296,9 +300,15 @@ fieldset.coursesearchbox label {
     font-size: large;
 }
 
+/* Non-bootstrap selects with a size show their contents outside of the element.
+ * Remove when we update to stable bootstrap 4. (MDL-56511) */
+select[size],
 select[multiple] {
     overflow: auto;
 }
+select[size=1] {
+    overflow: visible;
+}
 
 textarea[data-auto-rows] {
     overflow-x: hidden;
@@ -331,3 +341,15 @@ textarea[data-auto-rows] {
     margin-left: $grid-gutter-width / 2;
     max-width: 30rem;
 }
+
+/** Atto fields do not have form-control because that would break the layout of the editor.
+    So they need these extra styles to highlight the editor when there is a validation error. */
+.has-danger .editor_atto_content.form-control,
+.has-danger .editor_atto_content.form-control-danger {
+    @include form-control-validation($brand-danger);
+    background-image: $form-icon-danger;
+    padding-right: ($input-padding-x * 3);
+    background-repeat: no-repeat;
+    background-position: center right ($input-height / 4);
+    background-size: ($input-height / 2) ($input-height / 2);
+}
index fe4efa9..14a2042 100644 (file)
@@ -26,9 +26,15 @@ a:first-of-type > .icon {
     margin-left: 0;
 }
 
-// Fix the first item in a drop down menu (it's a list of links and icons, but has been made vertical.
-.dropdown .dropdown-item .icon {
-    margin-left: 0;
+// Fix the space between the dropdown icon and the caret.
+.action-menu {
+    .dropdown-toggle {
+        .icon {
+            // Use a negative margin on the icon to ensure that the dropdown toggle
+            // is an icon and not just text in which case the margin is needed.
+            margin-right: -$caret-width;
+        }
+    }
 }
 
 // YUI 2 Tree View icons must not have a margin left.
index 13eeea1..56f83c8 100644 (file)
@@ -4,10 +4,12 @@ $region-container-height: 500px;
 $region-container-width: 380px;
 $region-container-z-index: 1;
 $region-header-height: 25px;
+$region-footer-height: 30px;
 $content-item-hover-colour-bg: #79b5e6;
 $content-item-hover-colour-text: #fff;
 $content-item-selected-colour-bg: #4f94cd;
 $content-item-unread-colour: #f4f4f4;
+$content-header-footer-height: $region-header-height + $region-footer-height;
 
 @mixin invisible() {
     opacity: 0;
@@ -103,6 +105,14 @@ $content-item-unread-colour: #f4f4f4;
     box-sizing: border-box;
 }
 
+.popover-region-footer-container {
+    height: $region-footer-height;
+    text-align: center;
+    border-top: $standard-border;
+    background-color: $popover-bg;
+    padding-top: 3px;
+}
+
 .popover-region-header-text {
     float: left;
     margin: 0;
@@ -123,6 +133,11 @@ $content-item-unread-colour: #f4f4f4;
         height: 12px;
         width: 12px;
     }
+
+    .newmessage-link {
+        margin-right: 10px;
+    }
+
     label {
         display: inline-block;
         text-align: center;
@@ -131,7 +146,7 @@ $content-item-unread-colour: #f4f4f4;
 }
 
 .popover-region-content-container {
-    height: calc(100% - 25px);
+    height: calc(100% - #{$content-header-footer-height});
     width: 100%;
     overflow-y: auto;
     -webkit-overflow-scrolling: touch;
index 409a539..7554f4f 100644 (file)
 .userlist h3 .action-icon {
     display: none;
 }
+
+#page-enrol-users .popover {
+    max-width: none;
+}
+
+.user-enroller-panel {
+    width: 600px;
+}
index bd65dc7..ef9fcbc 100644 (file)
@@ -7,7 +7,7 @@
             <button class="fp-file-download btn btn-secondary">{{#str}}download{{/str}}</button>
             <button class="fp-file-delete btn btn-secondary">{{#str}}delete{{/str}}</button>
             <button class="fp-file-setmain btn btn-secondary">{{#str}}setmainfile, repository{{/str}}</button>
-            <span class="fp-file-setmain-help btn btn-secondary">{{helpicon}}</span>
+            <span class="fp-file-setmain-help">{{{helpicon}}}</span>
             <button class="fp-file-zip btn btn-secondary">{{#str}}zip, editor{{/str}}</button>
             <button class="fp-file-unzip btn btn-secondary">{{#str}}unzip{{/str}}</button>
         </div>
index 3458ad8..fa3b35a 100644 (file)
@@ -1,37 +1,92 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template theme_boost/flat_navigation
+
+    Display the flat navigation for the boost theme
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * flatnavigation - array of flat_navigation_nodes
+      * showdivider - boolean
+      * action - string
+      * isactive - boolean
+      * get_indent - integer
+      * is_section - boolean
+      * text - HTML
+
+    Example context (json):
+    {
+        "flatnavigation" : [
+            {
+                "showdivider": false,
+                "action": "#",
+                "isactive": true,
+                "get_indent": 1,
+                "is_section": false,
+                "text": "First"
+            },{
+                "showdivider": true,
+                "action": "#",
+                "isactive": false,
+                "get_indent": 0,
+                "is_section": true,
+                "text": "Last &amp; Second"
+            }
+        ]
+    }
+}}
 <nav class="list-group">
 {{# flatnavigation }}
     {{#showdivider}}
 </nav>
 <nav class="list-group m-t-1">
     {{/showdivider}}
-    {{#get_smscreenonly}}
-    <div class="hidden-lg-up">
-    {{/get_smscreenonly}}
     {{#action}}
-    <a class="list-group-item list-group-item-action {{#isactive}}font-weight-bold{{/isactive}}" href="{{{action}}}"><span class="m-l-{{get_indent}}">
-        {{#is_section}}
-            <div class="media">
-                <span class="media-left">
-                    {{#pix}}i/folder{{/pix}}
-                </span>
-                <span class="media-body">{{text}}</span>
-            </div>
-        {{/is_section}}
-        {{^is_section}}
-            {{text}}
-        {{/is_section}}
-    </span></a>
+    <a class="list-group-item list-group-item-action {{#isactive}}font-weight-bold{{/isactive}}" href="{{{action}}}">
+        <div class="m-l-{{get_indent}}">
+            {{#is_section}}
+                <div class="media">
+                    <span class="media-left">
+                        {{#pix}}i/folder{{/pix}}
+                    </span>
+                    <span class="media-body">{{{text}}}</span>
+                </div>
+            {{/is_section}}
+            {{^is_section}}
+                {{{text}}}
+            {{/is_section}}
+        </div>
+    </a>
     {{/action}}
     {{^action}}
-    <div class="list-group-item"><span class="m-l-{{get_indent}}">
-        {{#is_section}}
-            {{#pix}}i/folder{{/pix}}
-        {{/is_section}}
-        {{text}}
-    </span></div>
-    {{/action}}
-    {{#get_smscreenonly}}
+    <div class="list-group-item">
+        <div class="m-l-{{get_indent}}">
+            {{#is_section}}
+                {{#pix}}i/folder{{/pix}}
+            {{/is_section}}
+            {{{text}}}
+        </div>
     </div>
-    {{/get_smscreenonly}}
+    {{/action}}
 {{/ flatnavigation }}
 </nav>
index 21be34b..8da3f49 100644 (file)
+// Bootstrap 4 compatibility for bootstrapbased themes.
+// Disable some stylelint rules which are necessary these compatbility classses.
+/* stylelint-disable declaration-no-important, block-no-empty */
 
 // Add support for card. We map it to well.
 // This is not meant to be perfect - but at least applies some styles to a bs4 card.
 .card:extend(.well) {
-    /* stylelint-disable-line no-empty-source */
 }
 
-// Padding and margin utitilies.
-.padding-margin(3);
+// Due to lessphp limitations, the following classes must be declared statically without use of a loop.
+.p-l-1 {
+    padding-left: 1 * @baseFontSize !important;
+}
+.p-r-1 {
+    padding-right: 1 * @baseFontSize !important;
+}
+.p-t-1 {
+    padding-top: 1 * @baseFontSize !important;
+}
+.p-b-1 {
+    padding-bottom: 1 * @baseFontSize !important;
+}
+.p-x-1 {
+    padding-left: 1 * @baseFontSize !important;
+    padding-right: 1 * @baseFontSize !important;
+}
+.p-y-1 {
+    padding-top: 1 * @baseFontSize !important;
+    padding-bottom: 1 * @baseFontSize !important;
+}
+.p-a-1 {
+    padding: 1 * @baseFontSize !important;
+}
 
-.padding-margin(@n, @i : 1) when (@i =< @n) {
-    @amount: @i * @baseFontSize;
-    .p-l-@{i} {
-        padding-left: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .p-r-@{i} {
-        padding-right: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .p-t-@{i} {
-        padding-top: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .p-b-@{i} {
-        padding-bottom: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .p-x-@{i} {
-        padding-left: @amount !important; /* stylelint-disable-line declaration-no-important */
-        padding-right: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .p-y-@{i} {
-        padding-top: @amount !important; /* stylelint-disable-line declaration-no-important */
-        padding-bottom: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .p-a-@{i} {
-        padding: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
+.m-l-1 {
+    margin-left: 1 * @baseFontSize !important;
+}
+.m-r-1 {
+    margin-right: 1 * @baseFontSize !important;
+}
+.m-t-1 {
+    margin-top: 1 * @baseFontSize !important;
+}
+.m-b-1 {
+    margin-bottom: 1 * @baseFontSize !important;
+}
+.m-x-1 {
+    margin-left: 1 * @baseFontSize !important;
+    margin-right: 1 * @baseFontSize !important;
+}
+.m-y-1 {
+    margin-top: 1 * @baseFontSize !important;
+    margin-bottom: 1 * @baseFontSize !important;
+}
+.m-a-1 {
+    margin: 1 * @baseFontSize !important;
+}
 
-    .m-l-@{i} {
-        margin-left: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .m-r-@{i} {
-        margin-right: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .m-t-@{i} {
-        margin-top: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .m-b-@{i} {
-        margin-bottom: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .m-x-@{i} {
-        margin-left: @amount !important; /* stylelint-disable-line declaration-no-important */
-        margin-right: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .m-y-@{i} {
-        margin-top: @amount !important; /* stylelint-disable-line declaration-no-important */
-        margin-bottom: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .m-a-@{i} {
-        margin: @amount !important; /* stylelint-disable-line declaration-no-important */
-    }
-    .padding-margin(@n, (@i + 1));
+.p-l-2 {
+    padding-left: 2 * @baseFontSize !important;
+}
+.p-r-2 {
+    padding-right: 2 * @baseFontSize !important;
+}
+.p-t-2 {
+    padding-top: 2 * @baseFontSize !important;
+}
+.p-b-2 {
+    padding-bottom: 2 * @baseFontSize !important;
+}
+.p-x-2 {
+    padding-left: 2 * @baseFontSize !important;
+    padding-right: 2 * @baseFontSize !important;
+}
+.p-y-2 {
+    padding-top: 2 * @baseFontSize !important;
+    padding-bottom: 2 * @baseFontSize !important;
+}
+.p-a-2 {
+    padding: 2 * @baseFontSize !important;
+}
+
+.m-l-2 {
+    margin-left: 2 * @baseFontSize !important;
+}
+.m-r-2 {
+    margin-right: 2 * @baseFontSize !important;
+}
+.m-t-2 {
+    margin-top: 2 * @baseFontSize !important;
+}
+.m-b-2 {
+    margin-bottom: 2 * @baseFontSize !important;
+}
+.m-x-2 {
+    margin-left: 2 * @baseFontSize !important;
+    margin-right: 2 * @baseFontSize !important;
+}
+.m-y-2 {
+    margin-top: 2 * @baseFontSize !important;
+    margin-bottom: 2 * @baseFontSize !important;
+}
+.m-a-2 {
+    margin: 2 * @baseFontSize !important;
+}
+
+.p-l-3 {
+    padding-left: 3 * @baseFontSize !important;
+}
+.p-r-3 {
+    padding-right: 3 * @baseFontSize !important;
+}
+.p-t-3 {
+    padding-top: 3 * @baseFontSize !important;
+}
+.p-b-3 {
+    padding-bottom: 3 * @baseFontSize !important;
+}
+.p-x-3 {
+    padding-left: 3 * @baseFontSize !important;
+    padding-right: 3 * @baseFontSize !important;
+}
+.p-y-3 {
+    padding-top: 3 * @baseFontSize !important;
+    padding-bottom: 3 * @baseFontSize !important;
+}
+.p-a-3 {
+    padding: 3 * @baseFontSize !important;
+}
+
+.m-l-3 {
+    margin-left: 3 * @baseFontSize !important;
+}
+.m-r-3 {
+    margin-right: 3 * @baseFontSize !important;
+}
+.m-t-3 {
+    margin-top: 3 * @baseFontSize !important;
+}
+.m-b-3 {
+    margin-bottom: 3 * @baseFontSize !important;
+}
+.m-x-3 {
+    margin-left: 3 * @baseFontSize !important;
+    margin-right: 3 * @baseFontSize !important;
+}
+.m-y-3 {
+    margin-top: 3 * @baseFontSize !important;
+    margin-bottom: 3 * @baseFontSize !important;
+}
+.m-a-3 {
+    margin: 3 * @baseFontSize !important;
 }
 
 .d-inline {
-    display: inline !important; /* stylelint-disable-line declaration-no-important */
+    display: inline !important;
 }
 .d-block {
-    display: block !important; /* stylelint-disable-line declaration-no-important */
+    display: block !important;
 }
 .d-inline-block {
-    display: inline-block !important; /* stylelint-disable-line declaration-no-important */
+    display: inline-block !important;
 }
 
 // Horizontal centering.
 .m-x-auto {
-    margin-left: auto !important; /* stylelint-disable-line declaration-no-important */
-    margin-right: auto !important; /* stylelint-disable-line declaration-no-important */
+    margin-left: auto !important;
+    margin-right: auto !important;
 }
 
 // Support img-fluid.
 .img-fluid:extend(.img-responsive) {
-    /* stylelint-disable-line no-empty-source */
 }
 
 // Text danger
 .text-danger:extend(.text-error) {
-    /* stylelint-disable-line no-empty-source */
 }
 
 .btn-secondary {
 
 // Floats
 .pull-xs-left:extend(.pull-left) {
-    /* stylelint-disable-line no-empty-source */
 }
 
 .pull-xs-right:extend(.pull-right) {
-    /* stylelint-disable-line no-empty-source */
 }
 
 .text-nowrap {
-    white-space: nowrap !important; /* stylelint-disable-line declaration-no-important */
+    white-space: nowrap !important;
 }
index de9d52a..326f417 100644 (file)
@@ -113,15 +113,3 @@ button {
     }
 }
 
-.user-enroller-panel {
-    .uep-search-results {
-        .user,
-        .cohort {
-            .options {
-                .enrol {
-                    .btn-mini
-                }
-            }
-        }
-    }
-}
index 9b8d4e6..fd6cef3 100644 (file)
     box-sizing: border-box;
 }
 
+.popover-region-footer-container {
+    height: 30px;
+    text-align: center;
+    border-top: 1px solid #ddd;
+    background-color: @popoverBackground;
+
+    .popover-region-seeall-text {
+        padding-top: 4px;
+    }
+}
+
 .popover-region-header-text {
     float: left;
     margin: 0;
         height: 12px;
         width: 12px;
     }
+    .newmessage-link {
+        margin-right: 10px;
+    }
     label {
         display: inline-block;
         text-align: center;
 }
 
 .popover-region-content-container {
-    height: ~"calc(100% - 25px)";
+    height: ~"calc(100% - 56px)";
     width: 100%;
     overflow-y: auto;
     -webkit-overflow-scrolling: touch;
 
         .popover-region-header-actions {
             float: left;
+
+            .newmessage-link {
+                margin-left: 10px;
+            }
         }
     }
 
index aea5397..373d895 100644 (file)
@@ -7796,6 +7796,15 @@ body.path-question-type .mform fieldset.hidden {
   border-bottom: 1px solid #ddd;
   box-sizing: border-box;
 }
+.popover-region-footer-container {
+  height: 30px;
+  text-align: center;
+  border-top: 1px solid #ddd;
+  background-color: #fff;
+}
+.popover-region-footer-container .popover-region-seeall-text {
+  padding-top: 4px;
+}
 .popover-region-header-text {
   float: left;
   margin: 0;
@@ -7815,13 +7824,16 @@ body.path-question-type .mform fieldset.hidden {
   height: 12px;
   width: 12px;
 }
+.popover-region-header-actions .newmessage-link {
+  margin-right: 10px;
+}
 .popover-region-header-actions label {
   display: inline-block;
   text-align: center;
   margin-bottom: 0;
 }
 .popover-region-content-container {
-  height: calc(100% - 25px);
+  height: calc(100% - 56px);
   width: 100%;
   overflow-y: auto;
   -webkit-overflow-scrolling: touch;
@@ -8060,6 +8072,9 @@ body.path-question-type .mform fieldset.hidden {
 .dir-rtl .popover-region .popover-region-header-actions {
   float: left;
 }
+.dir-rtl .popover-region .popover-region-header-actions .newmessage-link {
+  margin-left: 10px;
+}
 .dir-rtl .navbar .popover-region {
   float: left;
 }
@@ -15874,7 +15889,6 @@ form.search-cohort input[type="text"],
 form.search-cohort input[type="submit"],
 form.search-cohort label {
   margin-top: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 form.search-cohort label {
   margin-bottom: 0;
@@ -15890,20 +15904,6 @@ button.yui3-button.closebutton {
 button.yui3-button.closebutton:hover {
   background-position: 0 0;
 }
-.user-enroller-panel .uep-search-results .user .options .enrol,
-.user-enroller-panel .uep-search-results .cohort .options .enrol {
-  padding: 0 6px;
-  font-size: 10.5px;
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-}
-.user-enroller-panel .uep-search-results .user .options .enrol .label,
-.user-enroller-panel .uep-search-results .cohort .options .enrol .label,
-.user-enroller-panel .uep-search-results .user .options .enrol .badge,
-.user-enroller-panel .uep-search-results .cohort .options .enrol .badge {
-  top: 0;
-}
 .gradetreebox h4 {
   font-size: 14px;
 }
@@ -19020,224 +19020,157 @@ input[type="reset"] .caret {
 ul.indented-list {
   margin-left: 20px;
 }
-.card {
-  /* stylelint-disable-line no-empty-source */
-}
+/* stylelint-disable declaration-no-important, block-no-empty */
 .p-l-1 {
   padding-left: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-r-1 {
   padding-right: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-t-1 {
   padding-top: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-b-1 {
   padding-bottom: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-x-1 {
   padding-left: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
   padding-right: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-y-1 {
   padding-top: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
   padding-bottom: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-a-1 {
   padding: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-l-1 {
   margin-left: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-r-1 {
   margin-right: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-t-1 {
   margin-top: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-b-1 {
   margin-bottom: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-x-1 {
   margin-left: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
   margin-right: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-y-1 {
   margin-top: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
   margin-bottom: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-a-1 {
   margin: 14px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-l-2 {
   padding-left: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-r-2 {
   padding-right: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-t-2 {
   padding-top: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-b-2 {
   padding-bottom: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-x-2 {
   padding-left: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
   padding-right: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-y-2 {
   padding-top: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
   padding-bottom: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-a-2 {
   padding: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-l-2 {
   margin-left: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-r-2 {
   margin-right: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-t-2 {
   margin-top: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-b-2 {
   margin-bottom: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-x-2 {
   margin-left: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
   margin-right: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-y-2 {
   margin-top: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
   margin-bottom: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-a-2 {
   margin: 28px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-l-3 {
   padding-left: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-r-3 {
   padding-right: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-t-3 {
   padding-top: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-b-3 {
   padding-bottom: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-x-3 {
   padding-left: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
   padding-right: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-y-3 {
   padding-top: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
   padding-bottom: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .p-a-3 {
   padding: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-l-3 {
   margin-left: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-r-3 {
   margin-right: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-t-3 {
   margin-top: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-b-3 {
   margin-bottom: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-x-3 {
   margin-left: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
   margin-right: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-y-3 {
   margin-top: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
   margin-bottom: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-a-3 {
   margin: 42px !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .d-inline {
   display: inline !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .d-block {
   display: block !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .d-inline-block {
   display: inline-block !important;
-  /* stylelint-disable-line declaration-no-important */
 }
 .m-x-auto {
   margin-left: auto !important;
-  /* stylelint-disable-line declaration-no-important */
   margin-right: auto !important;
-  /* stylelint-disable-line declaration-no-important */
-}
-.img-fluid {
-  /* stylelint-disable-line no-empty-source */
-}
-.text-danger {
-  /* stylelint-disable-line no-empty-source */
 }
 .btn-secondary {
   background-color: #fff;
@@ -19274,13 +19207,6 @@ ul.indented-list {
 .tag-primary[href] {
   background-color: #0378a9;
 }
-.pull-xs-left {
-  /* stylelint-disable-line no-empty-source */
-}
-.pull-xs-right {
-  /* stylelint-disable-line no-empty-source */
-}
 .text-nowrap {
   white-space: nowrap !important;
-  /* stylelint-disable-line declaration-no-important */
 }
index efb429b..090e45b 100644 (file)
@@ -1,7 +1,7 @@
 /* Custom CSS
 -------------------------*/
 div.logo {
-    background: url([[setting:logo]]) no-repeat 0 0;
+    background: url([[setting:logo]]) 0 0 no-repeat;
     display: block;
     float: left;
     height: 75px;
index efb429b..090e45b 100644 (file)
@@ -1,7 +1,7 @@
 /* Custom CSS
 -------------------------*/
 div.logo {
-    background: url([[setting:logo]]) no-repeat 0 0;
+    background: url([[setting:logo]]) 0 0 no-repeat;
     display: block;
     float: left;
     height: 75px;
index e49f805..e23de8f 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016111500.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016111600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.2beta+ (Build: 20161115)'; // Human-friendly version name
+$release  = '3.2beta+ (Build: 20161116)'; // Human-friendly version name
 
 $branch   = '32';                       // This version's branch.
 $maturity = MATURITY_BETA;             // This version's maturity level.