Merge branch 'wip-mdl-56211' of https://github.com/rajeshtaneja/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 9 Jan 2017 09:58:57 +0000 (09:58 +0000)
committerDan Poltawski <dan@moodle.com>
Mon, 9 Jan 2017 09:58:57 +0000 (09:58 +0000)
134 files changed:
admin/environment.xml
admin/settings/server.php
admin/tool/lpimportcsv/classes/framework_importer.php
admin/tool/lpimportcsv/continue.php [deleted file]
admin/tool/lpimportcsv/index.php
admin/tool/lpimportcsv/lang/en/tool_lpimportcsv.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/upgrade.txt [new file with mode: 0644]
admin/tool/uploaduser/lang/en/tool_uploaduser.php
admin/tool/usertours/lang/en/tool_usertours.php
auth/classes/output/login.php
auth/tests/behat/rememberusername.feature [new file with mode: 0644]
availability/condition/grade/styles.css [deleted file]
availability/upgrade.txt
blocks/news_items/tests/behat/display_news.feature
calendar/renderer.php
completion/criteria/completion_criteria_date.php
course/externallib.php
course/tests/courselib_test.php
course/tests/externallib_test.php
install/lang/es_mx_kids/langconfig.php [new file with mode: 0644]
install/lang/hi/langconfig.php
install/lang/ja/error.php
install/lang/ja/install.php
install/lang/tr/admin.php
lang/en/admin.php
lang/en/group.php
lang/en/moodle.php
lib/adminlib.php
lib/behat/classes/util.php
lib/classes/output/mustache_template_finder.php
lib/classes/scss.php
lib/db/access.php
lib/db/services.php
lib/db/upgrade.php
lib/dml/moodle_database.php
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/editor/atto/plugins/media/lang/en/atto_media.php
lib/filelib.php
lib/formslib.php
lib/htaccess [deleted file]
lib/javascript-static.js
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/phpmailer/moodle_phpmailer.php
lib/templates/action_menu.mustache
lib/templates/action_menu_item.mustache
lib/templates/action_menu_link.mustache
lib/templates/action_menu_trigger.mustache
lib/templates/actions.mustache
lib/templates/chart.mustache
lib/templates/chooser.mustache
lib/templates/chooser_item.mustache
lib/templates/dataformat_selector.mustache
lib/templates/email_fromname.mustache
lib/templates/email_html.mustache
lib/templates/email_subject.mustache
lib/templates/email_text.mustache
lib/templates/help_icon.mustache
lib/templates/hover_tooltip.mustache
lib/templates/loading.mustache
lib/templates/login.mustache
lib/templates/modal_cancel.mustache
lib/templates/pix_icon.mustache
lib/templates/progress_bar.mustache
lib/templates/select_time.mustache
lib/templates/signup_form_layout.mustache
lib/templates/skip_links.mustache
lib/tests/behat/behat_hooks.php
lib/tests/moodlelib_test.php
lib/tests/scss_test.php [new file with mode: 0644]
lib/tests/weblib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
message/amd/build/message_area_messages.min.js
message/amd/src/message_area_messages.js
message/templates/add_contact_button.mustache
message/templates/remove_contact_button.mustache
mod/assign/externallib.php
mod/assign/locallib.php
mod/assign/view.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/classes/post_form.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/lesson/locallib.php
mod/lesson/pagetypes/endofbranch.php
mod/lti/tests/behat/addtype.feature
mod/quiz/lib.php
mod/scorm/datamodels/aicclib.php
mod/scorm/datamodels/scorm_12.js
mod/scorm/datamodels/scorm_12lib.php
mod/scorm/locallib.php
mod/scorm/tests/behat/add_scorm.feature
mod/scorm/tests/behat/completion_condition_require_status.feature
mod/scorm/tests/behat/missing_org.feature [new file with mode: 0644]
mod/scorm/tests/packages/readme_moodle.txt
mod/scorm/tests/packages/singlescobasic_missingorg.zip [new file with mode: 0644]
mod/scorm/version.php
mod/scorm/view.js
report/log/classes/renderable.php
report/log/classes/table_log.php
report/log/lang/en/report_log.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/filemanager.scss
theme/boost/settings.php
theme/boost/templates/admin_setting_tabs.mustache
theme/boost/templates/core/action_menu.mustache
theme/boost/templates/core/action_menu_item.mustache
theme/boost/templates/core/action_menu_link.mustache
theme/boost/templates/core/chooser.mustache
theme/boost/templates/core/dataformat_selector.mustache
theme/boost/templates/core/help_icon.mustache
theme/boost/templates/core/progress_bar.mustache
theme/boost/templates/core/select_time.mustache
theme/boost/templates/core/signup_form_layout.mustache
theme/boost/templates/core/skip_links.mustache
theme/boost/templates/core_form/element-advcheckbox-inline.mustache
theme/boost/templates/core_form/element-advcheckbox.mustache
theme/boost/templates/core_form/element-checkbox-inline.mustache
theme/boost/templates/core_form/element-checkbox.mustache
theme/boost/templates/nav-drawer.mustache
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/theme_boost/admin_setting_tabs.mustache
version.php

index 8c35fb6..55502ee 100644 (file)
           <ON_CHECK message="unoconvwarning" />
         </FEEDBACK>
       </CUSTOM_CHECK>
-      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_tls_libraries" level="optional">
-        <FEEDBACK>
-          <ON_CHECK message="tlswarning" />
-        </FEEDBACK>
-      </CUSTOM_CHECK>
       <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
         <FEEDBACK>
           <ON_CHECK message="libcurlwarning" />
index 3c56ff1..2ad2777 100644 (file)
@@ -29,9 +29,9 @@ if ($primaryadmin) {
     $primaryadminname  = NULL;
 }
 $temp->add(new admin_setting_configtext('supportname', new lang_string('supportname', 'admin'),
-  new lang_string('configsupportname', 'admin'), $primaryadminname, PARAM_NOTAGS));
+    new lang_string('configsupportname', 'admin'), $primaryadminname, PARAM_NOTAGS));
 $setting = new admin_setting_configtext('supportemail', new lang_string('supportemail', 'admin'),
-  new lang_string('configsupportemail', 'admin'), $primaryadminemail, PARAM_NOTAGS);
+    new lang_string('configsupportemail', 'admin'), $primaryadminemail, PARAM_EMAIL);
 $setting->set_force_ltr(true);
 $temp->add($setting);
 $temp->add(new admin_setting_configtext('supportpage', new lang_string('supportpage', 'admin'), new lang_string('configsupportpage', 'admin'), '', PARAM_URL));
