Merge branch 'MDL-56822-master' of git://github.com/cameron1729/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 17 Nov 2016 06:33:39 +0000 (14:33 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 17 Nov 2016 06:33:39 +0000 (14:33 +0800)
123 files changed:
admin/tool/lp/classes/external/cohort_summary_exporter.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/tests/externallib_test.php
admin/upgradesettings.php
backup/util/ui/tests/behat/behat_backup.php
blocks/login/block_login.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/meta/locallib.php
enrol/yui/rolemanager/rolemanager.js
index.php
lang/en/cache.php
lang/en/error.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/configonlylib.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/questionlib.php
lib/setup.php
lib/templates/popover_region.mustache
lib/tests/coursecatlib_test.php
lib/tests/upgrade_util_test.php
lib/webdavlib.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/amd/build/grading_panel.min.js
mod/assign/amd/src/grading_panel.js
mod/assign/feedback/editpdf/fpdi/fpdi_bridge.php
mod/assign/feedback/editpdf/fpdi/readme_moodle.txt
mod/assign/locallib.php
mod/lti/view.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/lib.php
mod/quiz/renderer.php
mod/quiz/report/attemptsreport_table.php
mod/quiz/report/overview/overview_table.php
mod/quiz/report/overview/report.php
mod/wiki/locallib.php
question/classes/bank/search/category_condition.php
question/classes/bank/view.php
question/type/calculated/questiontype.php
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form-debug.js
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form-min.js
question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form.js
question/type/ddmarker/yui/src/form/js/form.js
question/type/gapselect/renderer.php
question/type/match/renderer.php
question/type/multianswer/renderer.php
question/type/multianswer/tests/walkthrough_test.php
question/type/multichoice/renderer.php
question/type/numerical/renderer.php
question/type/shortanswer/renderer.php
question/type/truefalse/renderer.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/modules.scss
theme/boost/scss/moodle/popover-region.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/moodle/undo.scss
theme/boost/scss/moodle/user.scss
theme/boost/templates/core/filemanager_fileselect.mustache
theme/boost/templates/flat_navigation.mustache
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/buttons.less
theme/bootstrapbase/less/moodle/popover_region.less
theme/bootstrapbase/less/moodle/undo.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 e25a57b..e8a5fc8 100644 (file)
@@ -31,7 +31,7 @@ use moodle_url;
 use moodle_exception;
 
 /**
- * API exposed by tool_mobile
+ * API exposed by tool_mobile, to be used mostly by external functions.
  *
  * @copyright  2016 Juan Leyva
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -106,6 +106,8 @@ class api {
         // We need this to make work the format text functions.
         $PAGE->set_context($context);
 
+        list($authinstructions, $notusedformat) = external_format_text($CFG->auth_instructions, FORMAT_MOODLE, $context->id);
+        list($maintenancemessage, $notusedformat) = external_format_text($CFG->maintenance_message, FORMAT_MOODLE, $context->id);
         $settings = array(
             'wwwroot' => $CFG->wwwroot,
             'httpswwwroot' => $CFG->httpswwwroot,
@@ -115,12 +117,12 @@ class api {
             'authloginviaemail' => $CFG->authloginviaemail,
             'registerauth' => $CFG->registerauth,
             'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
-            'authinstructions' => format_text($CFG->auth_instructions),
+            'authinstructions' => $authinstructions,
             'authnoneenabled' => (int) is_enabled_auth('none'),
             'enablewebservices' => $CFG->enablewebservices,
             'enablemobilewebservice' => $CFG->enablemobilewebservice,
             'maintenanceenabled' => $CFG->maintenance_enabled,
-            'maintenancemessage' => format_text($CFG->maintenance_message),
+            'maintenancemessage' => $maintenancemessage,
         );
 
         $typeoflogin = get_config('tool_mobile', 'typeoflogin');
@@ -162,9 +164,10 @@ class api {
         if (empty($section) or $section == 'frontpagesettings') {
             require_once($CFG->dirroot . '/course/format/lib.php');
             // First settings that anyone can deduce.
-            $settings->fullname = $SITE->fullname;
-            $settings->shortname = $SITE->shortname;
-            $settings->summary = $SITE->summary;
+            $settings->fullname = external_format_string($SITE->fullname, $context->id);
+            $settings->shortname = external_format_string($SITE->shortname, $context->id);
+            list($settings->summary, $settings->summaryformat) = external_format_text($SITE->summary, $SITE->summaryformat,
+                                                                                        $context->id);
             $settings->frontpage = $CFG->frontpage;
             $settings->frontpageloggedin = $CFG->frontpageloggedin;
             $settings->maxcategorydepth = $CFG->maxcategorydepth;
index 010b1c9..fbbca7d 100644 (file)
@@ -64,6 +64,9 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
 
         // Test default values.
         $context = context_system::instance();
+        list($authinstructions, $notusedformat) = external_format_text($CFG->auth_instructions, FORMAT_MOODLE, $context->id);
+        list($maintenancemessage, $notusedformat) = external_format_text($CFG->maintenance_message, FORMAT_MOODLE, $context->id);
+
         $expected = array(
             'wwwroot' => $CFG->wwwroot,
             'httpswwwroot' => $CFG->httpswwwroot,
@@ -73,12 +76,12 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             'authloginviaemail' => $CFG->authloginviaemail,
             'registerauth' => $CFG->registerauth,
             'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
-            'authinstructions' => format_text($CFG->auth_instructions),
+            'authinstructions' => $authinstructions,
             'authnoneenabled' => (int) is_enabled_auth('none'),
             'enablewebservices' => $CFG->enablewebservices,
             'enablemobilewebservice' => $CFG->enablemobilewebservice,
             'maintenanceenabled' => $CFG->maintenance_enabled,
-            'maintenancemessage' => format_text($CFG->maintenance_message),
+            'maintenancemessage' => $maintenancemessage,
             'typeoflogin' => api::LOGIN_VIA_APP,
             'warnings' => array()
         );
@@ -92,8 +95,9 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         set_config('logo', 'mock.png', 'core_admin');
         set_config('logocompact', 'mock.png', 'core_admin');
 
+        list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
         $expected['registerauth'] = 'email';
-        $expected['authinstructions'] = format_text($authinstructions);
+        $expected['authinstructions'] = $authinstructions;
         $expected['typeoflogin'] = api::LOGIN_VIA_BROWSER;
         $expected['launchurl'] = "$CFG->wwwroot/$CFG->admin/tool/mobile/launch.php";
 
@@ -120,12 +124,16 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $result = external::get_config();
         $result = external_api::clean_returnvalue(external::get_config_returns(), $result);
 
+        // SITE summary is null in phpunit which gets transformed to an empty string by format_text.
+        list($sitesummary, $unused) = external_format_text($SITE->summary, $SITE->summaryformat, context_system::instance()->id);
+
         // Test default values.
         $context = context_system::instance();
         $expected = array(
             array('name' => 'fullname', 'value' => $SITE->fullname),
             array('name' => 'shortname', 'value' => $SITE->shortname),
-            array('name' => 'summary', 'value' => $SITE->summary),
+            array('name' => 'summary', 'value' => $sitesummary),
+            array('name' => 'summaryformat', 'value' => FORMAT_HTML),
             array('name' => 'frontpage', 'value' => $CFG->frontpage),
             array('name' => 'frontpageloggedin', 'value' => $CFG->frontpageloggedin),
             array('name' => 'maxcategorydepth', 'value' => $CFG->maxcategorydepth),
@@ -141,9 +149,9 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
 
         // Change a value and retrieve filtering by section.
         set_config('commentsperpage', 1);
-        $expected[9]['value'] = 1;
-        unset($expected[10]);
+        $expected[10]['value'] = 1;
         unset($expected[11]);
+        unset($expected[12]);
 
         $result = external::get_config('frontpagesettings');
         $result = external_api::clean_returnvalue(external::get_config_returns(), $result);
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 47f8588..5699ba6 100644 (file)
@@ -78,20 +78,24 @@ class block_login extends block_base {
 
             $this->content->text .= "\n".'<form class="loginform" id="login" method="post" action="'.get_login_url().'" '.$autocomplete.'>';
 
-            $this->content->text .= '<div class="c1 fld username"><label for="login_username">'.$strusername.'</label>';
-            $this->content->text .= '<input type="text" name="username" id="login_username" value="'.s($username).'" /></div>';
+            $this->content->text .= '<div class="form-group"><label for="login_username">'.$strusername.'</label>';
+            $this->content->text .= '<input type="text" name="username" id="login_username" class="form-control" value="'.s($username).'" /></div>';
 
-            $this->content->text .= '<div class="c1 fld password"><label for="login_password">'.get_string('password').'</label>';
+            $this->content->text .= '<div class="form-group"><label for="login_password">'.get_string('password').'</label>';
 
-            $this->content->text .= '<input type="password" name="password" id="login_password" value="" '.$autocomplete.' /></div>';
+            $this->content->text .= '<input type="password" name="password" id="login_password" class="form-control" value="" '.$autocomplete.' /></div>';
 
             if (isset($CFG->rememberusername) and $CFG->rememberusername == 2) {
                 $checked = $username ? 'checked="checked"' : '';
-                $this->content->text .= '<div class="c1 rememberusername"><input type="checkbox" name="rememberusername" id="rememberusername" value="1" '.$checked.'/>';
-                $this->content->text .= ' <label for="rememberusername">'.get_string('rememberusername', 'admin').'</label></div>';
+                $this->content->text .= '<div class="form-check-inline">';
+                $this->content->text .= '<input type="checkbox" name="rememberusername" id="rememberusername" class="form-check-input" value="1" '.$checked.'/>';
+                $this->content->text .= ' <label for="rememberusername" class="form-check-label">'.get_string('rememberusername', 'admin').'</label>';
+                $this->content->text .= '</div>';
             }
 
-            $this->content->text .= '<div class="c1 btn"><input type="submit" value="'.get_string('login').'" /></div>';
+            $this->content->text .= '<div class="form-group">';
+            $this->content->text .= '<input type="submit" class="btn btn-primary btn-block" value="'.get_string('login').'" />';
+            $this->content->text .= '</div>';
 
             $this->content->text .= "</form>\n";
 
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 a7af91c..0366c4d 100644 (file)
@@ -307,11 +307,12 @@ function enrol_meta_sync($courseid = NULL, $verbose = false) {
                                 ELSE 0 END) AS timeend
               FROM {user_enrolments} pue
               JOIN {enrol} pe ON (pe.id = pue.enrolid AND pe.enrol <> 'meta' AND pe.enrol $enabled)
-              JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' $onecourse)
+              JOIN {enrol} e ON (e.customint1 = pe.courseid AND e.enrol = 'meta' AND e.status = :enrolstatus $onecourse)
               JOIN {user} u ON (u.id = pue.userid AND u.deleted = 0)
          LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = pue.userid)
              WHERE ue.id IS NULL
              GROUP BY pue.userid, e.id";
+    $params['enrolstatus'] = ENROL_INSTANCE_ENABLED;
 
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $ue) {
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 fe57dd3..f7c5ac8 100644 (file)
@@ -22,6 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['activityisscheduledfordeletion'] = 'Activity deletion in progress...';
 $string['authnotexisting'] = 'The autorization plugin doesn\'t exist';
 $string['backupcontainexternal'] = 'This backup file contains external Moodle Network Hosts that are not configured locally';
 $string['backuptablefail'] = 'Backup tables could NOT be set up successfully!';
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 72e5d51..670d69b 100644 (file)
@@ -89,18 +89,16 @@ function min_clean_param($value, $type) {
  * @return string
  */
 function min_fix_utf8($value) {
-    // Lower error reporting because glibc throws bogus notices.
-    $olderror = error_reporting();
-    if ($olderror & E_NOTICE) {
-        error_reporting($olderror ^ E_NOTICE);
-    }
-
     // No null bytes expected in our data, so let's remove it.
     $value = str_replace("\0", '', $value);
 
     static $buggyiconv = null;
     if ($buggyiconv === null) {
+        set_error_handler(function () {
+            return true;
+        });
         $buggyiconv = (!function_exists('iconv') or iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
+        restore_error_handler();
     }
 
     if ($buggyiconv) {
@@ -116,11 +114,7 @@ function min_fix_utf8($value) {
         }
 
     } else {
-        $result = iconv('UTF-8', 'UTF-8//IGNORE', $value);
-    }
-
-    if ($olderror & E_NOTICE) {
-        error_reporting($olderror);
+        $result = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
     }
 
     return $result;
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..664cf40 100644 (file)
@@ -2517,6 +2517,7 @@ function get_login_url() {
  * @return mixed Void, exit, and die depending on path
  * @throws coding_exception
  * @throws require_login_exception
+ * @throws moodle_exception
  */
 function require_login($courseorid = null, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
     global $CFG, $SESSION, $USER, $PAGE, $SITE, $DB, $OUTPUT;
@@ -2684,8 +2685,8 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
     // Make sure the USER has a sesskey set up. Used for CSRF protection.
     sesskey();
 
-    // Do not bother admins with any formalities.
-    if (is_siteadmin()) {
+    // Do not bother admins with any formalities, except for activities pending deletion.
+    if (is_siteadmin() && !($cm && $cm->deletioninprogress)) {
         // Set the global $COURSE.
         if ($cm) {
             $PAGE->set_cm($cm, $course);
@@ -2873,6 +2874,15 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
         }
     }
 
+    // Check whether the activity has been scheduled for deletion. If so, then deny access, even for admins.
+    if ($cm && $cm->deletioninprogress) {
+        if ($preventredirect) {
+            throw new moodle_exception('activityisscheduledfordeletion');
+        }
+        require_once($CFG->dirroot . '/course/lib.php');
+        redirect(course_get_url($course), get_string('activityisscheduledfordeletion', 'error'));
+    }
+
     // Check visibility of activity to current user; includes visible flag, conditional availability, etc.
     if ($cm && !$cm->uservisible) {
         if ($preventredirect) {
@@ -4438,6 +4448,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 0962a24..af4b916 100644 (file)
@@ -984,7 +984,8 @@ function question_category_select_menu($contexts, $top = false, $currentcat = 0,
         $options[] = array($group => $opts);
     }
     echo html_writer::label(get_string('questioncategory', 'core_question'), 'id_movetocategory', false, array('class' => 'accesshide'));
-    echo html_writer::select($options, 'category', $selected, $choose, array('id' => 'id_movetocategory'));
+    $attrs = array('id' => 'id_movetocategory', 'class' => 'custom-select');
+    echo html_writer::select($options, 'category', $selected, $choose, $attrs);
 }
 
 /**
index f80ce6b..fb16784 100644 (file)
@@ -553,8 +553,8 @@ if (!PHPUNIT_TEST or PHPUNIT_UTIL) {
 }
 
 // Acceptance tests needs special output to capture the errors,
-// but not necessary for behat CLI command.
-if (defined('BEHAT_SITE_RUNNING') && !defined('BEHAT_TEST')) {
+// but not necessary for behat CLI command and init script.
+if (defined('BEHAT_SITE_RUNNING') && !defined('BEHAT_TEST') && !defined('BEHAT_UTIL')) {
     require_once(__DIR__ . '/behat/lib.php');
     set_error_handler('behat_error_handler', E_ALL | E_STRICT);
 }
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 1f7b641..12441f2 100644 (file)
@@ -74,7 +74,7 @@ class webdav_client {
     private $_header='';
     private $_body='';
     private $_connection_closed = false;
-    private $_maxheaderlenth = 1000;
+    private $_maxheaderlenth = 65536;
     private $_digestchallenge = null;
     private $_cnonce = '';
     private $_nc = 0;
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 6ab437b..a051f93 100644 (file)
@@ -47,6 +47,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 90f6866..a125cb5 100644 (file)
Binary files a/mod/assign/amd/build/grading_panel.min.js and b/mod/assign/amd/build/grading_panel.min.js differ
index 1c43216..e4a2358 100644 (file)
@@ -89,7 +89,7 @@ define(['jquery', 'core/yui', 'core/notification', 'core/templates', 'core/fragm
         }
 
         // Copy data from notify students checkbox which was moved out of the form.
-        var checked = $('[data-region="grading-actions-form"] [name="sendstudentnotifications"]').val();
+        var checked = $('[data-region="grading-actions-form"] [name="sendstudentnotifications"]').prop("checked");
         $('.gradeform [name="sendstudentnotifications"]').val(checked);
     };
 
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 b45d032..7bf4b84 100644 (file)
@@ -6260,7 +6260,7 @@ class assign {
             if ($current->grade !== null) {
                 $current->grade = floatval($current->grade);
             }
-            $gradechanged = $gradecolpresent && $current->grade !== $modified->grade;
+            $gradechanged = $gradecolpresent && grade_floats_different($current->grade, $modified->grade);
             $markingallocationchanged = $this->get_instance()->markingworkflow &&
                                         $this->get_instance()->markingallocation &&
                                             ($modified->allocatedmarker !== false) &&
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 b8830de..6a463be 100644 (file)
@@ -159,16 +159,17 @@ class edit_renderer extends \plugin_renderer_base {
         $output = '';
         $output .= html_writer::start_div('maxgrade');
         $output .= html_writer::start_tag('form', array('method' => 'post', 'action' => 'edit.php',
-                'class' => 'quizsavegradesform'));
+                'class' => 'quizsavegradesform form-inline'));
         $output .= html_writer::start_tag('fieldset', array('class' => 'invisiblefieldset'));
         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
         $output .= html_writer::input_hidden_params($pageurl);
         $a = html_writer::empty_tag('input', array('type' => 'text', 'id' => 'inputmaxgrade',
                 'name' => 'maxgrade', 'size' => ($structure->get_decimal_places_for_grades() + 2),
-                'value' => $structure->formatted_quiz_grade()));
+                'value' => $structure->formatted_quiz_grade(),
+                'class' => 'form-control'));
         $output .= html_writer::tag('label', get_string('maximumgradex', '', $a),
                 array('for' => 'inputmaxgrade'));
-        $output .= html_writer::empty_tag('input', array('type' => 'submit',
+        $output .= html_writer::empty_tag('input', array('type' => 'submit', 'class' => 'btn btn-secondary m-l-1',
                 'name' => 'savechanges', 'value' => get_string('save', 'quiz')));
         $output .= html_writer::end_tag('fieldset');
         $output .= html_writer::end_tag('form');
@@ -198,6 +199,7 @@ class edit_renderer extends \plugin_renderer_base {
             'name'  => 'repaginate',
             'id'    => 'repaginatecommand',
             'value' => get_string('repaginatecommand', 'quiz'),
+            'class' => 'btn btn-secondary m-b-1',
         );
         if (!$structure->can_be_repaginated()) {
             $buttonoptions['disabled'] = 'disabled';
@@ -226,9 +228,14 @@ class edit_renderer extends \plugin_renderer_base {
         $hiddenurl->param('sesskey', sesskey());
 
         $select = html_writer::select($perpage, 'questionsperpage',
-                $structure->get_questions_per_page(), false);
+                $structure->get_questions_per_page(), false, array('class' => 'custom-select'));
 
-        $buttonattributes = array('type' => 'submit', 'name' => 'repaginate', 'value' => get_string('go'));
+        $buttonattributes = array(
+            'type' => 'submit',
+            'name' => 'repaginate',
+            'value' => get_string('go'),
+            'class' => 'btn btn-secondary m-l-1'
+        );
 
         $formcontent = html_writer::tag('form', html_writer::div(
                     html_writer::input_hidden_params($hiddenurl) .
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 7e18eab..cad1645 100644 (file)
@@ -216,7 +216,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
                 'value' => sesskey()));
         $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
         $output .= html_writer::empty_tag('input', array('type' => 'submit',
-                'class' => 'questionflagsavebutton', 'name' => 'savingflags',
+                'class' => 'questionflagsavebutton btn btn-secondary', 'name' => 'savingflags',
                 'value' => get_string('saveflags', 'question')));
         $output .= html_writer::end_tag('div');
         $output .= html_writer::end_tag('div');
@@ -239,7 +239,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
             return html_writer::empty_tag('input', array('type' => 'button',
                     'value' => get_string('finishreview', 'quiz'),
                     'id' => 'secureclosebutton',
-                    'class' => 'mod_quiz-next-nav'));
+                    'class' => 'mod_quiz-next-nav btn btn-primary'));
 
         } else {
             return html_writer::link($url, get_string('finishreview', 'quiz'),
@@ -351,7 +351,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
      * @return string HTML fragment.
      */
     protected function render_quiz_nav_question_button(quiz_nav_question_button $button) {
-        $classes = array('qnbutton', $button->stateclass, $button->navmethod);
+        $classes = array('qnbutton', $button->stateclass, $button->navmethod, 'btn', 'btn-secondary');
         $extrainfo = array();
 
         if ($button->currentpage) {
@@ -536,7 +536,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
         $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
         if ($page > 0 && $navmethod == 'free') {
             $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'previous',
-                    'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav'));
+                    'value' => get_string('navigateprevious', 'quiz'), 'class' => 'mod_quiz-prev-nav btn btn-secondary'));
         }
         if ($lastpage) {
             $nextlabel = get_string('endtest', 'quiz');
@@ -544,7 +544,7 @@ class mod_quiz_renderer extends plugin_renderer_base {
             $nextlabel = get_string('navigatenext', 'quiz');
         }
         $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
-                'value' => $nextlabel, 'class' => 'mod_quiz-next-nav'));
+                'value' => $nextlabel, 'class' => 'mod_quiz-next-nav btn btn-primary'));
         $output .= html_writer::end_tag('div');
 
         return $output;
index 146f4da..79d9afb 100644 (file)
@@ -593,7 +593,7 @@ abstract class quiz_attempts_report_table extends table_sql {
     protected function submit_buttons() {
         global $PAGE;
         if (has_capability('mod/quiz:deleteattempts', $this->context)) {
-            echo '<input type="submit" id="deleteattemptsbutton" name="delete" value="' .
+            echo '<input type="submit" class="btn btn-secondary m-r-1" id="deleteattemptsbutton" name="delete" value="' .
                     get_string('deleteselected', 'quiz_overview') . '"/>';
             $PAGE->requires->event_handler('#deleteattemptsbutton', 'click', 'M.util.show_confirm_dialog',
                     array('message' => get_string('deleteattemptcheck', 'quiz')));
index 7ff7371..616e1ff 100644 (file)
@@ -189,7 +189,7 @@ class quiz_overview_table extends quiz_attempts_report_table {
 
     protected function submit_buttons() {
         if (has_capability('mod/quiz:regrade', $this->context)) {
-            echo '<input type="submit" name="regrade" value="' .
+            echo '<input type="submit" class="btn btn-secondary m-r-1" name="regrade" value="' .
                     get_string('regradeselected', 'quiz_overview') . '"/>';
         }
         parent::submit_buttons();
index 63d2ae2..71cb1b0 100644 (file)
@@ -184,11 +184,11 @@ class quiz_overview_report extends quiz_attempts_report {
                     echo '<form action="'.$displayurl->out_omit_querystring().'">';
                     echo '<div>';
                     echo html_writer::input_hidden_params($displayurl);
-                    echo '<input type="submit" name="regradeall" value="'.$regradealllabel.'"/>';
-                    echo '<input type="submit" name="regradealldry" value="' .
+                    echo '<input type="submit" class="btn btn-secondary" name="regradeall" value="'.$regradealllabel.'"/>';
+                    echo '<input type="submit" class="btn btn-secondary m-l-1" name="regradealldry" value="' .
                             $regradealldrylabel . '"/>';
                     if ($regradesneeded) {
-                        echo '<input type="submit" name="regradealldrydo" value="' .
+                        echo '<input type="submit" class="btn btn-secondary m-l-1" name="regradealldrydo" value="' .
                                 $regradealldrydolabel . '"/>';
                     }
                     echo '</div>';
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 4ad0d8e..abd7dc6 100644 (file)
@@ -135,7 +135,7 @@ class category_condition extends condition {
         echo \html_writer::start_div('choosecategory');
         $catmenu = question_category_options($contexts, false, 0, true);
         echo \html_writer::label(get_string('selectacategory', 'question'), 'id_selectacategory');
-        echo \html_writer::select($catmenu, 'category', $current, array(), array('class' => 'searchoptions', 'id' => 'id_selectacategory'));
+        echo \html_writer::select($catmenu, 'category', $current, array(), array('class' => 'searchoptions custom-select', 'id' => 'id_selectacategory'));
         echo \html_writer::end_div() . "\n";
     }
 
index 42c17cf..102531d 100644 (file)
@@ -756,11 +756,11 @@ class view {
 
             // Print delete and move selected question.
             if ($caneditall) {
-                echo '<input type="submit" name="deleteselected" value="' . get_string('delete') . "\" />\n";
+                echo '<input type="submit" class="btn btn-secondary" name="deleteselected" value="' . get_string('delete') . "\" />\n";
             }
 
             if ($canmoveall && count($addcontexts)) {
-                echo '<input type="submit" name="move" value="' . get_string('moveto', 'question') . "\" />\n";
+                echo '<input type="submit" class="btn btn-secondary" name="move" value="' . get_string('moveto', 'question') . "\" />\n";
                 question_category_select_menu($addcontexts, false, 0, "{$category->id},{$category->contextid}");
             }
         }
index 8ab1e5e..e5f5c66 100644 (file)
@@ -760,19 +760,19 @@ class qtype_calculated extends question_type {
             }
             $menu1 = html_writer::label(get_string('lengthoption', 'qtype_calculated'),
                 'menucalclength', false, array('class' => 'accesshide'));
-            $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null);
+            $menu1 .= html_writer::select($lengthoptions, 'calclength[]', $regs[4], null, array('class' => 'custom-select'));
 
             $options = array('uniform' => get_string('uniformbit', 'qtype_calculated'),
                 'loguniform' => get_string('loguniformbit', 'qtype_calculated'));
             $menu2 = html_writer::label(get_string('distributionoption', 'qtype_calculated'),
                 'menucalcdistribution', false, array('class' => 'accesshide'));
-            $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null);
-            return '<input type="submit" onclick="'
+            $menu2 .= html_writer::select($options, 'calcdistribution[]', $regs[1], null, array('class' => 'custom-select'));
+            return '<input type="submit" class="btn btn-secondary" onclick="'
                 . "getElementById('addform').regenerateddefid.value='{$defid}'; return true;"
                 .'" value="'. get_string('generatevalue', 'qtype_calculated') . '"/><br/>'
-                . '<input type="text" size="3" name="calcmin[]" '
+                . '<input type="text" class="form-control" size="3" name="calcmin[]" '
                 . " value=\"{$regs[2]}\"/> &amp; <input name=\"calcmax[]\" "
-                . ' type="text" size="3" value="' . $regs[3] .'"/> '
+                . ' type="text" class="form-control" size="3" value="' . $regs[3] .'"/> '
                 . $menu1 . '<br/>'
                 . $menu2;
         } else {
index ef1d23d..f929ebf 100644 (file)
Binary files a/question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form-debug.js and b/question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form-debug.js differ
index 41fc7ca..f7dc5e9 100644 (file)
Binary files a/question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form-min.js and b/question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form-min.js differ
index ef1d23d..f929ebf 100644 (file)
Binary files a/question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form.js and b/question/type/ddmarker/yui/build/moodle-qtype_ddmarker-form/moodle-qtype_ddmarker-form.js differ
index f8c2694..117abff 100644 (file)
@@ -33,7 +33,7 @@ Y.extend(DDMARKER_FORM, M.qtype_ddmarker.dd_base_class, {
                 '<div class="markertexts"></div>' +
                 '<div class="droparea"></div>' +
                 '<div class="dropzones"></div>' +
-                '<ul class="pager">' +
+                '<ul class="pager unstyled list-unstyled">' +
                 '<li><span id="xcoordpreview">X = </span></li>' +
                 '<li><span id="ycoordpreview">Y = </span></li>' +
                 '</ul>' +
index 2faaf7d..177e0e5 100644 (file)
@@ -46,7 +46,7 @@ class qtype_gapselect_renderer extends qtype_elements_embedded_in_question_text_
 
         $attributes = array(
             'id'     => $this->box_id($qa, 'p' . $place),
-             'class' => 'place' . $place,
+             'class' => 'custom-select place' . $place,
         );
         $groupclass = 'group' . $group;
 
index 8f973e0..a1094bf 100644 (file)
@@ -82,7 +82,7 @@ class qtype_match_renderer extends qtype_with_combined_feedback_renderer {
                             'menu' . $qa->get_qt_field_name('sub' . $key), false,
                             array('class' => 'accesshide')) .
                     html_writer::select($choices, $qa->get_qt_field_name('sub' . $key), $selected,
-                            array('0' => 'choose'), array('disabled' => $options->readonly)) .
+                            array('0' => 'choose'), array('disabled' => $options->readonly, 'class' => 'custom-select m-l-1')) .
                     ' ' . $feedbackimage, array('class' => $classes));
 
             $result .= html_writer::end_tag('tr');
index 748bcec..2308f1a 100644 (file)
@@ -222,6 +222,7 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
             'value' => $response,
             'id' => $qa->get_qt_field_name($fieldname),
             'size' => $size,
+            'class' => 'form-control',
         );
         if ($options->readonly) {
             $inputattributes['readonly'] = 'readonly';
@@ -229,7 +230,7 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
 
         $feedbackimg = '';
         if ($options->correctness) {
-            $inputattributes['class'] = $this->feedback_class($matchinganswer->fraction);
+            $inputattributes['class'] .= ' ' . $this->feedback_class($matchinganswer->fraction);
             $feedbackimg = $this->feedback_image($matchinganswer->fraction);
         }
 
@@ -244,7 +245,7 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
                 s($correctanswer->answer), $options);
 
-        $output = html_writer::start_tag('span', array('class' => 'subquestion'));
+        $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline'));
         $output .= html_writer::tag('label', get_string('answer'),
                 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
         $output .= html_writer::empty_tag('input', $inputattributes);
index e29f6a2..a7f21fd 100644 (file)
@@ -116,7 +116,7 @@ class qtype_multianswer_walkthrough_test extends qbehaviour_walkthrough_test_bas
         $this->check_current_mark(null);
         $this->check_current_output(
                 $this->get_contains_marked_out_of_summary(),
-                new question_pattern_expectation('~<input[^>]* class="incorrect" [^>]*/>~'),
+                new question_pattern_expectation('~<input[^>]* class="[^"]*incorrect[^"]*" [^>]*/>~'),
                 $this->get_contains_subq_status(question_state::$gaveup),
                 $this->get_does_not_contain_validation_error_expectation());
     }
index bc114eb..f333c1d 100644 (file)
@@ -97,7 +97,7 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb
                         $question->make_html_inline($question->format_text(
                                 $ans->answer, $ans->answerformat,
                                 $qa, 'question', 'answer', $ansid)),
-                    array('for' => $inputattributes['id']));
+                    array('for' => $inputattributes['id'], 'class' => 'm-l-1'));
 
             // Param $options->suppresschoicefeedback is a hack specific to the
             // oumultiresponse question type. It would be good to refactor to
index d1b7c0a..1d403e0 100644 (file)
@@ -49,6 +49,7 @@ class qtype_numerical_renderer extends qtype_renderer {
             'value' => $currentanswer,
             'id' => $inputname,
             'size' => 80,
+            'class' => 'form-control',
         );
 
         if ($options->readonly) {
@@ -65,7 +66,7 @@ class qtype_numerical_renderer extends qtype_renderer {
             } else {
                 $fraction = 0;
             }
-            $inputattributes['class'] = $this->feedback_class($fraction);
+            $inputattributes['class'] .= ' ' . $this->feedback_class($fraction);
             $feedbackimg = $this->feedback_image($fraction);
         }
 
@@ -123,7 +124,7 @@ class qtype_numerical_renderer extends qtype_renderer {
         $result = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
 
         if (!$placeholder) {
-            $result .= html_writer::start_tag('div', array('class' => 'ablock'));
+            $result .= html_writer::start_tag('div', array('class' => 'ablock form-inline'));
             $result .= html_writer::tag('label', get_string('answercolon', 'qtype_numerical'), array('for' => $inputattributes['id']));
             $result .= html_writer::tag('span', $input, array('class' => 'answer'));
             $result .= html_writer::end_tag('div');
index cfdca17..0a724eb 100644 (file)
@@ -47,6 +47,7 @@ class qtype_shortanswer_renderer extends qtype_renderer {
             'value' => $currentanswer,
             'id' => $inputname,
             'size' => 80,
+            'class' => 'form-control',
         );
 
         if ($options->readonly) {
@@ -61,7 +62,7 @@ class qtype_shortanswer_renderer extends qtype_renderer {
             } else {
                 $fraction = 0;
             }
-            $inputattributes['class'] = $this->feedback_class($fraction);
+            $inputattributes['class'] .= ' ' . $this->feedback_class($fraction);
             $feedbackimg = $this->feedback_image($fraction);
         }
 
@@ -84,7 +85,7 @@ class qtype_shortanswer_renderer extends qtype_renderer {
         $result = html_writer::tag('div', $questiontext, array('class' => 'qtext'));
 
         if (!$placeholder) {
-            $result .= html_writer::start_tag('div', array('class' => 'ablock'));
+            $result .= html_writer::start_tag('div', array('class' => 'ablock form-inline'));
             $result .= html_writer::tag('label', get_string('answer', 'qtype_shortanswer',
                     html_writer::tag('span', $input, array('class' => 'answer'))),
                     array('for' => $inputattributes['id']));
index 61828ba..60adc34 100644 (file)
@@ -90,10 +90,10 @@ class qtype_truefalse_renderer extends qtype_renderer {
 
         $radiotrue = html_writer::empty_tag('input', $trueattributes) .
                 html_writer::tag('label', get_string('true', 'qtype_truefalse'),
-                array('for' => $trueattributes['id']));
+                array('for' => $trueattributes['id'], 'class' => 'm-l-1'));
         $radiofalse = html_writer::empty_tag('input', $falseattributes) .
                 html_writer::tag('label', get_string('false', 'qtype_truefalse'),
-                array('for' => $falseattributes['id']));
+                array('for' => $falseattributes['id'], 'class' => 'm-l-1'));
 
         $result = '';
         $result .= html_writer::tag('div', $question->format_questiontext($qa),
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 e7f3141..33f7c55 100644 (file)
@@ -342,6 +342,34 @@ div#dock {
 }
 
 // Quiz module
+.path-mod-quiz .mod-quiz-edit-content {
+    // Force the quiz edit page to always be large enough to prevent scrolling when accessing the menu.
+    margin-bottom: 10rem;
+}
+
+#page-mod-quiz-mod #id_reviewoptionshdr .col-md-3,
+#page-mod-quiz-mod #id_reviewoptionshdr .col-md-9 {
+    width: auto;
+}
+#page-mod-quiz-mod #id_reviewoptionshdr .form-group {
+    float: left;
+    width: 20rem;
+    display: inline-block;
+    height: 12rem;
+}
+
+#page-mod-quiz-mod #id_reviewoptionshdr .btn-link {
+    line-height: 1.5;
+    vertical-align: bottom;
+}
+#page-mod-quiz-mod #id_reviewoptionshdr .form-inline {
+    float: left;
+    clear: left;
+}
+#page-mod-quiz-mod #id_reviewoptionshdr .form-check-inline {
+    width: 90%;
+    height: 22px;
+}
 
 // Question navigation block.
 .path-mod-quiz #mod_quiz_navblock {
