Merge branch 'MDL-24064-master' of git://github.com/FMCorz/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 13 Jun 2016 01:17:53 +0000 (09:17 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 13 Jun 2016 01:17:53 +0000 (09:17 +0800)
232 files changed:
.travis.yml
admin/roles/classes/preset.php
admin/tool/customlang/db/upgrade.php
admin/tool/log/db/upgrade.php
admin/tool/log/store/database/db/upgrade.php
admin/tool/log/store/standard/db/upgrade.php
admin/tool/lp/amd/build/user_competency_course_navigation.min.js
admin/tool/lp/amd/src/user_competency_course_navigation.js
admin/tool/lp/styles.css
admin/tool/lp/templates/user_competency_summary.mustache
admin/tool/lp/templates/user_competency_summary_in_course.mustache
admin/tool/monitor/db/upgrade.php
auth/cas/db/upgrade.php
auth/ldap/db/upgrade.php
auth/manual/db/upgrade.php
auth/mnet/db/upgrade.php
backup/backup.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_qtype_plugin.class.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.expectation [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.test [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.expectation [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.test [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.expectation [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.test [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.expectation [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.test [new file with mode: 0644]
backup/moodle2/tests/restore_gradebook_structure_step_test.php [new file with mode: 0644]
blocks/badges/db/upgrade.php
blocks/blog_recent/tests/behat/block_blog_recent.feature [new file with mode: 0644]
blocks/blog_recent/tests/behat/block_blog_recent_activity.feature [new file with mode: 0644]
blocks/blog_recent/tests/behat/block_blog_recent_course.feature [new file with mode: 0644]
blocks/blog_recent/tests/behat/block_blog_recent_frontpage.feature [new file with mode: 0644]
blocks/calendar_month/db/upgrade.php
blocks/calendar_upcoming/db/upgrade.php
blocks/comments/tests/behat/block_comment_dashboard.feature [new file with mode: 0644]
blocks/community/db/upgrade.php
blocks/completionstatus/db/upgrade.php
blocks/course_overview/tests/behat/quiz_overview.feature [new file with mode: 0644]
blocks/course_summary/db/upgrade.php
blocks/glossary_random/backup/moodle2/restore_glossary_random_block_task.class.php
blocks/glossary_random/block_glossary_random.php
blocks/glossary_random/tests/behat/glossary_random_global.feature [new file with mode: 0644]
blocks/html/db/upgrade.php
blocks/lp/competencies_to_review.php
blocks/lp/plans_to_review.php
blocks/messages/tests/behat/block_messages_course.feature [new file with mode: 0644]
blocks/messages/tests/behat/block_messages_dashboard.feature [new file with mode: 0644]
blocks/messages/tests/behat/block_messages_frontpage.feature [new file with mode: 0644]
blocks/navigation/amd/build/navblock.min.js
blocks/navigation/amd/src/navblock.js
blocks/navigation/block_navigation.php
blocks/navigation/db/upgrade.php
blocks/navigation/styles.css
blocks/news_items/block_news_items.php
blocks/online_users/tests/behat/block_online_users_course.feature [new file with mode: 0644]
blocks/online_users/tests/behat/block_online_users_dashboard.feature [new file with mode: 0644]
blocks/online_users/tests/behat/block_online_users_frontpage.feature [new file with mode: 0644]
blocks/private_files/tests/behat/block_private_files_activity.feature [new file with mode: 0644]
blocks/private_files/tests/behat/block_private_files_course.feature [new file with mode: 0644]
blocks/private_files/tests/behat/block_private_files_dashboard.feature [new file with mode: 0644]
blocks/private_files/tests/behat/block_private_files_frontpage.feature [new file with mode: 0644]
blocks/private_files/tests/fixtures/testfile.txt [new file with mode: 0644]
blocks/quiz_results/db/upgrade.php
blocks/recent_activity/db/upgrade.php
blocks/rss_client/db/upgrade.php
blocks/section_links/db/upgrade.php
blocks/selfcompletion/db/upgrade.php
blocks/settings/amd/build/settingsblock.min.js
blocks/settings/amd/src/settingsblock.js
blocks/settings/block_settings.php
blocks/settings/db/upgrade.php
blocks/settings/styles.css
comment/comment_post.php
course/externallib.php
course/format/lib.php
enrol/database/db/upgrade.php
enrol/flatfile/db/upgrade.php
enrol/guest/db/upgrade.php
enrol/imsenterprise/db/upgrade.php
enrol/manual/ajax.php
enrol/manual/db/upgrade.php
enrol/manual/lib.php
enrol/manual/manage.php
enrol/manual/yui/quickenrolment/quickenrolment.js
enrol/mnet/db/upgrade.php
enrol/paypal/db/upgrade.php
enrol/self/db/upgrade.php
filter/mediaplugin/db/upgrade.php
filter/tex/db/upgrade.php
grade/grading/form/guide/db/upgrade.php
grade/grading/form/rubric/db/upgrade.php
grade/lib.php
grade/report/user/db/upgrade.php
install/lang/ca/admin.php
install/lang/ca/install.php
install/lang/he/admin.php
install/lang/he/install.php
install/lang/he/moodle.php
install/lang/oc_lnc/error.php
install/lang/pt/install.php
install/lang/sv/moodle.php
lang/en/error.php
lang/en/question.php
lib/accesslib.php
lib/antivirus/clamav/db/upgrade.php
lib/blocklib.php
lib/classes/session/memcached.php
lib/db/upgrade.php
lib/editor/atto/autosave-ajax.php
lib/editor/atto/db/upgrade.php
lib/editor/atto/plugins/equation/db/upgrade.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/build.json
lib/editor/atto/yui/src/editor/js/autosave-io.js [new file with mode: 0644]
lib/editor/atto/yui/src/editor/js/autosave.js
lib/editor/atto/yui/src/editor/meta/editor.json
lib/editor/tinymce/db/upgrade.php
lib/editor/tinymce/plugins/spellchecker/db/upgrade.php
lib/medialib.php
lib/myprofilelib.php
lib/questionlib.php
lib/tests/behat/behat_hooks.php
lib/tests/questionlib_test.php
lib/tests/weblib_format_text_test.php
lib/upgrade.txt
lib/weblib.php
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js
lib/yui/build/moodle-core-dock/moodle-core-dock.js
lib/yui/build/moodle-core-event/moodle-core-event-debug.js
lib/yui/build/moodle-core-event/moodle-core-event-min.js
lib/yui/build/moodle-core-event/moodle-core-event.js
lib/yui/src/dock/js/dock.js
lib/yui/src/dock/meta/dock.json
lib/yui/src/event/js/event.js
message/externallib.php
message/lib.php
message/output/email/db/upgrade.php
message/output/jabber/db/upgrade.php
message/output/popup/db/upgrade.php
mod/assign/db/upgrade.php
mod/assign/feedback/comments/db/upgrade.php
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/file/db/upgrade.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/submission/comments/db/upgrade.php
mod/assign/submission/file/db/upgrade.php
mod/assign/submission/onlinetext/db/upgrade.php
mod/assign/templates/grading_navigation.mustache
mod/assign/tests/base_test.php
mod/assign/tests/locallib_test.php
mod/assignment/db/upgrade.php
mod/book/db/upgrade.php
mod/chat/db/upgrade.php
mod/chat/lib.php
mod/choice/db/upgrade.php
mod/choice/lib.php
mod/data/data.js
mod/data/db/upgrade.php
mod/feedback/db/upgrade.php
mod/folder/db/upgrade.php
mod/forum/db/upgrade.php
mod/forum/lib.php
mod/forum/styles.css
mod/glossary/db/upgrade.php
mod/imscp/db/upgrade.php
mod/label/db/upgrade.php
mod/lesson/db/upgrade.php
mod/lti/db/upgrade.php
mod/lti/locallib.php
mod/page/db/upgrade.php
mod/quiz/db/upgrade.php
mod/quiz/lib.php
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/overview/report.php
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/tests/lib_test.php
mod/resource/db/upgrade.php
mod/scorm/db/upgrade.php
mod/survey/db/upgrade.php
mod/url/db/upgrade.php
mod/wiki/db/upgrade.php
mod/workshop/db/upgrade.php
mod/workshop/form/accumulative/db/upgrade.php
mod/workshop/form/comments/db/upgrade.php
mod/workshop/form/numerrors/db/upgrade.php
mod/workshop/form/rubric/db/upgrade.php
mod/workshop/lib.php
mod/workshop/mod_form.php
my/lib.php
portfolio/boxnet/db/upgrade.php
portfolio/download/lib.php
portfolio/googledocs/db/upgrade.php
portfolio/picasa/db/upgrade.php
question/behaviour/manualgraded/db/upgrade.php
question/category.php
question/format.php
question/format/gift/format.php
question/format/gift/tests/giftformat_test.php
question/type/calculated/db/upgrade.php
question/type/ddmarker/db/upgrade.php
question/type/essay/db/upgrade.php
question/type/match/db/upgrade.php
question/type/multianswer/backup/moodle2/restore_qtype_multianswer_plugin.class.php
question/type/multianswer/db/upgrade.php
question/type/multichoice/db/upgrade.php
question/type/numerical/db/upgrade.php
question/type/random/db/upgrade.php
question/type/randomsamatch/db/upgrade.php
question/type/shortanswer/db/upgrade.php
report/competency/amd/build/user_course_navigation.min.js
report/competency/amd/src/user_course_navigation.js
repository/alfresco/db/upgrade.php
repository/boxnet/db/upgrade.php
repository/dropbox/db/upgrade.php
repository/googledocs/db/upgrade.php
repository/picasa/db/upgrade.php
theme/more/db/upgrade.php
user/profile/field/datetime/field.class.php
version.php
webservice/rest/locallib.php
webservice/rest/tests/server_test.php [new file with mode: 0644]

index 47f3462..d1438f3 100644 (file)
@@ -27,16 +27,19 @@ env:
     # Postgres is significantly is pretty reasonable in its run-time.
 
     # Run unit tests on MySQL
-    - DB=mysqli   PHPUNIT=true    INSTALL=false   CITEST=false
+    - DB=mysqli   TASK=PHPUNIT
 
     # Run CI Tests without running PHPUnit.
-    - DB=none     PHPUNIT=false   INSTALL=false   CITEST=true
+    - DB=none     TASK=CITEST
 
     # Run unit tests on Postgres
-    - DB=pgsql    PHPUNIT=true    INSTALL=false   CITEST=false
+    - DB=pgsql    TASK=PHPUNIT
 
     # Perform an upgrade test too.
-    - DB=pgsql    PHPUNIT=false   INSTALL=true   CITEST=false   UPGRADE=true
+    - DB=pgsql    TASK=UPGRADE
+
+    # Run a check for unbuilt files with Grunt.
+    - DB=none     TASK=GRUNT
 
 matrix:
     # Enable fast finish.
@@ -47,22 +50,26 @@ matrix:
     exclude:
         # MySQL - it's just too slow.
         # Exclude it on all versions except for 7.0
-        # - env: DB=mysqli   PHPUNIT=true    INSTALL=false   CITEST=false
+        # - env: DB=mysqli   TASK=PHPUNIT
         #   php: 5.6
         #
-        # - env: DB=mysqli   PHPUNIT=true    INSTALL=false   CITEST=false
+        # - env: DB=mysqli   TASK=PHPUNIT
         #   php: 5.5
 
-        - env: DB=mysqli   PHPUNIT=true    INSTALL=false   CITEST=false
+        - env: DB=mysqli   TASK=PHPUNIT
+          php: 5.4
+
+        - env: DB=none     TASK=GRUNT
           php: 5.4
 
         # Moodle 2.7 is not compatible with PHP 7 for the upgrade test.
-        - env: DB=pgsql    PHPUNIT=false   INSTALL=true   CITEST=false   UPGRADE=true
+        - env: DB=pgsql    TASK=UPGRADE
           php: 7.0
 
 cache:
     directories:
       - $HOME/.composer/cache
+      - $HOME/.npm
 
 install:
     # Disable xdebug. We aren't generating code coverage, and it has a huge impact upon test performance.
@@ -70,6 +77,7 @@ install:
 
     # Set the encrypted GITHUB_TOKEN if it's available to raise the API limit.
     - if [ -n "$GITHUB_APITOKEN" ]; then composer config github-oauth.github.com $GITHUB_APITOKEN; fi
+    - echo 'auth.json' >> .git/info/exclude
 
     # Install composer dependencies.
     # We need --no-interaction in case we hit API limits for composer. This causes it to fall back to a standard clone.
@@ -78,7 +86,7 @@ install:
 
 before_script:
     - >
-      if [ "$INSTALL" = 'true' -o "$PHPUNIT" = 'true' ];
+      if [ "$TASK" = 'PHPUNIT' -o "$TASK" = 'UPGRADE' ];
       then
         # Copy generic configuration in place.
         cp config-dist.php config.php ;
@@ -120,38 +128,37 @@ before_script:
           mysql -u root -e 'SET GLOBAL innodb_file_per_table=ON;' ;
           mysql -e 'CREATE DATABASE travis_ci_test DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_bin;' ;
         fi
+      fi
 
-        if [ "$PHPUNIT" = 'true' ];
-        then
-          # Create a directory for the phpunit dataroot.
-          mkdir -p "$HOME"/roots/phpunit
+    - >
+      if [ "$TASK" = 'PHPUNIT' ];
+      then
+        # Create a directory for the phpunit dataroot.
+        mkdir -p "$HOME"/roots/phpunit
 
-          # The phpunit dataroot and prefix..
-          sed -i \
-            -e "/require_once/i \\\$CFG->phpunit_dataroot = '\/home\/travis\/roots\/phpunit';" \
-            -e "/require_once/i \\\$CFG->phpunit_prefix = 'p_';" \
-            config.php ;
+        # The phpunit dataroot and prefix..
+        sed -i \
+          -e "/require_once/i \\\$CFG->phpunit_dataroot = '\/home\/travis\/roots\/phpunit';" \
+          -e "/require_once/i \\\$CFG->phpunit_prefix = 'p_';" \
+          config.php ;
 
-          # Initialise PHPUnit for Moodle.
-          php admin/tool/phpunit/cli/init.php
-        fi
+        # Initialise PHPUnit for Moodle.
+        php admin/tool/phpunit/cli/init.php
       fi
 
-script:
-    ########################################################################
-    # PHPUnit
-    ########################################################################
     - >
-      if [ "$PHPUNIT" = 'true' ];
+      if [ "$TASK" = 'GRUNT' ];
       then
-        vendor/bin/phpunit;
+        npm install ;
+        npm install -g grunt ;
+        grunt ;
       fi
 
     ########################################################################
     # CI Tests
     ########################################################################
     - >
-      if [ "$CITEST" = 'true' ];
+      if [ "$TASK" = 'CITEST' ];
       then
         # Note - this is deliberately placed in the script section as we
         # should not add any code until after phpunit has run.
@@ -174,18 +181,11 @@ script:
         export phpcmd=`which php`;
       fi
 
-    # Actually run the CI Tests - do this outside of the main test to make output clearer.
-    - >
-      if [ "$CITEST" = 'true' ];
-      then
-        bash local/ci/php_lint/php_lint.sh;
-      fi
-
     ########################################################################
     # Upgrade test
     ########################################################################
     - >
-      if [ "$UPGRADE" = 'true' ];
+      if [ "$TASK" = 'UPGRADE' ];
       then
         # We need the official upstream.
         git remote add upstream https://github.com/moodle/moodle.git;
@@ -205,7 +205,36 @@ script:
 
         # The local_ci repository can be used to check upgrade savepoints.
         git clone https://github.com/moodlehq/moodle-local_ci.git local/ci ;
+      fi
 
+script:
+    - >
+      if [ "$TASK" = 'PHPUNIT' ];
+      then
+        vendor/bin/phpunit;
+      fi
+
+    - >
+      if [ "$TASK" = 'CITEST' ];
+      then
+        bash local/ci/php_lint/php_lint.sh;
+      fi
+
+    - >
+      if [ "$TASK" = 'GRUNT' ];
+      then
+        # Add all files to the git index and then run diff --cached to see all changes.
+        # This ensures that we get the status of all files, including new files.
+        git add . ;
+        git diff --cached --exit-code ;
+      fi
+
+    ########################################################################
+    # Upgrade test
+    ########################################################################
+    - >
+      if [ "$TASK" = 'UPGRADE' ];
+      then
         cp local/ci/check_upgrade_savepoints/check_upgrade_savepoints.php ./check_upgrade_savepoints.php
         result=`php check_upgrade_savepoints.php`;
         # Check if there are problems
index 85904eb..256f635 100644 (file)
@@ -71,8 +71,9 @@ class core_role_preset {
         $dom->appendChild($top);
 
         $top->appendChild($dom->createElement('shortname', $role->shortname));
-        $top->appendChild($dom->createElement('name', $role->name));
-        $top->appendChild($dom->createElement('description', $role->description));
+        $top->appendChild($dom->createElement('name', htmlspecialchars($role->name, ENT_COMPAT | ENT_HTML401, 'UTF-8')));
+        $top->appendChild($dom->createElement('description', htmlspecialchars($role->description, ENT_COMPAT | ENT_HTML401,
+                'UTF-8')));
         $top->appendChild($dom->createElement('archetype', $role->archetype));
 
         $contextlevels = $dom->createElement('contextlevels');
index b3c5c03..b63626f 100644 (file)
@@ -38,5 +38,8 @@ function xmldb_tool_customlang_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index ef219e0..042ed97 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_tool_log_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index dc92035..21e34e0 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_logstore_database_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 4e23361..4834535 100644 (file)
@@ -53,5 +53,8 @@ function xmldb_logstore_standard_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2016041200, 'logstore', 'standard');
     }
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index e11b466..674172f 100644 (file)
Binary files a/admin/tool/lp/amd/build/user_competency_course_navigation.min.js and b/admin/tool/lp/amd/build/user_competency_course_navigation.min.js differ
index a7a43ba..bf61fe5 100644 (file)
@@ -75,8 +75,6 @@ define(['jquery'], function($) {
     UserCompetencyCourseNavigation.prototype._courseId = null;
     /** @type {String} Plugin base url. */
     UserCompetencyCourseNavigation.prototype._baseUrl = null;
-    /** @type {Boolean} Ignore the first change event for users. */
-    UserCompetencyCourseNavigation.prototype._ignoreFirstUser = null;
     /** @type {Boolean} Ignore the first change event for competencies. */
     UserCompetencyCourseNavigation.prototype._ignoreFirstCompetency = null;
 
index 701fc99..39d4e6e 100644 (file)
 .path-admin-tool-lp [data-region="competencylinktree"] select {
     width: 100%;
 }
+.path-admin-tool-lp [data-region] .generaltable.fullwidth {
+    clear: both;
+}
 
 .path-admin-tool-lp .competency-rule-points {
     margin-top: 10px;
index 4be2d6f..9097caa 100644 (file)
@@ -64,7 +64,7 @@
         <dt>{{#str}}rating, tool_lp{{/str}}</dt>
         <dd>{{gradename}}
             {{#cangrade}}
-                <buttn class="btn" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
+                <button class="btn" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
             {{/cangrade}}
         </dd>
         {{#js}}
index 0d05efc..03f63e4 100644 (file)
@@ -75,7 +75,7 @@
         <dt>{{#str}}rating, tool_lp{{/str}}</dt>
         <dd>{{gradename}}
             {{#cangrade}}
-                <buttn class="btn" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
+                <button class="btn" id="rate_{{uniqid}}">{{#str}}rate, tool_lp{{/str}}</button>
             {{/cangrade}}
         </dd>
         {{/usercompetencycourse}}
index f02556d..9f8e53b 100644 (file)
@@ -59,5 +59,8 @@ function xmldb_tool_monitor_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index d421185..22fcb7b 100644 (file)
@@ -52,5 +52,8 @@ function xmldb_auth_cas_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 762815c..3526ffc 100644 (file)
@@ -52,5 +52,8 @@ function xmldb_auth_ldap_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 3c0ae55..d9b32b2 100644 (file)
@@ -40,5 +40,8 @@ function xmldb_auth_manual_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index bfdef15..2daae51 100644 (file)
@@ -40,5 +40,8 @@ function xmldb_auth_mnet_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index a5a660d..e7dccee 100644 (file)
@@ -135,7 +135,7 @@ abstract class backup implements checksumable {
     /**
      * Usually same than major release zero version, mainly for informative/historic purposes.
      */
-    const RELEASE = '3.1';
+    const RELEASE = '3.2';
 }
 
 /*
index da219a1..ac786ed 100644 (file)
@@ -950,8 +950,11 @@ class backup_gradebook_structure_step extends backup_structure_step {
         $grade_setting = new backup_nested_element('grade_setting', 'id', array(
             'name', 'value'));
 
+        $gradebook_attributes = new backup_nested_element('attributes', null, array('calculations_freeze'));
 
         // Build the tree
+        $gradebook->add_child($gradebook_attributes);
+
         $gradebook->add_child($grade_categories);
         $grade_categories->add_child($grade_category);
 
@@ -966,14 +969,15 @@ class backup_gradebook_structure_step extends backup_structure_step {
         $gradebook->add_child($grade_settings);
         $grade_settings->add_child($grade_setting);
 
+        // Define sources
+
         // Add attribute with gradebook calculation freeze date if needed.
+        $attributes = new stdClass();
         $gradebookcalculationfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
         if ($gradebookcalculationfreeze) {
-            $gradebook->add_attributes(array('calculations_freeze'));
-            $gradebook->get_attribute('calculations_freeze')->set_value($gradebookcalculationfreeze);
+            $attributes->calculations_freeze = $gradebookcalculationfreeze;
         }
-
-        // Define sources
+        $gradebook_attributes->set_source_array([$attributes]);
 
         //Include manual, category and the course grade item
         $grade_items_sql ="SELECT * FROM {grade_items}
index 60f599a..7520aa3 100644 (file)
@@ -181,7 +181,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
                 $info = new stdClass();
                 $info->filequestionid = $oldquestionid;
                 $info->dbquestionid   = $newquestionid;
-                $info->answer         = $data->answertext;
+                $info->answer         = s($data->answertext);
                 throw new restore_step_exception('error_question_answers_missing_in_db', $info);
             }
             $newitemid = $this->questionanswercache[$data->answertext];
index 158249b..e7e1ee2 100644 (file)
@@ -121,6 +121,21 @@ class restore_gradebook_structure_step extends restore_structure_step {
             return false;
         }
 
+        // Identify the backup we're dealing with.
+        $backuprelease = floatval($this->get_task()->get_info()->backup_release); // The major version: 2.9, 3.0, ...
+        $backupbuild = 0;
+        preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
+        if (!empty($matches[1])) {
+            $backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
+        }
+
+        // On older versions the freeze value has to be converted.
+        // We do this from here as it is happening right before the file is read.
+        // This only targets the backup files that can contain the legacy freeze.
+        if ($backupbuild > 20150618 && ($backuprelease < 3.0 || $backupbuild < 20160527)) {
+            $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
+        }
+
         // Arrived here, execute the step
         return true;
      }
@@ -129,7 +144,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         $paths = array();
         $userinfo = $this->task->get_setting_value('users');
 
-        $paths[] = new restore_path_element('gradebook', '/gradebook');
+        $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
         $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
         $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
         if ($userinfo) {
@@ -141,7 +156,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         return $paths;
     }
 
-    protected function process_gradebook($data) {
+    protected function process_attributes($data) {
         // For non-merge restore types:
         // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
         $target = $this->get_task()->get_target();
@@ -581,6 +596,85 @@ class restore_gradebook_structure_step extends restore_structure_step {
             }
         }
     }
+
+    /**
+     * Rewrite step definition to handle the legacy freeze attribute.
+     *
+     * In previous backups the calculations_freeze property was stored as an attribute of the
+     * top level node <gradebook>. The backup API, however, do not process grandparent nodes.
+     * It only processes definitive children, and their parent attributes.
+     *
+     * We had:
+     *
+     * <gradebook calculations_freeze="20160511">
+     *   <grade_categories>
+     *     <grade_category id="10">
+     *       <depth>1</depth>
+     *       ...
+     *     </grade_category>
+     *   </grade_categories>
+     *   ...
+     * </gradebook>
+     *
+     * And this method will convert it to:
+     *
+     * <gradebook >
+     *   <attributes>
+     *     <calculations_freeze>20160511</calculations_freeze>
+     *   </attributes>
+     *   <grade_categories>
+     *     <grade_category id="10">
+     *       <depth>1</depth>
+     *       ...
+     *     </grade_category>
+     *   </grade_categories>
+     *   ...
+     * </gradebook>
+     *
+     * Note that we cannot just load the XML file in memory as it could potentially be huge.
+     * We can also completely ignore if the node <attributes> is already in the backup
+     * file as it never existed before.
+     *
+     * @param string $filepath The absolute path to the XML file.
+     * @return void
+     */
+    protected function rewrite_step_backup_file_for_legacy_freeze($filepath) {
+        $foundnode = false;
+        $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml';
+        $fr = fopen($filepath, 'r');
+        $fw = fopen($newfile, 'w');
+        if ($fr && $fw) {
+            while (($line = fgets($fr, 4096)) !== false) {
+                if (!$foundnode && strpos($line, '<gradebook ') === 0) {
+                    $foundnode = true;
+                    $matches = array();
+                    $pattern = '@calculations_freeze=.([0-9]+).@';
+                    if (preg_match($pattern, $line, $matches)) {
+                        $freeze = $matches[1];
+                        $line = preg_replace($pattern, '', $line);
+                        $line .= "  <attributes>\n    <calculations_freeze>$freeze</calculations_freeze>\n  </attributes>\n";
+                    }
+                }
+                fputs($fw, $line);
+            }
+            if (!feof($fr)) {
+                throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.');
+            }
+            fclose($fr);
+            fclose($fw);
+            if (!rename($newfile, $filepath)) {
+                throw new restore_step_exception('Error while attempting to rename the gradebook step file.');
+            }
+        } else {
+            if ($fr) {
+                fclose($fr);
+            }
+            if ($fw) {
+                fclose($fw);
+            }
+        }
+    }
+
 }
 
 /**
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.expectation b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.expectation
new file mode 100644 (file)
index 0000000..99e8d85
--- /dev/null
@@ -0,0 +1,10 @@
+<gradebook >
+  <attributes>
+    <calculations_freeze>20160511</calculations_freeze>
+  </attributes>
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.test b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.test
new file mode 100644 (file)
index 0000000..830eca6
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebook calculations_freeze="20160511">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.expectation b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.expectation
new file mode 100644 (file)
index 0000000..b4f21d4
--- /dev/null
@@ -0,0 +1,10 @@
+<gradebook some_other_value="false" >
+  <attributes>
+    <calculations_freeze>20160511</calculations_freeze>
+  </attributes>
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.test b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.test
new file mode 100644 (file)
index 0000000..04f3b63
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebook some_other_value="false" calculations_freeze="20160511">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.expectation b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.expectation
new file mode 100644 (file)
index 0000000..c61f19d
--- /dev/null
@@ -0,0 +1,10 @@
+<gradebook some_other_value="false"  and_another_value="42">
+  <attributes>
+    <calculations_freeze>20160511</calculations_freeze>
+  </attributes>
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.test b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.test
new file mode 100644 (file)
index 0000000..39c46bc
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebook some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.expectation b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.expectation
new file mode 100644 (file)
index 0000000..71070aa
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebookplugin some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebookplugin>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.test b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.test
new file mode 100644 (file)
index 0000000..71070aa
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebookplugin some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebookplugin>
diff --git a/backup/moodle2/tests/restore_gradebook_structure_step_test.php b/backup/moodle2/tests/restore_gradebook_structure_step_test.php
new file mode 100644 (file)
index 0000000..26856de
--- /dev/null
@@ -0,0 +1,92 @@
+<?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/>.
+
+/**
+ * Test for restore_stepslib.
+ *
+ * @package core_backup
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * Test for restore_stepslib.
+ *
+ * @package core_backup
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_backup_restore_gradebook_structure_step_testcase extends advanced_testcase {
+
+    /**
+     * Provide tests for rewrite_step_backup_file_for_legacy_freeze based upon fixtures.
+     *
+     * @return array
+     */
+    public function rewrite_step_backup_file_for_legacy_freeze_provider() {
+        $fixturesdir = realpath(__DIR__ . '/fixtures/rewrite_step_backup_file_for_legacy_freeze/');
+        $tests = [];
+        $iterator = new \RecursiveIteratorIterator(
+                new \RecursiveDirectoryIterator($fixturesdir),
+                \RecursiveIteratorIterator::LEAVES_ONLY);
+
+        foreach ($iterator as $sourcefile) {
+            $pattern = '/\.test$/';
+            if (!preg_match($pattern, $sourcefile)) {
+                continue;
+            }
+
+            $expectfile = preg_replace($pattern, '.expectation', $sourcefile);
+            $test = array($sourcefile, $expectfile);
+            $tests[basename($sourcefile)] = $test;
+        }
+
+        return $tests;
+    }
+
+    /**
+     * @dataProvider rewrite_step_backup_file_for_legacy_freeze_provider
+     * @param   string  $source     The source file to test
+     * @param   string  $expected   The expected result of the transformation
+     */
+    public function test_rewrite_step_backup_file_for_legacy_freeze($source, $expected) {
+        $restore = $this->getMockBuilder('\restore_gradebook_structure_step')
+            ->setMethods(null)
+            ->disableOriginalConstructor()
+            ->getMock()
+            ;
+
+        // Copy the file somewhere as the rewrite_step_backup_file_for_legacy_freeze will write the file.
+        $dir = make_request_directory(true);
+        $filepath = $dir . DIRECTORY_SEPARATOR . 'file.xml';
+        copy($source, $filepath);
+
+        $rc = new \ReflectionClass('\restore_gradebook_structure_step');
+        $rcm = $rc->getMethod('rewrite_step_backup_file_for_legacy_freeze');
+        $rcm->setAccessible(true);
+        $rcm->invoke($restore, $filepath);
+
+        // Check the result.
+        $this->assertFileEquals($expected, $filepath);
+    }
+}
index a9ff3a1..e62f9b8 100644 (file)
@@ -82,5 +82,8 @@ function xmldb_block_badges_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
diff --git a/blocks/blog_recent/tests/behat/block_blog_recent.feature b/blocks/blog_recent/tests/behat/block_blog_recent.feature
new file mode 100644 (file)
index 0000000..6c3f73b
--- /dev/null
@@ -0,0 +1,34 @@
+@block @block_blog_recent
+Feature: Feature: Users can use the recent blog entries block to view recent blog entries.
+  In order to enable the recent blog entries in a course
+  As a teacher
+  I can add recent blog entries block to a course
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+
+  Scenario: Add the recent blogs block to a course when blogs are disabled
+    Given I log in as "admin"
+    And the following config values are set as admin:
+      | enableblogs | 0 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Recent blog entries" block
+    Then I should see "Blogging is disabled!" in the "Recent blog entries" "block"
+
+  Scenario: Add the recent blogs block to a course when there are not any blog posts
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Recent blog entries" block
+    Then I should see "No recent entries" in the "Recent blog entries" "block"
diff --git a/blocks/blog_recent/tests/behat/block_blog_recent_activity.feature b/blocks/blog_recent/tests/behat/block_blog_recent_activity.feature
new file mode 100644 (file)
index 0000000..fae3f4a
--- /dev/null
@@ -0,0 +1,117 @@
+@block @block_blog_menu @mod_assign @block_blog_recent
+Feature: Students can use the recent blog entries block to view recent entries on an activity page
+  In order to enable the recent blog entries block an activity page
+  As a teacher
+  I can add the recent blog entries block to an activity page
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+      | student2 | Student | 2 | student2@example.com | S2 |
+    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 |
+      | student2 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment 1 |
+      | Description | Offline text |
+      | assignsubmission_file_enabled | 0 |
+    And I follow "Test assignment 1"
+    And I add the "Blog menu" block
+    And I add the "Recent blog entries" block
+    And I log out
+
+  Scenario: Students use the recent blog entries block to view blogs
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    When I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    Then I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Test assignment 1"
+    And I should see "S1 First Blog"
+    And I follow "S1 First Blog"
+    And I should see "This is my awesome blog!"
+
+  Scenario: Students only see a few entries in the recent blog entries block
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    # Blog 1 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    # Blog 2 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Second Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Second Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    # Blog 3 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Third Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Third Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    # Blog 4 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Fourth Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Fourth Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Test assignment 1"
+    And I follow "Add an entry about this Assignment"
+    # Blog 5 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Fifth Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I should see "S1 Fifth Blog"
+    And I should see "This is my awesome blog!"
+    When I follow "Test assignment 1"
+    And I should not see "S1 First Blog"
+    And I should see "S1 Second Blog"
+    And I should see "S1 Third Blog"
+    And I should see "S1 Fourth Blog"
+    And I should see "S1 Fifth Blog"
+    And I follow "S1 Fifth Blog"
+    And I should see "This is my awesome blog!"
+    Then I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Test assignment 1"
+    And I configure the "Recent blog entries" block
+    And I set the following fields to these values:
+      | id_config_numberofrecentblogentries | 2 |
+    And I press "Save changes"
+    And I should see "S1 Fourth Blog"
+    And I should see "S1 Fifth Blog"
diff --git a/blocks/blog_recent/tests/behat/block_blog_recent_course.feature b/blocks/blog_recent/tests/behat/block_blog_recent_course.feature
new file mode 100644 (file)
index 0000000..f06fad3
--- /dev/null
@@ -0,0 +1,107 @@
+@block @block_blog_menu @block_blog_recent
+Feature: Students can use the recent blog entries block to view recent entries on a course page
+  In order to enable the recent blog entries block a course page
+  As a teacher
+  I can add the recent blog entries block to a course page
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | student1 | Student | 1 | student1@example.com | S1 |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+    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 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Blog menu" block
+    And I add the "Recent blog entries" block
+    And I log out
+
+  Scenario: Students use the recent blog entries block to view blogs
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    When I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    Then I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "C1"
+    And I should see "S1 First Blog"
+    And I follow "S1 First Blog"
+    And I should see "This is my awesome blog!"
+
+  Scenario: Students only see a few entries in the recent blog entries block
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Add an entry about this course"
+    # Blog 1 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I follow "C1"
+    And I follow "Add an entry about this course"
+    # Blog 2 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Second Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Second Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "C1"
+    And I follow "Add an entry about this course"
+    # Blog 3 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Third Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Third Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "C1"
+    And I follow "Add an entry about this course"
+    # Blog 4 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Fourth Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Fourth Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "C1"
+    And I follow "Add an entry about this course"
+    # Blog 5 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Fifth Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I should see "S1 Fifth Blog"
+    And I should see "This is my awesome blog!"
+    When I follow "C1"
+    And I should not see "S1 First Blog"
+    And I should see "S1 Second Blog"
+    And I should see "S1 Third Blog"
+    And I should see "S1 Fourth Blog"
+    And I should see "S1 Fifth Blog"
+    And I follow "S1 Fifth Blog"
+    And I should see "This is my awesome blog!"
+    Then I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I configure the "Recent blog entries" block
+    And I set the following fields to these values:
+      | id_config_numberofrecentblogentries | 2 |
+    And I press "Save changes"
+    And I should see "S1 Fourth Blog"
+    And I should see "S1 Fifth Blog"
diff --git a/blocks/blog_recent/tests/behat/block_blog_recent_frontpage.feature b/blocks/blog_recent/tests/behat/block_blog_recent_frontpage.feature
new file mode 100644 (file)
index 0000000..2f5a7d5
--- /dev/null
@@ -0,0 +1,96 @@
+@block @block_blog_recent
+Feature: Feature: Students can use the recent blog entries block to view recent entries on the frontpage
+  In order to enable the recent blog entries block on the frontpage
+  As an admin
+  I can add the recent blog entries block to the frontpage
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Recent blog entries" block
+    And I log out
+
+  Scenario: Students use the recent blog entries block to view blogs
+    Given I log in as "student1"
+    And I am on site homepage
+    And I navigate to "Site blogs" node in "Site pages"
+    And I follow "Add a new entry"
+    When I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    Then I should see "S1 First Blog"
+    And I should see "This is my awesome blog!"
+    And I am on site homepage
+    And I should see "S1 First Blog"
+    And I follow "S1 First Blog"
+    And I should see "This is my awesome blog!"
+
+  Scenario: Students only see a few entries in the recent blog entries block
+    Given I log in as "student1"
+    And I am on site homepage
+    And I navigate to "Site blogs" node in "Site pages"
+    And I follow "Add a new entry"
+    # Blog 1 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 First Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I follow "Add a new entry"
+    # Blog 2 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Second Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Second Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Add a new entry"
+    # Blog 3 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Third Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Third Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Add a new entry"
+    # Blog 4 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Fourth Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I wait "1" seconds
+    And I should see "S1 Fourth Blog"
+    And I should see "This is my awesome blog!"
+    And I follow "Add a new entry"
+    # Blog 5 of 5
+    And I set the following fields to these values:
+      | Entry title | S1 Fifth Blog |
+      | Blog entry body | This is my awesome blog! |
+    And I press "Save changes"
+    And I should see "S1 Fifth Blog"
+    And I should see "This is my awesome blog!"
+    When I am on site homepage
+    And I should not see "S1 First Blog"
+    And I should see "S1 Second Blog"
+    And I should see "S1 Third Blog"
+    And I should see "S1 Fourth Blog"
+    And I should see "S1 Fifth Blog"
+    And I follow "S1 Fifth Blog"
+    And I should see "This is my awesome blog!"
+    Then I log out
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I configure the "Recent blog entries" block
+    And I set the following fields to these values:
+      | id_config_numberofrecentblogentries | 2 |
+    And I press "Save changes"
+    And I should see "S1 Fourth Blog"
+    And I should see "S1 Fifth Blog"
index c83b12a..df1c068 100644 (file)
@@ -82,5 +82,8 @@ function xmldb_block_calendar_month_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 0836409..8d57d38 100644 (file)
@@ -82,5 +82,8 @@ function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
diff --git a/blocks/comments/tests/behat/block_comment_dashboard.feature b/blocks/comments/tests/behat/block_comment_dashboard.feature
new file mode 100644 (file)
index 0000000..66e49c6
--- /dev/null
@@ -0,0 +1,29 @@
+@block @block_comments
+Feature: Enable Block comments on the dashboard and view comments
+  In order to enable the comments block on a the dashboard
+  As a teacher
+  I can add the comments block to my dashboard
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | Frist | teacher1@example.com |
+
+  Scenario: Add the comments block on the dashboard and add comments with Javascript disabled
+    When I log in as "teacher1"
+    And I press "Customise this page"
+    And I add the "Comments" block
+    And I follow "Show comments"
+    And I add "I'm a comment from the teacher" comment to comments block
+    Then I should see "I'm a comment from the teacher"
+
+  @javascript
+  Scenario: Add the comments block on the dashboard and add comments with Javascript enabled
+    When I log in as "teacher1"
+    And I press "Customise this page"
+    And I add the "Comments" block
+    And I add "I'm a comment from the teacher" comment to comments block
+    Then I should see "I'm a comment from the teacher"
index 3a3e7b1..e4e537f 100644 (file)
@@ -55,5 +55,8 @@ function xmldb_block_community_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index f44773a..9767c51 100644 (file)
@@ -57,5 +57,8 @@ function xmldb_block_completionstatus_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
diff --git a/blocks/course_overview/tests/behat/quiz_overview.feature b/blocks/course_overview/tests/behat/quiz_overview.feature
new file mode 100644 (file)
index 0000000..238591f
--- /dev/null
@@ -0,0 +1,94 @@
+@block @block_course_overview @mod_quiz
+Feature: View the quiz being due
+  In order to know what quizzes are due
+  As a student
+  I can visit my dashboard
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+      | Course 2 | C2        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | C1     | student        |
+      | student2 | C2     | student        |
+      | teacher1 | C1     | editingteacher |
+      | teacher1 | C2     | editingteacher |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                    | timeclose  |
+      | quiz     | C1     | Q1A      | Quiz 1A No deadline     | 0          |
+      | quiz     | C1     | Q1B      | Quiz 1B Past deadline   | 1337       |
+      | quiz     | C1     | Q1C      | Quiz 1C Future deadline | 9000000000 |
+      | quiz     | C1     | Q1D      | Quiz 1D Future deadline | 9000000000 |
+      | quiz     | C1     | Q1E      | Quiz 1E Future deadline | 9000000000 |
+      | quiz     | C2     | Q2A      | Quiz 2A Future deadline | 9000000000 |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | qtype     | name           | questiontext              | questioncategory |
+      | truefalse | First question | Answer the first question | Test questions   |
+    And quiz "Quiz 1A No deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1B Past deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1C Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1D Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1E Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 2A Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+
+  Scenario: View my quizzes that are due
+    Given I log in as "student1"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+    And I log out
+    And I log in as "student2"
+    And I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should not see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should see "Quiz 2A Future deadline" in the "Course overview" "block"
+
+  Scenario: View my quizzes that are due and never finished
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Quiz 1D Future deadline"
+    And I press "Attempt quiz now"
+    And I follow "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I follow "Course 1"
+    And I follow "Quiz 1E Future deadline"
+    And I press "Attempt quiz now"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+
index 1678a1b..2905aae 100644 (file)
@@ -57,5 +57,8 @@ function xmldb_block_course_summary_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index cfc5bc1..236351c 100644 (file)
@@ -60,19 +60,27 @@ class restore_glossary_random_block_task extends restore_block_task {
         if ($configdata = $DB->get_field('block_instances', 'configdata', array('id' => $blockid))) {
             $config = unserialize(base64_decode($configdata));
             if (!empty($config->glossary)) {
-                // Get glossary mapping and replace it in config
                 if ($glossarymap = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'glossary', $config->glossary)) {
-                    $mappedglossary = $DB->get_record('glossary', array('id' => $glossarymap->newitemid),
-                        'id,course,globalglossary', MUST_EXIST);
-                    $config->glossary = $mappedglossary->id;
-                    $config->courseid = $mappedglossary->course;
-                    $config->globalglossary = $mappedglossary->globalglossary;
-                    $configdata = base64_encode(serialize($config));
-                    $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid));
+                    // Get glossary mapping and replace it in config
+                    $config->glossary = $glossarymap->newitemid;
+                } else if ($this->is_samesite()) {
+                    // We are restoring on the same site, check if glossary can be used in the block in this course.
+                    $glossaryid = $DB->get_field_sql("SELECT id FROM {glossary} " .
+                        "WHERE id = ? AND (course = ? OR globalglossary = 1)",
+                        [$config->glossary, $this->get_courseid()]);
+                    if (!$glossaryid) {
+                        unset($config->glossary);
+                    }
                 } else {
                     // The block refers to a glossary not present in the backup file.
-                    $DB->set_field('block_instances', 'configdata', '', array('id' => $blockid));
+                    unset($config->glossary);
                 }
+                // Unset config variables that are no longer used.
+                unset($config->globalglossary);
+                unset($config->courseid);
+                // Save updated config.
+                $configdata = base64_encode(serialize($config));
+                $DB->set_field('block_instances', 'configdata', $configdata, array('id' => $blockid));
             }
         }
     }
index 6ca3bb5..bf9f6a7 100644 (file)
@@ -29,6 +29,12 @@ define('BGR_NEXTALPHA',    '3');
 
 class block_glossary_random extends block_base {
 
+    /**
+     * @var cm_info|stdClass has properties 'id' (course module id) and 'uservisible'
+     *     (whether the glossary is visible to the current user)
+     */
+    protected $glossarycm = null;
+
     function init() {
         $this->title = get_string('pluginname','block_glossary_random');
     }
@@ -58,6 +64,11 @@ class block_glossary_random extends block_base {
         //check if it's time to put a new entry in cache
         if (time() > $this->config->nexttime) {
 
+            if (!($cm = $this->get_glossary_cm()) || !$cm->uservisible) {
+                // Skip generating of the cache if we can't display anything to the current user.
+                return false;
+            }
+
             // place glossary concept and definition in $pref->cache
             if (!$numberofentries = $DB->count_records('glossary_entries',
                                                        array('glossaryid'=>$this->config->glossary, 'approved'=>1))) {
@@ -65,20 +76,6 @@ class block_glossary_random extends block_base {
                 $this->instance_config_commit();
             }
 
-            // Get glossary instance, if not found then return without error, as this will be handled in get_content.
-            if (!$glossary = $DB->get_record('glossary', array('id' => $this->config->glossary))) {
-                return false;
-            }
-
-            $this->config->globalglossary = $glossary->globalglossary;
-
-            // Save course id in config, so we can get correct course module.
-            $this->config->courseid = $glossary->course;
-
-            // Get module and context, to be able to rewrite urls
-            if (! $cm = get_coursemodule_from_instance('glossary', $glossary->id, $this->config->courseid)) {
-                return false;
-            }
             $glossaryctx = context_module::instance($cm->id);
 
             $limitfrom = 0;
@@ -156,88 +153,103 @@ class block_glossary_random extends block_base {
         }
     }
 
-    function instance_allow_multiple() {
-    // Are you going to allow multiple instances of each block?
-    // If yes, then it is assumed that the block WILL USE per-instance configuration
-        return true;
+    /**
+     * Replace the instance's configuration data with those currently in $this->config;
+     */
+    function instance_config_commit($nolongerused = false) {
+        // Unset config variables that are no longer used.
+        unset($this->config->globalglossary);
+        unset($this->config->courseid);
+        parent::instance_config_commit($nolongerused);
     }
 
-    function get_content() {
-        global $USER, $CFG, $DB;
-
+    /**
+     * Checks if glossary is available - it should be either located in the same course or be global
+     *
+     * @return null|cm_info|stdClass object with properties 'id' (course module id) and 'uservisible'
+     */
+    protected function get_glossary_cm() {
+        global $DB;
         if (empty($this->config->glossary)) {
-            $this->content = new stdClass();
-            if ($this->user_can_edit()) {
-                $this->content->text = get_string('notyetconfigured','block_glossary_random');
-            } else {
-                $this->content->text = '';
-            }
-            $this->content->footer = '';
-            return $this->content;
+            // No glossary is configured.
+            return null;
         }
 
-        require_once($CFG->dirroot.'/course/lib.php');
+        if (!empty($this->glossarycm)) {
+            return $this->glossarycm;
+        }
 
-        // If $this->config->globalglossary is not set then get glossary info from db.
-        if (!isset($this->config->globalglossary)) {
-            if (!$glossary = $DB->get_record('glossary', array('id' => $this->config->glossary))) {
-                return '';
-            } else {
-                $this->config->courseid = $glossary->course;
-                $this->config->globalglossary = $glossary->globalglossary;
-                $this->instance_config_commit();
+        if (!empty($this->page->course->id)) {
+            // First check if glossary belongs to the current course (we don't need to make any DB queries to find it).
+            $modinfo = get_fast_modinfo($this->page->course);
+            if (isset($modinfo->instances['glossary'][$this->config->glossary])) {
+                $this->glossarycm = $modinfo->instances['glossary'][$this->config->glossary];
+                if ($this->glossarycm->uservisible) {
+                    // The glossary is in the same course and is already visible to the current user,
+                    // no need to check if it is global, save on DB query.
+                    return $this->glossarycm;
+                }
             }
         }
 
-        $modinfo = get_fast_modinfo($this->config->courseid);
-        // If deleted glossary or non-global glossary on different course page, then reset.
-        if (!isset($modinfo->instances['glossary'][$this->config->glossary])
-                || ((empty($this->config->globalglossary) && ($this->config->courseid != $this->page->course->id)))) {
+        // Find course module id for the given glossary, only if it is global.
+        $cm = $DB->get_record_sql("SELECT cm.id, cm.visible AS uservisible
+              FROM {course_modules} cm
+                   JOIN {modules} md ON md.id = cm.module
+                   JOIN {glossary} g ON g.id = cm.instance
+             WHERE g.id = :instance AND md.name = :modulename AND g.globalglossary = 1",
+            ['instance' => $this->config->glossary, 'modulename' => 'glossary']);
+
+        if ($cm) {
+            // This is a global glossary, create an object with properties 'id' and 'uservisible'. We don't need any
+            // other information so why bother retrieving it. Full access check is skipped for global glossaries for
+            // performance reasons.
+            $this->glossarycm = $cm;
+        } else if (empty($this->glossarycm)) {
+            // Glossary does not exist. Remove it in the config so we don't repeat this check again later.
             $this->config->glossary = 0;
-            $this->config->cache = '';
             $this->instance_config_commit();
+        }
 
-            $this->content = new stdClass();
-            if ($this->user_can_edit()) {
-                $this->content->text = get_string('notyetconfigured','block_glossary_random');
-            } else {
-                $this->content->text = '';
-            }
-            $this->content->footer = '';
+        return $this->glossarycm;
+    }
+
+    function instance_allow_multiple() {
+    // Are you going to allow multiple instances of each block?
+    // If yes, then it is assumed that the block WILL USE per-instance configuration
+        return true;
+    }
+
+    function get_content() {
+        if ($this->content !== null) {
             return $this->content;
         }
+        $this->content = (object)['text' => '', 'footer' => ''];
 
-        $cm = $modinfo->instances['glossary'][$this->config->glossary];
-        if (!has_capability('mod/glossary:view', context_module::instance($cm->id))) {
-            return '';
+        if (!$cm = $this->get_glossary_cm()) {
+            if ($this->user_can_edit()) {
+                $this->content->text = get_string('notyetconfigured', 'block_glossary_random');
+            }
+            return $this->content;
         }
 
         if (empty($this->config->cache)) {
             $this->config->cache = '';
         }
 
-        if ($this->content !== NULL) {
-            return $this->content;
-        }
-
-        $this->content = new stdClass();
-
-        // Show glossary if visible and place links in footer.
-        if ($cm->visible) {
+        if ($cm->uservisible) {
+            // Show glossary if visible and place links in footer.
             $this->content->text = $this->config->cache;
             if (has_capability('mod/glossary:write', context_module::instance($cm->id))) {
-                $this->content->footer = '<a href="'.$CFG->wwwroot.'/mod/glossary/edit.php?cmid='.$cm->id
-                .'" title="'.$this->config->addentry.'">'.$this->config->addentry.'</a><br />';
-            } else {
-                $this->content->footer = '';
+                $this->content->footer = html_writer::link(new moodle_url('/mod/glossary/edit.php', ['cmid' => $cm->id]),
+                    format_string($this->config->addentry)) . '<br/>';
             }
 
-            $this->content->footer .= '<a href="'.$CFG->wwwroot.'/mod/glossary/view.php?id='.$cm->id
-                .'" title="'.$this->config->viewglossary.'">'.$this->config->viewglossary.'</a>';
-
-        // Otherwise just place some text, no link.
+            $this->content->footer .= html_writer::link(new moodle_url('/mod/glossary/view.php', ['id' => $cm->id]),
+                format_string($this->config->viewglossary));
         } else {
-            $this->content->footer = $this->config->invisible;
+            // Otherwise just place some text, no link.
+            $this->content->footer = format_string($this->config->invisible);
         }
 
         return $this->content;
diff --git a/blocks/glossary_random/tests/behat/glossary_random_global.feature b/blocks/glossary_random/tests/behat/glossary_random_global.feature
new file mode 100644 (file)
index 0000000..2eea426
--- /dev/null
@@ -0,0 +1,84 @@
+@block @block_glossary_random
+Feature: Random glossary entry block linking to global glossary
+  In order to show the entries from glossary
+  As a teacher
+  I can add the random glossary entry to a course page
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+      | Course 2 | C2        |
+    And the following "activities" exist:
+      | activity   | name             | intro                          | course               | idnumber  | globalglossary | defaultapproval |
+      | glossary   | Tips and Tricks  | Frontpage glossary description | C2 | glossary0 | 1              | 1               |
+    And the following "users" exist:
+      | username | firstname | lastname | email             |
+      | student1 | Sam1      | Student1 | student1@example.com |
+      | teacher1 | Terry1    | Teacher1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | C1     | student        |
+      | teacher1 | C1     | editingteacher |
+
+  Scenario: View random (last) entry in the global glossary
+    When I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 2"
+    And I follow "Tips and Tricks"
+    And I press "Add a new entry"
+    And I set the following fields to these values:
+      | Concept    | Never come late               |
+      | Definition | Come in time for your classes |
+    And I press "Save changes"
+    And I log out
+    # As a teacher add a block to the course page linking to the global glossary.
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Random glossary entry" block
+    And I configure the "block_glossary_random" block
+    And I set the following fields to these values:
+      | Title                           | Tip of the day      |
+      | Take entries from this glossary | Tips and Tricks     |
+      | How a new entry is chosen       | Last modified entry |
+    And I press "Save changes"
+    Then I should see "Never come late" in the "Tip of the day" "block"
+    And I should not see "Add a new entry" in the "Tip of the day" "block"
+    And I should see "View all entries" in the "Tip of the day" "block"
+    And I log out
+    # Student who can't see the module is still able to view entries in this block (because the glossary was marked as global)
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I should see "Never come late" in the "Tip of the day" "block"
+    And I should not see "Add a new entry" in the "Tip of the day" "block"
+    And I should see "View all entries" in the "Tip of the day" "block"
+    And I log out
+
+  Scenario: Removing the global glossary that is used in random glossary block
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Random glossary entry" block
+    And I configure the "block_glossary_random" block
+    And I set the following fields to these values:
+      | Title                           | Tip of the day      |
+      | Take entries from this glossary | Tips and Tricks     |
+      | How a new entry is chosen       | Last modified entry |
+    And I press "Save changes"
+    And I log out
+    And I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 2"
+    And I follow "Tips and Tricks"
+    And I follow "Edit settings"
+    And I set the field "globalglossary" to "0"
+    And I press "Save and return to course"
+    And I am on site homepage
+    And I follow "Course 1"
+    Then I should see "Please configure this block using the edit icon." in the "Tip of the day" "block"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And "Tip of the day" "block" should not exist
+    And I log out
index 4b5a51a..7f27cf1 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_block_html_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index ec91f28..817620c 100644 (file)
@@ -35,6 +35,7 @@ $url = new moodle_url('/blocks/lp/competencies_to_review.php');
 $PAGE->set_context(context_user::instance($USER->id));
 $PAGE->set_url($url);
 $PAGE->set_title($toreviewstr);
+$PAGE->set_pagelayout('standard');
 $PAGE->navbar->add($toreviewstr, $url);
 
 $output = $PAGE->get_renderer('block_lp');
index 973c1ae..8643bcf 100644 (file)
@@ -35,6 +35,7 @@ $url = new moodle_url('/blocks/lp/plans_to_review.php');
 $PAGE->set_context(context_user::instance($USER->id));
 $PAGE->set_url($url);
 $PAGE->set_title($toreviewstr);
+$PAGE->set_pagelayout('standard');
 $PAGE->navbar->add($toreviewstr, $url);
 
 $output = $PAGE->get_renderer('block_lp');
diff --git a/blocks/messages/tests/behat/block_messages_course.feature b/blocks/messages/tests/behat/block_messages_course.feature
new file mode 100644 (file)
index 0000000..068b0e0
--- /dev/null
@@ -0,0 +1,58 @@
+@block @block_messages
+Feature: The messages block allows users to list new messages an a course
+  In order to enable the messages block in a course
+  As a teacher
+  I can add the messages block to a course and view my messages
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    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 |
+
+  Scenario: View the block by a user with messaging disabled.
+    Given the following config values are set as admin:
+      | messaging       | 0 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    When I turn editing mode on
+    And I add the "Messages" block
+    Then I should see "Messaging is disabled on this site" in the "Messages" "block"
+
+  Scenario: View the block by a user who does not have any messages.
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    When I turn editing mode on
+    And I add the "Messages" block
+    Then I should see "No messages waiting" in the "Messages" "block"
+
+  Scenario: View the block by a user who has messages.
+    Given I log in as "student1"
+    And I follow "Messages" in the user menu
+    And I send "This is message 1" message to "Teacher 1" user
+    And I send "This is message 2" message to "Teacher 1" user
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    When I turn editing mode on
+    And I add the "Messages" block
+    Then I should see "Student 1" in the "Messages" "block"
+
+  Scenario: Use the block to send a message to a user.
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Messages" block
+    And I follow "Messages"
+    And I send "This is message 1" message to "Student 1" user
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "Teacher 1" in the "Messages" "block"
diff --git a/blocks/messages/tests/behat/block_messages_dashboard.feature b/blocks/messages/tests/behat/block_messages_dashboard.feature
new file mode 100644 (file)
index 0000000..8eb1918
--- /dev/null
@@ -0,0 +1,48 @@
+@block @block_messages
+Feature: The messages block allows users to list new messages on the dashboard
+  In order to enable the messages block on the dashboard
+  As a user
+  I can add the messages block to a my dashboard and view my messages
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+
+  Scenario: View the block by a user with messaging disabled.
+    Given the following config values are set as admin:
+      | messaging       | 0 |
+    And I log in as "teacher1"
+    And I press "Customise this page"
+    When I add the "Messages" block
+    Then I should see "Messaging is disabled on this site" in the "Messages" "block"
+
+  Scenario: View the block by a user who does not have any messages.
+    Given I log in as "teacher1"
+    And I press "Customise this page"
+    When I add the "Messages" block
+    Then I should see "No messages waiting" in the "Messages" "block"
+
+  Scenario: View the block by a user who has messages.
+    Given I log in as "student1"
+    And I follow "Messages" in the user menu
+    And I send "This is message 1" message to "Teacher 1" user
+    And I send "This is message 2" message to "Teacher 1" user
+    And I log out
+    When I log in as "teacher1"
+    And I press "Customise this page"
+    And I add the "Messages" block
+    Then I should see "Student 1" in the "Messages" "block"
+
+  Scenario: Use the block to send a message to a user.
+    Given I log in as "teacher1"
+    And I press "Customise this page"
+    And I add the "Messages" block
+    And I follow "Messages"
+    And I send "This is message 1" message to "Student 1" user
+    And I log out
+    When I log in as "student1"
+    And I press "Customise this page"
+    And I add the "Messages" block
+    Then I should see "Teacher 1" in the "Messages" "block"
diff --git a/blocks/messages/tests/behat/block_messages_frontpage.feature b/blocks/messages/tests/behat/block_messages_frontpage.feature
new file mode 100644 (file)
index 0000000..df00991
--- /dev/null
@@ -0,0 +1,56 @@
+@block @block_messages
+Feature: The messages block allows users to list new messages on the frontpage
+  In order to enable the messages block on the frontpage
+  As an admin
+  I can add the messages block to a the frontpage and view my messages
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Messages" block
+    And I log out
+
+  Scenario: View the block by a user with messaging disabled.
+    Given the following config values are set as admin:
+      | messaging       | 0 |
+    And I log in as "admin"
+    And I am on site homepage
+    When I navigate to "Turn editing on" node in "Front page settings"
+    And I should see "Messaging is disabled on this site" in the "Messages" "block"
+    Then I navigate to "Turn editing off" node in "Front page settings"
+    And I should not see "Messaging is disabled on this site"
+
+  Scenario: View the block by a user who does not have any messages.
+    Given I log in as "teacher1"
+    When I am on site homepage
+    Then I should see "No messages waiting" in the "Messages" "block"
+
+  Scenario: Try to view the block as a guest user.
+    Given I log in as "guest"
+    When I am on site homepage
+    Then I should not see "Messages"
+
+  Scenario: View the block by a user who has messages.
+    Given I log in as "student1"
+    And I follow "Messages" in the user menu
+    And I send "This is message 1" message to "Teacher 1" user
+    And I send "This is message 2" message to "Teacher 1" user
+    And I log out
+    When I log in as "teacher1"
+    And I am on site homepage
+    Then I should see "Student 1" in the "Messages" "block"
+
+  Scenario: Use the block to send a message to a user.
+    Given I log in as "teacher1"
+    And I am on site homepage
+    And I follow "Messages"
+    And I send "This is message 1" message to "Student 1" user
+    And I log out
+    When I log in as "student1"
+    And I am on site homepage
+    Then I should see "Teacher 1" in the "Messages" "block"
index 5a5d7b9..2025554 100644 (file)
Binary files a/blocks/navigation/amd/build/navblock.min.js and b/blocks/navigation/amd/build/navblock.min.js differ
index 14b14bb..40e8678 100644 (file)
  */
 define(['jquery', 'core/tree'], function($, Tree) {
     return {
-        init: function() {
-            new Tree(".block_navigation .block_tree");
+        init: function(instanceid) {
+            var navTree = new Tree(".block_navigation .block_tree");
+            navTree.finishExpandingGroup = function(item) {
+                Tree.prototype.finishExpandingGroup.call(this, item);
+                Y.use('moodle-core-event', function() {
+                    Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+                        instanceid: instanceid
+                    });
+                });
+            };
+            navTree.collapseGroup = function(item) {
+                Tree.prototype.collapseGroup.call(this, item);
+                Y.use('moodle-core-event', function() {
+                    Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+                        instanceid: instanceid
+                    });
+                });
+            };
         }
     };
 });
index 31a3fc8..e7a5c11 100644 (file)
@@ -108,8 +108,11 @@ class block_navigation extends block_base {
      */
     function get_required_javascript() {
         parent::get_required_javascript();
+        $arguments = array(
+            'instanceid' => $this->instance->id
+        );
         $this->page->requires->string_for_js('viewallcourses', 'moodle');
-        $this->page->requires->js_call_amd('block_navigation/navblock', 'init', array());
+        $this->page->requires->js_call_amd('block_navigation/navblock', 'init', $arguments);
     }
 
     /**
index 430bb9e..f137d38 100644 (file)
@@ -64,5 +64,8 @@ function xmldb_block_navigation_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 47ef1c2..140ed42 100644 (file)
@@ -3,6 +3,7 @@
 .block_navigation .block_tree ul {margin-left: 18px;}
 .block_navigation .block_tree p.hasicon {text-indent: -21px; padding-left: 21px;}
 .block_navigation .block_tree p.hasicon img {width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top;}
+.block_navigation .block_tree p.hasicon.visibleifjs {display: block;}
 
 .block_navigation .block_tree .tree_item {cursor:pointer; padding-left: 0;margin:3px 0px; background-position: 0 50%; background-repeat: no-repeat;}
 .block_navigation .block_tree .tree_item.branch {padding-left: 21px;}
index c8b7f2c..9cd9406 100644 (file)
@@ -81,7 +81,6 @@ class block_news_items extends block_base {
             $groupmode    = groups_get_activity_groupmode($cm);
             $currentgroup = groups_get_activity_group($cm, true);
 
-
             if (forum_user_can_post_discussion($forum, $currentgroup, $groupmode, $cm, $context)) {
                 $text .= '<div class="newlink"><a href="'.$CFG->wwwroot.'/mod/forum/post.php?forum='.$forum->id.'">'.
                           get_string('addanewtopic', 'forum').'</a>...</div>';
@@ -96,7 +95,8 @@ class block_news_items extends block_base {
             // This sort will ignore pinned posts as we want the most recent.
             $sort = forum_get_default_sort_order(true, 'p.modified', 'd', false);
             if (! $discussions = forum_get_discussions($cm, $sort, false,
-                                                       $currentgroup, $this->page->course->newsitems) ) {
+                                                        -1, $this->page->course->newsitems,
+                                                        false, -1, 0, FORUM_POSTS_ALL_USER_GROUPS) ) {
                 $text .= '('.get_string('nonews', 'forum').')';
                 $this->content->text = $text;
                 return $this->content;
diff --git a/blocks/online_users/tests/behat/block_online_users_course.feature b/blocks/online_users/tests/behat/block_online_users_course.feature
new file mode 100644 (file)
index 0000000..f202235
--- /dev/null
@@ -0,0 +1,41 @@
+@block @block_online_users
+Feature: The online users block allow you to see who is currently online
+  In order to enable the online users block on an course page
+  As a teacher
+  I can add the online users block to a course page
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+
+    And the following "course enrolments" exist:
+      | user | course | role           |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student        |
+
+  Scenario: Add the online users on course page and see myself
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    When I add the "Online users" block
+    Then I should see "Teacher 1" in the "Online users" "block"
+
+  Scenario: Add the online users on course page and see other logged in users
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Online users" block
+    And I log out
+    And I log in as "student2"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    Then I should see "Teacher 1" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
+    And I should not see "Student 2" in the "Online users" "block"
diff --git a/blocks/online_users/tests/behat/block_online_users_dashboard.feature b/blocks/online_users/tests/behat/block_online_users_dashboard.feature
new file mode 100644 (file)
index 0000000..ecde2a0
--- /dev/null
@@ -0,0 +1,26 @@
+@block @block_online_users
+Feature: The online users block allow you to see who is currently online
+  In order to use the online users block on the dashboard
+  As a user
+  I can view the online users block on my dashboard
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+
+  Scenario: View the online users block on the dashboard and see myself
+    Given I log in as "teacher1"
+    Then I should see "Teacher 1" in the "Online users" "block"
+
+  Scenario: View the online users block on the dashboard and see other logged in users
+    Given I log in as "student2"
+    And I log out
+    And I log in as "student1"
+    And I log out
+    When  I log in as "teacher1"
+    Then I should see "Teacher 1" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
+    And I should see "Student 2" in the "Online users" "block"
diff --git a/blocks/online_users/tests/behat/block_online_users_frontpage.feature b/blocks/online_users/tests/behat/block_online_users_frontpage.feature
new file mode 100644 (file)
index 0000000..3e4b56d
--- /dev/null
@@ -0,0 +1,48 @@
+@block @block_online_users
+Feature: The online users block allow you to see who is currently online
+  In order to enable the online users block on the front page page
+  As an admin
+  I can add the online users block to the front page page
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+
+  Scenario: View the online users block on the front page and see myself
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    When I add the "Online users" block
+    Then I should see "Admin User" in the "Online users" "block"
+
+  Scenario: View the online users block on the front page as a logged in user
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Online users" block
+    And I log out
+    And I log in as "student2"
+    And I log out
+    When I log in as "student1"
+    And I am on site homepage
+    Then I should see "Admin User" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
+    And I should see "Student 2" in the "Online users" "block"
+
+  Scenario: View the online users block on the front page as a guest
+    Given I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Online users" block
+    And I log out
+    And I log in as "student2"
+    And I log out
+    And I log in as "student1"
+    And I log out
+    When I log in as "guest"
+    And I am on site homepage
+    Then I should see "Admin User" in the "Online users" "block"
+    And I should see "Student 1" in the "Online users" "block"
+    And I should see "Student 2" in the "Online users" "block"
diff --git a/blocks/private_files/tests/behat/block_private_files_activity.feature b/blocks/private_files/tests/behat/block_private_files_activity.feature
new file mode 100644 (file)
index 0000000..ef48e37
--- /dev/null
@@ -0,0 +1,29 @@
+@block @block_private_files @file_upload @javascript
+Feature: The private files block allows users to store files privately in moodle
+  In order to store a private file in moodle
+  As a teacher
+  I can upload the file to my private files area using the private files block in an activity
+
+  Scenario: Upload a file to the private files block in an activity
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And the following "activities" exist:
+      | activity | course | idnumber | name           | intro                 |
+      | page    | C1      | page1    | Test page name | Test page description |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Test page name"
+    And I add the "Private files" block
+    And I should see "No files available" in the "Private files" "block"
+    When I follow "Manage private files..."
+    And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager
+    And I press "Save changes"
+    Then I should see "testfile.txt" in the "Private files" "block"
diff --git a/blocks/private_files/tests/behat/block_private_files_course.feature b/blocks/private_files/tests/behat/block_private_files_course.feature
new file mode 100644 (file)
index 0000000..8ed28b3
--- /dev/null
@@ -0,0 +1,25 @@
+@block @block_private_files @file_upload @javascript
+Feature: The private files block allows users to store files privately in moodle
+  In order to store a private file in moodle
+  As a teacher
+  I can upload the file to my private files area using the private files block in a course
+
+  Scenario: Upload a file to the private files block from a course
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add the "Private files" block
+    And I should see "No files available" in the "Private files" "block"
+    When I follow "Manage private files..."
+    And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager
+    And I press "Save changes"
+    Then I should see "testfile.txt" in the "Private files" "block"
diff --git a/blocks/private_files/tests/behat/block_private_files_dashboard.feature b/blocks/private_files/tests/behat/block_private_files_dashboard.feature
new file mode 100644 (file)
index 0000000..976ae98
--- /dev/null
@@ -0,0 +1,17 @@
+@block @block_private_files @file_upload @javascript
+Feature: The private files block allows users to store files privately in moodle
+  In order to store a private file in moodle
+  As a user
+  I can upload the file to my private files area using the private files block on the dashboard
+
+  Scenario: Upload a file to the private files block from the dashboard
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And I log in as "teacher1"
+    And "Private files" "block" should exist
+    And I should see "No files available" in the "Private files" "block"
+    When I follow "Manage private files..."
+    And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager
+    And I press "Save changes"
+    Then I should see "testfile.txt" in the "Private files" "block"
diff --git a/blocks/private_files/tests/behat/block_private_files_frontpage.feature b/blocks/private_files/tests/behat/block_private_files_frontpage.feature
new file mode 100644 (file)
index 0000000..77d5756
--- /dev/null
@@ -0,0 +1,34 @@
+@block @block_private_files @file_upload
+Feature: The private files block allows users to store files privately in moodle
+  In order to store a private file in moodle
+  As a teacher
+  I can upload the file to my private files area using the private files block from the front page
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Private files" block
+    And I log out
+
+  Scenario: Try to view the private files block as a guest
+    Given I log in as "guest"
+    When I am on site homepage
+    Then "Private files" "block" should not exist
+
+  @javascript
+  Scenario: Upload a file to the private files block from the frontpage
+    Given I log in as "teacher1"
+    And I am on site homepage
+    And "Private files" "block" should exist
+    And I should see "No files available" in the "Private files" "block"
+    When I follow "Manage private files..."
+    And I upload "blocks/private_files/tests/fixtures/testfile.txt" file to "Files" filemanager
+    And I press "Save changes"
+    Then I should see "testfile.txt" in the "Private files" "block"
diff --git a/blocks/private_files/tests/fixtures/testfile.txt b/blocks/private_files/tests/fixtures/testfile.txt
new file mode 100644 (file)
index 0000000..9f4b6d8
--- /dev/null
@@ -0,0 +1 @@
+This is a test file
index ec753c9..129ecc9 100644 (file)
@@ -107,5 +107,8 @@ function xmldb_block_quiz_results_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
\ No newline at end of file
index 02e8be0..e2c9193 100644 (file)
@@ -56,5 +56,8 @@ function xmldb_block_recent_activity_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index cf8294e..b8487d9 100644 (file)
@@ -53,5 +53,8 @@ function xmldb_block_rss_client_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 66f7957..d570716 100644 (file)
@@ -58,5 +58,8 @@ function xmldb_block_section_links_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 9dee1f5..374ea53 100644 (file)
@@ -57,5 +57,8 @@ function xmldb_block_selfcompletion_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 91aa6a5..8a8e746 100644 (file)
Binary files a/blocks/settings/amd/build/settingsblock.min.js and b/blocks/settings/amd/build/settingsblock.min.js differ
index 965aec5..bdccda5 100644 (file)
 /**
  * Load the settings block tree javscript
  *
- * @module     block_navigation/navblock
+ * @module     block_settings/settingsblock
  * @package    core
  * @copyright  2015 John Okely <john@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define(['jquery', 'core/tree'], function($, Tree) {
     return {
-        init: function(siteAdminNodeId) {
+        init: function(instanceid, siteAdminNodeId) {
             var adminTree = new Tree(".block_settings .block_tree");
             if (siteAdminNodeId) {
                 var siteAdminNode = adminTree.treeRoot.find('#' + siteAdminNodeId);
                 var siteAdminLink = siteAdminNode.children('a').first();
                 siteAdminLink.replaceWith('<span tabindex="0">' + siteAdminLink.html() + '</span>');
             }
+            adminTree.finishExpandingGroup = function(item) {
+                Tree.prototype.finishExpandingGroup.call(this, item);
+                Y.use('moodle-core-event', function() {
+                    Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+                        instanceid: instanceid
+                    });
+                });
+            };
+            adminTree.collapseGroup = function(item) {
+                Tree.prototype.collapseGroup.call(this, item);
+                Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+                    instanceid: instanceid
+                });
+                Y.use('moodle-core-event', function() {
+                    Y.Global.fire(M.core.globalEvents.BLOCK_CONTENT_UPDATED, {
+                        instanceid: instanceid
+                    });
+                });
+            };
         }
     };
 });
index beaa87f..1ed9409 100644 (file)
@@ -91,18 +91,13 @@ class block_settings extends block_base {
 
     function get_required_javascript() {
         global $PAGE;
-        $adminnodeid = null;
         $adminnode = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
-        if (!empty($adminnode)) {
-            $adminnodeid = $adminnode->id;
-        }
         parent::get_required_javascript();
         $arguments = array(
-            'id' => $this->instance->id,
-            'instance' => $this->instance->id,
-            'candock' => $this->instance_can_be_docked()
+            'instanceid' => $this->instance->id,
+            'adminnodeid' => $adminnode ? $adminnode->id : null
         );
-        $this->page->requires->js_call_amd('block_settings/settingsblock', 'init', array($adminnodeid));
+        $this->page->requires->js_call_amd('block_settings/settingsblock', 'init', $arguments);
     }
 
     /**
index bd96db6..c435ed7 100644 (file)
@@ -64,5 +64,8 @@ function xmldb_block_settings_upgrade($oldversion, $block) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 31b27ec..e9c0e10 100644 (file)
@@ -1,6 +1,7 @@
 .block_settings .block_tree ul {margin-left: 18px;}
 .block_settings .block_tree p.hasicon {text-indent: -21px; padding-left: 21px;}
 .block_settings .block_tree p.hasicon img {width: 16px; height: 16px; margin-top: 3px; margin-right: 5px; vertical-align: top;}
+.block_settings .block_tree p.hasicon.visibleifjs {display: block;}
 
 .block_settings .block_tree .tree_item.branch {padding-left: 21px;}
 .block_settings .block_tree .tree_item {cursor:pointer; margin:3px 0px; background-position: 0 50%; background-repeat: no-repeat;}
index a960293..bfe717b 100644 (file)
@@ -34,6 +34,11 @@ list($context, $course, $cm) = get_context_info_array($contextid);
 require_login($course, true, $cm);
 require_sesskey();
 
+if (!$course) {
+    // Require_login() does not set context if called without a $course, do it manually.
+    $PAGE->set_context($context);
+}
+
 $action    = optional_param('action',    '',  PARAM_ALPHA);
 $area      = optional_param('area',      '',  PARAM_AREA);
 $content   = optional_param('content',   '',  PARAM_RAW);
@@ -48,7 +53,9 @@ if ($action !== 'add') {
 
 $cmt = new stdClass;
 $cmt->contextid = $contextid;
-$cmt->courseid  = $course->id;
+if ($course) {
+    $cmt->courseid = $course->id;
+}
 $cmt->cm        = $cm;
 $cmt->area      = $area;
 $cmt->itemid    = $itemid;
index ea687c6..b348aa1 100644 (file)
@@ -2140,7 +2140,7 @@ class core_course_external extends external_api {
                 'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
                 'requiredcapabilities' => new external_multiple_structure(
                     new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
-                    VALUE_OPTIONAL
+                    'Optional list of required capabilities (used to filter the list)', VALUE_DEFAULT, array()
                 ),
                 'limittoenrolled' => new external_value(PARAM_BOOL, 'limit to enrolled courses', VALUE_DEFAULT, 0),
             )
index 208c188..20f39a4 100644 (file)
@@ -984,14 +984,14 @@ abstract class format_base {
         }
         if (!is_object($section)) {
             $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
-                'id,section,sequence');
+                'id,section,sequence,summary');
         }
         if (!$section || !$section->section) {
             // Not possible to delete 0-section.
             return false;
         }
 
-        if (!$forcedeleteifnotempty && !empty($section->sequence)) {
+        if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
             return false;
         }
 
index a2d2c53..5a44fd7 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_database_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index f16aa4d..8660e33 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_flatfile_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index e1111f7..4f44dbe 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_guest_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 0bc6134..1a7a9ad 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_enrol_imsenterprise_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 94ccd7b..ad454c4 100644 (file)
@@ -120,7 +120,7 @@ switch ($action) {
         }
 
         $roleid = optional_param('role', null, PARAM_INT);
-        $duration = optional_param('duration', 0, PARAM_INT);
+        $duration = optional_param('duration', 0, PARAM_FLOAT);
         $startdate = optional_param('startdate', 0, PARAM_INT);
         $recovergrades = optional_param('recovergrades', 0, PARAM_INT);
 
@@ -154,7 +154,7 @@ switch ($action) {
         if ($duration <= 0) {
             $timeend = 0;
         } else {
-            $timeend = $timestart + ($duration*24*60*60);
+            $timeend = $timestart + intval($duration*24*60*60);
         }
 
         $instances = $manager->get_enrolment_instances();
index 5ec65e3..8207e81 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_enrol_manual_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index cf131e6..f41dae3 100644 (file)
@@ -232,7 +232,7 @@ class enrol_manual_plugin extends enrol_plugin {
         $today = make_timestamp(date('Y', $now), date('m', $now), date('d', $now), 0, 0, 0);
         $startdateoptions[3] = get_string('today') . ' (' . userdate($today, $dateformat) . ')';
         $startdateoptions[4] = get_string('now', 'enrol_manual') . ' (' . userdate($now, get_string('strftimedatetimeshort')) . ')';
-        $defaultduration = $instance->enrolperiod > 0 ? $instance->enrolperiod / 86400 : '';
+        $defaultduration = $instance->enrolperiod > 0 ? $instance->enrolperiod / DAYSECS : '';
 
         $modules = array('moodle-enrol_manual-quickenrolment', 'moodle-enrol_manual-quickenrolment-skin');
         $arguments = array(
index 0b9ca4c..17627f8 100644 (file)
@@ -89,6 +89,9 @@ if ($extendperiod) {
 } else {
     $defaultperiod = $instance->enrolperiod;
 }
+if ($instance->enrolperiod > 0 && !isset($periodmenu[$instance->enrolperiod])) {
+    $periodmenu[$instance->enrolperiod] = format_time($instance->enrolperiod);
+}
 if (empty($extendbase)) {
     if (!$extendbase = get_config('enrol_manual', 'enrolstart')) {
         // Default to now if there is no system setting.
index b4132cc..5aeca32 100644 (file)
@@ -227,6 +227,7 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
         populateDuration : function() {
             var select = this.get(UEP.BASE).one('.'+CSS.ENROLMENTOPTION+'.'+CSS.DURATION+' select');
             var defaultvalue = this.get(UEP.DEFAULTDURATION);
+            var prefix = Math.round(defaultvalue) != defaultvalue ? '≈' : '';
             var index = 0, count = 0;
             var durationdays = M.util.get_string('durationdays', 'enrol', '{a}');
             for (var i = 1; i <= 365; i++) {
@@ -237,6 +238,11 @@ YUI.add('moodle-enrol_manual-quickenrolment', function(Y) {
                 }
                 select.append(option);
             }
+            if (!index && defaultvalue > 0) {
+                select.append(create('<option value="'+defaultvalue+'">'+durationdays.replace('{a}',
+                    prefix + (Math.round(defaultvalue * 100) / 100))+'</option>'));
+                index = ++count;
+            }
             select.set('selectedIndex', index);
         },
         getAssignableRoles : function(){
index 2a59641..b96eb18 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_mnet_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 1497469..8fab0c7 100644 (file)
@@ -54,5 +54,8 @@ function xmldb_enrol_paypal_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 82bb97d..8be21ea 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_enrol_self_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 2a70d61..0e05041 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_filter_mediaplugin_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 14aa3d8..f8320d7 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_filter_tex_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index eb50b3c..eb06597 100644 (file)
@@ -46,5 +46,8 @@ function xmldb_gradingform_guide_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2016051100, 'gradingform', 'guide');
     }
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 68a632f..3d6ca7f 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_gradingform_rubric_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 66cf003..1c0da1e 100644 (file)
@@ -1478,8 +1478,15 @@ class grade_structure {
                         $icon->pix = 'i/outcomes';
                         $icon->title = s(get_string('outcome', 'grades'));
                     } else {
-                        $icon->pix = 'icon';
-                        $icon->component = $element['object']->itemmodule;
+                        $modinfo = get_fast_modinfo($element['object']->courseid);
+                        $module = $element['object']->itemmodule;
+                        $instanceid = $element['object']->iteminstance;
+                        if (isset($modinfo->instances[$module][$instanceid])) {
+                            $icon->url = $modinfo->instances[$module][$instanceid]->get_icon_url();
+                        } else {
+                            $icon->pix = 'icon';
+                            $icon->component = $element['object']->itemmodule;
+                        }
                         $icon->title = s(get_string('modulename', $element['object']->itemmodule));
                     }
                 } else if ($element['object']->itemtype == 'manual') {
@@ -1504,6 +1511,8 @@ class grade_structure {
             if ($spacerifnone) {
                 $outputstr = $OUTPUT->spacer() . ' ';
             }
+        } else if (isset($icon->url)) {
+            $outputstr = html_writer::img($icon->url, $icon->title, $icon->attributes);
         } else {
             $outputstr = $OUTPUT->pix_icon($icon->pix, $icon->title, $icon->component, $icon->attributes);
         }
index f79d0d1..c80ab9d 100644 (file)
@@ -45,5 +45,8 @@ function xmldb_gradereport_user_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index b820f0e..05f7740 100644 (file)
@@ -42,3 +42,4 @@ L\'opció --help us orientarà.';
 $string['cliyesnoprompt'] = 'Escriu y (significa Sí) o n (significa No)';
 $string['environmentrequireinstall'] = 'cal instal·lar-lo i habilitar-lo';
 $string['environmentrequireversion'] = 'esteu executant la versió {$a->current} i es requereix la {$a->needed}';
+$string['upgradekeyset'] = 'Clau d\'actualització (deixeu-ho en blanc per no establir-ne cap)';
index 8e4178b..02b3354 100644 (file)
@@ -67,14 +67,11 @@ $string['pathserrcreatedataroot'] = 'L\'instal·lador no pot crear el directori
 $string['pathshead'] = 'Confirmeu els camins';
 $string['pathsrodataroot'] = 'No es pot escriure en el directori dataroot.';
 $string['pathsroparentdataroot'] = 'No es pot escriure en el directori pare ({$a->parent}). L\'instal·lador no pot crear el directori ({$a->dataroot}).';
-$string['pathssubadmindir'] = 'Alguns serveis d\'allotjament web (pocs) utilitzen un URL especial /admin p. ex. per a accedir a un tauler de control o quelcom semblant. Malauradament això entra en conflicte amb la ubicació estàndard de les pàgines d\'administració de Moodle. Podeu arreglar aquest problema canviant el nom del directori d\'administració de Moodle en la vostra instal·lació i posant el nou nom aquí. Per exemple <em>moodleadmin</em>. Això modificarà els enllaços d\'administració de Moodle.';
+$string['pathssubadmindir'] = 'Alguns serveis d\'allotjament web (pocs) utilitzen /admin com a URL especial perquè accediu a un tauler de control o quelcom semblant. Malauradament, això entra en conflicte amb la ubicació estàndard de les pàgines d\'administració de Moodle. Podeu arreglar aquest problema canviant el nom del directori d\'administració de Moodle en la vostra instal·lació i posant el nou nom aquí. Per exemple: <em>moodleadmin</em>. Això arreglarà els enllaços d\'administració de Moodle.';
 $string['pathssubdataroot'] = 'Necessiteu un espai on Moodle pugui desar els fitxers penjats. Aquest directori hauria de tenir permisos de lectura I ESCRIPTURA per a l\'usuari del servidor web (normalment \'nobody\' o \'apache\'), però no cal que sigui accessible directament via web. L\'instal·lador provarà de crear-lo si no existeix.';
 $string['pathssubdirroot'] = 'Camí complet del directori d\'instal·lació de Moodle.';
-$string['pathssubwwwroot'] = 'L\'adreça web completa on s\'accedirà a Moodle.
-No és possible accedir a Moodle en diferents adreces.
-Si el vostre lloc té múltiples adreces públiques haureu de configurar redireccions permanents per a totes excepte aquesta.
-Si el vostre lloc és accessible tant des d\'Internet com des d\'una intranet, utilitzeu aquí l\'adreça pública i configureu el DNS de manera que els usuaris de la intranet puguin utilitzar també l\'adreça pública.
-Si l\'adreça no és correcta, canvieu l\'URL en el vostre navegador per reiniciar la instal·lació amb un altre valor.';
+$string['pathssubwwwroot'] = '<p>L\'adreça web completa on s\'accedirà a Moodle; per exemple, l\'adreça que els usuaris introduiran a la barra d\'adreces del navegador per accedir a Moodle.</p> <p> No és possible accedir a Moodle utilitzant diferents adreces. Si el vostre lloc és accessible a través de diferents adreces, trieu-ne la més fàcil i configureu una redirecció permanent per a cadascuna de les altres adreces.</p> <p>
+Si el vostre lloc és accessible tant des d\'Internet com des d\'una xarxa interna (anomenada de vegades intranet), utilitzeu l\'adreça pública aquí.</p> <p>Si l\'adreça actual no és correcta, canvieu l\'URL a la barra d\'adreces del navegador i reinicieu la instal·lació.';
 $string['pathsunsecuredataroot'] = 'La ubicació del dataroot no és segura.';
 $string['pathswrongadmindir'] = 'No existeix el directori d\'administració';
 $string['phpextension'] = 'Extensió PHP {$a}';
@@ -84,7 +81,7 @@ $string['phpversionhelp'] = '<p>Moodle necessita una versió de PHP 4.3.0 o 5.1.
 <p>Us cal actualitzar el PHP o traslladar Moodle a un ordinador amb una versió de PHP més recent.<br />(Si esteu utilitzant la versió 5.0.x, alternativament també podríeu tornar enrere a la 4.4.x)</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Esteu veient aquesta pàgina perquè heu instal·lat amb èxit i heu executat el paquet <strong>{$a->packname} {$a->packversion}</strong>. Felicitacions.';
-$string['welcomep30'] = 'Aquesta versió de <strong>{$a->installername}</strong> inclou les aplicacions necessàries per crear un entorn en el qual funcioni <strong>Moodle</strong>:';
+$string['welcomep30'] = 'Aquesta versió de <strong>{$a->installername}</strong> inclou les aplicacions necessàries per crear un entorn en el qual funcioni <strong>Moodle</strong>, concretament:';
 $string['welcomep40'] = 'El paquet inclou també <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'L\'ús de totes les aplicacions d\'aquest paquet és governat per les seves llicències respectives. El paquet <strong>{$a->installername}</strong> complet és
 <a href="http://www.opensource.org/docs/definition_plain.html">codi font obert</a> i es distribueix
index c9f613d..5374ba9 100644 (file)
@@ -42,4 +42,4 @@ $string['cliunknowoption'] = 'אפשרויות לא מוכרות :
 אנא השתמש באפשרות העזרה.';
 $string['cliyesnoprompt'] = 'רשום y (שפרושו כן) או n (שפרושו לא)';
 $string['environmentrequireinstall'] = 'נדרש להתקין/לאפשר זאת';
-$string['environmentrequireversion'] = '×\92×\99רסת {$a->needed} × ×\93רשת ×\95×\90ת×\94 ×\9eר×\99×¥ {$a->current}';
+$string['environmentrequireversion'] = '×\92×\99רסת {$a->needed} × ×\93רשת ×\90×\9a ×\94×\92×\99רס×\94 ×\94× ×\95×\9b×\97×\99ת ×\94×\99×\90 {$a->current}';
index 8dfd3a2..f58e8a3 100644 (file)
@@ -86,10 +86,10 @@ $string['pathsunsecuredataroot'] = 'ספריית המידע (Data Directory) ל
 $string['pathswrongadmindir'] = 'ספריית ה-admin לא קיימת';
 $string['phpextension'] = 'הרחבת PHP {$a}';
 $string['phpversion'] = 'גירסת PHP';
-$string['phpversionhelp'] = '<p>גרסת PHP חייבת להיות לפחות 4.3.0 או 5.1.0 (בגרסאות 5.0.x קיימות מספר בעיות ידועות) </p>
+$string['phpversionhelp'] = '<p>גרסת PHP חייבת להיות לפחות 4.3.0 או 5.1.0 (בגרסאות 5.0 קיימות מספר בעיות ידועות) </p>
 <p> במערכת שלך פועלת כרגע גרסת {$a} </p>
 <p> אתה חייב לשדרג את גרסת ה-PHP שלך או לעבור למחשב מארח עם עם גירסת PHP חדשה! <br/>
-(במקרים של גרסת 5.0.x תוכל גם לרדת בגרסה ל- 4.4.x)
+(במקרים של גרסת 5.0 תוכל גם לרדת בגרסה ל- 4.4)
 </p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'הינך רואה את עמוד זה מפני שהתקנת והפעלת בהלכה את <strong>{$a->packname} {$a->packversion}</strong>
index 46b031b..7ce183a 100644 (file)
@@ -33,4 +33,4 @@ defined('MOODLE_INTERNAL') || die();
 $string['language'] = 'שפת ממשק';
 $string['next'] = 'הבא';
 $string['previous'] = 'קודם';
-$string['reload'] = '×\98×¢×\9f מחדש';
+$string['reload'] = '×\98×¢×\99× ×\94 מחדש';
index 5c1a07e..7dcce90 100644 (file)
@@ -30,9 +30,9 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['cannotcreatedboninstall'] = '<p>Impossible de crear la banca de donadas.</p>
-<p>La banca de donadas indicada existís pas e l\'utilizaire especificat a pas las autorizacions que permeton de crear una banca de donadas.</p>.
-<p>L\'administrator del site deu repassar la configuracion de la banca de donadas.</p>';
+$string['cannotcreatedboninstall'] = '<p>Impossible de crear la basa de donadas.</p>
+<p>La banca de donadas indicada existís pas e l\'utilizaire especificat a pas las autorizacions que permeton de crear una basa de donadas.</p>.
+<p>L\'administrator del site deu repassar la configuracion de la basa de donadas.</p>';
 $string['cannotcreatelangdir'] = 'Creacion del dorsièr lang impossible';
 $string['cannotcreatetempdir'] = 'Creacion del dorsièr temp impossibla';
 $string['cannotdownloadcomponents'] = 'Telecargament dels components impossible';
@@ -42,7 +42,7 @@ $string['cannotsavemd5file'] = 'Enregistrament del fichièr md5 impossible';
 $string['cannotsavezipfile'] = 'Enregistrament del fichièr ZIP impossible';
 $string['cannotunzipfile'] = 'Descompression del fichièr ZIP impossibla';
 $string['componentisuptodate'] = 'Lo component es a jorn';
-$string['dmlexceptiononinstall'] = '<p>Una error de banca de donadas s\'es produita [{$a->errorcode}].<br />{$a->debuginfo}</p>';
+$string['dmlexceptiononinstall'] = '<p>Una error de basa de donadas s\'es produita [{$a->errorcode}].<br />{$a->debuginfo}</p>';
 $string['downloadedfilecheckfailed'] = 'La verificacion del fichièr telecargat a fracassat';
 $string['invalidmd5'] = 'Lo còdi de contraròtle md5 es pas valid';
 $string['missingrequiredfield'] = 'Un camp obligatòri es pas completat';
index 12ae97a..a4b6a21 100644 (file)
@@ -54,7 +54,7 @@ $string['paths'] = 'Caminhos';
 $string['pathserrcreatedataroot'] = 'O programa de instalação não conseguiu criar a pasta de dados <b>{$a->dataroot}</b>.';
 $string['pathshead'] = 'Confirmar caminhos';
 $string['pathsrodataroot'] = 'A pasta de dados não tem permissões de escrita.';
-$string['pathsroparentdataroot'] = 'A pasta pai <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
+$string['pathsroparentdataroot'] = 'A pasta ascendente <b>{$a->parent}</b> não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta <b>{$a->dataroot}</b>.';
 $string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta <strong>admin</strong> em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta <strong>admin</strong> na instalação do Moodle e indicando aqui o novo nome a utilizar. Por exemplo:<br /><br /><b>moodleadmin</b><br /><br />Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
 $string['pathssubdataroot'] = '<p>Uma diretoria em que o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.</p>
 <p>Esta diretoria deve ser legível e gravável pelo utilizador do servidor web (geralmente \'www-data\', \'nobody\', ou \'apache\').</p>
index 6a92867..6505f91 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Språk';
+$string['moodlelogo'] = 'Moodie-logotyp';
 $string['next'] = 'Nästa';
 $string['previous'] = 'Tidigare';
 $string['reload'] = 'Uppdatera';
index 6e8f415..93db2e5 100644 (file)
@@ -224,6 +224,7 @@ $string['duplicateroleshortname'] = 'There is already a role with this short nam
 $string['duplicateusername'] = 'Duplicate username - skipping record';
 $string['emailfail'] = 'Emailing failed';
 $string['error'] = 'Error occurred';
+$string['error_question_answers_missing_in_db'] = 'Failed to find an answer matching "{$a->answer}" in the question_answers database table. This occurred while restoring the question with id {$a->filequestionid} in the backup file, which has been matched to the existing question with id {$a->dbquestionid} in the database.';
 $string['errorprocessingarchive'] = 'Error processing archive file';
 $string['errorcleaningdirectory'] = 'Error cleaning directory "{$a}"';
 $string['errorcopyingfiles'] = 'Error copying files';
index 766167f..d245288 100644 (file)
@@ -65,7 +65,7 @@ $string['categorycurrent'] = 'Current category';
 $string['categorycurrentuse'] = 'Use this category';
 $string['categorydoesnotexist'] = 'This category does not exist';
 $string['categoryinfo'] = 'Category info';
-$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of them may be old, hidden, questions that are still in use in some existing quizzes). Please choose another category to move them to.';
+$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of them may be old, hidden, questions, or Random questions that are still in use in some existing quizzes). Please choose another category to move them to.';
 $string['categorymoveto'] = 'Save in category';
 $string['categorynamecantbeblank'] = 'The category name cannot be blank.';
 $string['clickflag'] = 'Flag question';
index 32362d1..a2c4eb8 100644 (file)
@@ -980,20 +980,17 @@ function load_course_context($userid, context_course $coursecontext, &$accessdat
     }
 
     // now get overrides of interesting roles in all interesting contexts (this course + children + parents)
-    $params = array('c'=>$coursecontext->id);
+    $params = array('pathprefix' => $coursecontext->path . '/%');
     list($parentsaself, $rparams) = $DB->get_in_or_equal($coursecontext->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'pc_');
     $params = array_merge($params, $rparams);
     list($roleids, $rparams) = $DB->get_in_or_equal($roles, SQL_PARAMS_NAMED, 'r_');
     $params = array_merge($params, $rparams);
 
     $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
-                 FROM {role_capabilities} rc
-                 JOIN {context} ctx
-                      ON (ctx.id = rc.contextid)
-                 JOIN {context} cctx
-                      ON (cctx.id = :c
-                          AND (ctx.id $parentsaself OR ctx.path LIKE ".$DB->sql_concat('cctx.path',"'/%'")."))
+                 FROM {context} ctx
+                 JOIN {role_capabilities} rc ON rc.contextid = ctx.id
                 WHERE rc.roleid $roleids
+                  AND (ctx.id $parentsaself OR ctx.path LIKE :pathprefix)
              ORDER BY rc.capability"; // fixed capability order is necessary for rdef dedupe
     $rs = $DB->get_recordset_sql($sql, $params);
 
index 618657e..45eb5c1 100644 (file)
@@ -31,5 +31,8 @@ defined('MOODLE_INTERNAL') || die();
  * @return bool
  */
 function xmldb_antivirus_clamav_upgrade($oldversion) {
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 9a7c996..23d99f5 100644 (file)
@@ -2073,21 +2073,31 @@ function blocks_delete_instance($instance, $nolongerused = false, $skipblockstab
 function blocks_delete_instances($instanceids) {
     global $DB;
 
-    $instances = $DB->get_recordset_list('block_instances', 'id', $instanceids);
-    foreach ($instances as $instance) {
-        blocks_delete_instance($instance, false, true);
+    $limit = 1000;
+    $count = count($instanceids);
+    $chunks = [$instanceids];
+    if ($count > $limit) {
+        $chunks = array_chunk($instanceids, $limit);
     }
-    $instances->close();
 
-    $DB->delete_records_list('block_positions', 'blockinstanceid', $instanceids);
-    $DB->delete_records_list('block_instances', 'id', $instanceids);
+    // Perform deletion for each chunk.
+    foreach ($chunks as $chunk) {
+        $instances = $DB->get_recordset_list('block_instances', 'id', $chunk);
+        foreach ($instances as $instance) {
+            blocks_delete_instance($instance, false, true);
+        }
+        $instances->close();
+
+        $DB->delete_records_list('block_positions', 'blockinstanceid', $chunk);
+        $DB->delete_records_list('block_instances', 'id', $chunk);
 
-    $preferences = array();
-    foreach ($instanceids as $instanceid) {
-        $preferences[] = 'block' . $instanceid . 'hidden';
-        $preferences[] = 'docked_block_instance_' . $instanceid;
+        $preferences = array();
+        foreach ($chunk as $instanceid) {
+            $preferences[] = 'block' . $instanceid . 'hidden';
+            $preferences[] = 'docked_block_instance_' . $instanceid;
+        }
+        $DB->delete_records_list('user_preferences', 'name', $preferences);
     }
-    $DB->delete_records_list('user_preferences', 'name', $preferences);
 }
 
 /**
index bf1678b..5a1fd4d 100644 (file)
@@ -136,7 +136,12 @@ class memcached extends handler {
         ini_set('memcached.sess_locking', '1'); // Locking is required!
 
         // Try to configure lock and expire timeouts - ignored if memcached is before version 2.2.0.
-        ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
+        if (version_compare($version, '3.0.0-dev') >= 0) {
+            ini_set('memcached.sess_lock_wait_max', $this->acquiretimeout * 1000);
+        } else {
+            ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout);
+        }
+
         ini_set('memcached.sess_lock_expire', $this->lockexpire);
     }
 
index 22db5bf..ee468b6 100644 (file)
@@ -2069,5 +2069,8 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2016051700.01);
     }
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 6626075..a31117c 100644 (file)
@@ -27,156 +27,195 @@ define('AJAX_SCRIPT', true);
 require_once(dirname(__FILE__) . '/../../../config.php');
 require_once($CFG->libdir . '/filestorage/file_storage.php');
 
-$contextid = required_param('contextid', PARAM_INT);
-$elementid = required_param('elementid', PARAM_ALPHANUMEXT);
-$pagehash = required_param('pagehash', PARAM_ALPHANUMEXT);
-$pageinstance = required_param('pageinstance', PARAM_ALPHANUMEXT);
+// Clean up actions.
+$actions = array_map(function($actionparams) {
+    $action = isset($actionparams['action']) ? $actionparams['action'] : null;
+    $params = [];
+    $keys = [
+        'action' => PARAM_ALPHA,
+        'contextid' => PARAM_INT,
+        'elementid' => PARAM_ALPHANUMEXT,
+        'pagehash' => PARAM_ALPHANUMEXT,
+        'pageinstance' => PARAM_ALPHANUMEXT
+    ];
+
+    if ($action == 'save') {
+        $keys['drafttext'] = PARAM_RAW;
+    } else if ($action == 'resume') {
+        $keys['draftid'] = PARAM_INT;
+    }
+
+    foreach ($keys as $key => $type) {
+        // Replicate required_param().
+        if (!isset($actionparams[$key])) {
+            print_error('missingparam', '', '', $key);
+        }
+        $params[$key] = clean_param($actionparams[$key], $type);
+    }
+
+    return $params;
+}, isset($_REQUEST['actions']) ? $_REQUEST['actions'] : []);
+
 $now = time();
 // This is the oldest time any autosave text will be recovered from.
 // This is so that there is a good chance the draft files will still exist (there are many variables so
 // this is impossible to guarantee).
 $before = $now - 60*60*24*4;
 
-list($context, $course, $cm) = get_context_info_array($contextid);
+$context = context_system::instance();
 $PAGE->set_url('/lib/editor/atto/autosave-ajax.php');
 $PAGE->set_context($context);
 
-require_login($course, false, $cm);
-require_sesskey();
-
+require_login();
 if (isguestuser()) {
     print_error('accessdenied', 'admin');
 }
+require_sesskey();
 
 if (!in_array('atto', explode(',', get_config('core', 'texteditors')))) {
     print_error('accessdenied', 'admin');
 }
 
-$action = required_param('action', PARAM_ALPHA);
+$responses = array();
+foreach ($actions as $actionparams) {
+
+    $action = $actionparams['action'];
+    $contextid = $actionparams['contextid'];
+    $elementid = $actionparams['elementid'];
+    $pagehash = $actionparams['pagehash'];
+    $pageinstance = $actionparams['pageinstance'];
+
+    if ($action === 'save') {
+        $drafttext = $actionparams['drafttext'];
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $record = $DB->get_record('editor_atto_autosave', $params);
+        if ($record && $record->pageinstance != $pageinstance) {
+            print_error('concurrent access from the same user is not supported');
+            die();
+        }
 
-$response = array();
+        if (!$record) {
+            $record = new stdClass();
+            $record->elementid = $elementid;
+            $record->userid = $USER->id;
+            $record->pagehash = $pagehash;
+            $record->contextid = $contextid;
+            $record->drafttext = $drafttext;
+            $record->pageinstance = $pageinstance;
+            $record->timemodified = $now;
 
-if ($action === 'save') {
-    $drafttext = required_param('drafttext', PARAM_RAW);
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
+            $DB->insert_record('editor_atto_autosave', $record);
 
-    $record = $DB->get_record('editor_atto_autosave', $params);
-    if ($record && $record->pageinstance != $pageinstance) {
-        print_error('concurrent access from the same user is not supported');
-        die();
-    }
+            // No response means no error.
+            $responses[] = null;
+            continue;
+        } else {
+            $record->drafttext = $drafttext;
+            $record->timemodified = time();
+            $DB->update_record('editor_atto_autosave', $record);
 
-    if (!$record) {
-        $record = new stdClass();
-        $record->elementid = $elementid;
-        $record->userid = $USER->id;
-        $record->pagehash = $pagehash;
-        $record->contextid = $contextid;
-        $record->drafttext = $drafttext;
-        $record->pageinstance = $pageinstance;
-        $record->timemodified = $now;
-
-        $DB->insert_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    } else {
-        $record->drafttext = $drafttext;
-        $record->timemodified = time();
-        $DB->update_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    }
-} else if ($action == 'resume') {
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
-
-    $newdraftid = required_param('draftid', PARAM_INT);
-
-    $record = $DB->get_record('editor_atto_autosave', $params);
-
-    if (!$record) {
-        $record = new stdClass();
-        $record->elementid = $elementid;
-        $record->userid = $USER->id;
-        $record->pagehash = $pagehash;
-        $record->contextid = $contextid;
-        $record->pageinstance = $pageinstance;
-        $record->pagehash = $pagehash;
-        $record->draftid = $newdraftid;
-        $record->timemodified = time();
-        $record->drafttext = '';
-
-        $DB->insert_record('editor_atto_autosave', $record);
-
-        // No response means no error.
-        die();
-    } else {
-        // Copy all draft files from the old draft area.
-        $usercontext = context_user::instance($USER->id);
-        $stale = $record->timemodified < $before;
-        require_once($CFG->libdir . '/filelib.php');
-
-        $fs = get_file_storage();
-        $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
-
-        $lastfilemodified = 0;
-        foreach ($files as $file) {
-            $lastfilemodified = max($lastfilemodified, $file->get_timemodified());
-        }
-        if ($record->timemodified < $lastfilemodified) {
-            $stale = true;
+            // No response means no error.
+            $responses[] = null;
+            continue;
         }
 
-        if (!$stale) {
-            // This function copies all the files in one draft area, to another area (in this case it's
-            // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
-            $newdrafttext = file_save_draft_area_files($record->draftid,
-                                                       $usercontext->id,
-                                                       'user',
-                                                       'draft',
-                                                       $newdraftid,
-                                                       array(),
-                                                       $record->drafttext);
-
-            // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
-            $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
-                                                         'draftfile.php',
-                                                         $usercontext->id,
-                                                         'user',
-                                                         'draft',
-                                                         $newdraftid);
-            $record->drafttext = $newdrafttext;
+    } else if ($action == 'resume') {
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $newdraftid = $actionparams['draftid'];
+
+        $record = $DB->get_record('editor_atto_autosave', $params);
 
+        if (!$record) {
+            $record = new stdClass();
+            $record->elementid = $elementid;
+            $record->userid = $USER->id;
+            $record->pagehash = $pagehash;
+            $record->contextid = $contextid;
             $record->pageinstance = $pageinstance;
+            $record->pagehash = $pagehash;
             $record->draftid = $newdraftid;
             $record->timemodified = time();
-            $DB->update_record('editor_atto_autosave', $record);
+            $record->drafttext = '';
 
-            // A response means the draft has been restored and here is the auto-saved text.
-            $response['result'] = $record->drafttext;
-            echo json_encode($response);
-        } else {
-            $DB->delete_records('editor_atto_autosave', array('id' => $record->id));
+            $DB->insert_record('editor_atto_autosave', $record);
 
             // No response means no error.
+            $responses[] = null;
+            continue;
+
+        } else {
+            // Copy all draft files from the old draft area.
+            $usercontext = context_user::instance($USER->id);
+            $stale = $record->timemodified < $before;
+            require_once($CFG->libdir . '/filelib.php');
+
+            $fs = get_file_storage();
+            $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true);
+
+            $lastfilemodified = 0;
+            foreach ($files as $file) {
+                $lastfilemodified = max($lastfilemodified, $file->get_timemodified());
+            }
+            if ($record->timemodified < $lastfilemodified) {
+                $stale = true;
+            }
+
+            if (!$stale) {
+                // This function copies all the files in one draft area, to another area (in this case it's
+                // another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
+                $newdrafttext = file_save_draft_area_files($record->draftid,
+                                                           $usercontext->id,
+                                                           'user',
+                                                           'draft',
+                                                           $newdraftid,
+                                                           array(),
+                                                           $record->drafttext);
+
+                // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
+                $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext,
+                                                             'draftfile.php',
+                                                             $usercontext->id,
+                                                             'user',
+                                                             'draft',
+                                                             $newdraftid);
+                $record->drafttext = $newdrafttext;
+
+                $record->pageinstance = $pageinstance;
+                $record->draftid = $newdraftid;
+                $record->timemodified = time();
+                $DB->update_record('editor_atto_autosave', $record);
+
+                // A response means the draft has been restored and here is the auto-saved text.
+                $response = ['result' => $record->drafttext];
+                $responses[] = $response;
+
+            } else {
+                $DB->delete_records('editor_atto_autosave', array('id' => $record->id));
+
+                // No response means no error.
+                $responses[] = null;
+            }
+            continue;
         }
-        die();
+
+    } else if ($action == 'reset') {
+        $params = array('elementid' => $elementid,
+                        'userid' => $USER->id,
+                        'pagehash' => $pagehash,
+                        'contextid' => $contextid);
+
+        $DB->delete_records('editor_atto_autosave', $params);
+        $responses[] = null;
+        continue;
     }
-} else if ($action == 'reset') {
-    $params = array('elementid' => $elementid,
-                    'userid' => $USER->id,
-                    'pagehash' => $pagehash,
-                    'contextid' => $contextid);
-
-    $DB->delete_records('editor_atto_autosave', $params);
-    die();
 }
 
-print_error('invalidarguments');
+echo json_encode($responses);
index 87a5bcf..c419bf5 100644 (file)
@@ -86,5 +86,8 @@ function xmldb_editor_atto_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 63a1c85..1a11a63 100644 (file)
@@ -42,5 +42,8 @@ function xmldb_atto_equation_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
\ No newline at end of file
index 09ba0b7..fbcfd8b 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 912122e..c8557d0 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 27f74d3..d2b7ea5 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index 3984d2f..cd310b4 100644 (file)
@@ -7,6 +7,7 @@
                 "notify.js",
                 "textarea.js",
                 "autosave.js",
+                "autosave-io.js",
                 "clean.js",
                 "commands.js",
                 "toolbar.js",
diff --git a/lib/editor/atto/yui/src/editor/js/autosave-io.js b/lib/editor/atto/yui/src/editor/js/autosave-io.js
new file mode 100644 (file)
index 0000000..a37352e
--- /dev/null
@@ -0,0 +1,244 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A autosave function for the Atto editor.
+ *
+ * @module     moodle-editor_atto-autosave-io
+ * @submodule  autosave-io
+ * @package    editor_atto
+ * @copyright  2016 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+var EditorAutosaveIoDispatcherInstance = null;
+
+function EditorAutosaveIoDispatcher() {
+    EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);
+    this._submitEvents = {};
+    this._queue = [];
+    this._throttle = null;
+}
+EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';
+EditorAutosaveIoDispatcher.ATTRS = {
+
+    /**
+     * The relative path to the ajax script.
+     *
+     * @attribute autosaveAjaxScript
+     * @type String
+     * @default '/lib/editor/atto/autosave-ajax.php'
+     * @readOnly
+     */
+    autosaveAjaxScript: {
+        value: '/lib/editor/atto/autosave-ajax.php',
+        readOnly: true
+    },
+
+    /**
+     * The time buffer for the throttled requested.
+     *
+     * @attribute delay
+     * @type Number
+     * @default 50
+     * @readOnly
+     */
+    delay: {
+        value: 50,
+        readOnly: true
+    }
+
+};
+Y.extend(EditorAutosaveIoDispatcher, Y.Base, {
+
+    /**
+     * Dispatch an IO request.
+     *
+     * This method will put the requests in a queue in order to attempt to bulk them.
+     *
+     * @param  {Object} params    The parameters of the request.
+     * @param  {Object} context   The context in which the callbacks are called.
+     * @param  {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
+     *                            optional keys defining the callbacks to call. Success and Complete
+     *                            functions will receive the response as parameter. Success and Complete
+     *                            may receive an object containing the error key, use this to confirm
+     *                            that no errors occured.
+     * @return {Void}
+     */
+    dispatch: function(params, context, callbacks) {
+        if (this._throttle) {
+            this._throttle.cancel();
+        }
+
+        this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);
+        this._queue.push([params, context, callbacks]);
+    },
+
+    /**
+     * Dispatches the requests in the queue.
+     *
+     * @return {Void}
+     */
+    _processDispatchQueue: function() {
+        var queue = this._queue,
+            data = {};
+
+        this._queue = [];
+        if (queue.length < 1) {
+            return;
+        }
+
+        Y.Array.each(queue, function(item, index) {
+            data[index] = item[0];
+        });
+
+        Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
+            method: 'POST',
+            data: Y.QueryString.stringify({
+                actions: data,
+                sesskey: M.cfg.sesskey
+            }),
+            on: {
+                start: this._makeIoEventCallback('start', queue),
+                complete: this._makeIoEventCallback('complete', queue),
+                failure: this._makeIoEventCallback('failure', queue),
+                end: this._makeIoEventCallback('end', queue),
+                success: this._makeIoEventCallback('success', queue)
+            }
+        });
+    },
+
+    /**
+     * Creates a function that dispatches an IO response to callbacks.
+     *
+     * @param  {String} event The type of event.
+     * @param  {Array} queue The queue.
+     * @return {Function}
+     */
+    _makeIoEventCallback: function(event, queue) {
+        var noop = function() {};
+        return function() {
+            var response = arguments[1],
+                parsed = {};
+
+            if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'
+                    && typeof response.responseText !== 'undefined' && response.responseText !== '')) {
+
+                // Success and complete events need to parse the response.
+                parsed = JSON.parse(response.responseText) || {};
+            }
+
+            Y.Array.each(queue, function(item, index) {
+                var context = item[1],
+                    cb = (item[2] && item[2][event]) || noop,
+                    arg;
+
+                if (parsed && parsed.error) {
+                    // The response is an error, we send it to everyone.
+                    arg = parsed;
+                } else if (parsed) {
+                    // The response was parsed, we only communicate the relevant portion of the response.
+                    arg = parsed[index];
+                }
+
+                cb.apply(context, [arg]);
+            });
+        };
+    },
+
+    /**
+     * Form submit handler.
+     *
+     * @param  {EventFacade} e The event.
+     * @return {Void}
+     */
+    _onSubmit: function(e) {
+        var data = {},
+            id = e.currentTarget.generateID(),
+            params = this._submitEvents[id];
+
+        if (!params || params.ios.length < 1) {
+            return;
+        }
+
+        Y.Array.each(params.ios, function(param, index) {
+            data[index] = param;
+        });
+
+        Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
+            method: 'POST',
+            data: Y.QueryString.stringify({
+                actions: data,
+                sesskey: M.cfg.sesskey
+            }),
+            sync: true
+        });
+    },
+
+    /**
+     * Registers a request to be made on form submission.
+     *
+     * @param  {Node} node The forum node we will listen to.
+     * @param  {Object} params Parameters for the IO request.
+     * @return {Void}
+     */
+    whenSubmit: function(node, params) {
+        if (typeof this._submitEvents[node.generateID()] === 'undefined') {
+            this._submitEvents[node.generateID()] = {
+                event: node.on('submit', this._onSubmit, this),
+                ios: []
+            };
+        }
+        this._submitEvents[node.get('id')].ios.push([params]);
+    }
+
+});
+EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();
+
+
+function EditorAutosaveIo() {}
+EditorAutosaveIo.prototype = {
+
+    /**
+     * Dispatch an IO request.
+     *
+     * This method will put the requests in a queue in order to attempt to bulk them.
+     *
+     * @param  {Object} params    The parameters of the request.
+     * @param  {Object} context   The context in which the callbacks are called.
+     * @param  {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
+     *                            optional keys defining the callbacks to call. Success and Complete
+     *                            functions will receive the response as parameter. Success and Complete
+     *                            may receive an object containing the error key, use this to confirm
+     *                            that no errors occured.
+     * @return {Void}
+     */
+    autosaveIo: function(params, context, callbacks) {
+        EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);
+    },
+
+    /**
+     * Registers a request to be made on form submission.
+     *
+     * @param  {Node} form The forum node we will listen to.
+     * @param  {Object} params Parameters for the IO request.
+     * @return {Void}
+     */
+    autosaveIoOnSubmit: function(form, params) {
+        EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);
+    }
+
+};
+Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);
index 4dd4ed4..27f227a 100644 (file)
@@ -65,19 +65,6 @@ EditorAutosave.ATTRS= {
     pageHash: {
         value: '',
         writeOnce: true
-    },
-
-    /**
-     * The relative path to the ajax script.
-     *
-     * @attribute autosaveAjaxScript
-     * @type String
-     * @default '/lib/editor/atto/autosave-ajax.php'
-     * @readOnly
-     */
-    autosaveAjaxScript: {
-        value: '/lib/editor/atto/autosave-ajax.php',
-        readOnly: true
     }
 };
 
@@ -118,8 +105,7 @@ EditorAutosave.prototype = {
             form,
             optiontype = null,
             options = this.get('filepickeroptions'),
-            params,
-            url;
+            params;
 
         if (!this.get('autosaveEnabled')) {
             // Autosave disabled for this instance.
@@ -135,99 +121,73 @@ EditorAutosave.prototype = {
 
         // First see if there are any saved drafts.
         // Make an ajax request.
-        url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
         params = {
-            sesskey: M.cfg.sesskey,
             contextid: this.get('contextid'),
             action: 'resume',
-            drafttext: '',
             draftid: draftid,
             elementid: this.get('elementid'),
             pageinstance: this.autosaveInstance,
             pagehash: this.get('pageHash')
         };
 
-        Y.io(url, {
-            method: 'POST',
-            data: params,
-            context: this,
-            on: {
-                success: function(id,o) {
-                    var response_json;
-                    if (typeof o.responseText !== "undefined" && o.responseText !== "") {
-                        response_json = JSON.parse(o.responseText);
+        this.autosaveIo(params, this, {
+            success: function(response) {
+                if (response === null) {
+                    // This can happen when there is nothing to resume from.
+                    return;
+                } else if (!response) {
+                    Y.log('Invalid response received.', 'debug', LOGNAME_AUTOSAVE);
+                    return;
+                }
 
-                        // Revert untouched editor contents to an empty string.
-                        // Check for FF and Chrome.
-                        if (response_json.result === '<p></p>' || response_json.result === '<p><br></p>' ||
-                            response_json.result === '<br>') {
-                            response_json.result = '';
-                        }
+                // Revert untouched editor contents to an empty string.
+                // Check for FF and Chrome.
+                if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
+                    response.result === '<br>') {
+                    response.result = '';
+                }
 
-                        // Check for IE 9 and 10.
-                        if (response_json.result === '<p>&nbsp;</p>' || response_json.result === '<p><br>&nbsp;</p>') {
-                            response_json.result = '';
-                        }
+                // Check for IE 9 and 10.
+                if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
+                    response.result = '';
+                }
 
-                        if (response_json.error || typeof response_json.result === 'undefined') {
-                            Y.log('Error occurred recovering draft text: ' + response_json.error, 'debug', LOGNAME_AUTOSAVE);
-                            this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
-                                    NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
-                        } else if (response_json.result !== this.textarea.get('value') &&
-                                response_json.result !== '') {
-                            Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
-                            this.recoverText(response_json.result);
-                        }
-                        this._fireSelectionChanged();
-                    }
-                },
-                failure: function() {
+                if (response.error || typeof response.result === 'undefined') {
+                    Y.log('Error occurred recovering draft text: ' + response.error, 'debug', LOGNAME_AUTOSAVE);
                     this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
                             NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
+                } else if (response.result !== this.textarea.get('value') &&
+                        response.result !== '') {
+                    Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE);
+                    this.recoverText(response.result);
                 }
+                this._fireSelectionChanged();
+
+            },
+            failure: function() {
+                this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
+                        NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
             }
         });
 
         // Now setup the timer for periodic saves.
-
         var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
         this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
 
         // Now setup the listener for form submission.
         form = this.textarea.ancestor('form');
         if (form) {
-            form.on('submit', this.resetAutosave, this);
+            this.autosaveIoOnSubmit(form, {
+                action: 'reset',
+                contextid: this.get('contextid'),
+                elementid: this.get('elementid'),
+                pageinstance: this.autosaveInstance,
+                pagehash: this.get('pageHash')
+            });
         }
         return this;
     },
 
-    /**
-     * Clear the autosave text because the form was submitted normally.
-     *
-     * @method resetAutosave
-     * @chainable
-     */
-    resetAutosave: function() {
-        // Make an ajax request to reset the autosaved text.
-        var url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
-        var params = {
-            sesskey: M.cfg.sesskey,
-            contextid: this.get('contextid'),
-            action: 'reset',
-            elementid: this.get('elementid'),
-            pageinstance: this.autosaveInstance,
-            pagehash: this.get('pageHash')
-        };
-
-        Y.io(url, {
-            method: 'POST',
-            data: params,
-            sync: true
-        });
-        return this;
-    },
-
-
     /**
      * Recover a previous version of this text and show a message.
      *
@@ -283,29 +243,23 @@ EditorAutosave.prototype = {
             };
 
             // Reusable error handler - must be passed the correct context.
-            var ajaxErrorFunction = function(code, response) {
+            var ajaxErrorFunction = function(response) {
                 var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
-                Y.log('Error while autosaving text:' + code, 'warn', LOGNAME_AUTOSAVE);
+                Y.log('Error while autosaving text', 'warn', LOGNAME_AUTOSAVE);
                 Y.log(response, 'warn', LOGNAME_AUTOSAVE);
                 this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
             };
 
-            Y.io(url, {
-                method: 'POST',
-                data: params,
-                context: this,
-                on: {
-                    error: ajaxErrorFunction,
-                    failure: ajaxErrorFunction,
-                    success: function(code, response) {
-                        if (response.responseText !== "") {
-                            Y.soon(Y.bind(ajaxErrorFunction, this, [code, response]));
-                        } else {
-                            // All working.
-                            this.lastText = newText;
-                            this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
-                                    NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
-                        }
+            this.autosaveIo(params, this, {
+                failure: ajaxErrorFunction,
+                success: function(response) {
+                    if (response && response.error) {
+                        Y.soon(Y.bind(ajaxErrorFunction, this, [response]));
+                    } else {
+                        // All working.
+                        this.lastText = newText;
+                        this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
+                                NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
                     }
                 }
             });
index a8f9ff3..65750e3 100644 (file)
@@ -15,7 +15,8 @@
             "moodle-core-notification-confirm",
             "moodle-editor_atto-rangy",
             "handlebars",
-            "timers"
+            "timers",
+            "querystring-stringify"
         ]
     },
     "moodle-editor_atto-plugin": {
index 14204f5..2ed7bbf 100644 (file)
@@ -56,5 +56,8 @@ function xmldb_editor_tinymce_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 2186346..75ed576 100644 (file)
@@ -36,5 +36,8 @@ function xmldb_tinymce_spellchecker_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 393769d..23d4379 100644 (file)
@@ -1123,8 +1123,8 @@ OET;
                 // is a simplified version, does not take into account old browser
                 // versions or manual plugins.
                 if ($ext === 'ogv' || $ext === 'webm') {
-                    // Formats .ogv and .webm are not supported in IE or Safari.
-                    if (core_useragent::is_ie() || core_useragent::is_safari()) {
+                    // Formats .ogv and .webm are not supported in IE, Edge or Safari.
+                    if (core_useragent::is_ie() || core_useragent::is_edge() || core_useragent::is_safari()) {
                         continue;
                     }
                 } else {
@@ -1195,8 +1195,8 @@ OET;
             $ext = core_media::get_extension($url);
             if (in_array($ext, $extensions)) {
                 if ($ext === 'ogg' || $ext === 'oga') {
-                    // Formats .ogg and .oga are not supported in IE or Safari.
-                    if (core_useragent::is_ie() || core_useragent::is_safari()) {
+                    // Formats .ogg and .oga are not supported in IE, Edge, or Safari.
+                    if (core_useragent::is_ie() || core_useragent::is_edge() || core_useragent::is_safari()) {
                         continue;
                     }
                 } else {
index e397770..119091a 100644 (file)
@@ -90,9 +90,9 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
                 $url = $userauthplugin->edit_profile_url();
                 if (empty($url)) {
                     if (empty($course)) {
-                        $url = new moodle_url('/user/edit.php', array('userid' => $user->id, 'returnto' => 'profile'));
+                        $url = new moodle_url('/user/edit.php', array('id' => $user->id, 'returnto' => 'profile'));
                     } else {
-                        $url = new moodle_url('/user/edit.php', array('userid' => $user->id, 'course' => $course->id,
+                        $url = new moodle_url('/user/edit.php', array('id' => $user->id, 'course' => $course->id,
                             'returnto' => 'profile'));
                     }
                 }
index 0179887..0962a24 100644 (file)
@@ -220,6 +220,37 @@ function match_grade_options($gradeoptionsfull, $grade, $matchgrades = 'error')
     }
 }
 
+/**
+ * Remove stale questions from a category.
+ *
+ * While questions should not be left behind when they are not used any more,
+ * it does happen, maybe via restore, or old logic, or uncovered scenarios. When
+ * this happens, the users are unable to delete the question category unless
+ * they move those stale questions to another one category, but to them the
+ * category is empty as it does not contain anything. The purpose of this function
+ * is to detect the questions that may have gone stale and remove them.
+ *
+ * You will typically use this prior to checking if the category contains questions.
+ *
+ * The stale questions (unused and hidden to the user) handled are:
+ * - hidden questions
+ * - random questions
+ *
+ * @param int $categoryid The category ID.
+ */
+function question_remove_stale_questions_from_category($categoryid) {
+    global $DB;
+
+    $select = 'category = :categoryid AND (qtype = :qtype OR hidden = :hidden)';
+    $params = ['categoryid' => $categoryid, 'qtype' => 'random', 'hidden' => 1];
+    $questions = $DB->get_recordset_select("question", $select, $params, '', 'id');
+    foreach ($questions as $question) {
+        // The function question_delete_question does not delete questions in use.
+        question_delete_question($question->id);
+    }
+    $questions->close();
+}
+
 /**
  * Category is about to be deleted,
  * 1/ All questions are deleted for this question category.
index c2a9862..0b95413 100644 (file)
@@ -429,8 +429,19 @@ class behat_hooks extends behat_base {
             return false;
         }
 
-        list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
-        $this->saveScreenshot($filename, $dir);
+        // Some drivers (e.g. chromedriver) may throw an exception while trying to take a screenshot.  If this isn't handled,
+        // the behat run dies.  We don't want to lose the information about the failure that triggered the screenshot,
+        // so let's log the exception message to a file (to explain why there's no screenshot) and allow the run to continue,
+        // handling the failure as normal.
+        try {
+            list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
+            $this->saveScreenshot($filename, $dir);
+        } catch (Exception $e) {
+            // Catching all exceptions as we don't know what the driver might throw.
+            list ($dir, $filename) = $this->get_faildump_filename($scope, 'txt');
+            $message = "Could not save screenshot due to an error\n" . $e->getMessage();
+            file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $message);
+        }
     }
 
     /**
@@ -442,9 +453,14 @@ class behat_hooks extends behat_base {
     protected function take_contentdump(AfterStepScope $scope) {
         list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
 
-        $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w');
-        fwrite($fh, $this->getSession()->getPage()->getContent());
-        fclose($fh);
+        try {
+            // Driver may throw an exception during getContent(), so do it first to avoid getting an empty file.
+            $content = $this->getSession()->getPage()->getContent();
+        } catch (Exception $e) {
+            // Catching all exceptions as we don't know what the driver might throw.
+            $content = "Could not save contentdump due to an error\n" . $e->getMessage();
+        }
+        file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $content);
     }
 
     /**
index 969e487..b699f72 100644 (file)
@@ -393,4 +393,54 @@ class core_questionlib_testcase extends advanced_testcase {
         $criteria = array('category' => $qcat->id);
         $this->assertEquals(0, $DB->count_records('question', $criteria));
     }
+
+    public function test_question_remove_stale_questions_from_category() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $dg = $this->getDataGenerator();
+        $course = $dg->create_course();
+        $quiz = $dg->create_module('quiz', ['course' => $course->id]);
+
+        $qgen = $dg->get_plugin_generator('core_question');
+        $context = context_system::instance();
+
+        $qcat1 = $qgen->create_question_category(['contextid' => $context->id]);
+        $q1a = $qgen->create_question('shortanswer', null, ['category' => $qcat1->id]);     // Will be hidden.
+        $q1b = $qgen->create_question('random', null, ['category' => $qcat1->id]);          // Will not be used.
+        $DB->set_field('question', 'hidden', 1, ['id' => $q1a->id]);
+
+        $qcat2 = $qgen->create_question_category(['contextid' => $context->id]);
+        $q2a = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden.
+        $q2b = $qgen->create_question('shortanswer', null, ['category' => $qcat2->id]);     // Will be hidden but used.
+        $q2c = $qgen->create_question('random', null, ['category' => $qcat2->id]);          // Will not be used.
+        $q2d = $qgen->create_question('random', null, ['category' => $qcat2->id]);          // Will be used.
+        $DB->set_field('question', 'hidden', 1, ['id' => $q2a->id]);
+        $DB->set_field('question', 'hidden', 1, ['id' => $q2b->id]);
+        quiz_add_quiz_question($q2b->id, $quiz);
+        quiz_add_quiz_question($q2d->id, $quiz);
+
+        $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
+        $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
+
+        // Non-existing category, nothing will happen.
+        question_remove_stale_questions_from_category(0);
+        $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat1->id]));
+        $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
+
+        // First category, should be empty afterwards.
+        question_remove_stale_questions_from_category($qcat1->id);
+        $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
+        $this->assertEquals(4, $DB->count_records('question', ['category' => $qcat2->id]));
+        $this->assertFalse($DB->record_exists('question', ['id' => $q1a->id]));
+        $this->assertFalse($DB->record_exists('question', ['id' => $q1b->id]));
+
+        // Second category, used questions should be left untouched.
+        question_remove_stale_questions_from_category($qcat2->id);
+        $this->assertEquals(0, $DB->count_records('question', ['category' => $qcat1->id]));
+        $this->assertEquals(2, $DB->count_records('question', ['category' => $qcat2->id]));
+        $this->assertFalse($DB->record_exists('question', ['id' => $q2a->id]));
+        $this->assertTrue($DB->record_exists('question', ['id' => $q2b->id]));
+        $this->assertFalse($DB->record_exists('question', ['id' => $q2c->id]));
+        $this->assertTrue($DB->record_exists('question', ['id' => $q2d->id]));
+    }
 }
index deecb6f..d0be09f 100644 (file)
@@ -95,4 +95,67 @@ class core_weblib_format_text_testcase extends advanced_testcase {
         $this->assertEquals('<div class="no-overflow"><p>:-)</p></div>',
                 format_text('<p>:-)</p>', FORMAT_HTML, array('overflowdiv' => true)));
     }
+
+    /**
+     * Test adding blank target attribute to links
+     *
+     * @dataProvider format_text_blanktarget_testcases
+     * @param string $link The link to add target="_blank" to
+     * @param string $expected The expected filter value
+     */
+    public function test_format_text_blanktarget($link, $expected) {
+        $actual = format_text($link, FORMAT_MOODLE, array('blanktarget' => true, 'filter' => false, 'noclean' => true));
+        $this->assertEquals($expected, $actual);
+    }
+
+    /**
+     * Data provider for the test_format_text_blanktarget testcase
+     *
+     * @return array of testcases
+     */
+    public function format_text_blanktarget_testcases() {
+        return [
+            'Simple link' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
+                    ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with rel' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="nofollow noreferrer"' .
+                    ' target="_blank">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with rel noreferrer' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" rel="noreferrer"' .
+                 ' target="_blank">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with target' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_self">' .
+                    'Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with target blank' => [
+                '<a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank">Hey, that\'s pretty good!</a>',
+                '<div class="text_to_html"><a href="https://www.youtube.com/watch?v=JeimE8Wz6e4" target="_blank"' .
+                    ' rel="noreferrer">Hey, that\'s pretty good!</a></div>'
+            ],
+            'Link with Frank\'s casket inscription' => [
+                '<a href="https://en.wikipedia.org/wiki/Franks_Casket">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻ' .
+                    'ᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a>',
+                '<div class="text_to_html"><a href="https://en.wikipedia.org/wiki/Franks_Casket" target="_blank" ' .
+                    'rel="noreferrer">ᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁᚠᛁᛋᚳ᛫ᚠᛚᚩᛞᚢ᛫ᚪᚻᚩᚠᚩᚾᚠᛖᚱᚷ ᛖᚾ' .
+                    'ᛒᛖᚱᛁᚷ ᚹᚪᚱᚦᚷᚪ᛬ᛋᚱᛁᚳᚷᚱᚩᚱᚾᚦᚫᚱᚻᛖᚩᚾᚷᚱᛖᚢᛏᚷᛁᛋᚹᚩᛗ ᚻᚱᚩᚾᚫᛋᛒᚪᚾ ᛗᚫᚷᛁ</a></div>'
+             ],
+            'No link' => [
+                'Some very boring text written with the Latin script',
+                '<div class="text_to_html">Some very boring text written with the Latin script</div>'
+            ],
+            'No link with Thror\'s map runes' => [
+                'ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ',
+                '<div class="text_to_html">ᛋᛏᚫᚾᛞ ᛒᚣ ᚦᛖ ᚷᚱᛖᚣ ᛋᛏᚩᚾᛖ ᚻᚹᛁᛚᛖ ᚦᛖ ᚦᚱᚢᛋᚻ ᚾᚩᚳᛋ ᚫᚾᛞ ᚦᛖ ᛋᛖᛏᛏᛁᚾᚷ ᛋᚢᚾ ᚹᛁᚦ ᚦᛖ ᛚᚫᛋᛏ ᛚᛁᚷᚻᛏ ᚩᚠ ᛞᚢᚱᛁᚾᛋ ᛞᚫᚣ ᚹ' .
+                'ᛁᛚᛚ ᛋᚻᛁᚾᛖ ᚢᛈᚩᚾ ᚦᛖ ᚳᛖᚣᚻᚩᛚᛖ</div>'
+            ]
+        ];
+    }
 }
index 3326bd0..7218a1e 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
+=== 3.2 ===
+
+* New option 'blanktarget' added to format_text. This option adds target="_blank" to links
+
 === 3.1 ===
 
 * Webservice function core_course_search_courses accepts a new parameter 'limittoenrolled' to filter the results
index fe5b2c3..c86dc0e 100644 (file)
@@ -760,7 +760,6 @@ class moodle_url {
         if ($forcedownload) {
             $params['forcedownload'] = 1;
         }
-        $path = rtrim($path, '/');
         $url = new moodle_url($urlbase, $params);
         $url->set_slashargument($path);
         return $url;
@@ -1175,6 +1174,7 @@ function format_text_menu() {
  *                      with the class no-overflow before being returned. Default false.
  *      allowid     :   If true then id attributes will not be removed, even when
  *                      using htmlpurifier. Default false.
+ *      blanktarget :   If true all <a> tags will have target="_blank" added unless target is explicitly specified.
  * </pre>
  *
  * @staticvar array $croncache
@@ -1222,6 +1222,7 @@ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseidd
     if (!isset($options['overflowdiv'])) {
         $options['overflowdiv'] = false;
     }
+    $options['blanktarget'] = !empty($options['blanktarget']);
 
     // Calculate best context.
     if (empty($CFG->version) or $CFG->version < 2013051400 or during_initial_install()) {
@@ -1318,6 +1319,26 @@ function format_text($text, $format = FORMAT_MOODLE, $options = null, $courseidd
         $text = html_writer::tag('div', $text, array('class' => 'no-overflow'));
     }
 
+    if ($options['blanktarget']) {
+        $domdoc = new DOMDocument();
+        $domdoc->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $text);
+        foreach ($domdoc->getElementsByTagName('a') as $link) {
+            if ($link->hasAttribute('target') && strpos($link->getAttribute('target'), '_blank') === false) {
+                continue;
+            }
+            $link->setAttribute('target', '_blank');
+            if (strpos($link->getAttribute('rel'), 'noreferrer') === false) {
+                $link->setAttribute('rel', trim($link->getAttribute('rel') . ' noreferrer'));
+            }
+        }
+
+        // This regex is nasty and I don't like it. The correct way to solve this is by loading the HTML like so:
+        // $domdoc->loadHTML($text, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); however it seems like the libxml
+        // version that travis uses doesn't work properly and ends up leaving <html><body>, so I'm forced to use
+        // this regex to remove those tags.
+        $text = trim(preg_replace('~<(?:!DOCTYPE|/?(?:html|body))[^>]*>\s*~i', '', $domdoc->saveHTML($domdoc->documentElement)));
+    }
+
     return $text;
 }
 
index bd12367..6d060fb 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js differ
index 293b9c1..a6e9b29 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js differ
index 7e6f8cd..35bb1e3 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock.js differ
index ca46f73..75d33c1 100644 (file)
Binary files a/lib/yui/build/moodle-core-event/moodle-core-event-debug.js and b/lib/yui/build/moodle-core-event/moodle-core-event-debug.js differ
index ced026e..7a13d0d 100644 (file)
Binary files a/lib/yui/build/moodle-core-event/moodle-core-event-min.js and b/lib/yui/build/moodle-core-event/moodle-core-event-min.js differ
index 6a18fe6..a54d395 100644 (file)
Binary files a/lib/yui/build/moodle-core-event/moodle-core-event.js and b/lib/yui/build/moodle-core-event/moodle-core-event.js differ
index 8e03abc..e16d3e4 100644 (file)
@@ -62,6 +62,9 @@ M.core.dock._dockableblocks = {};
  */
 M.core.dock.init = function() {
     Y.all(SELECTOR.dockableblock).each(M.core.dock.registerDockableBlock);
+    Y.Global.on(M.core.globalEvents.BLOCK_CONTENT_UPDATED, function(e) {
+        M.core.dock.notifyBlockChange(e.instanceid);
+    }, this);
     BODY.delegate('click', M.core.dock.dockBlock, SELECTOR.blockmoveto);
     BODY.delegate('key', M.core.dock.dockBlock, SELECTOR.blockmoveto, 'enter');
 };
index 9f88e2a..7616fbd 100644 (file)
@@ -7,7 +7,8 @@
         "event-mouseenter",
         "event-resize",
         "escape",
-        "moodle-core-dock-loader"
+        "moodle-core-dock-loader",
+        "moodle-core-event"
     ]
   },
   "moodle-core-dock-loader": {
index 5a51c2a..f30f900 100644 (file)
@@ -50,7 +50,15 @@ M.core.globalEvents = M.core.globalEvents || {
      * @param formid {string} Id of form with error.
      * @param elementid {string} Id of element with error.
      */
-    FORM_ERROR: "form_error"
+    FORM_ERROR: "form_error",
+
+    /**
+     * This event is triggered when the content of a block has changed
+     *
+     * @event "block_content_updated"
+     * @param instanceid ID of the block instance that was updated
+     */
+    BLOCK_CONTENT_UPDATED: "block_content_updated"
 };
 
 
index 88d0a5f..6702c3b 100644 (file)
@@ -137,7 +137,8 @@ class core_message_external extends external_api {
             if ($success && empty($contactlist[$message['touserid']]) && !empty($blocknoncontacts)) {
                 // The user isn't a contact and they have selected to block non contacts so this message won't be sent.
                 $success = false;
-                $errormessage = get_string('userisblockingyounoncontact', 'message');
+                $errormessage = get_string('userisblockingyounoncontact', 'message',
+                        fullname(core_user::get_user($message['touserid'])));
             }
 
             //now we can send the message (at least try)
index 282d7e9..3904025 100644 (file)
@@ -983,6 +983,7 @@ function message_format_message_text($message, $forcetexttohtml = false) {
 
     $options = new stdClass();
     $options->para = false;
+    $options->blanktarget = true;
 
     $format = $message->fullmessageformat;
 
index e5ad531..e1a5f2d 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_message_email_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 3979a91..0570cc0 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_message_jabber_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 5c08828..c1834c9 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_message_popup_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index d5e358e..c0935c5 100644 (file)
@@ -197,5 +197,8 @@ function xmldb_assign_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 3c35f2f..1e0d2dd 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_assignfeedback_comments_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 599653b..c8bbaee 100644 (file)
@@ -65,5 +65,8 @@ function xmldb_assignfeedback_editpdf_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2016021600, 'assignfeedback', 'editpdf');
     }
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 347dbdf..8525ac1 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index 084e92b..a5c1922 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 347dbdf..8525ac1 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 6912735..8d842f8 100644 (file)
@@ -1106,7 +1106,7 @@ EDITOR.prototype = {
         drawingcanvas.setStyle('height', page.height + 'px');
 
         // Update page select.
-        this.get_dialogue_element(SELECTOR.PAGESELECT).set('value', this.currentpage);
+        this.get_dialogue_element(SELECTOR.PAGESELECT).set('selectedIndex', this.currentpage);
 
         this.resize(); // Internally will call 'redraw', after checking the dialogue size.
     },
index ecfe0ba..7a0ab50 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_assignfeedback_file_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index c4ce567..cd23c8e 100644 (file)
@@ -1411,7 +1411,11 @@ function assign_get_completion_state($course, $cm, $userid, $type) {
 
     // If completion option is enabled, evaluate it and return true/false.
     if ($assign->get_instance()->completionsubmit) {
-        $submission = $assign->get_user_submission($userid, false);
+        if ($assign->get_instance()->teamsubmission) {
+            $submission = $assign->get_group_submission($userid, 0, false);
+        } else {
+            $submission = $assign->get_user_submission($userid, false);
+        }
         return $submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED;
     } else {
         // Completion option is not enabled so just return $type.
index c5a2b1e..5160225 100644 (file)
@@ -5466,11 +5466,10 @@ class assign {
      * @param int $updatetime
      * @return void
      */
-    public function send_notification($userfrom,
-                                      $userto,
-                                      $messagetype,
-                                      $eventtype,
-                                      $updatetime) {
+    public function send_notification($userfrom, $userto, $messagetype, $eventtype, $updatetime) {
+        global $USER;
+        $userid = core_user::is_real_user($userfrom->id) ? $userfrom->id : $USER->id;
+        $uniqueid = $this->get_uniqueid_for_user($userid);
         self::send_assignment_notification($userfrom,
                                            $userto,
                                            $messagetype,
@@ -5482,7 +5481,7 @@ class assign {
                                            $this->get_module_name(),
                                            $this->get_instance()->name,
                                            $this->is_blind_marking(),
-                                           $this->get_uniqueid_for_user($userfrom->id));
+                                           $uniqueid);
     }
 
     /**
@@ -5633,7 +5632,12 @@ class assign {
             $this->update_submission($submission, $userid, true, $instance->teamsubmission);
             $completion = new completion_info($this->get_course());
             if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-                $completion->update_state($this->get_course_module(), COMPLETION_COMPLETE, $userid);
+                $this->update_activity_completion_records($instance->teamsubmission,
+                                                          $instance->requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          COMPLETION_COMPLETE,
+                                                          $completion);
             }
 
             if (!empty($data->submissionstatement) && $USER->id == $userid) {
@@ -6325,7 +6329,12 @@ class assign {
         }
         $completion = new completion_info($this->get_course());
         if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-            $completion->update_state($this->get_course_module(), $complete, $USER->id);
+            $this->update_activity_completion_records($instance->teamsubmission,
+                                                      $instance->requireallteammemberssubmit,
+                                                      $submission,
+                                                      $USER->id,
+                                                      $complete,
+                                                      $completion);
         }
 
         if (!$instance->submissiondrafts) {
@@ -7991,6 +8000,42 @@ class assign {
         }
         return $this->get_course_module()->id . '_' . $id;
     }
+
+    /**
+     * Updates and creates the completion records in mdl_course_modules_completion.
+     *
+     * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
+     * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
+     * @param obj $submission the submission
+     * @param int $userid the user id
+     * @param int $complete
+     * @param obj $completion
+     *
+     * @return null
+     */
+    protected function update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+
+        if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
+            ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
+             $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
+
+            $members = groups_get_members($submission->groupid);
+
+            foreach ($members as $member) {
+                $completion->update_state($this->get_course_module(), $complete, $member->id);
+            }
+        } else {
+            $completion->update_state($this->get_course_module(), $complete, $userid);
+        }
+
+        return;
+    }
+
 }
 
 /**
index 412f72f..be6166d 100644 (file)
@@ -209,32 +209,6 @@ class mod_assign_mod_form extends moodleform_mod {
         $this->apply_admin_defaults();
 
         $this->add_action_buttons();
-
-        // Add warning popup/noscript tag, if grades are changed by user.
-        $hasgrade = false;
-        if (!empty($this->_instance)) {
-            $hasgrade = $DB->record_exists_select('assign_grades',
-                                                  'assignment = ? AND grade <> -1',
-                                                  array($this->_instance));
-        }
-
-        if ($mform->elementExists('grade') && $hasgrade) {
-            $module = array(
-                'name' => 'mod_assign',
-                'fullpath' => '/mod/assign/module.js',
-                'requires' => array('node', 'event'),
-                'strings' => array(array('changegradewarning', 'mod_assign'))
-                );
-            $PAGE->requires->js_init_call('M.mod_assign.init_grade_change', null, false, $module);
-
-            // Add noscript tag in case.
-            $noscriptwarning = $mform->createElement('static',
-                                                     'warning',
-                                                     null,
-                                                     html_writer::tag('noscript',
-                                                     get_string('changegradewarning', 'mod_assign')));
-            $mform->insertElementBefore($noscriptwarning, 'grade');
-        }
     }
 
     /**
index 273eade..5f4d740 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_assignsubmission_comments_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 62f1376..9cf89ba 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_assignsubmission_file_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 1b1907f..9189c63 100644 (file)
@@ -41,5 +41,8 @@ function xmldb_assignsubmission_onlinetext_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }
index 51488d0..93f5093 100644 (file)
@@ -33,7 +33,7 @@
 }}
 <div data-region="grading-navigation" class="row-fluid">
 <div data-region="assignment-info" class="span4">
-<a href="{{config.wwwroot}}/course/view.php?id={{courseid}}">{{coursename}}</a><br/>
+<a href="{{config.wwwroot}}/course/view.php?id={{courseid}}">{{{coursename}}}</a><br/>
 <a href="{{config.wwwroot}}/mod/assign/view.php?id={{cmid}}">{{name}}</a>
 {{#caneditsettings}}
 <a href="{{config.wwwroot}}/course/modedit.php?update={{cmid}}&return=1">{{#pix}}t/edit, core,{{#str}}editsettings{{/str}}{{/pix}}</a>
index 57c234b..5c92b96 100644 (file)
@@ -90,7 +90,7 @@ class mod_assign_base_testcase extends advanced_testcase {
 
         $this->resetAfterTest(true);
 
-        $this->course = $this->getDataGenerator()->create_course();
+        $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
         $this->teachers = array();
         for ($i = 0; $i < self::DEFAULT_TEACHER_COUNT; $i++) {
             array_push($this->teachers, $this->getDataGenerator()->create_user());
@@ -350,4 +350,18 @@ class testable_assign extends assign {
 
         return $mform;
     }
+
+    public function testable_update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+        return parent::update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion);
+    }
 }
index 2062696..65670a5 100644 (file)
@@ -2631,4 +2631,94 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $grade = $assign->get_user_grade($this->students[0]->id, false);
         $this->assertEquals('30.0', $grade->grade);
     }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_solitary_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                'requireallteammemberssubmit' => 0));
+
+        $cm = $assign->get_course_module();
+
+        $student = $this->students[0];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        $this->setUser($student);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student->id, true);
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(0, 0, $submission,
+                $student->id, COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_team_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                 'teamsubmission' => 1));
+
+        $cm = $assign->get_course_module();
+
+        $student1 = $this->students[0];
+        $student2 = $this->students[1];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        // Put both users into a group.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student2->id));
+
+        $this->setUser($student1);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student1->id, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $submission->groupid = $group1->id;
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(1, 0, $submission, $student1->id,
+                COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
 }
index c083cc4..e6c5ca3 100644 (file)
@@ -33,5 +33,8 @@ function xmldb_assignment_upgrade($oldversion) {
     // Moodle v3.0.0 release upgrade line.
     // Put any upgrade step following this.
 
+    // Moodle v3.1.0 release upgrade line.
+    // Put any upgrade step following this.
+
     return true;
 }