@@ -237,7 +237,7 @@ $temp->add(new admin_setting_configtext('smtpmaxbulk', new lang_string('smtpmaxb
 $temp->add(new admin_setting_heading('noreplydomainheading', new lang_string('noreplydomain', 'admin'),
         new lang_string('noreplydomaindetail', 'admin')));
 $temp->add(new admin_setting_configtext('noreplyaddress', new lang_string('noreplyaddress', 'admin'),
-          new lang_string('confignoreplyaddress', 'admin'), 'noreply@' . get_host_from_url($CFG->wwwroot), PARAM_NOTAGS));
+          new lang_string('confignoreplyaddress', 'admin'), 'noreply@' . get_host_from_url($CFG->wwwroot), PARAM_EMAIL));
 $temp->add(new admin_setting_configtextarea('allowedemaildomains',
         new lang_string('allowedemaildomains', 'admin'),
         new lang_string('configallowedemaildomains', 'admin'),
index e784814..82b3516 100644 (file)
@@ -53,6 +53,10 @@ class framework_importer {
     protected $importer = null;
     protected $foundheaders = array();
     protected $scalecache = array();
+    /** @var bool $useprogressbar Control whether importing should use progress bars or not. */
+    protected $useprogressbar = false;
+    /** @var \core\progress\display_if_slow|null $progress The progress bar instance. */
+    protected $progress = null;
 
     /**
      * Store an error message for display later
@@ -164,8 +168,11 @@ class framework_importer {
      * @param string delimiter The specified delimiter for the file.
      * @param string importid The id of the csv import.
      * @param array mappingdata The mapping data from the import form.
+     * @param bool $useprogressbar Whether progress bar should be displayed, to avoid html output on CLI.
      */
-    public function __construct($text = null, $encoding = null, $delimiter = null, $importid = 0, $mappingdata = null) {
+    public function __construct($text = null, $encoding = null, $delimiter = null, $importid = 0, $mappingdata = null,
+            $useprogressbar = false) {
+
         global $CFG;
 
         // The format of our records is:
@@ -204,7 +211,7 @@ class framework_importer {
         }
 
         $this->foundheaders = $this->importer->get_columns();
-
+        $this->useprogressbar = $useprogressbar;
         $domainid = 1;
 
         $flat = array();
@@ -264,8 +271,19 @@ class framework_importer {
             $this->fail(get_string('invalidimportfile', 'tool_lpimportcsv'));
             return;
         } else {
+            // We are calling from browser, display progress bar.
+            if ($this->useprogressbar === true) {
+                $this->progress = new \core\progress\display_if_slow(get_string('processingfile', 'tool_lpimportcsv'));
+                $this->progress->start_html();
+            } else {
+                // Avoid html output on CLI scripts.
+                $this->progress = new \core\progress\none();
+            }
+            $this->progress->start_progress('', count($this->flat));
             // Build a tree from this flat list.
+            raise_memory_limit(MEMORY_EXTRA);
             $this->add_children($this->framework, '');
+            $this->progress->end_progress();
         }
     }
 
@@ -278,6 +296,7 @@ class framework_importer {
     public function add_children(& $node, $parentidnumber) {
         foreach ($this->flat as $competency) {
             if ($competency->parentidnumber == $parentidnumber) {
+                $this->progress->increment_progress();
                 $node->children[] = $competency;
                 $this->add_children($competency, $competency->idnumber);
             }
@@ -443,17 +462,28 @@ class framework_importer {
         $record->contextid = context_system::instance()->id;
 
         $framework = api::create_framework($record);
+        if ($this->useprogressbar === true) {
+            $this->progress = new \core\progress\display_if_slow(get_string('importingfile', 'tool_lpimportcsv'));
+            $this->progress->start_html();
+        } else {
+            $this->progress = new \core\progress\none();
+        }
 
+        $this->progress->start_progress('', (count($this->framework->children) * 2));
+        raise_memory_limit(MEMORY_EXTRA);
         // Now all the children.
         foreach ($this->framework->children as $comp) {
+            $this->progress->increment_progress();
             $this->create_competency($comp, null, $framework);
         }
 
         // Now create the rules.
         foreach ($this->framework->children as $record) {
+            $this->progress->increment_progress();
             $this->set_rules($record);
             $this->set_related($record);
         }
+        $this->progress->end_progress();
 
         $this->importer->cleanup();
         return $framework;
diff --git a/admin/tool/lpimportcsv/continue.php b/admin/tool/lpimportcsv/continue.php
deleted file mode 100644 (file)
index ef98a48..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?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/>.
-
-/**
- * Page to continue after an action.
- *
- * @package    tool_lpimportcsv
- * @copyright  2015 Damyon Wiese
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-require_once(__DIR__ . '/../../../config.php');
-require_once($CFG->libdir.'/adminlib.php');
-
-$pagetitle = get_string('pluginname', 'tool_lpimportcsv');
-
-$context = context_system::instance();
-
-$id = required_param('id', PARAM_INT);
-$url = new moodle_url("/admin/tool/lpimportcsv/index.php");
-$PAGE->set_context($context);
-$PAGE->set_url($url);
-$PAGE->set_title($pagetitle);
-$PAGE->set_pagelayout('admin');
-$PAGE->set_heading($pagetitle);
-
-echo $OUTPUT->header();
-echo $OUTPUT->heading($pagetitle);
-$urlparams = ['competencyframeworkid' => $id, 'pagecontextid' => $context->id];
-$frameworksurl = new moodle_url('/admin/tool/lp/competencies.php', $urlparams);
-echo $OUTPUT->notification(get_string('competencyframeworkcreated', 'tool_lp'), 'notifysuccess');
-echo $OUTPUT->continue_button($frameworksurl);
-
-echo $OUTPUT->footer();
index a2fc6cb..63d5a2c 100644 (file)
@@ -37,6 +37,7 @@ $PAGE->set_pagelayout('admin');
 $PAGE->set_heading($pagetitle);
 
 $form = null;
+echo $OUTPUT->header();
 if (optional_param('needsconfirm', 0, PARAM_BOOL)) {
     $form = new \tool_lpimportcsv\form\import($url->out(false));
 } else if (optional_param('confirm', 0, PARAM_BOOL)) {
@@ -53,7 +54,7 @@ if ($form->is_cancelled()) {
 
     if ($data->confirm) {
         $importid = $data->importid;
-        $importer = new \tool_lpimportcsv\framework_importer(null, null, null, $importid, $data);
+        $importer = new \tool_lpimportcsv\framework_importer(null, null, null, $importid, $data, true);
 
         $error = $importer->get_error();
         if ($error) {
@@ -61,21 +62,23 @@ if ($form->is_cancelled()) {
             $form->set_import_error($error);
         } else {
             $framework = $importer->import();
-            redirect(new moodle_url('continue.php', array('id' => $framework->get_id())));
+            $urlparams = ['competencyframeworkid' => $framework->get_id(), 'pagecontextid' => $context->id];
+            $frameworksurl = new moodle_url('/admin/tool/lp/competencies.php', $urlparams);
+            echo $OUTPUT->notification(get_string('competencyframeworkcreated', 'tool_lp'), 'notifysuccess');
+            echo $OUTPUT->continue_button($frameworksurl);
             die();
         }
     } else {
         $text = $form->get_file_content('importfile');
         $encoding = $data->encoding;
         $delimiter = $data->delimiter_name;
-        $importer = new \tool_lpimportcsv\framework_importer($text, $encoding, $delimiter);
+        $importer = new \tool_lpimportcsv\framework_importer($text, $encoding, $delimiter, 0, null, true);
         $confirmform = new \tool_lpimportcsv\form\import_confirm(null, $importer);
         $form = $confirmform;
         $pagetitle = get_string('confirmcolumnmappings', 'tool_lpimportcsv');
     }
 }
 
-echo $OUTPUT->header();
 echo $OUTPUT->heading($pagetitle);
 
 $form->display();
index 8c10b04..fd8d9cf 100644 (file)
@@ -38,11 +38,13 @@ $string['importfile'] = 'CSV framework description file';
 $string['importfile_help'] = 'A competency framework may be imported via text file. The format of the file can be determined by creating a new competency framework on the site and then exporting it.';
 $string['importfile_link'] = 'admin/tool/lpimportcsv';
 $string['import'] = 'Import';
+$string['importingfile'] = 'Importing file data';
 $string['invalidimportfile'] = 'File format is invalid.';
 $string['isframework'] = 'Is framework';
 $string['noframeworks'] = 'No competency frameworks have been created yet';
 $string['parentidnumber'] = 'Parent ID number';
 $string['pluginname'] = 'Import competency framework';
+$string['processingfile'] = 'Processing file';
 $string['relatedidnumbers'] = 'Cross-referenced competency ID numbers';
 $string['ruleconfig'] = 'Rule config (optional)';
 $string['ruleoutcome'] = 'Rule outcome (optional)';
index 9885b4e..f10fb60 100644 (file)
@@ -124,6 +124,7 @@ class api {
             'enablemobilewebservice' => $CFG->enablemobilewebservice,
             'maintenanceenabled' => $CFG->maintenance_enabled,
             'maintenancemessage' => $maintenancemessage,
+            'mobilecssurl' => !empty($CFG->mobilecssurl) ? $CFG->mobilecssurl : '',
         );
 
         $typeoflogin = get_config('tool_mobile', 'typeoflogin');
index 1b3cdb6..b527a49 100644 (file)
@@ -148,6 +148,7 @@ class external extends external_api {
                 'compactlogourl' => new external_value(PARAM_URL, 'The site compact logo URL', VALUE_OPTIONAL),
                 'typeoflogin' => new external_value(PARAM_INT, 'The type of login. 1 for app, 2 for browser, 3 for embedded.'),
                 'launchurl' => new external_value(PARAM_URL, 'SSO login launch URL. Empty if it won\'t be used.', VALUE_OPTIONAL),
+                'mobilecssurl' => new external_value(PARAM_URL, 'Mobile custom CSS theme', VALUE_OPTIONAL),
                 'warnings' => new external_warnings(),
             )
         );
index fbbca7d..f145cd0 100644 (file)
@@ -83,6 +83,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             'maintenanceenabled' => $CFG->maintenance_enabled,
             'maintenancemessage' => $maintenancemessage,
             'typeoflogin' => api::LOGIN_VIA_APP,
+            'mobilecssurl' => '',
             'warnings' => array()
         );
         $this->assertEquals($expected, $result);
diff --git a/admin/tool/mobile/upgrade.txt b/admin/tool/mobile/upgrade.txt
new file mode 100644 (file)
index 0000000..ccfc819
--- /dev/null
@@ -0,0 +1,7 @@
+This files describes changes in tool_mobile code.
+Information provided here is intended especially for developers.
+
+=== 3.3 ===
+
+ * External function tool_mobile::get_public_config now returns the mobilecssurl field (Mobile custom CSS theme).
+
index d499660..ce23d22 100644 (file)
@@ -33,7 +33,7 @@ $string['deleteerrors'] = 'Delete errors';
 $string['encoding'] = 'Encoding';
 $string['errormnetadd'] = 'Can not add remote users';
 $string['errors'] = 'Errors';
-$string['invalidupdatetype'] = 'You can not select this option with the chosen \'Upload type\'';
+$string['invalidupdatetype'] = 'This option cannot be selected with the chosen upload type.';
 $string['invaliduserdata'] = 'Invalid data detected for user {$a} and it has been automatically cleaned.';
 $string['nochanges'] = 'No changes';
 $string['pluginname'] = 'User upload';
index 414e640..e6d9d2f 100644 (file)
@@ -146,9 +146,9 @@ $string['tour1_content_navigation'] = 'Major navigation is now through this nav
 $string['tour1_title_customisation'] = 'Customisation';
 $string['tour1_content_customisation'] = 'To customise the look of your site and the front page, use the settings menu in the corner of this header. Try turning editing on right now.';
 $string['tour1_title_blockregion'] = 'Block region';
-$string['tour1_content_blockregion'] = 'There is still a block region over here. We recommend removing the Navigation and Settings blocks completely, as all the functionality is elsewhere in the Boost theme.';
+$string['tour1_content_blockregion'] = 'There is still a block region over here. We recommend removing the Navigation and Administration blocks completely, as all the functionality is elsewhere in the Boost theme.';
 $string['tour1_title_addingblocks'] = 'Adding blocks';
-$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle mobile app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
+$string['tour1_content_addingblocks'] = 'In fact, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so as a general rule it\'s much better to make sure your site works well without any blocks.';
 $string['tour1_title_end'] = 'End of tour';
 $string['tour1_content_end'] = 'This has been a user tour, a new feature in Moodle 3.2. It won\'t show again unless you reset it using the link in the footer. As an admin you can also create your own tours like this!';
 
@@ -158,14 +158,14 @@ $string['tour2_content_welcome'] = 'Welcome to the Boost theme for Moodle 3.2. I
 $string['tour2_title_customisation'] = 'Customisation';
 $string['tour2_content_customisation'] = 'To change any course settings, use the settings menu in the corner of this header. You will find a similar settings menu on the home page of every activity, too. Try turning editing on right now.';
 $string['tour2_title_navigation'] = 'Navigation';
-$string['tour2_content_navigation'] = 'Major navigation is now though this nav drawer. Use the button at the top to hide or show it. You will see that there are links for major sections of your course.';
+$string['tour2_content_navigation'] = 'Navigation is now through this nav drawer. Use the button at the top to hide or show it. You will see that there are links for sections of your course.';
 $string['tour2_title_opendrawer'] = 'Open the nav drawer';
 $string['tour2_content_opendrawer'] = 'Try opening the nav drawer now.';
 $string['tour2_title_participants'] = 'Course participants';
 $string['tour2_content_participants'] = 'View participants here. This is also where you go to add or remove students.';
 $string['tour2_title_addblock'] = 'Add a block';
-$string['tour2_content_addblock'] = 'If you enable editing mode you can add blocks from the nav drawer. However, think carefully about including any blocks on your pages. Blocks are not shown on Moodle mobile app, so for the best student experience it is much better to make sure your course works well without any blocks.';
+$string['tour2_content_addblock'] = 'If you turn editing on you can add blocks from the nav drawer. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
 $string['tour2_title_addingblocks'] = 'Adding blocks';
-$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown on Moodle mobile app, so for the best student experience it is much better to make sure your course works well without any blocks.';
+$string['tour2_content_addingblocks'] = 'You can add blocks to this page using this button. However, think carefully about including any blocks on your pages. Blocks are not shown on the Moodle Mobile app, so for the best user experience it is better to make sure your course works well without any blocks.';
 $string['tour2_title_end'] = 'End of tour';
 $string['tour2_content_end'] = 'This has been a user tour, a new feature in Moodle 3.2. It won\'t show again unless you reset it using the link in the footer. The site admin can also create further tours for this site if required.';
index e5e4e7d..5b924bb 100644 (file)
@@ -151,6 +151,7 @@ class login implements renderable, templatable {
         $data->rememberusername = $this->rememberusername;
         $data->passwordautocomplete = $this->passwordautocomplete;
         $data->signupurl = $this->signupurl->out(false);
+        $data->username = $this->username;
 
         return $data;
     }
diff --git a/auth/tests/behat/rememberusername.feature b/auth/tests/behat/rememberusername.feature
new file mode 100644 (file)
index 0000000..754ce8f
--- /dev/null
@@ -0,0 +1,50 @@
+@core @core_auth
+Feature: Test the 'remember username' feature works.
+  In order to see my saved username on the login form
+  As a user
+  I need to have logged in once before and clicked 'Remember username'
+
+  Background:
+    Given the following "users" exist:
+      | username |
+      | teacher1 |
+
+  # Given the user has logged in and selected 'Remember username', when they log in again, then their username should be remembered.
+  Scenario: Check that 'remember username' works without javascript for teachers.
+    # Log in the first time and check the 'remember username' box.
+    Given I am on homepage
+    And I click on "Log in" "link" in the ".logininfo" "css_element"
+    And I set the field "Username" to "teacher1"
+    And I set the field "Password" to "teacher1"
+    And I set the field "Remember username" to "1"
+    And I press "Log in"
+    And I log out
+    # Log out and check that the username was remembered.
+    When I am on homepage
+    And I click on "Log in" "link" in the ".logininfo" "css_element"
+    Then the field "username" matches value "teacher1"
+    And the field "Remember username" matches value "1"
+
+  # Given the user has logged in before and selected 'Remember username', when they log in again and unset 'Remember username', then
+  # their username should be forgotten for future log in attempts.
+  Scenario: Check that 'remember username' unsetting works without javascript for teachers.
+    # Log in the first time and check the 'remember username' box.
+    Given I am on homepage
+    And I click on "Log in" "link" in the ".logininfo" "css_element"
+    And I set the field "Username" to "teacher1"
+    And I set the field "Password" to "teacher1"
+    And I set the field "Remember username" to "1"
+    And I press "Log in"
+    And I log out
+    # Log in again, unsetting the 'remember username' field.
+    When I am on homepage
+    And I click on "Log in" "link" in the ".logininfo" "css_element"
+    And I set the field "Password" to "teacher1"
+    And I set the field "Remember username" to "0"
+    And I press "Log in"
+    And I log out
+    # Check username has been forgotten.
+    Then I am on homepage
+    And I click on "Log in" "link" in the ".logininfo" "css_element"
+    Then the field "username" matches value ""
+    And the field "Remember username" matches value "0"
diff --git a/availability/condition/grade/styles.css b/availability/condition/grade/styles.css
deleted file mode 100644 (file)
index 5112690..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-#fitem_id_availabilityconditionsjson .availability_grade input[type=text] {
-    width: 3em;
-}
index 92d3b8d..a509b02 100644 (file)
@@ -2,6 +2,11 @@ This files describes API changes in /availability/*.
 
 The information here is intended only for developers.
 
+=== 3.2 ===
+
+* Condition plugins must replace the CSS selector "#fitem_id_availabilityconditionsjson" with ".availability-field".
+  This selector is often used in your plugin's yui/src/form/js/form.js file.
+
 === 2.9 ===
 
 * Condition plugins can now implement a new include_after_restore function to
index ada6a9e..2093c76 100644 (file)
@@ -13,7 +13,7 @@ Feature: Latest announcements block displays the course latest news
     And I create a course with:
       | Course full name | Course 1 |
       | Course short name | C1 |
-      | News items to show | 5 |
+      | Number of announcements | 5 |
     And I enrol "Teacher 1" user as "Teacher"
     And I log out
     And I log in as "teacher1"
@@ -36,13 +36,13 @@ Feature: Latest announcements block displays the course latest news
     And I should see "Discussion Three" in the "Latest announcements" "block"
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
-      | News items to show | 2 |
+      | Number of announcements | 2 |
     And I press "Save and display"
     And I should not see "Discussion One" in the "Latest announcements" "block"
     And I should see "Discussion Two" in the "Latest announcements" "block"
     And I should see "Discussion Three" in the "Latest announcements" "block"
     And I navigate to "Edit settings" in current page administration
     And I set the following fields to these values:
-      | News items to show | 0 |
+      | Number of announcements | 0 |
     And I press "Save and display"
     And "Latest announcements" "block" should not exist
index d7cecd1..40530a1 100644 (file)
@@ -675,7 +675,7 @@ class core_calendar_renderer extends plugin_renderer_base {
         } else {
             // Assemble pollinterval control.
             $html .= html_writer::start_tag('div', array('style' => 'float:left;'));
-            $html .= html_writer::start_tag('select', array('name' => 'pollinterval'));
+            $html .= html_writer::start_tag('select', array('name' => 'pollinterval', 'class' => 'custom-select'));
             foreach (calendar_get_pollinterval_choices() as $k => $v) {
                 $attributes = array();
                 if ($k == $subscription->pollinterval) {
@@ -687,15 +687,17 @@ class core_calendar_renderer extends plugin_renderer_base {
             $html .= html_writer::end_tag('select');
             $html .= html_writer::end_tag('div');
         }
-        $html .= html_writer::start_tag('div', array('style' => 'float:right;'));
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'course', 'value' => $courseid));
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'id', 'value' => $subscription->id));
+        $html .= html_writer::start_tag('div', array('class' => 'btn-group pull-right'));
         if (!empty($subscription->url)) {
             $html .= html_writer::tag('button', get_string('update'), array('type'  => 'submit', 'name' => 'action',
+                                                                            'class' => 'btn btn-secondary',
                                                                             'value' => CALENDAR_SUBSCRIPTION_UPDATE));
         }
         $html .= html_writer::tag('button', get_string('remove'), array('type'  => 'submit', 'name' => 'action',
+                                                                        'class' => 'btn btn-secondary',
                                                                         'value' => CALENDAR_SUBSCRIPTION_REMOVE));
         $html .= html_writer::end_tag('div');
         $html .= html_writer::end_tag('form');
index 9327466..e3fbb6d 100644 (file)
@@ -213,4 +213,18 @@ class completion_criteria_date extends completion_criteria {
     public function get_icon($alt, array $attributes = null) {
         return new pix_icon('i/calendar', $alt, 'moodle', $attributes);
     }
+
+    /**
+     * Shift the date when resetting course.
+     *
+     * @param int $courseid the course id
+     * @param int $timeshift number of seconds to shift date
+     * @return boolean was the operation successful?
+     */
+    public static function update_date($courseid, $timeshift) {
+        if ($criteria = self::fetch(array('course' => $courseid))) {
+            $criteria->timeend = $criteria->timeend + $timeshift;
+            $criteria->update();
+        }
+    }
 }
index 3221469..73277e0 100644 (file)
@@ -1644,12 +1644,6 @@ class core_course_external extends external_api {
                 }
             }
 
-            // Check category depth is <= maxdepth (do not check for user who can manage categories).
-            if ((!empty($CFG->maxcategorydepth) && count($parents) > $CFG->maxcategorydepth)
-                    and !has_capability('moodle/category:manage', $context)) {
-                $excludedcats[$category->id] = 'depth';
-            }
-
             // Check the user can use the category context.
             $context = context_coursecat::instance($category->id);
             try {
@@ -3153,4 +3147,78 @@ class core_course_external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_updates_since_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseid' => new external_value(PARAM_INT, 'Course id to check'),
+                'since' => new external_value(PARAM_INT, 'Check updates since this time stamp'),
+                'filter' => new external_multiple_structure(
+                    new external_value(PARAM_ALPHANUM, 'Area name: configuration, fileareas, completion, ratings, comments,
+                                                        gradeitems, outcomes'),
+                    'Check only for updates in these areas', VALUE_DEFAULT, array()
+                )
+            )
+        );
+    }
+
+    /**
+     * Check if there are updates affecting the user for the given course since the given time stamp.
+     *
+     * This function is a wrapper of self::check_updates for retrieving all the updates since a given time for all the activities.
+     *
+     * @param int $courseid the list of modules to check
+     * @param int $since check updates since this time stamp
+     * @param array $filter check only for updates in these areas
+     * @return array list of updates and warnings
+     * @throws moodle_exception
+     * @since Moodle 3.3
+     */
+    public static function get_updates_since($courseid, $since, $filter = array()) {
+        global $CFG, $DB;
+
+        $params = self::validate_parameters(
+            self::get_updates_since_parameters(),
+            array(
+                'courseid' => $courseid,
+                'since' => $since,
+                'filter' => $filter,
+            )
+        );
+
+        $course = get_course($params['courseid']);
+        $modinfo = get_fast_modinfo($course);
+        $tocheck = array();
+
+        // Retrieve all the visible course modules for the current user.
+        $cms = $modinfo->get_cms();
+        foreach ($cms as $cm) {
+            if (!$cm->uservisible) {
+                continue;
+            }
+            $tocheck[] = array(
+                'id' => $cm->id,
+                'contextlevel' => 'module',
+                'since' => $params['since'],
+            );
+        }
+
+        return self::check_updates($course->id, $tocheck, $params['filter']);
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.3
+     */
+    public static function get_updates_since_returns() {
+        return self::check_updates_returns();
+    }
 }
index d8e6a6f..b6a9045 100644 (file)
@@ -3213,14 +3213,24 @@ class core_course_courselib_testcase extends advanced_testcase {
      * @param int $resultingenddate
      */
     public function test_course_dates_reset($startdate, $enddate, $resetstartdate, $resetenddate, $resultingstartdate, $resultingenddate) {
-        global $DB;
+        global $CFG, $DB;
+
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
 
         $this->resetAfterTest(true);
 
+        $CFG->enablecompletion = true;
+
         $this->setTimezone('UTC');
 
-        $record = array('startdate' => $startdate, 'enddate' => $enddate);
+        $record = array('startdate' => $startdate, 'enddate' => $enddate, 'enablecompletion' => 1);
         $originalcourse = $this->getDataGenerator()->create_course($record);
+        $coursecriteria = new completion_criteria_date(array('course' => $originalcourse->id, 'timeend' => $startdate + DAYSECS));
+        $coursecriteria->insert();
+
+        $activitycompletiondate = $startdate + DAYSECS;
+        $data = $this->getDataGenerator()->create_module('data', array('course' => $originalcourse->id),
+                        array('completion' => 1, 'completionexpected' => $activitycompletiondate));
 
         $resetdata = new stdClass();
         $resetdata->id = $originalcourse->id;
@@ -3234,6 +3244,12 @@ class core_course_courselib_testcase extends advanced_testcase {
 
         $this->assertEquals($resultingstartdate, $course->startdate);
         $this->assertEquals($resultingenddate, $course->enddate);
+
+        $coursecompletioncriteria = completion_criteria_date::fetch(array('course' => $originalcourse->id));
+        $this->assertEquals($resultingstartdate + DAYSECS, $coursecompletioncriteria->timeend);
+
+        $this->assertEquals($resultingstartdate + DAYSECS, $DB->get_field('course_modules', 'completionexpected',
+            array('id' => $data->cmid)));
     }
 
     /**
index 083e558..70d25af 100644 (file)
@@ -278,8 +278,19 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertEquals($DB->count_records('course_categories'), count($categories));
 
-        // Call without required capability (it will fail cause of the search on idnumber).
         $this->unassignUserCapability('moodle/category:manage', $context->id, $roleid);
+
+        // Ensure maxdepthcategory is 2 and retrieve all categories without category:manage capability. It should retrieve all
+        // visible categories as well.
+        set_config('maxcategorydepth', 2);
+        $categories = core_course_external::get_categories();
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
+
+        $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories));
+
+        // Call without required capability (it will fail cause of the search on idnumber).
         $this->expectException('moodle_exception');
         $categories = core_course_external::get_categories(array(
             array('key' => 'id', 'value' => $category1->id),
@@ -2139,6 +2150,12 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(0, $result['instances']);
         $this->assertCount(0, $result['warnings']);
 
+        // Test with get_updates_since the same data.
+        $result = core_course_external::get_updates_since($course->id, $since);
+        $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
+        $this->assertCount(0, $result['instances']);
+        $this->assertCount(0, $result['warnings']);
+
         // Update a module after a second.
         $this->waitForSecond();
         set_coursemodule_name($modules['forum']['cm']->id, 'New forum name');
@@ -2157,6 +2174,23 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         }
         $this->assertTrue($found);
 
+        // Test with get_updates_since the same data.
+        $result = core_course_external::get_updates_since($course->id, $since);
+        $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result);
+        $this->assertCount(1, $result['instances']);
+        $this->assertCount(0, $result['warnings']);
+        $found = false;
+        $this->assertCount(1, $result['instances']);
+        $this->assertCount(0, $result['warnings']);
+        foreach ($result['instances'] as $module) {
+            foreach ($module['updates'] as $update) {
+                if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') {
+                    $found = true;
+                }
+            }
+        }
+        $this->assertTrue($found);
+
         // Do not retrieve the configuration field.
         $filter = array('files');
         $found = false;
diff --git a/install/lang/es_mx_kids/langconfig.php b/install/lang/es_mx_kids/langconfig.php
new file mode 100644 (file)
index 0000000..0aa04d5
--- /dev/null
@@ -0,0 +1,34 @@
+<?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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package   installer
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'es_mx';
+$string['thislanguage'] = 'Español - México - kids';
index 599c105..2d1be1f 100644 (file)
@@ -31,4 +31,4 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['thisdirection'] = 'ltr';
-$string['thislanguage'] = 'िहनà¥\8dदी';
+$string['thislanguage'] = 'हिà¤\82दी';
index 4b88978..4e85def 100644 (file)
@@ -40,14 +40,14 @@ $string['cannotdownloadzipfile'] = 'ZIPファイルをダウンロードでき
 $string['cannotfindcomponent'] = 'コンポーネントを見つけることができません。';
 $string['cannotsavemd5file'] = 'mp5ファイルを保存できません。';
 $string['cannotsavezipfile'] = 'ZIPファイルを保存できません。';
-$string['cannotunzipfile'] = 'ZIPファイルを解凍できません。';
+$string['cannotunzipfile'] = 'ZIPファイルを展開できません。';
 $string['componentisuptodate'] = 'コンポーネントは最新です。';
 $string['dmlexceptiononinstall'] = '<p>データベースエラーが発生しました: [{$a->errorcode}]<br />{$a->debuginfo}</p>';
 $string['downloadedfilecheckfailed'] = 'ダウンロードファイルのチェックに失敗しました。';
 $string['invalidmd5'] = 'チェック変数が正しくありません - 再度お試しください。';
 $string['missingrequiredfield'] = 'いくつかの必須入力フィールドが入力されていません。';
 $string['remotedownloaderror'] = '<p>あなたのサーバへのコンポーネントのダウンロードに失敗しました。プロクシ設定を確認してください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
-<p><a href="{$a->url}">{$a->url}</a>ファイルを手動でダウンロードした後、あなたのサーバの「{$a->dest}」にコピーおよび解凍してください。</p>';
+<p><a href="{$a->url}">{$a->url}</a>ファイルを手動でダウンロードした後、あなたのサーバの「{$a->dest}」にコピーおよび展開してください。</p>';
 $string['wrongdestpath'] = '宛先パスが正しくありません。';
 $string['wrongsourcebase'] = 'ソースURLベースが正しくありません。';
 $string['wrongzipfilename'] = 'ZIPファイル名が正しくありません。';
index 45f6683..08e14b1 100644 (file)
@@ -73,7 +73,7 @@ $string['pathssubdataroot'] = '<p>ユーザによってアップロードされ
 <p>ウェブからは直接アクセスできないようにしてください。</p>
 <p>現在ディレクトリが存在しない場合、インストレーションプロセスは作成を試みます。</p';
 $string['pathssubdirroot'] = '<p>Moodleコードを含むディレクトリに関するフルパスです。</p>';
-$string['pathssubwwwroot'] = '<p>Moodleã\81«ã\82¢ã\82¯ã\82»ã\82¹ã\81\99ã\82\8bã\81\93ã\81¨ã\81®ã\81§ã\81\8dã\82\8bã\83\95ã\83«ã\82¦ã\82§ã\83\96ã\82¢ã\83\89ã\83¬ã\82¹ã\81§ã\81\99ã\80\82ä¾\8bã\81\88ã\81°ã\80\81ã\83¦ã\83¼ã\82¶ã\81\8cã\83\96ã\83©ã\82¦ã\82¶ã\81®ã\82¢ã\83\89ã\83¬ã\82¹ã\83\90ã\83¼ã\81«å\85¥å\8a\9bã\81\97ã\81¦Moodleã\81«ã\82¢ã\82¯ã\82»ã\82¹ã\81\99ã\82\8bã\81\9fã\82\81ã\81®ã\82¢ã\83\89ã\83¬ã\82¹ã\81§ã\81\99ã\80\82</p>
+$string['pathssubwwwroot'] = '<p>Moodleにアクセスすることのできるフルウェブアドレスです。例えばユーザがブラウザのアドレスバーに入力してMoodleにアクセスするためのアドレスです。</p>
 
 <p>複数アドレスを使用してMoodleにアクセスすることはできません。あなたのサイトに複数アドレスからアクセスできる場合、最も簡単なアドレスを選択して、すべてのアドレスにパーマネントリダイレクトを設定してください。</p>
 
index 214b82c..c518055 100644 (file)
@@ -42,3 +42,4 @@ Lütfen --help seçeneğini kullanın.';
 $string['cliyesnoprompt'] = 'e (evet) veya h (hayır) yazın';
 $string['environmentrequireinstall'] = 'yüklenmiş ve etkinleştirilmiş olmalıdır';
 $string['environmentrequireversion'] = 'sürüm {$a->needed} gerekli ve şu anda {$a->current} çalışıyor';
+$string['upgradekeyset'] = 'Yükseltme tuşu (ayarlanmak istenmiyorsa boş bırakın)';
index ca87cd7..9bc70e3 100644 (file)
@@ -27,7 +27,7 @@ $string['accounts'] = 'Accounts';
 $string['addcategory'] = 'Add a category';
 $string['additionalhtml'] = 'Additional HTML';
 $string['additionalhtml_heading'] = 'Additional HTML to be added to every page.';
-$string['additionalhtml_desc'] = 'These settings allow you to specify HTML that you want added to every page. You can set HTML that will be added within the HEAD tag for the page, immediately after the BODY tag has been opened, or immediately before the body tag is closed.<br />Doing this allows you add custom headers or footers on every page, or add support for services like Google Analytics very easily and independent of your chosen theme.';
+$string['additionalhtml_desc'] = 'These settings allow you to specify HTML that you want added to every page. You can set HTML that will be added within the HEAD tag for the page, immediately after the BODY tag has been opened, or immediately before the body tag is closed.<br />Doing this allows you to add custom headers or footers on every page, or add support for services like Google Analytics very easily, independent of your chosen theme.';
 $string['additionalhtmlhead'] = 'Within HEAD';
 $string['additionalhtmlhead_desc'] = 'Content here will be added to the bottom of the HEAD tag for every page.';
 $string['additionalhtmltopofbody'] = 'When BODY is opened';
@@ -393,7 +393,7 @@ $string['cronwarning'] = 'The <a href="{$a}">cron.php maintenance script</a> has
 $string['cronwarningcli'] = 'The cli/cron.php maintenance script has not been run for at least 24 hours.';
 $string['ctyperequired'] = 'The ctype PHP extension is now required by Moodle, in order to improve site performance and to offer multilingual compatibility.';
 $string['curlsecurityallowedport'] = 'cURL allowed ports list';
-$string['curlsecurityallowedportsyntax'] = 'Put every entry on one line. Valid entries are integer numbers only.';
+$string['curlsecurityallowedportsyntax'] = 'List of port numbers that cURL can connect to. Valid entries are integer numbers only. Put each entry on a new line. If left empty, then all ports are allowed. If set, in almost all cases, both 443 and 80 should be specified for cURL to connect to standard HTTPS and HTTP ports.';
 $string['curlsecurityblockedhosts'] = 'cURL blocked hosts list';
 $string['curlsecurityblockedhostssyntax'] = 'Put each entry on a new line. Valid entries are either full IPv4 or IPv6 addresses (such as 192.168.10.1, 0:0:0:0:0:0:0:1, ::1, fe80::) which match a single host; or CIDR notation (such as 231.54.211.0/20 or fe80::/64); or a range of IP addresses (such as 231.3.56.10-20 or fe80::1111-bbbb) where the range applies to the last group of the address; or domain names (such as localhost or example.com); or wildcard domain names (such as *.example.com or *.sub.example.com). Blank lines are not allowed.';
 $string['curlsecurityurlblocked'] = 'The URL is blocked.';
@@ -969,6 +969,7 @@ $string['roleswithexceptions'] = '{$a->roles}, with {$a->exceptions}';
 $string['rssglobaldisabled'] = 'Disabled at server level';
 $string['save'] = 'Save';
 $string['savechanges'] = 'Save changes';
+$string['scssinvalid'] = 'SCSS code is not valid, fails with: {$a}';
 $string['search'] = 'Search';
 $string['searchalldeleted'] = 'All indexed contents have been deleted';
 $string['searchareaenabled'] = 'Search area enabled';
index 3f0ea77..bd2a605 100644 (file)
@@ -107,7 +107,7 @@ $string['groupmembersselected'] = 'Members of selected group';
 $string['groupmode'] = 'Group mode';
 $string['groupmode_help'] = 'This setting has 3 options:
 
-* No groups - There are no sub groups, everyone is part of one big community
+* No groups
 * Separate groups - Each group member can only see their own group, others are invisible
 * Visible groups - Each group member works in their own group, but can also see other groups
 
index b7cdece..d89a33c 100644 (file)
@@ -329,7 +329,7 @@ $string['coursehelpforce'] = 'Force the course group mode to every activity in t
 $string['coursehelpformat'] = 'The course main page will be displayed in this format.';
 $string['coursehelphiddensections'] = 'How the hidden sections in the course are displayed to students.';
 $string['coursehelpmaximumupload'] = 'Define the largest size of file that can be uploaded in this course, limited by the site-wide setting.';
-$string['coursehelpnewsitemsnumber'] = 'Number of recent items from the news forum appearing in the latest news block on the course page. If set to zero, the latest news block will not be displayed.';
+$string['coursehelpnewsitemsnumber'] = 'Number of recent announcements appearing in the latest announcements block on the course page. If set to zero, the announcements forum will not be created.';
 $string['coursehelpnumberweeks'] = 'Number of sections in the course (applies to certain course formats only).';
 $string['coursehelpshowgrades'] = 'Enable the display of the gradebook. It does not prevent grades from being displayed within the individual activities.';
 $string['coursehidden'] = 'This course is currently unavailable to students';
@@ -666,20 +666,14 @@ If you need help, please contact the site administrator,
 {$a->admin}';
 $string['emailpasswordchangeinfodisabled'] = 'Hi {$a->firstname},
 
-Someone (probably you) has requested a new password for your
-account on \'{$a->sitename}\'.
+Someone (probably you) has requested a new password for your account on \'{$a->sitename}\'.
 
-Unfortunately your account on this site is disabled and can not be reset,
-please contact the site administrator,
-{$a->admin}';
+Unfortunately your account on this site is disabled, so the password cannot be reset. Please contact the site administrator {$a->admin}.';
 $string['emailpasswordchangeinfofail'] = 'Hi {$a->firstname},
 
-Someone (probably you) has requested a new password for your
-account on \'{$a->sitename}\'.
+Someone (probably you) has requested a new password for your account on \'{$a->sitename}\'.
 
-Unfortunately passwords can not be reset on this site,
-please contact the site administrator,
-{$a->admin}';
+Unfortunately passwords cannot be reset on this site. Please contact the site administrator {$a->admin}.';
 $string['emailpasswordchangeinfosubject'] = '{$a}: Change password information';
 $string['emailpasswordsent'] = 'Thank you for confirming the change of password.
 An email containing your new password has been sent to your address at<br /><b>{$a->email}</b>.<br />
@@ -842,7 +836,7 @@ $string['frontpagedescription'] = 'Front page summary';
 $string['frontpagedescriptionhelp'] = 'This summary can be displayed on the front page using the course/site summary block.';
 $string['frontpageformat'] = 'Front page format';
 $string['frontpageformatloggedin'] = 'Front page format when logged in';
-$string['frontpagenews'] = 'News items';
+$string['frontpagenews'] = 'Announcements';
 $string['frontpagesettings'] = 'Front page settings';
 $string['fulllistofcourses'] = 'All courses';
 $string['fullname'] = 'Full name'; /* @deprecated - Use fullnamecourse or fullnameuser or some own context specific string. */
@@ -1267,9 +1261,13 @@ $string['newpicture_help'] = 'To add a new picture, browse and select an image (
 $string['newpictureusernotsetup'] = 'A profile picture can only be added once all required profile information has been saved.';
 $string['newsectionname'] = 'New name for section {$a}';
 $string['newsitem'] = 'news item';
-$string['newsitems'] = 'news items';
-$string['newsitemsnumber'] = 'News items to show';
-$string['newsitemsnumber_help'] = 'This setting determines how many recent items appear in the latest news block on the course page. If set to "0 news items" then the latest news block will not be displayed.';
+$string['newsitems'] = 'announcements';
+$string['newsitemsnumber'] = 'Number of announcements';
+$string['newsitemsnumber_help'] = 'The announcements forum is a special forum which is created automatically in the course, has forced subscription set by default, and only users with appropriate permissions (by default teachers) can post in it.
+
+This setting determines how many recent announcements appear in the latest announcements block.
+
+If an announcements forum is not required in the course, this setting should be set to zero.';
 $string['newuser'] = 'New user';
 $string['newusernewpasswordsubj'] = 'New user account';
 $string['newusernewpasswordtext'] = 'Hi {$a->firstname},
@@ -1732,7 +1730,7 @@ $string['sitehome'] = 'Site home';
 $string['sitelegacyfiles'] = 'Legacy site files';
 $string['sitelogs'] = 'Site logs';
 $string['sitemessage'] = 'Message users';
-$string['sitenews'] = 'Site news';
+$string['sitenews'] = 'Site announcements';
 $string['sitepages'] = 'Site pages';
 $string['sitepartlist'] = 'You do not have the required permissions to view the participants list';
 $string['sitepartlist0'] = 'You must be a site teacher to be allowed to see the site participants list';
index faa3b64..aca0b82 100644 (file)
@@ -10180,3 +10180,41 @@ class admin_setting_searchsetupinfo extends admin_setting {
     }
 
 }
+
+/**
+ * Used to validate the contents of SCSS code and ensuring they are parsable.
+ *
+ * It does not attempt to detect undefined SCSS variables because it is designed
+ * to be used without knowledge of other config/scss included.
+ *
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2016 Dan Poltawski <dan@moodle.com>
+ */
+class admin_setting_scsscode extends admin_setting_configtextarea {
+
+    /**
+     * Validate the contents of the SCSS to ensure its parsable. Does not
+     * attempt to detect undefined scss variables.
+     *
+     * @param string $data The scss code from text field.
+     * @return mixed bool true for success or string:error on failure.
+     */
+    public function validate($data) {
+        if (empty($data)) {
+            return true;
+        }
+
+        $scss = new core_scss();
+        try {
+            $scss->compile($data);
+        } catch (Leafo\ScssPhp\Exception\ParserException $e) {
+            return get_string('scssinvalid', 'admin', $e->getMessage());
+        } catch (Leafo\ScssPhp\Exception\CompilerException $e) {
+            // Silently ignore this - it could be a scss variable defined from somewhere
+            // else which we are not examining here.
+            return true;
+        }
+
+        return true;
+    }
+}
index b211865..09cf9cc 100644 (file)
@@ -117,6 +117,9 @@ class behat_util extends testing_util {
         // Set editor autosave to high value, so as to avoid unwanted ajax.
         set_config('autosavefrequency', '604800', 'editor_atto');
 
+        // Set noreplyaddress to an example domain, as it should be valid email address and test site can be a localhost.
+        set_config('noreplyaddress', 'noreply@example.com');
+
         // Keeps the current version of database and dataroot.
         self::store_versions_hash();
 
index baaa605..397fa76 100644 (file)
@@ -76,10 +76,16 @@ class mustache_template_finder {
 
         // First check the theme.
         $dirs[] = $CFG->dirroot . '/theme/' . $themename . '/templates/' . $component . '/';
+        if (isset($CFG->themedir)) {
+            $dirs[] = $CFG->themedir . '/' . $themename . '/templates/' . $component . '/';
+        }
         // Now check the parent themes.
         // Search each of the parent themes second.
         foreach ($parents as $parent) {
             $dirs[] = $CFG->dirroot . '/theme/' . $parent . '/templates/' . $component . '/';
+            if (isset($CFG->themedir)) {
+                $dirs[] = $CFG->themedir . '/' . $parent . '/templates/' . $component . '/';
+            }
         }
 
         $dirs[] = $compdirectory . '/templates/';
index 5a5e2ab..a8aa4a9 100644 (file)
@@ -98,4 +98,58 @@ class core_scss extends \Leafo\ScssPhp\Compiler {
         return $this->compile($content);
     }
 
+    /**
+     * Compile child; returns a value to halt execution
+     *
+     * @param array $child
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+     *
+     * @return array|null
+     */
+    protected function compileChild($child, \Leafo\ScssPhp\Formatter\OutputBlock $out) {
+        switch($child[0]) {
+            case \Leafo\ScssPhp\Type::T_SCSSPHP_IMPORT_ONCE:
+            case \Leafo\ScssPhp\Type::T_IMPORT:
+                list(, $rawpath) = $child;
+                $rawpath = $this->reduce($rawpath);
+                $path = $this->compileStringContent($rawpath);
+                if ($path = $this->findImport($path)) {
+                    if ($this->is_valid_file($path)) {
+                        return parent::compileChild($child, $out);
+                    } else {
+                        // Sneaky stuff, don't let non scss file in.
+                        debugging("Can't import scss file - " . $path, DEBUG_DEVELOPER);
+                    }
+                }
+                break;
+            default:
+                return parent::compileChild($child, $out);
+        }
+    }
+
+    /**
+     * Is the given file valid for import ?
+     *
+     * @param $path
+     * @return bool
+     */
+    protected function is_valid_file($path) {
+        global $CFG;
+
+        $realpath = realpath($path);
+
+        // Additional theme directory.
+        $addthemedirectory = core_component::get_plugin_types()['theme'];
+        $addrealroot = realpath($addthemedirectory);
+
+        // Original theme directory.
+        $themedirectory = $CFG->dirroot . "/theme";
+        $realroot = realpath($themedirectory);
+
+        // File should end in .scss and must be in sites theme directory, else ignore it.
+        $pathvalid = $realpath !== false;
+        $pathvalid = $pathvalid && (substr($path, -5) === '.scss');
+        $pathvalid = $pathvalid && (strpos($realpath, $realroot) === 0 || strpos($realpath, $addrealroot) === 0);
+        return $pathvalid;
+    }
 }
index 10242db..8154cf2 100644 (file)
@@ -69,6 +69,7 @@ $capabilities = array(
         'contextlevel' => CONTEXT_SYSTEM,
         'archetypes' => array(
             'manager' => CAP_ALLOW,
+            'coursecreator' => CAP_ALLOW,
         )
     ),
 
index d5f4cbc..9dbead8 100644 (file)
@@ -344,6 +344,15 @@ $functions = array(
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_course_get_updates_since' => array(
+        'classname' => 'core_course_external',
+        'methodname' => 'get_updates_since',
+        'classpath' => 'course/externallib.php',
+        'description' => 'Check if there are updates affecting the user for the given course since the given time stamp.',
+        'type' => 'read',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_enrol_get_course_enrolment_methods' => array(
         'classname' => 'core_enrol_external',
         'methodname' => 'get_course_enrolment_methods',
index da20823..20e304f 100644 (file)
@@ -2436,5 +2436,34 @@ function xmldb_main_upgrade($oldversion) {
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2016122800.00) {
+        // Find all roles with the coursecreator archetype.
+        $coursecreatorroleids = $DB->get_records('role', array('archetype' => 'coursecreator'), '', 'id');
+
+        $context = context_system::instance();
+        $capability = 'moodle/site:configview';
+
+        foreach ($coursecreatorroleids as $roleid => $notused) {
+
+            // Check that the capability has not already been assigned. If it has then it's either already set
+            // to allow or specifically set to prohibit or prevent.
+            if (!$DB->record_exists('role_capabilities', array('roleid' => $roleid, 'capability' => $capability))) {
+                // Assign the capability.
+                $cap = new stdClass();
+                $cap->contextid    = $context->id;
+                $cap->roleid       = $roleid;
+                $cap->capability   = $capability;
+                $cap->permission   = CAP_ALLOW;
+                $cap->timemodified = time();
+                $cap->modifierid   = 0;
+
+                $DB->insert_record('role_capabilities', $cap);
+            }
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2016122800.00);
+    }
+
     return true;
 }
index bf37ffd..078c237 100644 (file)
@@ -136,6 +136,11 @@ abstract class moodle_database {
      */
     private $inorequaluniqueindex = 1;
 
+    /**
+     * @var boolean variable use to temporarily disable logging.
+     */
+    protected $skiplogging = false;
+
     /**
      * Constructor - Instantiates the database, specifying if it's external (connect to other systems) or not (Moodle DB).
      *              Note that this affects the decision of whether prefix checks must be performed or not.
@@ -487,6 +492,11 @@ abstract class moodle_database {
      * @return void
      */
     public function query_log($error=false) {
+        // Logging disabled by the driver.
+        if ($this->skiplogging) {
+            return;
+        }
+
         $logall    = !empty($this->dboptions['logall']);
         $logslow   = !empty($this->dboptions['logslow']) ? $this->dboptions['logslow'] : false;
         $logerrors = !empty($this->dboptions['logerrors']);
@@ -525,6 +535,20 @@ abstract class moodle_database {
         }
     }
 
+    /**
+     * Disable logging temporarily.
+     */
+    protected function query_log_prevent() {
+        $this->skiplogging = true;
+    }
+
+    /**
+     * Restore old logging behavior.
+     */
+    protected function query_log_allow() {
+        $this->skiplogging = false;
+    }
+
     /**
      * Returns the time elapsed since the query started.
      * @return float Seconds with microseconds
index 7215308..cf55544 100644 (file)
@@ -183,6 +183,9 @@ class mssql_native_moodle_database extends moodle_database {
             throw new dml_connection_exception($dberr);
         }
 
+        // Disable logging until we are fully setup.
+        $this->query_log_prevent();
+
         // already connected, select database and set some env. variables
         $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX);
         $result = mssql_select_db($this->dbname, $this->mssql);
@@ -238,6 +241,9 @@ class mssql_native_moodle_database extends moodle_database {
         // Fetch/offset is supported staring from SQL Server 2012.
         $this->supportsoffsetfetch = $serverinfo['version'] > '11';
 
+        // We can enable logging now.
+        $this->query_log_allow();
+
         // Connection stabilised and configured, going to instantiate the temptables controller
         $this->temptables = new mssql_native_moodle_temptables($this);
 
index 6048cdd..ea7676a 100644 (file)
@@ -449,6 +449,9 @@ class mysqli_native_moodle_database extends moodle_database {
             throw new dml_connection_exception($dberr);
         }
 
+        // Disable logging until we are fully setup.
+        $this->query_log_prevent();
+
         $this->query_start("--set_charset()", null, SQL_QUERY_AUX);
         $this->mysqli->set_charset('utf8');
         $this->query_end(true);
@@ -466,6 +469,9 @@ class mysqli_native_moodle_database extends moodle_database {
             $this->query_end($result);
         }
 
+        // We can enable logging now.
+        $this->query_log_allow();
+
         // Connection stabilised and configured, going to instantiate the temptables controller
         $this->temptables = new mysqli_native_moodle_temptables($this);
 
index 343663f..6f91610 100644 (file)
@@ -189,6 +189,9 @@ class oci_native_moodle_database extends moodle_database {
             throw new dml_connection_exception($dberr);
         }
 
+        // Disable logging until we are fully setup.
+        $this->query_log_prevent();
+
         // Make sure moodle package is installed - now required.
         if (!$this->oci_package_installed()) {
             try {
@@ -216,6 +219,9 @@ class oci_native_moodle_database extends moodle_database {
         //note: do not send "ALTER SESSION SET NLS_NUMERIC_CHARACTERS='.,'" !
         //      instead fix our PHP code to convert "," to "." properly!
 
+        // We can enable logging now.
+        $this->query_log_allow();
+
         // Connection stabilised and configured, going to instantiate the temptables controller
         $this->temptables = new oci_native_moodle_temptables($this, $this->unique_session_id);
 
index 06e6d77..623d628 100644 (file)
@@ -200,6 +200,9 @@ class sqlsrv_native_moodle_database extends moodle_database {
             throw new dml_connection_exception($dberr);
         }
 
+        // Disable logging until we are fully setup.
+        $this->query_log_prevent();
+
         // Allow quoted identifiers
         $sql = "SET QUOTED_IDENTIFIER ON";
         $this->query_start($sql, null, SQL_QUERY_AUX);
@@ -246,6 +249,9 @@ class sqlsrv_native_moodle_database extends moodle_database {
         // Fetch/offset is supported staring from SQL Server 2012.
         $this->supportsoffsetfetch = $serverinfo['version'] > '11';
 
+        // We can enable logging now.
+        $this->query_log_allow();
+
         // Connection established and configured, going to instantiate the temptables controller
         $this->temptables = new sqlsrv_native_moodle_temptables($this);
 
index 6c57e13..1d2682a 100644 (file)
@@ -60,7 +60,7 @@ $string['languagesinstalled'] = 'Languages installed';
 $string['link'] = 'Link';
 $string['loop'] = 'Loop';
 $string['metadata'] = 'Metadata';
-$string['metadata_help'] = 'Metadata tracks, for use from a script, may be used only if the player supports metadata';
+$string['metadata_help'] = 'Metadata tracks, for use from a script, may be used only if the player supports metadata.';
 $string['metadatasourcelabel'] = 'Metadata track URL';
 $string['mute'] = 'Muted';
 $string['pluginname'] = 'Media';
index 9b1e02c..b568436 100644 (file)
@@ -3056,8 +3056,9 @@ class curl {
                 $this->responsefinished = false;
                 $this->response = array();
             }
-            list($key, $value) = explode(" ", rtrim($header, "\r\n"), 2);
-            $key = rtrim($key, ':');
+            $parts = explode(" ", rtrim($header, "\r\n"), 2);
+            $key = rtrim($parts[0], ':');
+            $value = isset($parts[1]) ? $parts[1] : null;
             if (!empty($this->response[$key])) {
                 if (is_array($this->response[$key])) {
                     $this->response[$key][] = $value;
index de7dfca..2c8a3ea 100644 (file)
@@ -1240,31 +1240,15 @@ abstract class moodleform {
      *                      $enhancement = 'smartselect';
      *                      $options = array('selectablecategories' => true|false)
      *
-     * @since Moodle 2.0
      * @param string|element $element form element for which Javascript needs to be initalized
      * @param string $enhancement which init function should be called
      * @param array $options options passed to javascript
      * @param array $strings strings for javascript
+     * @deprecated since Moodle 3.3 MDL-57471
      */
     function init_javascript_enhancement($element, $enhancement, array $options=array(), array $strings=null) {
-        global $PAGE;
-        if (is_string($element)) {
-            $element = $this->_form->getElement($element);
-        }
-        if (is_object($element)) {
-            $element->_generateId();
-            $elementid = $element->getAttribute('id');
-            $PAGE->requires->js_init_call('M.form.init_'.$enhancement, array($elementid, $options));
-            if (is_array($strings)) {
-                foreach ($strings as $string) {
-                    if (is_array($string)) {
-                        call_user_func_array(array($PAGE->requires, 'string_for_js'), $string);
-                    } else {
-                        $PAGE->requires->string_for_js($string, 'moodle');
-                    }
-                }
-            }
-        }
+        debugging('$mform->init_javascript_enhancement() is deprecated and no longer does anything. '.
+            'smartselect uses should be converted to the searchableselector form element.', DEBUG_DEVELOPER);
     }
 
     /**
diff --git a/lib/htaccess b/lib/htaccess
deleted file mode 100644 (file)
index 690c352..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-# On some PHP servers it may help if this file is copied
-# to the main moodle directory and renamed .htaccess
-#
-# As soon as you do this, check your web site.  Is it
-# still working OK?  If you are getting a "configuration
-# error" then you may need to enable overrides by editing
-# the main httpd.conf for Apache and in the main server
-# or virtual server area, adding something like:
-#
-# <Directory /web/moodle>
-#     AllowOverride All
-# </Directory>
-#
-
-### Firstly, if you are using Apache 2, you need the following
-### three lines to allow Apache to pass a PATH_INFO variable
-### correctly for URLs like http://server/file.php/arg1/arg2
-
-<IfDefine APACHE2>
-    AcceptPathInfo on
-</IfDefine>
-
-### Secondly, you can define the default files in the Moodle
-### directories as follows:
-
-DirectoryIndex index.php index.html index.htm
-
-### Thirdly, set up some PHP variables that Moodle needs
-
-php_flag file_uploads            1
-php_flag short_open_tag          1
-php_flag session.auto_start      0
-php_flag session.bug_compat_warn 0
-
-### Fourthly, sometimes Apache limits the size of uploaded files
-### (this is a separate limit to the one in PHP, see below).
-### The setting here turns off this limitation
-
-LimitRequestBody 0
-
-
-### These are optional - you may not want to override php.ini
-### To enable them, remove the leading hash (#)
-
-#php_value upload_max_filesize 2M
-#php_value post_max_size 2M
-#php_value session.gc_maxlifetime 7200
-
-
-### You can change the following line to point to the
-### error/index.php file in your Moodle distribution.
-### It provides a form which emails you (the admin)
-### about 404 errors (URL not found).
-
-#ErrorDocument 404 http://example.org/moodle/error/index.php
-
-
-### People have reported that these can help in some cases
-### (unusual) when you see errors about undefined functions
-
-#php_value auto_prepend_file none
-#php_value include_path .
-
-
index 4320ae2..9c7de46 100644 (file)
@@ -1063,43 +1063,18 @@ function filterByParent(elCollection, parentFinder) {
     return filteredCollection;
 }
 
-/*
-    All this is here just so that IE gets to handle oversized blocks
-    in a visually pleasing manner. It does a browser detect. So sue me.
-*/
-
+/**
+ * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
+ */
 function fix_column_widths() {
-    var agt = navigator.userAgent.toLowerCase();
-    if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
-        fix_column_width('left-column');
-        fix_column_width('right-column');
-    }
+    Y.log('fix_column_widths() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
 }
 
+/**
+ * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
+ */
 function fix_column_width(colName) {
-    if(column = document.getElementById(colName)) {
-        if(!column.offsetWidth) {
-            setTimeout("fix_column_width('" + colName + "')", 20);
-            return;
-        }
-
-        var width = 0;
-        var nodes = column.childNodes;
-
-        for(i = 0; i < nodes.length; ++i) {
-            if(nodes[i].className.indexOf("block") != -1 ) {
-                if(width < nodes[i].offsetWidth) {
-                    width = nodes[i].offsetWidth;
-                }
-            }
-        }
-
-        for(i = 0; i < nodes.length; ++i) {
-            if(nodes[i].className.indexOf("block") != -1 ) {
-                nodes[i].style.width = width + 'px';
-            }
-        }
-    }
+    Y.log('fix_column_width() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
 }
 
 
@@ -1462,209 +1437,11 @@ M.form = M.form || {};
 
 /**
  * Converts a nbsp indented select box into a multi drop down custom control much
- * like the custom menu. It also selectable categories on or off.
- *
- * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
- *
- * @param {YUI} Y
- * @param {string} id
- * @param {Array} options
+ * like the custom menu. Can no longer be used.
+ * @deprecated since Moodle 3.3
  */
-M.form.init_smartselect = function(Y, id, options) {
-    if (!id.match(/^id_/)) {
-        id = 'id_'+id;
-    }
-    var select = Y.one('select#'+id);
-    if (!select) {
-        return false;
-    }
-    Y.use('event-delegate',function(){
-        var smartselect = {
-            id : id,
-            structure : [],
-            options : [],
-            submenucount : 0,
-            currentvalue : null,
-            currenttext : null,
-            shownevent : null,
-            cfg : {
-                selectablecategories : true,
-                mode : null
-            },
-            nodes : {
-                select : null,
-                loading : null,
-                menu : null
-            },
-            init : function(Y, id, args, nodes) {
-                if (typeof(args)=='object') {
-                    for (var i in this.cfg) {
-                        if (args[i] || args[i]===false) {
-                            this.cfg[i] = args[i];
-                        }
-                    }
-                }
-
-                // Display a loading message first up
-                this.nodes.select = nodes.select;
-
-                this.currentvalue = this.nodes.select.get('selectedIndex');
-                this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
-
-                var options = Array();
-                options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
-                this.nodes.select.all('option').each(function(option, index) {
-                    var rawtext = option.get('innerHTML');
-                    var text = rawtext.replace(/^(&nbsp;)*/, '');
-                    if (rawtext === text) {
-                        text = rawtext.replace(/^(\s)*/, '');
-                        var depth = (rawtext.length - text.length ) + 1;
-                    } else {
-                        var depth = ((rawtext.length - text.length )/12)+1;
-                    }
-                    option.set('innerHTML', text);
-                    options['i'+index] = {text:text,depth:depth,index:index,children:[]};
-                }, this);
-
-                this.structure = [];
-                var structcount = 0;
-                for (var i in options) {
-                    var o = options[i];
-                    if (o.depth == 0) {
-                        this.structure.push(o);
-                        structcount++;
-                    } else {
-                        var d = o.depth;
-                        var current = this.structure[structcount-1];
-                        for (var j = 0; j < o.depth-1;j++) {
-                            if (current && current.children) {
-                                current = current.children[current.children.length-1];
-                            }
-                        }
-                        if (current && current.children) {
-                            current.children.push(o);
-                        }
-                    }
-                }
-
-                this.nodes.menu = Y.Node.create(this.generate_menu_content());
-                this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
-                this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
-                this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
-
-                if (this.cfg.mode == null) {
-                    var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
-                    if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
-                        this.cfg.mode = 'compact';
-                    } else {
-                        this.cfg.mode = 'spanning';
-                    }
-                }
-
-                if (this.cfg.mode == 'compact') {
-                    this.nodes.menu.addClass('compactmenu');
-                } else {
-                    this.nodes.menu.addClass('spanningmenu');
-                    this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
-                }
-
-                Y.one(document.body).append(this.nodes.menu);
-                var pos = this.nodes.select.getXY();
-                pos[0] += 1;
-                this.nodes.menu.setXY(pos);
-                this.nodes.menu.on('click', this.handle_click, this);
-
-                Y.one(window).on('resize', function(){
-                     var pos = this.nodes.select.getXY();
-                    pos[0] += 1;
-                    this.nodes.menu.setXY(pos);
-                 }, this);
-            },
-            generate_menu_content : function() {
-                var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
-                content += this.generate_submenu_content(this.structure[0], true);
-                content += '</ul></div>';
-                return content;
-            },
-            generate_submenu_content : function(item, rootelement) {
-                this.submenucount++;
-                var content = '';
-                if (item.children.length > 0) {
-                    if (rootelement) {
-                        content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
-                        content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
-                        content += '<div class="smartselect_menu_content">';
-                    } else {
-                        content += '<li class="smartselect_submenuitem">';
-                        var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
-                        content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
-                        content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
-                        content += '<div class="smartselect_submenu_content">';
-                    }
-                    content += '<ul>';
-                    for (var i in item.children) {
-                        content += this.generate_submenu_content(item.children[i],false);
-                    }
-                    content += '</ul>';
-                    content += '</div>';
-                    content += '</div>';
-                    if (rootelement) {
-                    } else {
-                        content += '</li>';
-                    }
-                } else {
-                    content += '<li class="smartselect_menuitem">';
-                    content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
-                    content += '</li>';
-                }
-                return content;
-            },
-            select : function(e) {
-                var t = e.target;
-                e.halt();
-                this.currenttext = t.get('innerHTML');
-                this.currentvalue = t.getAttribute('value');
-                this.nodes.select.set('selectedIndex', this.currentvalue);
-                this.hide_menu();
-            },
-            handle_click : function(e) {
-                var target = e.target;
-                if (target.hasClass('smartselect_mask')) {
-                    this.show_menu(e);
-                } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
-                    this.select(e);
-                } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
-                    this.show_sub_menu(e);
-                }
-            },
-            show_menu : function(e) {
-                e.halt();
-                var menu = e.target.ancestor().one('.smartselect_menu');
-                menu.addClass('visible');
-                this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
-            },
-            show_sub_menu : function(e) {
-                e.halt();
-                var target = e.target;
-                if (!target.hasClass('smartselect_submenuitem')) {
-                    target = target.ancestor('.smartselect_submenuitem');
-                }
-                if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
-                    target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
-                    return;
-                }
-                target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
-                target.one('.smartselect_submenu').addClass('visible');
-            },
-            hide_menu : function() {
-                this.nodes.menu.all('.visible').removeClass('visible');
-                if (this.shownevent) {
-                    this.shownevent.detach();
-                }
-            }
-        };
-        smartselect.init(Y, id, options, {select:select});
-    });
+M.form.init_smartselect = function() {
+    throw new Error('M.form.init_smartselect can not be used any more.');
 };
 
 /**
index f560d07..2eefa09 100644 (file)
@@ -5180,6 +5180,7 @@ function reset_course_userdata($data) {
     global $CFG, $DB;
     require_once($CFG->libdir.'/gradelib.php');
     require_once($CFG->libdir.'/completionlib.php');
+    require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
     require_once($CFG->dirroot.'/group/lib.php');
 
     $data->courseid = $data->id;
@@ -5224,6 +5225,27 @@ function reset_course_userdata($data) {
             \availability_date\condition::update_all_dates($data->courseid, $data->timeshift);
         }
 
+        // Update completion expected dates.
+        if ($CFG->enablecompletion) {
+            $modinfo = get_fast_modinfo($data->courseid);
+            $changed = false;
+            foreach ($modinfo->get_cms() as $cm) {
+                if ($cm->completion && !empty($cm->completionexpected)) {
+                    $DB->set_field('course_modules', 'completionexpected', $cm->completionexpected + $data->timeshift,
+                        array('id' => $cm->id));
+                    $changed = true;
+                }
+            }
+
+            // Clear course cache if changes made.
+            if ($changed) {
+                rebuild_course_cache($data->courseid, true);
+            }
+
+            // Update course date completion criteria.
+            \completion_criteria_date::update_date($data->courseid, $data->timeshift);
+        }
+
         $status[] = array('component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false);
     }
 
@@ -5766,7 +5788,13 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
     $tempreplyto = array();
 
     // Make sure that we fall back onto some reasonable no-reply address.
-    $noreplyaddress = empty($CFG->noreplyaddress) ? 'noreply@' . get_host_from_url($CFG->wwwroot) : $CFG->noreplyaddress;
+    $noreplyaddressdefault = 'noreply@' . get_host_from_url($CFG->wwwroot);
+    $noreplyaddress = empty($CFG->noreplyaddress) ? $noreplyaddressdefault : $CFG->noreplyaddress;
+
+    if (!validate_email($noreplyaddress)) {
+        debugging('email_to_user: Invalid noreply-email '.s($noreplyaddress));
+        $noreplyaddress = $noreplyaddressdefault;
+    }
 
     // Make up an email address for handling bounces.
     if (!empty($CFG->handlebounces)) {
@@ -5776,6 +5804,12 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
         $mail->Sender = $noreplyaddress;
     }
 
+    // Make sure that the explicit replyto is valid, fall back to the implicit one.
+    if (!empty($replyto) && !validate_email($replyto)) {
+        debugging('email_to_user: Invalid replyto-email '.s($replyto));
+        $replyto = $noreplyaddress;
+    }
+
     $alloweddomains = null;
     if (!empty($CFG->allowedemaildomains)) {
         $alloweddomains = explode(PHP_EOL, $CFG->allowedemaildomains);
@@ -5793,6 +5827,11 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '',
     // and that the senders email setting is either displayed to everyone, or display to only other users that are enrolled
     // in a course with the sender.
     } else if ($usetrueaddress && can_send_from_real_email_address($from, $user, $alloweddomains)) {
+        if (!validate_email($from->email)) {
+            debugging('email_to_user: Invalid from-email '.s($from->email).' - not sending');
+            // Better not to use $noreplyaddress in this case.
+            return false;
+        }
         $mail->From = $from->email;
         $fromdetails = new stdClass();
         $fromdetails->name = fullname($from);
index 1af75d2..e5955b4 100644 (file)
@@ -2104,7 +2104,7 @@ class global_navigation extends navigation_node {
                 $onclick = htmlspecialchars_decode($activity->onclick, ENT_QUOTES);
                 // Build the JS function the click event will call
                 $jscode = "function {$functionname}(e) { $propogrationhandler $onclick }";
-                $this->page->requires->js_init_code($jscode);
+                $this->page->requires->js_amd_inline($jscode);
                 // Override the default url with the new action link
                 $action = new action_link($action, $activityname, new component_action('click', $functionname));
             }
@@ -3748,7 +3748,14 @@ class flat_navigation extends navigation_node_collection {
         if ($course->id > 1) {
             // It's a real course.
             $url = new moodle_url('/course/view.php', array('id' => $course->id));
-            $flat = new flat_navigation_node(navigation_node::create($course->shortname, $url), 0);
+
+            $coursecontext = context_course::instance($course->id, MUST_EXIST);
+            // This is the name that will be shown for the course.
+            $coursename = empty($CFG->navshowfullcoursenames) ?
+                format_string($course->shortname, true, array('context' => $coursecontext)) :
+                format_string($course->fullname, true, array('context' => $coursecontext));
+
+            $flat = new flat_navigation_node(navigation_node::create($coursename, $url), 0);
             $flat->key = 'coursehome';
 
             $courseformat = course_get_format($course);
@@ -3913,34 +3920,38 @@ class settings_navigation extends navigation_node {
         if (isloggedin() && !isguestuser() && (!isset($SESSION->load_navigation_admin) || $SESSION->load_navigation_admin)) {
             $isadminpage = $this->is_admin_tree_needed();
 
-            if (has_capability('moodle/site:config', context_system::instance())) {
-                // Make sure this works even if config capability changes on the fly
-                // and also make it fast for admin right after login.
-                $SESSION->load_navigation_admin = 1;
-                if ($isadminpage) {
-                    $adminsettings = $this->load_administration_settings();
-                }
-
-            } else if (!isset($SESSION->load_navigation_admin)) {
-                $adminsettings = $this->load_administration_settings();
-                $SESSION->load_navigation_admin = (int)($adminsettings->children->count() > 0);
+            if (has_capability('moodle/site:configview', context_system::instance())) {
+                if (has_capability('moodle/site:config', context_system::instance())) {
+                    // Make sure this works even if config capability changes on the fly
+                    // and also make it fast for admin right after login.
+                    $SESSION->load_navigation_admin = 1;
+                    if ($isadminpage) {
+                        $adminsettings = $this->load_administration_settings();
+                    }
 
-            } else if ($SESSION->load_navigation_admin) {
-                if ($isadminpage) {
+                } else if (!isset($SESSION->load_navigation_admin)) {
                     $adminsettings = $this->load_administration_settings();
+                    $SESSION->load_navigation_admin = (int)($adminsettings->children->count() > 0);
+
+                } else if ($SESSION->load_navigation_admin) {
+                    if ($isadminpage) {
+                        $adminsettings = $this->load_administration_settings();
+                    }
                 }
-            }
 
-            // Print empty navigation node, if needed.
-            if ($SESSION->load_navigation_admin && !$isadminpage) {
-                if ($adminsettings) {
-                    // Do not print settings tree on pages that do not need it, this helps with performance.
-                    $adminsettings->remove();
-                    $adminsettings = false;
+                // Print empty navigation node, if needed.
+                if ($SESSION->load_navigation_admin && !$isadminpage) {
+                    if ($adminsettings) {
+                        // Do not print settings tree on pages that do not need it, this helps with performance.
+                        $adminsettings->remove();
+                        $adminsettings = false;
+                    }
+                    $siteadminnode = $this->add(get_string('administrationsite'), new moodle_url('/admin/search.php'),
+                            self::TYPE_SITE_ADMIN, null, 'siteadministration');
+                    $siteadminnode->id = 'expandable_branch_' . $siteadminnode->type . '_' .
+                            clean_param($siteadminnode->key, PARAM_ALPHANUMEXT);
+                    $siteadminnode->requiresajaxloading = 'true';
                 }
-                $siteadminnode = $this->add(get_string('administrationsite'), new moodle_url('/admin/search.php'), self::TYPE_SITE_ADMIN, null, 'siteadministration');
-                $siteadminnode->id = 'expandable_branch_'.$siteadminnode->type.'_'.clean_param($siteadminnode->key, PARAM_ALPHANUMEXT);
-                $siteadminnode->requiresajaxloading = 'true';
             }
         }
 
index 6c81aa7..21f40ff 100644 (file)
@@ -576,8 +576,6 @@ class core_renderer extends renderer_base {
             'loadinghelp',
         ), 'moodle');
 
-        $this->page->requires->js_function_call('setTimeout', array('fix_column_widths()', 20));
-
         $focus = $this->page->focuscontrol;
         if (!empty($focus)) {
             if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
index a25b992..63abdeb 100644 (file)
@@ -74,10 +74,10 @@ class moodle_phpmailer extends PHPMailer {
      */
     public function addCustomHeader($custom_header, $value = null) {
         if ($value === null and preg_match('/message-id:(.*)/i', $custom_header, $matches)) {
-            $this->MessageID = $matches[1];
+            $this->MessageID = trim($matches[1]);
             return true;
         } else if ($value !== null and strcasecmp($custom_header, 'message-id') === 0) {
-            $this->MessageID = $value;
+            $this->MessageID = trim($value);
             return true;
         } else {
             return parent::addCustomHeader($custom_header, $value);
index 6445e77..a6dc243 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/action_menu
+
     Action menu.
+
+    Example context (json):
+    {
+        "classes": "",
+        "primary": {
+            "items": [{"rawhtml": "<p>Item in primary menu</p>"}]
+        },
+        "secondary": {
+            "items": [{"rawhtml": "<p>Item in secondary menu</p>"}]
+        }
+    }
 }}
 <div class="{{classes}}" {{#attributes}}{{name}}="{{value}}"{{/attributes}}>
     {{#primary}}
@@ -37,8 +50,6 @@
             {{#items}}<li role="presentation">{{> core/action_menu_item }}</li>{{/items}}
         </ul>
     {{/secondary}}
-
-    </span>
 </div>
 {{#js}}
 require(['core/yui'], function(Y) {
index d00323a..bbbf864 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/action_menu_item
+
     Action menu item.
+
+    Example context (json):
+    {
+        "rawhtml": "<p>[rawhtml]</p>"
+    }
 }}
 {{#actionmenulink}}{{> core/action_menu_link }}{{/actionmenulink}}
 {{#actionmenufiller}}<span class="filler">&nbsp;</span>{{/actionmenufiller}}
index 3c41ad8..054df8b 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/action_menu_link
+
     Action menu link.
+
+    Example context (json):
+    {
+        "text": "Example link text",
+        "showtext": true,
+        "url": "http://example.com/link",
+        "classes": "menu-action",
+        "instance": "1"
+    }
 }}
 {{^disabled}}
     <a href="{{url}}" class="{{classes}}" {{#attributes}}{{name}}={{#quote}}{{value}}{{/quote}}{{/attributes}} {{#showtext}}aria-labelledby="actionmenuaction-{{instance}}"{{/showtext}}>{{#icon}}{{>core/pix_icon}}{{/icon}}{{#showtext}}<span class="menu-action-text" id="actionmenuaction-{{instance}}">{{{text}}}</span>{{/showtext}}</a>
index 977d382..2068373 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/action_menu_trigger
+
     Action menu trigger.
+
+    Example context (json):
+    {
+        "instance": "1",
+        "title": "Trigger me!",
+        "menutrigger": true
+    }
 }}
 <a href="#" class="toggle-display {{#menutrigger}}textmenu{{/menutrigger}}" id="action-menu-toggle-{{instance}}" title="{{title}}" role="menuitem">{{{actiontext}}}{{{menutrigger}}}{{#icon}}{{> core/pix_icon}}{{/icon}}{{#rawicon}}{{{.}}}{{/rawicon}}{{#menutrigger}}<b class="caret"></b>{{/menutrigger}}</a>
\ No newline at end of file
index 3d76246..40b2b69 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/actions
+
     Actions.
+
+    Example context (json):
+    {
+        "actions": [{"event": "event", "jsfunction": "Y.log", "id": "id"}]
+    }
 }}
 {{#js}}
     require(['core/yui'], function(Y) {
index 3486735..fdb9a94 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/chart
+
     Chart rendering.
+
+    Example context (json):
+    {
+        "withtable": true,
+        "chartdata": "null"
+    }
 }}
 <div class="chart-area" id="chart-area-{{uniqid}}">
-    <div class="chart-image" role="decoration" aria-describedby="chart-table-data-{{uniqid}}"></div>
+    <div class="chart-image" role="presentation" aria-describedby="chart-table-data-{{uniqid}}"></div>
     <div class="chart-table {{^withtable}}accesshide{{/withtable}}">
         <p class="chart-table-expand">
             <a href="#" aria-controls="chart-table-data-{{uniqid}}" role="button">
                 {{#str}}showchartdata, moodle{{/str}}
             </a>
         </p>
-        <div class="chart-table-data" id="chart-table-data-{{uniqid}}" {{#withtable}}aria-expanded="false"{{/withtable}}></div>
+        <div class="chart-table-data" id="chart-table-data-{{uniqid}}" {{#withtable}}role="complementary" aria-expanded="false"{{/withtable}}></div>
     </div>
 </div>
 
index eea553c..166cf5d 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/chooser
+
     Chooser.
+
+    Example context (json):
+    {
+        "title": "Chooser title",
+        "method": "post",
+        "actionurl": "http://example.org/test",
+        "instructions": "Choose one:",
+        "paramname": "param",
+        "sections": [{
+            "id": "section-1",
+            "label": "Section one",
+            "items": [{
+                "label": "item one",
+                "description": "description one"
+            }]
+        }]
+    }
 }}
 <div class="hd choosertitle">
     {{title}}
index cfdea06..722b2f5 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/chooser_item
+
     Chooser item.
+
+    Example context (json):
+    {
+        "id": "1",
+        "paramname": "param",
+        "value": "1",
+        "label": "item one",
+        "description": "description one"
+    }
 }}
 <div class="option">
     <label for="item_{{id}}">
index e961ae1..6f51101 100644 (file)
@@ -17,8 +17,7 @@
 {{!
     @template core/dataformat_selector
 
-    Template for all html emails. Note that it may wrap content formatted
-    elsewhere in another a module template.
+    Template for dataformat selection and download form.
 
     Context variables required for this template:
     * label
     * options
     * sesskey
     * submit
+
+    Example context (json):
+    {
+        "base": "http://example.org/",
+        "name": "test",
+        "label": "Download table data as",
+        "params": false,
+        "options": [{"label": "CSV", "name": "csv"}, {"label": "Excel", "name": "excel"}],
+        "submit": "Download"
+    }
 }}
 <form method="get" action="{{base}}" class="dataformatselector">
     <div class="mdl-align">
index ec43023..8bb9803 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/email_html
+    @template core/email_fromname
 
     Template for all email subjects.
 
     * fromname
     * replyto
     * replytoname
+
+    Example context (json):
+    {
+        "fromname": "Joe Bloggs"
+    }
 }}
 {{{fromname}}}
index e8c1aaa..a8d93d9 100644 (file)
     * replyto
     * replytoname
     * body
+
+    Example context (json):
+    {
+        "body": "Email body"
+    }
 }}
 {{{body}}}
index c67afdd..8454983 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/email_html
+    @template core/email_subject
 
     Template for all email subjects.
 
     * fromname
     * replyto
     * replytoname
+
+    Example context (json):
+    {
+        "subject": "Email subject"
+    }
 }}
 {{{subject}}}
index 8ffc85a..7e537c1 100644 (file)
     * replyto
     * replytoname
     * body
+
+    Example context (json):
+    {
+        "body": "Email body"
+    }
 }}
 {{{body}}}
index d73c8c3..72b2dcd 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/help_icon
+
     Help icon.
+
+    Example context (json):
+    {
+        "title": "Help with something",
+        "url": "http://example.org/help",
+        "linktext": "",
+        "icon":{
+            "attributes": [
+                {"name": "class", "value": "iconhelp"},
+                {"name": "src", "value": "../../../pix/help.svg"},
+                {"name": "alt", "value": "Help icon"}
+            ]
+        }
+    }
 }}
 <span class="helptooltip">
     <a href="{{url}}" title={{#quote}}{{title}}{{/quote}} aria-haspopup="true" target="_blank">{{#icon}}{{>core/pix_icon}}{{/icon}}{{#linktext}}{{.}}{{/linktext}}</a>
index b15e58a..0a85c16 100644 (file)
 
     Context variables required for this template:
     * none
+
+    Example context (json):
+    {
+    }
 }}
 <div class="hover-tooltip-container">
     {{$anchor}}{{/anchor}}
index 1c55525..d8199b9 100644 (file)
@@ -29,5 +29,8 @@
 
     Context variables required for this template:
     *
+
+    Example context (json):
+    {}
 }}
 <span class="loading-icon">{{#pix}} y/loading, core, {{#str}} loading {{/str}} {{/pix}}</span>
index 0096a07..3ee5e2a 100644 (file)
     @template core/login
 
     Moodle template for the login page.
+
+    Example context (json):
+    {
+        "autofocusform": false,
+        "canloginasguest": true,
+        "canloginbyemail": true,
+        "cansignup": true,
+        "error": "testerror",
+        "errorformatted": "Test error formatted",
+        "forgotpasswordurl": "http://example.com/login/forgot_password.php",
+        "hasidentityproviders": false,
+        "hasinstructions": true,
+        "instructions": "For full access to this site, you first need to create an account.",
+        "loginurl": "http://example.com/stable_master/login/index.php",
+        "rememberusername": true,
+        "passwordautocomplete": false,
+        "signupurl": "http://localhost/stable_master/login/signup.php",
+        "cookieshelpiconformatted": "",
+        "username": ""
+    }
 }}
 {{#hasinstructions}}
 <div class="loginbox clearfix twocolumns">
             });
         {{/autofocusform}}
     {{/error}}
-    })
+    });
 {{/js}}
index 14884c0..a358086 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/modal_save_cancel
+    @template core/modal_cancel
 
     Moodle modal template with one cancel button.
 
index e986757..403735c 100644 (file)
@@ -33,8 +33,9 @@
     Example context (json):
     {
         "attributes": [
-            { "name": "src", "value": "http://moodle.com/wp-content/themes/moodle/images/logo-hat2.png" },
-            { "name": "class", "value": "iconsmall" }
+            { "name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" },
+            { "name": "class", "value": "iconsmall" },
+            {"name": "alt", "value": "Alt text for icon"}
         ]
     }
 
index 7a2653e..ae8ac48 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/progress_bar
+
     Progress bar.
 
     Example context (json):
     {
-        id: 'progressbar_test',
-        width: '500'
+        "id": "progressbar_test",
+        "width": "500"
     }
 }}
 <div class="progressbar_container" style="width: {{width}}px;" id="{{id}}">
index 1661f33..e2954cd 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/select_time
+
     Select time.
+
+    Example context (json):
+    {
+        "id": "test-id",
+        "name": "test-name",
+        "label": "Test label",
+        "options": [
+            {"name": "Option 1", "value": "1"},
+            {"name": "Option 2", "value": "2"}
+        ]
+    }
 }}
 <label for="{{id}}" class="accesshide">{{label}}</label>
 <select name="{{name}}" id="{{id}}" {{#attributes}} {{name}}="{{value}}"{{/attributes}}>
index 80bd360..dee3e2d 100644 (file)
@@ -1,2 +1,10 @@
+{{!
+    @template core/signup_form_layout
+
+    Example context (json):
+    {
+        "formhtml": "<p>(Form html would go here)</p>"
+    }
+}}
 <h3>{{#str}}newaccount{{/str}}</h3>
 {{{formhtml}}}
index 8a1ef0d..6e17fa5 100644 (file)
@@ -1,3 +1,14 @@
+{{!
+    @template core/skip_links
+
+    Example context (json):
+    {
+        "links": [
+            {"url": "http://example.com/link1", "text": "Link 1"},
+            {"url": "http://example.com/link2", "text": "Link 2"}
+        ]
+    }
+}}
 <div class="skiplinks">
 {{#links}}
     <a href="#{{{url}}}" class="skip">{{{text}}}</a>
index 36d9279..239dd99 100644 (file)
@@ -386,28 +386,6 @@ class behat_hooks extends behat_base {
         $this->resize_window('medium');
     }
 
-    /**
-     * Executed after scenario to go to a page where no JS is executed.
-     * This will ensure there are no unwanted ajax calls from browser and
-     * site can be reset safely.
-     *
-     * @param AfterScenarioScope $scope scope passed by event fired after scenario.
-     * @AfterScenario
-     */
-    public function after_scenario(AfterScenarioScope $scope) {
-        try {
-            $this->wait_for_pending_js();
-            $this->getSession()->reset();
-        } catch (DriverException $e) {
-            // Try restart session, if DriverException caught.
-            try {
-                $this->getSession()->restart();
-            } catch (DriverException $e) {
-                // Do nothing, as this will be caught while starting session in before_scenario.
-            }
-        }
-    }
-
     /**
      * Wait for JS to complete before beginning interacting with the DOM.
      *
index f7e7352..c681b2b 100644 (file)
@@ -3425,4 +3425,27 @@ class core_moodlelib_testcase extends advanced_testcase {
              'samecourse' => false, 'result' => false],
         ];
     }
+
+    /**
+     * Test that generate_email_processing_address() returns valid email address.
+     */
+    public function test_generate_email_processing_address() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        $data = (object)[
+            'id' => 42,
+            'email' => 'my.email+from_moodle@example.com',
+        ];
+
+        $modargs = 'B'.base64_encode(pack('V', $data->id)).substr(md5($data->email), 0, 16);
+
+        $CFG->maildomain = 'example.com';
+        $CFG->mailprefix = 'mdl+';
+        $this->assertTrue(validate_email(generate_email_processing_address(0, $modargs)));
+
+        $CFG->maildomain = 'mail.example.com';
+        $CFG->mailprefix = 'mdl-';
+        $this->assertTrue(validate_email(generate_email_processing_address(23, $modargs)));
+    }
 }
diff --git a/lib/tests/scss_test.php b/lib/tests/scss_test.php
new file mode 100644 (file)
index 0000000..80f3cb5
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+
+/**
+ * This file contains the unittests for core scss.
+ *
+ * @package   core
+ * @category  phpunit
+ * @copyright 2016 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * This file contains the unittests for core scss.
+ *
+ * @package   core
+ * @category  phpunit
+ * @copyright 2016 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_scss_testcase extends advanced_testcase {
+
+    /**
+     * Data provider for is_valid_file
+     * @return array
+     */
+    public function is_valid_file_provider() {
+        $themedirectory = core_component::get_component_directory('theme_boost');
+        $realroot = realpath($themedirectory);
+        return [
+            "File import 1" => [
+                "path" => "../test.php",
+                "valid" => false
+            ],
+            "File import 2" => [
+                "path" => "../test.py",
+                "valid" => false
+            ],
+            "File import 3" => [
+                "path" => $realroot . "/scss/moodle.scss",
+                "valid" => true
+            ],
+            "File import 4" => [
+                "path" => $realroot . "/scss/../../../config.php",
+                "valid" => false
+            ],
+            "File import 5" => [
+                "path" => "/../../../../etc/passwd",
+                "valid" => false
+            ],
+            "File import 6" => [
+                "path" => "random",
+                "valid" => false
+            ]
+        ];
+    }
+
+    /**
+     * @dataProvider is_valid_file_provider
+     */
+    public function test_is_valid_file($path, $valid) {
+        $scss = new \core_scss();
+        $pathvalid = phpunit_util::call_internal_method($scss, 'is_valid_file', [$path], \core_scss::class);
+        $this->assertSame($valid, $pathvalid);
+    }
+}
\ No newline at end of file
index 0d7faf6..1cfc020 100644 (file)
@@ -665,4 +665,19 @@ EXPECTED;
         );
     }
 
+    /**
+     * Tests for validate_email() function.
+     */
+    public function test_validate_email() {
+
+        $this->assertTrue(validate_email('moodle@example.com'));
+        $this->assertTrue(validate_email('moodle@localhost.local'));
+        $this->assertTrue(validate_email('verp_email+is=mighty@moodle.org'));
+        $this->assertTrue(validate_email("but_potentially'dangerous'too@example.org"));
+        $this->assertTrue(validate_email('posts+AAAAAAAAAAIAAAAAAAAGQQAAAAABFSXz1eM/P/lR2bYyljM+@posts.moodle.org'));
+
+        $this->assertFalse(validate_email('moodle@localhost'));
+        $this->assertFalse(validate_email('"attacker\\" -oQ/tmp/ -X/var/www/vhost/moodle/backdoor.php  some"@email.com'));
+        $this->assertFalse(validate_email("moodle@example.com>\r\nRCPT TO:<victim@example.com"));
+    }
 }
index 2ab1711..83df38e 100644 (file)
@@ -2,8 +2,12 @@ This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
 === 3.3 ===
+
 * YUI module moodle-core-formautosubmit has been removed, use jquery .change() instead (see lib/templates/url_select.mustache for
   an example)
+* $mform->init_javascript_enhancement() is deprecated and no longer does anything. Existing uses of smartselect enhancement
+  should be switched to the searchableselector form element or other solutions.
+* Return value of the validate_email() is now proper boolean as documented. Previously the function could return 1, 0 or false.
 
 === 3.2 ===
 
index de6e6b4..0a61cfa 100644 (file)
@@ -2325,7 +2325,7 @@ function check_unoconv_version(environment_results $result) {
 }
 
 /**
- * Checks for up-to-date TLS libraries.
+ * Checks for up-to-date TLS libraries. NOTE: this is not currently used, see MDL-57262.
  *
  * @param environment_results $result object to update, if relevant.
  * @return environment_results|null updated results or null if unoconv path is not executable.
index 97da57a..0d9ce7f 100644 (file)
@@ -1088,12 +1088,12 @@ function page_get_doc_link_path(moodle_page $page) {
  */
 function validate_email($address) {
 
-    return (preg_match('#^[-!\#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+'.
+    return (bool)preg_match('#^[-!\#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+'.
                  '(\.[-!\#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+)*'.
                   '@'.
                   '[-!\#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+\.'.
                   '[-!\#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+$#',
-                  $address));
+                  $address);
 }
 
 /**
index c62ff2c..c47cd2e 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 3188100..9682d82 100644 (file)
@@ -75,6 +75,9 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
         /** @type {int} the number of messagess displayed */
         Messages.prototype._numMessagesDisplayed = 0;
 
+        /** @type {array} the messages displayed or about to be displayed on the page */
+        Messages.prototype._messageQueue = [];
+
         /** @type {int} the number of messages to retrieve */
         Messages.prototype._numMessagesToRetrieve = 20;
 
@@ -294,36 +297,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
             }
 
             // 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) {