@@ -350,7 +378,7 @@ div#dock {
         font-size: 14px;
         line-height: 20px;
         font-weight: normal;
-        background-color: $body-color;
+        background-color: $card-bg;
         background-image: none;
         height: 40px;
         width: 30px;
@@ -392,7 +420,7 @@ div#dock {
 
     .qnbutton .trafficlight {
         border: 0;
-        background: $body-color none center / 10px no-repeat scroll;
+        background: $card-bg none center / 10px no-repeat scroll;
         height: 20px;
         margin-top: 20px;
         border-radius: 0 0 3px 3px;
@@ -400,7 +428,7 @@ div#dock {
 
     .qnbutton.notyetanswered .trafficlight,
     .qnbutton.invalidanswer .trafficlight {
-        background-color: $body-color;
+        background-color: $card-bg;
     }
 
     .qnbutton.invalidanswer .trafficlight {
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 459e6c7..7ff7d46 100644 (file)
@@ -285,6 +285,10 @@ body.path-question-type {
 .que .comment {
     @extend .alert;
 }
+.que .outcome,
+.que .comment {
+    @extend .alert-warning;
+}
 
 .que .formulation {
     @extend .alert-info;
@@ -331,71 +335,12 @@ body.path-question-type {
     margin-bottom: 1.5em;
 }
 
-.que .correctness {
-    @extend label;
-
-    &.correct {
-        @extend .text-success;
-    }
-
-    &.partiallycorrect {
-        @extend .text-warning;
-    }
-
-    &.notanswered,
-    &.incorrect {
-        @extend .text-danger;
-    }
-}
-
 .que .validationerror {
     @extend .text-danger;
 }
 // copied from .formFieldState in mixin.less
 // and made more specific
 
-@mixin answerState($textColor, $backgroundColor) {
-    $borderColor: $textColor;
-    color: $textColor;
-    background-color: $backgroundColor;
-    border-color: $borderColor;
-
-    @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, .075)); // Redeclare so transitions work
-    &:focus {
-        border-color: darken($borderColor, 10%);
-        $shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px lighten($borderColor, 20%);
-
-        @include box-shadow($shadow);
-    }
-}
-
-.formulation .correct {
-    @extend .text-success;
-}
-
-.formulation .partiallycorrect {
-    @extend .text-warning;
-}
-
-.formulation .incorrect {
-    @extend .text-danger;
-}
-
-.formulation select.correct,
-.formulation input.correct {
-    @include answerState($btn-success-color, $btn-success-bg);
-}
-
-.formulation select.partiallycorrect,
-.formulation input.partiallycorrect {
-    @include answerState($btn-warning-color, $btn-warning-bg);
-}
-
-.formulation select.incorrect,
-.formulation input.incorrect {
-    @include answerState($btn-danger-color, $btn-danger-bg);
-}
-
 .que .grading,
 .que .comment,
 .que .commentlink,
@@ -648,3 +593,18 @@ body.path-question-type .mform fieldset.hidden {
     padding: 0;
     margin: 0.7em 0 0;
 }
+
+// Fix for accurate positioning of ddwtos drag and drop question types.
+.que.ddwtos {
+    box-sizing: content-box;
+}
+
+@include media-breakpoint-down(sm) {
+    .que .info {
+        float: none;
+        width: auto;
+    }
+    .que .content {
+        margin: 0;
+    }
+}
index 9556e7b..982e6bf 100644 (file)
@@ -100,37 +100,6 @@ fieldset.hidden {
     visibility: inherit;
 }
 
-
-/* .controls vs .controls
- *
- * in expanding course list, can be fixed in renderer though
- */
-
-
-/* .btn vs .btn
- *
- * links, inputs and buttons are commonly styled with .btn in
- * bootstrap but it seems that some buttons in Moodle are
- * wrapped in div.btn e.g. the login block's "login" button
- */
-
-div.c1.btn {
-    display: block;
-    padding: 0;
-    margin-bottom: 0;
-    font-size: inherit;
-    line-height: inherit;
-    text-align: inherit;
-    cursor: default;
-    color: inherit;
-    text-shadow: inherit;
-    background-color: inherit;
-    background-image: none;
-    background-repeat: no-repeat;
-    border: none;
-    border-radius: 0;
-    box-shadow: none;
-}
 /* .container vs .container
  *
  * bootstrap uses .container to set the width of the layout at 960px or so, Moodle uses it
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 289b6ae..100ebc6 100644 (file)
@@ -1,3 +1,34 @@
+{{!
+    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/login
+
+    Login page template
+
+    Example context (json):
+    {
+        "output": {
+            "doctype": "<!DOCTYPE html>",
+            "page_title": "Login page",
+            "favicon": "favicon.ico",
+            "main_content": "<h1>Headers keep HTML validators happy</h1>"
+        }
+    }
+}}
 {{{ output.doctype }}}
 <html {{{ output.htmlattributes }}}>
 <head>
@@ -42,3 +73,6 @@
 
 </body>
 </html>
+{{#js}}
+require(['theme_boost/loader']);
+{{/js}}
index 39fcaaf..a24aae0 100644 (file)
@@ -13,7 +13,7 @@
 
     {{{ output.standard_top_of_body_html }}}
 
-    <div id="page" class="container card">
+    <div id="page" class="container card card-block">
 
         <div class="row">
             <div class="col-xs-12 p-y-1">
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 b9e2813..35fd45d 100644 (file)
@@ -100,37 +100,6 @@ fieldset.hidden {
     visibility: inherit;
 }
 
-
-/* .controls vs .controls
- *
- * in expanding course list, can be fixed in renderer though
- */
-
-
-/* .btn vs .btn
- *
- * links, inputs and buttons are commonly styled with .btn in
- * bootstrap but it seems that some buttons in Moodle are
- * wrapped in div.btn e.g. the login block's "login" button
- */
-
-div.c1.btn {
-    display: block;
-    padding: 0;
-    margin-bottom: 0;
-    font-size: inherit;
-    line-height: inherit;
-    text-align: inherit;
-    cursor: default;
-    color: inherit;
-    text-shadow: inherit;
-    background-color: inherit;
-    background-image: none;
-    background-repeat: no-repeat;
-    border: none;
-    border-radius: 0;
-    box-shadow: none;
-}
 /* .container vs .container
  *
  * bootstrap uses .container to set the width of the layout at 960px or so, Moodle uses it
index aea5397..c9b970d 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;
 }
@@ -13506,33 +13521,6 @@ fieldset.hidden {
   display: inherit;
   visibility: inherit;
 }
-/* .controls vs .controls
- *
- * in expanding course list, can be fixed in renderer though
- */
-/* .btn vs .btn
- *
- * links, inputs and buttons are commonly styled with .btn in
- * bootstrap but it seems that some buttons in Moodle are
- * wrapped in div.btn e.g. the login block's "login" button
- */
-div.c1.btn {
-  display: block;
-  padding: 0;
-  margin-bottom: 0;
-  font-size: inherit;
-  line-height: inherit;
-  text-align: inherit;
-  cursor: default;
-  color: inherit;
-  text-shadow: inherit;
-  background-color: inherit;
-  background-image: none;
-  background-repeat: no-repeat;
-  border: none;
-  border-radius: 0;
-  box-shadow: none;
-}
 /* .container vs .container
  *
  * bootstrap uses .container to set the width of the layout at 960px or so, Moodle uses it
@@ -15874,7 +15862,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 +15877,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 +18993,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 +19180,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..dd0b386 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016111500.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016111600.02;              // 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.