-                    var newHtml = $('<div>' + html + '</div>');
-                    if (this._hasMatchingBlockTime(this.messageArea.node, newHtml, false)) {
-                        newHtml.find(SELECTORS.BLOCKTIME + ':first').remove();
-                    }
-                    // Show the new content.
-                    Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), newHtml, 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();
-                }
+                return this._addMessagesToDom(data.messages, shouldScrollBottom);
             }.bind(this)).always(function() {
                 // Mark that we are no longer busy loading data.
                 this._isLoadingMessages = false;
@@ -435,7 +410,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
                 // Fire an event to say the message was sent.
                 this.messageArea.trigger(Events.MESSAGESENT, [this._getUserId(), text]);
                 // Update the messaging area.
-                return this._addMessageToDom();
+                return this._addLastMessageToDom();
             }.bind(this)).then(function() {
                 // Ok, we are no longer sending a message.
                 this._isSendingMessage = false;
@@ -624,10 +599,58 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
         /**
          * Handles adding messages to the DOM.
          *
+         * @param {array} messages An array of messages to be added to the DOM.
+         * @param {boolean} shouldScrollBottom True will scroll to the bottom of the message window and show the new messages.
+         * @return {Promise} The promise resolved when the messages have been added to the DOM.
+         * @private
+         */
+        Messages.prototype._addMessagesToDom = function(messages, shouldScrollBottom) {
+            var numberreceived = 0;
+            var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
+            messages = messages.filter(function(message) {
+                var id = "" + message.id + message.isread;
+                // If the message is already queued to be rendered, remove from the list of messages.
+                if (this._messageQueue[id]) {
+                    return false;
+                }
+                // Filter out any messages already rendered.
+                var result = messagesArea.find(SELECTORS.MESSAGE + '[data-id="' + id + '"]');
+                // Any message we are rendering should go in the messageQueue.
+                if (!result.length) {
+                    this._messageQueue[id] = true;
+                }
+                return !result.length;
+            }.bind(this));
+            numberreceived = messages.length;
+            // We have the data - lets render the template with it.
+            return Templates.render('core_message/message_area_messages', {messages: messages}).then(function(html, js) {
+                // Check if we got something to do.
+                if (numberreceived > 0) {
+                    var newHtml = $('<div>' + html + '</div>');
+                    if (this._hasMatchingBlockTime(this.messageArea.node, newHtml, false)) {
+                        newHtml.find(SELECTORS.BLOCKTIME + ':first').remove();
+                    }
+                    // Show the new content.
+                    Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), newHtml, 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));
+        };
+
+        /**
+         * Handles adding the last message to the DOM.
+         *
          * @return {Promise} The promise resolved when the message has been added to the DOM.
          * @private
          */
-        Messages.prototype._addMessageToDom = function() {
+        Messages.prototype._addLastMessageToDom = function() {
             // Call the web service to return how the message should look.
             var promises = Ajax.call([{
                 methodname: 'core_message_data_for_messagearea_get_most_recent_message',
@@ -639,13 +662,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust
 
             // Add the message.
             return promises[0].then(function(data) {
-                return Templates.render('core_message/message_area_message', data);
-            }).then(function(html, js) {
-                Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), html, js);
-                // Empty the response text area.
+                return this._addMessagesToDom([data], true);
+            }.bind(this)).always(function() {
+                // Empty the response text area.text
                 this.messageArea.find(SELECTORS.SENDMESSAGETEXT).val('').trigger('input');
-                // Scroll down.
-                this._scrollBottom();
             }.bind(this)).fail(Notification.exception);
         };
 
index f944289..c06d661 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/add_contact_button
+    @template core_message/add_contact_button
 
     Template for the contents of the add contact button on the user's profile page.
 
index 6d8fec2..2f542f5 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core/add_contact_button
+    @template core_message/add_contact_button
 
     Template for the contents of the add contact button on the user's profile page.
 
     Context variables required for this template:
-    *
+    * none
+
+    Example context (json):
+    {
+    }
 }}
 <span>
     {{#pix}} t/removecontact, core {{/pix}}
index cd52c57..41f74c3 100644 (file)
@@ -895,7 +895,7 @@ class mod_assign_external extends external_api {
                             'locked'           => new external_value(PARAM_INT, 'locked', VALUE_OPTIONAL),
                             'mailed'           => new external_value(PARAM_INT, 'mailed', VALUE_OPTIONAL),
                             'extensionduedate' => new external_value(PARAM_INT, 'extension due date', VALUE_OPTIONAL),
-                            'workflowstate'    => new external_value(PARAM_TEXT, 'marking workflow state', VALUE_OPTIONAL),
+                            'workflowstate'    => new external_value(PARAM_ALPHA, 'marking workflow state', VALUE_OPTIONAL),
                             'allocatedmarker'  => new external_value(PARAM_INT, 'allocated marker', VALUE_OPTIONAL)
                         )
                     )
@@ -1147,7 +1147,7 @@ class mod_assign_external extends external_api {
                             'locked'           => new external_value(PARAM_INT, 'locked'),
                             'mailed'           => new external_value(PARAM_INT, 'mailed'),
                             'extensionduedate' => new external_value(PARAM_INT, 'extension due date'),
-                            'workflowstate'    => new external_value(PARAM_TEXT, 'marking workflow state', VALUE_OPTIONAL),
+                            'workflowstate'    => new external_value(PARAM_ALPHA, 'marking workflow state', VALUE_OPTIONAL),
                             'allocatedmarker'  => new external_value(PARAM_INT, 'allocated marker')
                         )
                     )
index 1658b28..2bdebc2 100644 (file)
@@ -2752,7 +2752,7 @@ class assign {
         $o = '';
 
         $pluginsubtype = required_param('pluginsubtype', PARAM_ALPHA);
-        $plugintype = required_param('plugin', PARAM_TEXT);
+        $plugintype = required_param('plugin', PARAM_PLUGIN);
         $pluginaction = required_param('pluginaction', PARAM_ALPHA);
 
         $plugin = $this->get_plugin_by_type($pluginsubtype, $plugintype);
@@ -2826,7 +2826,7 @@ class assign {
 
         $submissionid = optional_param('sid', 0, PARAM_INT);
         $gradeid = optional_param('gid', 0, PARAM_INT);
-        $plugintype = required_param('plugin', PARAM_TEXT);
+        $plugintype = required_param('plugin', PARAM_PLUGIN);
         $item = null;
         if ($pluginsubtype == 'assignsubmission') {
             $plugin = $this->get_submission_plugin_by_type($plugintype);
@@ -6170,7 +6170,7 @@ class assign {
             $record->userid = $userid;
             if ($modified >= 0) {
                 $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
-                $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_TEXT);
+                $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_ALPHA);
                 $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
             } else {
                 // This user was not in the grading table.
@@ -7337,7 +7337,7 @@ class assign {
         $mform->setType('userid', PARAM_INT);
 
         $mform->addElement('hidden', 'action', 'savesubmission');
-        $mform->setType('action', PARAM_TEXT);
+        $mform->setType('action', PARAM_ALPHA);
     }
 
     /**
index 571df31..830fec3 100644 (file)
@@ -37,7 +37,7 @@ require_capability('mod/assign:view', $context);
 
 $assign = new assign($context, $cm, $course);
 $urlparams = array('id' => $id,
-                  'action' => optional_param('action', '', PARAM_TEXT),
+                  'action' => optional_param('action', '', PARAM_ALPHA),
                   'rownum' => optional_param('rownum', 0, PARAM_INT),
                   'useridlistid' => optional_param('useridlistid', $assign->get_useridlist_key_id(), PARAM_ALPHANUM));
 
@@ -52,4 +52,4 @@ $assign->update_effective_access($USER->id);
 
 // Get the assign class to
 // render the page.
-echo $assign->view(optional_param('action', '', PARAM_TEXT));
+echo $assign->view(optional_param('action', '', PARAM_ALPHA));
index 1ece799..de8b038 100644 (file)
@@ -67,6 +67,11 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
 
         $newitemid = $DB->insert_record('forum', $data);
         $this->apply_activity_instance($newitemid);
+
+        // Add current enrolled user subscriptions if necessary.
+        $data->id = $newitemid;
+        $ctx = context_module::instance($this->task->get_moduleid());
+        forum_instance_created($ctx, $data);
     }
 
     protected function process_forum_discussion($data) {
index 520a2ec..6ddbaee 100644 (file)
@@ -260,9 +260,6 @@ class mod_forum_post_form extends moodleform {
         $mform->addElement('hidden', 'parent');
         $mform->setType('parent', PARAM_INT);
 
-        $mform->addElement('hidden', 'userid');
-        $mform->setType('userid', PARAM_INT);
-
         $mform->addElement('hidden', 'groupid');
         $mform->setType('groupid', PARAM_INT);
 
index 35dfa96..599c5b3 100644 (file)
@@ -61,7 +61,7 @@ $string['cannotdeletepost'] = 'You can\'t delete this post!';
 $string['cannoteditposts'] = 'You can\'t edit other people\'s posts!';
 $string['cannotfinddiscussion'] = 'Could not find the discussion in this forum';
 $string['cannotfindfirstpost'] = 'Could not find the first post in this forum';
-$string['cannotfindorcreateforum'] = 'Could not find or create a main news forum for the site';
+$string['cannotfindorcreateforum'] = 'Could not find or create a main announcements forum for the site';
 $string['cannotfindparentpost'] = 'Could not find top parent of post {$a}';
 $string['cannotmovefromsingleforum'] = 'Cannot move discussion from a simple single discussion forum';
 $string['cannotmovenotvisible'] = 'Forum not visible';
@@ -216,7 +216,7 @@ $string['forcesubscribed_help'] = 'This forum has been configured so that you ca
 $string['forcesubscribed'] = 'This forum forces everyone to be subscribed';
 $string['forum'] = 'Forum';
 $string['forum:addinstance'] = 'Add a new forum';
-$string['forum:addnews'] = 'Add news';
+$string['forum:addnews'] = 'Add announcements';
 $string['forum:addquestion'] = 'Add question';
 $string['forum:allowforcesubscribe'] = 'Allow force subscribe';
 $string['forum:canoverridediscussionlock'] = 'Reply to locked discussions';
@@ -239,7 +239,7 @@ $string['forum:postwithoutthrottling'] = 'Exempt from post threshold';
 $string['forumname'] = 'Forum name';
 $string['forumposts'] = 'Forum posts';
 $string['forum:rate'] = 'Rate posts';
-$string['forum:replynews'] = 'Reply to news';
+$string['forum:replynews'] = 'Reply to announcements';
 $string['forum:replypost'] = 'Reply to posts';
 $string['forums'] = 'Forums';
 $string['forum:splitdiscussions'] = 'Split discussions';
@@ -357,7 +357,7 @@ $string['noguestpost'] = 'Sorry, guests are not allowed to post.';
 $string['noguestsubscribe'] = 'Sorry, guests are not allowed to subscribe.';
 $string['noguesttracking'] = 'Sorry, guests are not allowed to set tracking options.';
 $string['nomorepostscontaining'] = 'No more posts containing \'{$a}\' were found';
-$string['nonews'] = 'No news has been posted yet';
+$string['nonews'] = 'No announcements have been posted yet.';
 $string['noonecansubscribenow'] = 'Subscriptions are now disallowed';
 $string['nopermissiontosubscribe'] = 'You do not have the permission to view forum subscribers';
 $string['nopermissiontoview'] = 'You do not have permissions to view this post';
index 7507944..79168f5 100644 (file)
@@ -4405,25 +4405,41 @@ function forum_add_new_post($post, $mform, $unused = null) {
 /**
  * Update a post.
  *
- * @param   stdClass    $post       The post to update
+ * @param   stdClass    $newpost    The post to update
  * @param   mixed       $mform      The submitted form
  * @param   string      $unused
  * @return  bool
  */
-function forum_update_post($post, $mform, $unused = null) {
-    global $DB;
+function forum_update_post($newpost, $mform, $unused = null) {
+    global $DB, $USER;
 
+    $post       = $DB->get_record('forum_posts', array('id' => $newpost->id));
     $discussion = $DB->get_record('forum_discussions', array('id' => $post->discussion));
     $forum      = $DB->get_record('forum', array('id' => $discussion->forum));
     $cm         = get_coursemodule_from_instance('forum', $forum->id);
     $context    = context_module::instance($cm->id);
 
+    // Allowed modifiable fields.
+    $modifiablefields = [
+        'subject',
+        'message',
+        'messageformat',
+        'messagetrust',
+        'timestart',
+        'timeend',
+        'pinned',
+        'attachments',
+    ];
+    foreach ($modifiablefields as $field) {
+        if (isset($newpost->{$field})) {
+            $post->{$field} = $newpost->{$field};
+        }
+    }
     $post->modified = time();
 
-    $DB->update_record('forum_posts', $post);
-
-    $discussion->timemodified = $post->modified; // last modified tracking
-    $discussion->usermodified = $post->userid;   // last modified tracking
+    // Last post modified tracking.
+    $discussion->timemodified = $post->modified;
+    $discussion->usermodified = $USER->id;
 
     if (!$post->parent) {   // Post is a discussion starter - update discussion title and times too
         $discussion->name      = $post->subject;
@@ -4434,16 +4450,15 @@ function forum_update_post($post, $mform, $unused = null) {
             $discussion->pinned = $post->pinned;
         }
     }
-    $post->message = file_save_draft_area_files($post->itemid, $context->id, 'mod_forum', 'post', $post->id,
+    $post->message = file_save_draft_area_files($newpost->itemid, $context->id, 'mod_forum', 'post', $post->id,
             mod_forum_post_form::editor_options($context, $post->id), $post->message);
-    $DB->set_field('forum_posts', 'message', $post->message, array('id'=>$post->id));
-
+    $DB->update_record('forum_posts', $post);
     $DB->update_record('forum_discussions', $discussion);
 
     forum_add_attachment($post, $forum, $cm, $mform);
 
     if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
-        forum_tp_mark_post_read($post->userid, $post, $post->forum);
+        forum_tp_mark_post_read($USER->id, $post, $post->forum);
     }
 
     // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
index 160d80f..4e02e44 100644 (file)
@@ -2955,7 +2955,7 @@ abstract class lesson_page extends lesson_base {
         for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
             $answer = clone($newanswer);
 
-            if (!empty($properties->answer_editor[$i])) {
+            if (isset($properties->answer_editor[$i])) {
                 if (is_array($properties->answer_editor[$i])) {
                     // Multichoice and true/false pages have an HTML editor.
                     $answer->answer = $properties->answer_editor[$i]['text'];
index c715677..634da76 100644 (file)
@@ -58,7 +58,8 @@ class lesson_page_type_endofbranch extends lesson_page {
 
     public function redirect_to_first_answer($canmanage) {
         global $USER, $PAGE;
-        $answer = array_shift($this->get_answers());
+        $answers = $this->get_answers();
+        $answer = array_shift($answers);
         $jumpto = $answer->jumpto;
         if ($jumpto == LESSON_RANDOMBRANCH) {
 
index f945ef7..1418189 100644 (file)
@@ -34,7 +34,7 @@ Feature: Add preconfigured tools via teacher interface
     And the field "Icon URL" matches value "http://download.moodle.org/unittest/test.jpg"
     And the field "Secure icon URL" matches value "https://download.moodle.org/unittest/test.jpg"
 
-  @javascript
+  @javascript @_switch_window
   Scenario: Add a preconfigured tool from a cartridge
     When I log in as "teacher1"
     And I follow "Course 1"
@@ -58,7 +58,7 @@ Feature: Add preconfigured tools via teacher interface
     And I press "Cancel"
     And I switch to the main window
 
-  @javascript
+  @javascript @_switch_window
   Scenario: Add and use a preconfigured tool
     When I log in as "teacher1"
     And I follow "Course 1"
index fd2b579..21b158f 100644 (file)
@@ -1194,8 +1194,11 @@ function quiz_update_events($quiz, $override = null) {
                    'instance'=>$quiz->id);
     if (!empty($override)) {
         // Only load events for this override.
-        $conds['groupid'] = isset($override->groupid)?  $override->groupid : 0;
-        $conds['userid'] = isset($override->userid)?  $override->userid : 0;
+        if (isset($override->userid)) {
+            $conds['userid'] = $override->userid;
+        } else {
+            $conds['groupid'] = $override->groupid;
+        }
     }
     $oldevents = $DB->get_records('event', $conds);
 
index dfd4e4e..d06cd5c 100644 (file)
@@ -83,6 +83,7 @@ function scorm_get_aicc_columns($row, $mastername='system_id') {
     $tok = strtok(strtolower($row), "\",\n\r");
     $result = new stdClass();
     $result->columns = array();
+    $result->mastercol = 0;
     $i = 0;
     while ($tok) {
         if ($tok != '') {
@@ -250,7 +251,13 @@ function scorm_parse_aicc(&$scorm) {
             $regexp = scorm_forge_cols_regexp($columns->columns, '(.+),');
             for ($i = 1; $i < count($rows); $i++) {
                 if (preg_match($regexp, $rows[$i], $matches)) {
-                    $courses[$courseid]->elements[$columns->mastercol + 1]->prerequisites = substr(trim($matches[2 - $columns->mastercol]), 1, -1);
+                    $elementid = trim($matches[$columns->mastercol + 1]);
+                    $elementid = trim(trim($elementid, '"'), "'"); // Remove any quotes.
+
+                    $prereq = trim($matches[2 - $columns->mastercol]);
+                    $prereq = trim(trim($prereq, '"'), "'"); // Remove any quotes.
+
+                    $courses[$courseid]->elements[$elementid]->prerequisites = $prereq;
                 }
             }
         }
index 6a34db8..2adb083 100644 (file)
@@ -169,7 +169,7 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
     var Initialized = false;
 
     function LMSInitialize (param) {
-        scoid = scorm_current_node ? scorm_current_node.scoid : scoid;
+        scoid = (scorm_current_node && scorm_current_node.scoid) ?  scorm_current_node.scoid : scoid;
         initdatamodel(scoid);
 
         errorCode = "0";
index bc7791a..306b9ac 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-/**
- * This is really a little language parser for AICC_SCRIPT
- * evaluates the expression and returns a boolean answer
- * see 2.3.2.5.1. Sequencing/Navigation Today  - from the SCORM 1.2 spec (CAM).
- *
- * @param string $prerequisites the aicc_script prerequisites expression
- * @param array  $usertracks the tracked user data of each SCO visited
- * @return boolean
- */
-function scorm_eval_prerequisites($prerequisites, $usertracks) {
-
-    // This is really a little language parser - AICC_SCRIPT is the reference
-    // see 2.3.2.5.1. Sequencing/Navigation Today  - from the SCORM 1.2 spec.
-    $element = '';
-    $stack = array();
-    $statuses = array(
-                'passed' => 'passed',
-                'completed' => 'completed',
-                'failed' => 'failed',
-                'incomplete' => 'incomplete',
-                'browsed' => 'browsed',
-                'not attempted' => 'notattempted',
-                'p' => 'passed',
-                'c' => 'completed',
-                'f' => 'failed',
-                'i' => 'incomplete',
-                'b' => 'browsed',
-                'n' => 'notattempted'
-                );
-    $i = 0;
-
-    // Expand the amp entities.
-    $prerequisites = preg_replace('/&amp;/', '&', $prerequisites);
-    // Find all my parsable tokens.
-    $prerequisites = preg_replace('/(&|\||\(|\)|\~)/', '\t$1\t', $prerequisites);
-    // Expand operators.
-    $prerequisites = preg_replace('/&/', '&&', $prerequisites);
-    $prerequisites = preg_replace('/\|/', '||', $prerequisites);
-    // Now - grab all the tokens.
-    $elements = explode('\t', trim($prerequisites));
-
-    // Process each token to build an expression to be evaluated.
-    $stack = array();
-    foreach ($elements as $element) {
-        $element = trim($element);
-        if (empty($element)) {
-            continue;
-        }
-        if (!preg_match('/^(&&|\|\||\(|\))$/', $element)) {
-            // Create each individual expression.
-            // Search for ~ = <> X*{} .
-
-            // Sets like 3*{S34, S36, S37, S39}.
-            if (preg_match('/^(\d+)\*\{(.+)\}$/', $element, $matches)) {
-                $repeat = $matches[1];
-                $set = explode(',', $matches[2]);
-                $count = 0;
-                foreach ($set as $setelement) {
-                    if (isset($usertracks[$setelement]) &&
-                       ($usertracks[$setelement]->status == 'completed' || $usertracks[$setelement]->status == 'passed')) {
-                        $count++;
-                    }
-                }
-                if ($count >= $repeat) {
-                    $element = 'true';
-                } else {
-                    $element = 'false';
-                }
-            } else if ($element == '~') {
-                // Not maps ~.
-                $element = '!';
-            } else if (preg_match('/^(.+)(\=|\<\>)(.+)$/', $element, $matches)) {
-                // Other symbols = | <> .
-                $element = trim($matches[1]);
-                if (isset($usertracks[$element])) {
-                    $value = trim(preg_replace('/(\'|\")/', '', $matches[3]));
-                    if (isset($statuses[$value])) {
-                        $value = $statuses[$value];
-                    }
-                    if ($matches[2] == '<>') {
-                        $oper = '!=';
-                    } else {
-                        $oper = '==';
-                    }
-                    $element = '(\''.$usertracks[$element]->status.'\' '.$oper.' \''.$value.'\')';
-                } else {
-                    $element = 'false';
-                }
-            } else {
-                // Everything else must be an element defined like S45 ...
-                if (isset($usertracks[$element]) &&
-                    ($usertracks[$element]->status == 'completed' || $usertracks[$element]->status == 'passed')) {
-                    $element = 'true';
-                } else {
-                    $element = 'false';
-                }
-            }
-
-        }
-        $stack[] = ' '.$element.' ';
-    }
-    return eval('return '.implode($stack).';');
-}
 
 /**
  * Sets up $userdata array and default values for SCORM 1.2 .
index 09ebae8..c713e72 100644 (file)
@@ -2239,3 +2239,108 @@ function scorm_launch_sco($scorm, $sco, $cm, $context, $scourl) {
     $event->add_record_snapshot('scorm_scoes', $sco);
     $event->trigger();
 }
+
+/**
+ * This is really a little language parser for AICC_SCRIPT
+ * evaluates the expression and returns a boolean answer
+ * see 2.3.2.5.1. Sequencing/Navigation Today  - from the SCORM 1.2 spec (CAM).
+ * Also used by AICC packages.
+ *
+ * @param string $prerequisites the aicc_script prerequisites expression
+ * @param array  $usertracks the tracked user data of each SCO visited
+ * @return boolean
+ */
+function scorm_eval_prerequisites($prerequisites, $usertracks) {
+
+    // This is really a little language parser - AICC_SCRIPT is the reference
+    // see 2.3.2.5.1. Sequencing/Navigation Today  - from the SCORM 1.2 spec.
+    $element = '';
+    $stack = array();
+    $statuses = array(
+        'passed' => 'passed',
+        'completed' => 'completed',
+        'failed' => 'failed',
+        'incomplete' => 'incomplete',
+        'browsed' => 'browsed',
+        'not attempted' => 'notattempted',
+        'p' => 'passed',
+        'c' => 'completed',
+        'f' => 'failed',
+        'i' => 'incomplete',
+        'b' => 'browsed',
+        'n' => 'notattempted'
+    );
+    $i = 0;
+
+    // Expand the amp entities.
+    $prerequisites = preg_replace('/&amp;/', '&', $prerequisites);
+    // Find all my parsable tokens.
+    $prerequisites = preg_replace('/(&|\||\(|\)|\~)/', '\t$1\t', $prerequisites);
+    // Expand operators.
+    $prerequisites = preg_replace('/&/', '&&', $prerequisites);
+    $prerequisites = preg_replace('/\|/', '||', $prerequisites);
+    // Now - grab all the tokens.
+    $elements = explode('\t', trim($prerequisites));
+
+    // Process each token to build an expression to be evaluated.
+    $stack = array();
+    foreach ($elements as $element) {
+        $element = trim($element);
+        if (empty($element)) {
+            continue;
+        }
+        if (!preg_match('/^(&&|\|\||\(|\))$/', $element)) {
+            // Create each individual expression.
+            // Search for ~ = <> X*{} .
+
+            // Sets like 3*{S34, S36, S37, S39}.
+            if (preg_match('/^(\d+)\*\{(.+)\}$/', $element, $matches)) {
+                $repeat = $matches[1];
+                $set = explode(',', $matches[2]);
+                $count = 0;
+                foreach ($set as $setelement) {
+                    if (isset($usertracks[$setelement]) &&
+                        ($usertracks[$setelement]->status == 'completed' || $usertracks[$setelement]->status == 'passed')) {
+                        $count++;
+                    }
+                }
+                if ($count >= $repeat) {
+                    $element = 'true';
+                } else {
+                    $element = 'false';
+                }
+            } else if ($element == '~') {
+                // Not maps ~.
+                $element = '!';
+            } else if (preg_match('/^(.+)(\=|\<\>)(.+)$/', $element, $matches)) {
+                // Other symbols = | <> .
+                $element = trim($matches[1]);
+                if (isset($usertracks[$element])) {
+                    $value = trim(preg_replace('/(\'|\")/', '', $matches[3]));
+                    if (isset($statuses[$value])) {
+                        $value = $statuses[$value];
+                    }
+                    if ($matches[2] == '<>') {
+                        $oper = '!=';
+                    } else {
+                        $oper = '==';
+                    }
+                    $element = '(\''.$usertracks[$element]->status.'\' '.$oper.' \''.$value.'\')';
+                } else {
+                    $element = 'false';
+                }
+            } else {
+                // Everything else must be an element defined like S45 ...
+                if (isset($usertracks[$element]) &&
+                    ($usertracks[$element]->status == 'completed' || $usertracks[$element]->status == 'passed')) {
+                    $element = 'true';
+                } else {
+                    $element = 'false';
+                }
+            }
+
+        }
+        $stack[] = ' '.$element.' ';
+    }
+    return eval('return '.implode($stack).';');
+}
index b7c09b0..0a04e3d 100644 (file)
@@ -1,4 +1,4 @@
-@mod @mod_scorm @_file_upload @_switch_frame
+@mod @mod_scorm @_file_upload @_switch_iframe
 Feature: Add scorm activity
   In order to let students access a scorm package
   As a teacher
index c33cdf9..9fa4437 100644 (file)
@@ -1,4 +1,4 @@
-@mod @mod_scorm @_file_upload @_switch_frame
+@mod @mod_scorm @_file_upload @_switch_iframe
 Feature: Scorm multi-sco completion
   In order to let students access a scorm package
   As a teacher
diff --git a/mod/scorm/tests/behat/missing_org.feature b/mod/scorm/tests/behat/missing_org.feature
new file mode 100644 (file)
index 0000000..9bd7552
--- /dev/null
@@ -0,0 +1,37 @@
+@mod @mod_scorm @_file_upload @_switch_iframe
+Feature: Check a SCORM package with missing Organisational structure.
+
+  @javascript
+  Scenario: Add a scorm activity to a course
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "SCORM package" to section "1"
+    And I set the following fields to these values:
+      | Name | MissingOrg SCORM package |
+      | Description | Description |
+    And I upload "mod/scorm/tests/packages/singlescobasic_missingorg.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    Then I should see "MissingOrg SCORM package"
+    And I should see "Normal"
+    And I should see "Preview"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "MissingOrg SCORM package"
+    And I should see "Normal"
+    And I press "Enter"
+    And I switch to "scorm_object" iframe
+    And I switch to "contentFrame" iframe
+    And I should see "Play of the game"
index 0d0c003..6c69131 100644 (file)
@@ -17,4 +17,5 @@ Other test packages
 * invalid.zip - zip file with an single html file, no SCORM config files, used for validation check.
 * validscorm.zip - non functional package with an imsmanifest.xml, used for validation check.
 * validaicc.zip - non functional package with AICC config files, used for validation check.
-* complexscorm.zip - copied from: https://github.com/jleyva/scorm-debugger
+* complexscorm.zip - copied from: https://github.com/jleyva/scorm-debugger.
+* singlescobasic_missingorg.zip - copy of scorm.com package but with missing org definition.
diff --git a/mod/scorm/tests/packages/singlescobasic_missingorg.zip b/mod/scorm/tests/packages/singlescobasic_missingorg.zip
new file mode 100644 (file)
index 0000000..deaeb30
Binary files /dev/null and b/mod/scorm/tests/packages/singlescobasic_missingorg.zip differ
index 56d6b74..3adf3dd 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120500;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2016122000;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2016112900;    // Requires this Moodle version.
 $plugin->component = 'mod_scorm';   // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 300;
index 22ab9c6..a80ddd0 100644 (file)
@@ -46,8 +46,7 @@ M.mod_scormform.init = function(Y) {
         // Onunload is called multiple times in the SCORM window - we only want to handle when it is actually closed.
         setTimeout(function() {
             if (winobj.closed) {
-                // Redirect the parent window to the course homepage.
-                parent.window.location = course_url;
+                window.location = course_url;
             }
         }, 800)
     }
index f5f12fa..56ae1a6 100644 (file)
@@ -442,15 +442,13 @@ class report_log_renderable implements renderable {
      * @return array list of origins.
      */
     public function get_origin_options() {
-        global $DB;
-        $origins = $DB->get_records_sql('select distinct origin from {logstore_standard_log} order by origin ASC');
         $ret = array();
         $ret[''] = get_string('allsources', 'report_log');
-        foreach ($origins as $origin) {
-            if (!empty($origin->origin)) {
-                $ret[$origin->origin] = get_string($origin->origin, 'report_log');
-            }
-        }
+        $ret['cli'] = get_string('cli', 'report_log');
+        $ret['restore'] = get_string('restore', 'report_log');
+        $ret['web'] = get_string('web', 'report_log');
+        $ret['ws'] = get_string('ws', 'report_log');
+        $ret['---'] = get_string('other', 'report_log');
         return $ret;
     }
 
index c1f3ff9..ad9e44a 100644 (file)
@@ -486,10 +486,20 @@ class report_log_table_log extends table_sql {
             $joins[] = "edulevel ".$edulevelsql;
             $params = array_merge($params, $edulevelparams);
         }
+
         // Origin.
         if (isset($this->filterparams->origin) && ($this->filterparams->origin != '')) {
-            $joins[] = "origin = :origin";
-            $params['origin'] = $this->filterparams->origin;
+            if ($this->filterparams->origin !== '---') {
+                // Filter by a single origin.
+                $joins[] = "origin = :origin";
+                $params['origin'] = $this->filterparams->origin;
+            } else {
+                // Filter by everything else.
+                list($originsql, $originparams) = $DB->get_in_or_equal(array('cli', 'restore', 'ws', 'web'),
+                    SQL_PARAMS_NAMED, 'origin', false);
+                $joins[] = "origin " . $originsql;
+                $params = array_merge($params, $originparams);
+            }
         }
 
         if (!($this->filterparams->logreader instanceof logstore_legacy\log\store)) {
index d764bdd..f37d48f 100644 (file)
@@ -39,6 +39,7 @@ $string['page'] = 'Page {$a}';
 $string['logsformat'] = 'Logs format';
 $string['nologreaderenabled'] = 'No log reader enabled';
 $string['origin'] = 'Source';
+$string['other'] = 'Other';
 $string['page-report-log-x'] = 'Any log report';
 $string['page-report-log-index'] = 'Course log report';
 $string['page-report-log-user'] = 'User course log report';
index d97e73d..c669075 100644 (file)
@@ -886,106 +886,6 @@ tr.flagged-tag a {
     text-align: left;
     border: 0 solid black;
 }
-/**
-* Smart Select Element
-*/
-.smartselect {
-    position: absolute;
-}
-
-.smartselect .smartselect_mask {
-    background-color: #fff;
-}
-
-.smartselect ul {
-    padding: 0;
-    margin: 0;
-}
-
-.smartselect ul li {
-    list-style: none;
-}
-
-.smartselect .smartselect_menu {
-    margin-right: 5px;
-}
-
-.safari .smartselect .smartselect_menu {
-    margin-left: 2px;
-}
-
-.smartselect .smartselect_menu,
-.smartselect .smartselect_submenu {
-    border: 1px solid #000;
-    background-color: #fff;
-    display: none;
-}
-
-.smartselect .smartselect_menu.visible,
-.smartselect .smartselect_submenu.visible {
-    display: block;
-}
-
-.smartselect .smartselect_menu_content ul li {
-    position: relative;
-    padding: 2px 5px;
-}
-
-.smartselect .smartselect_menu_content ul li a {
-    color: #333;
-    text-decoration: none;
-}
-
-.smartselect .smartselect_menu_content ul li a.selectable {
-    color: inherit;
-}
-
-.smartselect .smartselect_submenuitem {
-    background-image: url([[pix:moodle|t/collapsed]]);
-    background-repeat: no-repeat;
-    background-position: 100%;
-}
-/** Spanning mode */
-.smartselect.spanningmenu .smartselect_submenu {
-    position: absolute;
-    top: -1px;
-    left: 100%;
-}
-
-.smartselect.spanningmenu .smartselect_submenu a {
-    white-space: nowrap;
-    padding-right: 16px;
-}
-
-.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover {
-    text-decoration: underline;
-}
-/** Compact mode */
-.smartselect.compactmenu .smartselect_submenu {
-    position: relative;
-    margin: 2px -3px;
-    margin-left: 10px;
-    display: none;
-    border-width: 0;
-    z-index: 1010;
-}
-
-.smartselect.compactmenu .smartselect_submenu.visible {
-    display: block;
-}
-
-.smartselect.compactmenu .smartselect_menu {
-    z-index: 1000;
-    overflow: hidden;
-}
-
-.smartselect.compactmenu .smartselect_submenu .smartselect_submenu {
-    z-index: 1020;
-}
-
-.smartselect.compactmenu .smartselect_submenuitem:hover > .smartselect_menuitem_label {
-    font-weight: bold;
-}
 
 /**
 * Enrol
index 6c6b75f..f9f02f0 100644 (file)
 .file-picker .ygtvtn,
 .filemanager .ygtvtn {
     /*rtl:remove*/
-    background: url('[[pix:moodle|y/tn]]') 0 10px no-repeat;
+    background: url('[[pix:moodle|y/tn]]') 0 0 no-repeat;
     /*rtl:raw:
-    background: url('[[pix:moodle|y/tn_rtl]]') 2px 10px no-repeat;
+    background: url('[[pix:moodle|y/tn_rtl]]') 0 0 no-repeat;
     */
-    width: 17px;
-    height: 22px;
+    width: 19px;
+    height: 32px;
 }
 // first or middle sibling, collapsable
 .file-picker .ygtvtm,
 .file-picker .ygtvln,
 .filemanager .ygtvln {
     /*rtl:remove*/
-    background: url('[[pix:moodle|y/ln]]') 0 10px no-repeat;
+    background: url('[[pix:moodle|y/ln]]') 0 0 no-repeat;
     /*rtl:raw:
-    background: url('[[pix:moodle|y/ln_rtl]]') 2px 10px no-repeat;
+    background: url('[[pix:moodle|y/ln_rtl]]') 0 0 no-repeat;
     */
-    width: 17px;
-    height: 22px;
+    width: 19px;
+    height: 32px;
 }
 // Last sibling, collapsable
 .file-picker .ygtvlm,
 // the style for the empty cells that are used for rendering the depth of the node
 .file-picker .ygtvdepthcell,
 .filemanager .ygtvdepthcell {
-    background: url('[[pix:moodle|y/vline]]') 0 10px no-repeat;
+    background: url('[[pix:moodle|y/vline]]') 0 0 no-repeat;
     /*rtl:raw:
-    background-position: 2px 10px;
+    background-position: 0 0;
     */
     width: 17px;
     height: 32px;
index 326c205..15b2c39 100644 (file)
@@ -73,13 +73,13 @@ if ($ADMIN->fulltree) {
     $page = new admin_settingpage('theme_boost_advanced', get_string('advancedsettings', 'theme_boost'));
 
     // Raw SCSS to include before the content.
-    $setting = new admin_setting_configtextarea('theme_boost/scsspre',
+    $setting = new admin_setting_scsscode('theme_boost/scsspre',
         get_string('rawscsspre', 'theme_boost'), get_string('rawscsspre_desc', 'theme_boost'), '', PARAM_RAW);
     $setting->set_updatedcallback('theme_reset_all_caches');
     $page->add($setting);
 
     // Raw SCSS to include after the content.
-    $setting = new admin_setting_configtextarea('theme_boost/scss', get_string('rawscss', 'theme_boost'),
+    $setting = new admin_setting_scsscode('theme_boost/scss', get_string('rawscss', 'theme_boost'),
         get_string('rawscss_desc', 'theme_boost'), '', PARAM_RAW);
     $setting->set_updatedcallback('theme_reset_all_caches');
     $page->add($setting);
index 4d369b8..e1db0f2 100644 (file)
     You should have received a copy of the GNU General Public License
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
-<ul class="nav nav-tabs">
+{{!
+    @template theme_boost/admin_setting_tabs
+
+
+    Example context (json):
+    {
+        "tabs": [
+         {
+            "name": "tab1",
+            "active": 0,
+            "displayname": "Inactive tab1",
+            "html": "<p>Tab 1 content</p>"
+         },
+         {
+            "name": "tab2",
+            "active": 1,
+            "displayname": "Active tab2",
+            "html": "<p>Tab 2 content</p>"
+         }
+        ]
+    }
+}}
+<ul class="nav nav-tabs" role="tablist">
     {{#tabs}}
         <li class="nav-item">
             <a href="#{{name}}" class="nav-link {{#active}}active{{/active}}" data-toggle="tab" role="tab">{{displayname}}</a>
index 5befb9c..5a0d38f 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/action_menu
+
     Action menu.
+
+    Example context (json):
+    {
+        "classes": "",
+        "primary": {
+            "items": [{"rawhtml": "<p>Item in primary menu</p>"}]
+        },
+        "secondary": {
+            "items": [{"rawhtml": "<p>Item in secondary menu</p>"}]
+        }
+    }
 }}
 <div class="action-menu {{classes}}" {{#attributes}}{{name}}="{{value}}"{{/attributes}}>
     {{#primary}}
index d00323a..bbbf864 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/action_menu_item
+
     Action menu item.
+
+    Example context (json):
+    {
+        "rawhtml": "<p>[rawhtml]</p>"
+    }
 }}
 {{#actionmenulink}}{{> core/action_menu_link }}{{/actionmenulink}}
 {{#actionmenufiller}}<span class="filler">&nbsp;</span>{{/actionmenufiller}}
index 3c41ad8..756e95c 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/action_menu_link
+
     Action menu link.
+
+    Example context (json):
+    {
+        "text": "Example link text",
+        "showtext": true,
+        "url": "http://example.com/link"
+    }
 }}
 {{^disabled}}
     <a href="{{url}}" class="{{classes}}" {{#attributes}}{{name}}={{#quote}}{{value}}{{/quote}}{{/attributes}} {{#showtext}}aria-labelledby="actionmenuaction-{{instance}}"{{/showtext}}>{{#icon}}{{>core/pix_icon}}{{/icon}}{{#showtext}}<span class="menu-action-text" id="actionmenuaction-{{instance}}">{{{text}}}</span>{{/showtext}}</a>
index 9efbed1..5396e29 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/chooser
+
     Chooser.
+
+    Example context (json):
+    {
+        "title": "Chooser title",
+        "method": "post",
+        "actionurl": "http://example.org/test",
+        "instructions": "Choose one:",
+        "paramname": "param",
+        "sections": [{
+            "id": "section-1",
+            "label": "Section one",
+            "items": [{
+                "label": "item one",
+                "description": "description one"
+            }]
+        }]
+    }
 }}
 <div class="hd choosertitle">
     {{title}}
index 421dfc3..7c11ab3 100644 (file)
     * options
     * sesskey
     * submit
+
+    Example context (json):
+    {
+        "base": "http://example.org/",
+        "name": "test",
+        "value": "test",
+        "label": "Download table data as",
+        "params": false,
+        "options": [{"label": "CSV", "name": "csv"}, {"label": "Excel", "name": "excel"}],
+        "submit": "Download",
+        "sesskey": ""
+    }
 }}
 <form method="get" action="{{base}}" class="dataformatselector m-a-1">
     <div class="form-inline text-xs-right">
index 5abbe5b..161f0bc 100644 (file)
@@ -1,3 +1,22 @@
+{{!
+    @template core/help_icon
+
+    Help icon.
+
+    Example context (json):
+    {
+        "title": "Help with something",
+        "url": "http://example.org/help",
+        "linktext": "",
+        "icon":{
+            "attributes": [
+                {"name": "class", "value": "iconhelp"},
+                {"name": "src", "value": "../../../pix/help.svg"},
+                {"name": "alt", "value": "Help icon"}
+            ]
+        }
+    }
+}}
 <a class="btn btn-link p-a-0" role="button"
     data-container="body" data-toggle="popover"
     data-placement="{{#ltr}}right{{/ltr}}{{^ltr}}left{{/ltr}}" data-content="{{text}} {{completedoclink}}"
index 7a9d4f6..56f8212 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/progress_bar
+
     Progress bar.
 
     Example context (json):
     {
-        id: 'progressbar_test',
-        width: '500'
+        "id": "progressbar_test",
+        "width": "500"
     }
 }}
-<div class="row" id="{{id}}" class="progressbar_container">
+<div id="{{id}}" class="row progressbar_container">
     <div class="col-md-6 push-md-3">
         <p id="{{id}}_status" class="text-xs-center"></p>
         <progress id="{{id}}_bar" class="progress progress-striped progress-animated" value="0" max="100"></progress>
index 9116468..6493280 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template core/select_time
+
     Select time.
+
+    Example context (json):
+    {
+        "id": "test-id",
+        "name": "test-name",
+        "label": "Test label",
+        "options": [
+            {"name": "Option 1", "value": "1"},
+            {"name": "Option 2", "value": "2"}
+        ]
+    }
 }}
 <label for="{{id}}" class="sr-only">{{label}}</label>
 <select name="{{name}}" id="{{id}}" {{#attributes}} {{name}}="{{value}}"{{/attributes}} class="form-control">
index e6696cd..f62655e 100644 (file)
@@ -1,3 +1,13 @@
+{{!
+    @template core/signup_form_layout
+
+    Example context (json):
+    {
+        "logourl": "https://moodle.org/logo/moodle-logo.svg",
+        "sitename": "Site name",
+        "formhtml": "<p>(Form html would go here)</p>"
+    }
+}}
 <div class="container-fluid">
     <div class="row">
         <div class="col-md-8 push-md-2 col-xl-6 push-xl-3">
index 2405f5f..d8c49a7 100644 (file)
@@ -1,3 +1,14 @@
+{{!
+    @template core/skip_links
+
+    Example context (json):
+    {
+        "links": [
+            {"url": "http://example.com/link1", "text": "Link 1"},
+            {"url": "http://example.com/link2", "text": "Link 2"}
+        ]
+    }
+}}
 <div>
 {{#links}}
     <a class="sr-only sr-only-focusable" href="#{{{url}}}">{{{text}}}</a>
index 918f272..d22db82 100644 (file)
@@ -7,7 +7,7 @@
         <input type="hidden" name="{{element.name}}" value="{{element.frozenvalue}}">
     {{/element.frozen}}
 {{/element.hardfrozen}}
-<input type="checkbox" name="{{element.name}}"
+<input type="checkbox" name="{{element.name}}" class="{{element.extraclasses}}"
     id="{{element.id}}"
     {{#element.selectedvalue}}
         value="{{element.selectedvalue}}"
index a6cb35c..8ec8fe9 100644 (file)
@@ -18,6 +18,7 @@
         {{/element.hardfrozen}}
         <input type="checkbox"
             name="{{element.name}}"
+            class="{{element.extraclasses}}"
             {{#element.selectedvalue}}
                 value="{{element.selectedvalue}}"
             {{/element.selectedvalue}}
index 8e098f0..150d2a0 100644 (file)
@@ -4,7 +4,7 @@
         <input type="hidden" name="{{element.name}}" value="{{element.frozenvalue}}">
     {{/element.frozen}}
 {{/element.hardfrozen}}
-<input type="checkbox" name="{{element.name}}"
+<input type="checkbox" name="{{element.name}}" class="{{element.extraclasses}}"
     id="{{element.id}}"
     {{#element.value}}
         value="{{element.value}}"
index 3a63cc4..ad55331 100644 (file)
@@ -15,6 +15,7 @@
         {{/element.hardfrozen}}
         <input type="checkbox"
             name="{{element.name}}"
+            class="{{element.extraclasses}}"
             {{#element.value}}
                 value="{{element.value}}"
             {{/element.value}}
index e22c726..f95ed92 100644 (file)
@@ -1,3 +1,9 @@
+{{!
+    @template theme_boost/nav-drawer
+
+
+    Example context (json): {}
+}}
 <div id="nav-drawer" data-region="drawer" class="hidden-print moodle-has-zindex {{^navdraweropen}}closed{{/navdraweropen}}" aria-hidden="{{#navdraweropen}}false{{/navdraweropen}}{{^navdraweropen}}true{{/navdraweropen}}" tabindex="-1">
     {{> theme_boost/flat_navigation }}
 </div>
index ea2ac8e..bc20577 100644 (file)
@@ -934,89 +934,7 @@ tr.flagged-tag a {
     text-align: left;
     border: 0 solid black;
 }
-/**
-* Smart Select Element
-*/
-.smartselect {
-    position: absolute;
-}
-.smartselect .smartselect_mask {
-    background-color: #fff;
-}
-.smartselect ul {
-    padding: 0;
-    margin: 0;
-}
-.smartselect ul li {
-    list-style: none;
-}
-.smartselect .smartselect_menu {
-    margin-right: 5px;
-}
-.safari .smartselect .smartselect_menu {
-    margin-left: 2px;
-}
-.smartselect .smartselect_menu,
-.smartselect .smartselect_submenu {
-    border: 1px solid #000;
-    background-color: #fff;
-    display: none;
-}
-.smartselect .smartselect_menu.visible,
-.smartselect .smartselect_submenu.visible {
-    display: block;
-}
-.smartselect .smartselect_menu_content ul li {
-    position: relative;
-    padding: 2px 5px;
-}
-.smartselect .smartselect_menu_content ul li a {
-    color: #333;
-    text-decoration: none;
-}
-.smartselect .smartselect_menu_content ul li a.selectable {
-    color: inherit;
-}
-.smartselect .smartselect_submenuitem {
-    background-image: url([[pix:moodle|t/collapsed]]);
-    background-repeat: no-repeat;
-    background-position: 100%;
-}
-/** Spanning mode */
-.smartselect.spanningmenu .smartselect_submenu {
-    position: absolute;
-    top: -1px;
-    left: 100%;
-}
-.smartselect.spanningmenu .smartselect_submenu a {
-    white-space: nowrap;
-    padding-right: 16px;
-}
-.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover {
-    text-decoration: underline;
-}
-/** Compact mode */
-.smartselect.compactmenu .smartselect_submenu {
-    position: relative;
-    margin: 2px -3px;
-    margin-left: 10px;
-    display: none;
-    border-width: 0;
-    z-index: 1010;
-}
-.smartselect.compactmenu .smartselect_submenu.visible {
-    display: block;
-}
-.smartselect.compactmenu .smartselect_menu {
-    z-index: 1000;
-    overflow: hidden;
-}
-.smartselect.compactmenu .smartselect_submenu .smartselect_submenu {
-    z-index: 1020;
-}
-.smartselect.compactmenu .smartselect_submenuitem:hover > .smartselect_menuitem_label {
-    font-weight: bold;
-}
+
 /**
 * Registration
 */
index 299592d..fd9e7b8 100644 (file)
@@ -343,86 +343,48 @@ fieldset.coursesearchbox label {
 
 /* Section and module editing forms contain special JS components for the
    availability system (if enabled). */
-#fitem_id_availabilityconditionsjson {
-    *[aria-hidden=true] {
-        display: none;
-    }
-    select,
-    input[type=text] {
-        position: relative;
-        top: 4px;
-    }
-    label {
-        display: inline;
-    }
-    br + .availability-group {
-        display: inline-block;
-        margin-top: 8px;
-        margin-bottom: 8px;
-    }
-    .availability-group {
-        margin-right: 8px;
-    }
-    .availability-item {
-        margin-bottom: 6px;
-    }
-    .availability-none {
-        margin-left: 20px;
-        margin-bottom: 4px;
-    }
-    .availability-plugincontrols {
-        min-height: 40px;
-        padding: 2px 0 0 4px;
-        background: none repeat scroll 0% 0% @wellBackground;
-        border: 1px solid @grayLighter;
-        border-radius: 4px;
-        display: inline-block;
-        margin-right: 8px;
-        select {
-            width: auto;
-            max-width: 200px;
-        }
-    }
-    /* Eye icon in front of an item and delete icon after it. */
-    .availability-eye,
-    .availability-delete {
-        margin-right: 8px;
-    }
-    /* Hidden eye icon still takes up space. */
-    .availability-eye[aria-hidden=true] {
-        display: inline;
-        visibility: hidden;
-    }
-    /* Eye icons in front of child lists are aligned specially. */
-    .availability-list > .availability-eye img {
-        vertical-align: top;
-        margin-top: 12px;
-    }
-    /* Add button lines up with child elements. */
-    .availability-button {
-        margin-left: 15px;
-    }
-    /* Nested section is grey. */
-    .availability-childlist > .availability-inner {
-        display: inline-block;
-        background: @wellBackground;
-        border: 1px solid @grayLighter;
-        border-radius: 4px;
-        padding: 6px;
-        margin-bottom: 6px;
-    }
-    /* Second (and more) levels of nested sections are white. */
-    .availability-childlist .availability-childlist > .availability-inner {
-        background: white;
-    }
-    /* Connecting text needs to be indented. */
-    .availability-connector {
-        margin-left: 20px;
-        margin-bottom: 6px;
+#id_availabilityconditionsjson[aria-hidden=true],
+.availability-field [aria-hidden=true] {
+    display: none;
+}
+.availability-eye,
+.availability-delete {
+    margin-right: 8px;
+}
+/* Eye icons in front of child lists are aligned specially. */
+.availability-list > .availability-eye img {
+    vertical-align: top;
+    margin-top: 12px;
+}
+.availability-plugincontrols {
+    min-height: 40px;
+    padding: 2px 0 0 4px;
+    background: none repeat scroll 0% 0% @wellBackground;
+    border: 1px solid @grayLighter;
+    border-radius: 4px;
+    display: inline-block;
+    margin-right: 8px;
+    select {
+        width: auto;
+        max-width: 200px;
     }
 }
-
-
+.availability-field .availability-plugincontrols .availability-group select {
+    max-width: 12rem;
+}
+/* Nested section is grey. */
+.availability-childlist > .availability-inner {
+    display: inline-block;
+    background: @wellBackground;
+    border: 1px solid @grayLighter;
+    border-radius: 4px;
+    padding: 6px;
+    margin-bottom: 6px;
+}
+/* Second (and more) levels of nested sections are white. */
+.availability-childlist .availability-childlist > .availability-inner {
+    background: white;
+}
 /* Default form styling colours all text red. With availability conditions
    this looks excessive as we show 'Invalid' markers in specific places. */
 .mform .error .availability-field {
index 677e340..0faee0d 100644 (file)
@@ -895,89 +895,6 @@ tr.flagged-tag a {
   border: 0 solid black;
 }
 /**
-* Smart Select Element
-*/
-.smartselect {
-  position: absolute;
-}
-.smartselect .smartselect_mask {
-  background-color: #fff;
-}
-.smartselect ul {
-  padding: 0;
-  margin: 0;
-}
-.smartselect ul li {
-  list-style: none;
-}
-.smartselect .smartselect_menu {
-  margin-right: 5px;
-}
-.safari .smartselect .smartselect_menu {
-  margin-left: 2px;
-}
-.smartselect .smartselect_menu,
-.smartselect .smartselect_submenu {
-  border: 1px solid #000;
-  background-color: #fff;
-  display: none;
-}
-.smartselect .smartselect_menu.visible,
-.smartselect .smartselect_submenu.visible {
-  display: block;
-}
-.smartselect .smartselect_menu_content ul li {
-  position: relative;
-  padding: 2px 5px;
-}
-.smartselect .smartselect_menu_content ul li a {
-  color: #333;
-  text-decoration: none;
-}
-.smartselect .smartselect_menu_content ul li a.selectable {
-  color: inherit;
-}
-.smartselect .smartselect_submenuitem {
-  background-image: url([[pix:moodle|t/collapsed]]);
-  background-repeat: no-repeat;
-  background-position: 100%;
-}
-/** Spanning mode */
-.smartselect.spanningmenu .smartselect_submenu {
-  position: absolute;
-  top: -1px;
-  left: 100%;
-}
-.smartselect.spanningmenu .smartselect_submenu a {
-  white-space: nowrap;
-  padding-right: 16px;
-}
-.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover {
-  text-decoration: underline;
-}
-/** Compact mode */
-.smartselect.compactmenu .smartselect_submenu {
-  position: relative;
-  margin: 2px -3px;
-  margin-left: 10px;
-  display: none;
-  border-width: 0;
-  z-index: 1010;
-}
-.smartselect.compactmenu .smartselect_submenu.visible {
-  display: block;
-}
-.smartselect.compactmenu .smartselect_menu {
-  z-index: 1000;
-  overflow: hidden;
-}
-.smartselect.compactmenu .smartselect_submenu .smartselect_submenu {
-  z-index: 1020;
-}
-.smartselect.compactmenu .smartselect_submenuitem:hover > .smartselect_menuitem_label {
-  font-weight: bold;
-}
-/**
 * Registration
 */
 #page-admin-registration-register .registration_textfield {
@@ -14118,42 +14035,20 @@ fieldset.coursesearchbox label {
 }
 /* Section and module editing forms contain special JS components for the
    availability system (if enabled). */
-#fitem_id_availabilityconditionsjson {
-  /* Eye icon in front of an item and delete icon after it. */
-  /* Hidden eye icon still takes up space. */
-  /* Eye icons in front of child lists are aligned specially. */
-  /* Add button lines up with child elements. */
-  /* Nested section is grey. */
-  /* Second (and more) levels of nested sections are white. */
-  /* Connecting text needs to be indented. */
-}
-#fitem_id_availabilityconditionsjson *[aria-hidden=true] {
+#id_availabilityconditionsjson[aria-hidden=true],
+.availability-field [aria-hidden=true] {
   display: none;
 }
-#fitem_id_availabilityconditionsjson select,
-#fitem_id_availabilityconditionsjson input[type=text] {
-  position: relative;
-  top: 4px;
-}
-#fitem_id_availabilityconditionsjson label {
-  display: inline;
-}
-#fitem_id_availabilityconditionsjson br + .availability-group {
-  display: inline-block;
-  margin-top: 8px;
-  margin-bottom: 8px;
-}
-#fitem_id_availabilityconditionsjson .availability-group {
+.availability-eye,
+.availability-delete {
   margin-right: 8px;
 }
-#fitem_id_availabilityconditionsjson .availability-item {
-  margin-bottom: 6px;
-}
-#fitem_id_availabilityconditionsjson .availability-none {
-  margin-left: 20px;
-  margin-bottom: 4px;
+/* Eye icons in front of child lists are aligned specially. */
+.availability-list > .availability-eye img {
+  vertical-align: top;
+  margin-top: 12px;
 }
-#fitem_id_availabilityconditionsjson .availability-plugincontrols {
+.availability-plugincontrols {
   min-height: 40px;
   padding: 2px 0 0 4px;
   background: none repeat scroll 0% 0% #f5f5f5;
@@ -14162,26 +14057,15 @@ fieldset.coursesearchbox label {
   display: inline-block;
   margin-right: 8px;
 }
-#fitem_id_availabilityconditionsjson .availability-plugincontrols select {
+.availability-plugincontrols select {
   width: auto;
   max-width: 200px;
 }
-#fitem_id_availabilityconditionsjson .availability-eye,
-#fitem_id_availabilityconditionsjson .availability-delete {
-  margin-right: 8px;
+.availability-field .availability-plugincontrols .availability-group select {
+  max-width: 12rem;
 }
-#fitem_id_availabilityconditionsjson .availability-eye[aria-hidden=true] {
-  display: inline;
-  visibility: hidden;
-}
-#fitem_id_availabilityconditionsjson .availability-list > .availability-eye img {
-  vertical-align: top;
-  margin-top: 12px;
-}
-#fitem_id_availabilityconditionsjson .availability-button {
-  margin-left: 15px;
-}
-#fitem_id_availabilityconditionsjson .availability-childlist > .availability-inner {
+/* Nested section is grey. */
+.availability-childlist > .availability-inner {
   display: inline-block;
   background: #f5f5f5;
   border: 1px solid #eee;
@@ -14189,13 +14073,10 @@ fieldset.coursesearchbox label {
   padding: 6px;
   margin-bottom: 6px;
 }
-#fitem_id_availabilityconditionsjson .availability-childlist .availability-childlist > .availability-inner {
+/* Second (and more) levels of nested sections are white. */
+.availability-childlist .availability-childlist > .availability-inner {
   background: white;
 }
-#fitem_id_availabilityconditionsjson .availability-connector {
-  margin-left: 20px;
-  margin-bottom: 6px;
-}
 /* Default form styling colours all text red. With availability conditions
    this looks excessive as we show 'Invalid' markers in specific places. */
 .mform .error .availability-field {
index ba6240c..5659302 100644 (file)
     You should have received a copy of the GNU General Public License
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
-<ul class="nav nav-tabs">
+{{!
+    @template theme_boost/admin_setting_tabs
+
+
+    Example context (json):
+    {
+        "tabs": [
+        {
+            "name": "tab1",
+            "active": 0,
+            "displayname": "Inactive tab1",
+            "html": "<p>Tab 1 content</p>"
+         },
+         {
+            "name": "tab2",
+            "active": 1,
+            "displayname": "Active tab2",
+            "html": "<p>Tab 2 content</p>"
+         }
+        ]
+    }
+}}
+<ul class="nav nav-tabs" role="tablist">
     {{#tabs}}
         <li class="{{#active}}active{{/active}}">
             <a href="#{{name}}" data-toggle="tab" role="tab">{{displayname}}</a>
index bb1ea9a..4c751e0 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016122200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017010600.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.3dev (Build: 20161222)'; // Human-friendly version name
+$release  = '3.3dev (Build: 20170106)'; // Human-friendly version name
 
 $branch   = '33';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.