Merge branch 'install_master' of https://git.in.moodle.com/amosbot/moodle-install
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 3 Jun 2016 04:09:16 +0000 (12:09 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Fri, 3 Jun 2016 04:09:16 +0000 (12:09 +0800)
80 files changed:
admin/roles/classes/preset.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/templates/user_competency_summary.mustache
admin/tool/lp/templates/user_competency_summary_in_course.mustache
backup/moodle2/backup_stepslib.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/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/comments/tests/behat/block_comment_dashboard.feature [new file with mode: 0644]
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/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/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/settings/amd/build/settingsblock.min.js
blocks/settings/amd/src/settingsblock.js
blocks/settings/block_settings.php
comment/comment_post.php
enrol/manual/ajax.php
enrol/manual/lib.php
enrol/manual/manage.php
enrol/manual/yui/quickenrolment/quickenrolment.js
grade/lib.php
lang/en/question.php
lib/blocklib.php
lib/myprofilelib.php
lib/questionlib.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/lib.php
mod/chat/lib.php
mod/choice/lib.php
mod/data/data.js
mod/quiz/report/overview/report.php
mod/workshop/lib.php
mod/workshop/mod_form.php
my/lib.php
question/category.php
question/format/gift/format.php
question/format/gift/tests/giftformat_test.php
question/type/multianswer/backup/moodle2/restore_qtype_multianswer_plugin.class.php
report/competency/amd/build/user_course_navigation.min.js
report/competency/amd/src/user_course_navigation.js
user/profile/field/datetime/field.class.php

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 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 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 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 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);
+    }
+}
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"
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 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
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 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 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 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 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 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 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 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 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 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 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..a7aa3bf 100644 (file)
@@ -95,4 +95,56 @@ 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>'
+                ]
+        ];
+    }
 }
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..e094828 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($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()));
+    }
+
     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 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 53968b4..3c5d57a 100644 (file)
@@ -749,6 +749,7 @@ function chat_format_message_manually($message, $courseid, $sender, $currentuser
     // Parse the text to clean and filter it.
     $options = new stdClass();
     $options->para = false;
+    $options->blanktarget = true;
     $text = format_text($text, FORMAT_MOODLE, $options, $courseid);
 
     // And now check for special cases.
@@ -922,6 +923,7 @@ function chat_format_message_theme ($message, $chatuser, $currentuser, $grouping
     // Parse the text to clean and filter it.
     $options = new stdClass();
     $options->para = false;
+    $options->blanktarget = true;
     $text = format_text($text, FORMAT_MOODLE, $options, $courseid);
 
     // And now check for special cases.
index 0deb0a8..666df96 100644 (file)
@@ -79,20 +79,23 @@ function choice_user_outline($course, $user, $mod, $choice) {
 }
 
 /**
- * @global object
+ * Callback for the "Complete" report - prints the activity summary for the given user
+ *
  * @param object $course
  * @param object $user
  * @param object $mod
  * @param object $choice
- * @return string|void
  */
 function choice_user_complete($course, $user, $mod, $choice) {
     global $DB;
-    if ($answer = $DB->get_record('choice_answers', array("choiceid" => $choice->id, "userid" => $user->id))) {
-        $result = new stdClass();
-        $result->info = "'".format_string(choice_get_option_text($choice, $answer->optionid))."'";
-        $result->time = $answer->timemodified;
-        echo get_string("answered", "choice").": $result->info. ".get_string("updated", '', userdate($result->time));
+    if ($answers = $DB->get_records('choice_answers', array("choiceid" => $choice->id, "userid" => $user->id))) {
+        $info = [];
+        foreach ($answers as $answer) {
+            $info[] = "'" . format_string(choice_get_option_text($choice, $answer->optionid)) . "'";
+        }
+        core_collator::asort($info);
+        echo get_string("answered", "choice") . ": ". join(', ', $info) . ". " .
+                get_string("updated", '', userdate($answer->timemodified));
     } else {
         print_string("notanswered", "choice");
     }
index a733dbf..871de64 100644 (file)
@@ -6,9 +6,13 @@ function insert_field_tags(selectlist) {
     var value = selectlist.options[selectlist.selectedIndex].value;
     var editorname = 'template';
     if (typeof tinyMCE == 'undefined') {
-        var element = document.getElementsByName(editorname)[0];
-        // For inserting when in normal textareas
-        insertAtCursor(element, value);
+        if (document.execCommand('insertText')) {
+            document.execCommand('insertText', false, value);
+        } else {
+            var element = document.getElementsByName(editorname)[0];
+            // For inserting when in normal textareas
+            insertAtCursor(element, value);
+        }
     } else {
         tinyMCE.execInstanceCommand(editorname, 'mceInsertContent', false, value);
     }
index 646073a..f01eec8 100644 (file)
@@ -441,23 +441,25 @@ class quiz_overview_report extends quiz_attempts_report {
         if ($groupstudents) {
             list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
             $where .= " AND quiza.userid $usql";
-            $params += $uparams;
+            $params = array_merge($params, $uparams);
         }
 
-        $toregrade = $DB->get_records_sql("
+        $toregrade = $DB->get_recordset_sql("
                 SELECT quiza.uniqueid, qqr.slot
                 FROM {quiz_attempts} quiza
                 JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid
                 WHERE $where", $params);
 
-        if (!$toregrade) {
-            return;
-        }
-
         $attemptquestions = array();
         foreach ($toregrade as $row) {
             $attemptquestions[$row->uniqueid][] = $row->slot;
         }
+        $toregrade->close();
+
+        if (!$attemptquestions) {
+            return;
+        }
+
         $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
                 array_keys($attemptquestions));
 
index 9dc499b..01e5701 100644 (file)
@@ -81,6 +81,14 @@ function workshop_add_instance(stdclass $workshop) {
     $workshop->phaseswitchassessment = (int)!empty($workshop->phaseswitchassessment);
     $workshop->evaluation            = 'best';
 
+    if (isset($workshop->gradinggradepass)) {
+        $workshop->gradinggradepass = unformat_float($workshop->gradinggradepass);
+    }
+
+    if (isset($workshop->submissiongradepass)) {
+        $workshop->submissiongradepass = unformat_float($workshop->submissiongradepass);
+    }
+
     if (isset($workshop->submissionfiletypes)) {
         $workshop->submissionfiletypes = workshop::clean_file_extensions($workshop->submissionfiletypes);
     }
@@ -149,6 +157,14 @@ function workshop_update_instance(stdclass $workshop) {
     $workshop->latesubmissions       = (int)!empty($workshop->latesubmissions);
     $workshop->phaseswitchassessment = (int)!empty($workshop->phaseswitchassessment);
 
+    if (isset($workshop->gradinggradepass)) {
+        $workshop->gradinggradepass = unformat_float($workshop->gradinggradepass);
+    }
+
+    if (isset($workshop->submissiongradepass)) {
+        $workshop->submissiongradepass = unformat_float($workshop->submissiongradepass);
+    }
+
     if (isset($workshop->submissionfiletypes)) {
         $workshop->submissionfiletypes = workshop::clean_file_extensions($workshop->submissionfiletypes);
     }
index 64b9ac0..9fcaf27 100644 (file)
@@ -99,8 +99,7 @@ class mod_workshop_mod_form extends moodleform_mod {
         $mform->addElement('text', 'submissiongradepass', get_string('gradetopasssubmission', 'workshop'));
         $mform->addHelpButton('submissiongradepass', 'gradepass', 'grades');
         $mform->setDefault('submissiongradepass', '');
-        $mform->setType('submissiongradepass', PARAM_FLOAT);
-        $mform->addRule('submissiongradepass', null, 'numeric', null, 'client');
+        $mform->setType('submissiongradepass', PARAM_RAW);
 
         $label = get_string('gradinggrade', 'workshop');
         $mform->addGroup(array(
@@ -113,8 +112,7 @@ class mod_workshop_mod_form extends moodleform_mod {
         $mform->addElement('text', 'gradinggradepass', get_string('gradetopassgrading', 'workshop'));
         $mform->addHelpButton('gradinggradepass', 'gradepass', 'grades');
         $mform->setDefault('gradinggradepass', '');
-        $mform->setType('gradinggradepass', PARAM_FLOAT);
-        $mform->addRule('gradinggradepass', null, 'numeric', null, 'client');
+        $mform->setType('gradinggradepass', PARAM_RAW);
 
         $options = array();
         for ($i = 5; $i >= 0; $i--) {
@@ -397,11 +395,28 @@ class mod_workshop_mod_form extends moodleform_mod {
             }
         }
 
-        if ($data['submissiongradepass'] > $data['grade']) {
-            $errors['submissiongradepass'] = get_string('gradepassgreaterthangrade', 'grades', $data['grade']);
+        // Check that the submission grade pass is a valid number.
+        if (isset($data['submissiongradepass'])) {
+            $submissiongradefloat = unformat_float($data['submissiongradepass'], true);
+            if ($submissiongradefloat === false || $submissiongradefloat === null) {
+                $errors['submissiongradepass'] = get_string('err_numeric', 'form');
+            } else {
+                if ($submissiongradefloat > $data['grade']) {
+                    $errors['submissiongradepass'] = get_string('gradepassgreaterthangrade', 'grades', $data['grade']);
+                }
+            }
         }
-        if ($data['gradinggradepass'] > $data['gradinggrade']) {
-            $errors['gradinggradepass'] = get_string('gradepassgreaterthangrade', 'grades', $data['gradinggrade']);
+
+        // Check that the grade pass is a valid number.
+        if (isset($data['gradinggradepass'])) {
+            $gradepassfloat = unformat_float($data['gradinggradepass'], true);
+            if ($gradepassfloat === false || $gradepassfloat === null) {
+                $errors['gradinggradepass'] = get_string('err_numeric', 'form');
+            } else {
+                if ($gradepassfloat > $data['gradinggrade']) {
+                    $errors['gradinggradepass'] = get_string('gradepassgreaterthangrade', 'grades', $data['gradinggrade']);
+                }
+            }
         }
 
         return $errors;
index bd3219d..8ad1e70 100644 (file)
@@ -146,26 +146,30 @@ function my_reset_page($userid, $private=MY_PAGE_PRIVATE, $pagetype='my-index')
 function my_reset_page_for_all_users($private = MY_PAGE_PRIVATE, $pagetype = 'my-index') {
     global $DB;
 
+    // This may take a while. Raise the execution time limit.
+    core_php_time_limit::raise();
+
     // Find all the user pages.
     $where = 'userid IS NOT NULL AND private = :private';
     $params = array('private' => $private);
     $pages = $DB->get_recordset_select('my_pages', $where, $params, 'id, userid');
-    $pageids = array();
     $blockids = array();
 
     foreach ($pages as $page) {
-        $pageids[] = $page->id;
         $usercontext = context_user::instance($page->userid);
 
         // Find all block instances in that page.
-        $blocks = $DB->get_recordset('block_instances', array('parentcontextid' => $usercontext->id,
-            'pagetypepattern' => $pagetype), '', 'id, subpagepattern');
-        foreach ($blocks as $block) {
-            if (is_null($block->subpagepattern) || $block->subpagepattern == $page->id) {
-                $blockids[] = $block->id;
-            }
+        $blockswhere = 'parentcontextid = :parentcontextid AND
+            pagetypepattern = :pagetypepattern AND
+            (subpagepattern IS NULL OR subpagepattern = :subpagepattern)';
+        $blockswhereparams = [
+            'parentcontextid' => $usercontext->id,
+            'pagetypepattern' => $pagetype,
+            'subpagepattern' => $page->id
+        ];
+        if ($pageblockids = $DB->get_fieldset_select('block_instances', 'id', $blockswhere, $blockswhereparams)) {
+            $blockids = array_merge($blockids, $pageblockids);
         }
-        $blocks->close();
     }
     $pages->close();
 
@@ -178,9 +182,8 @@ function my_reset_page_for_all_users($private = MY_PAGE_PRIVATE, $pagetype = 'my
     }
 
     // Finally delete the pages.
-    if (!empty($pageids)) {
-        list($insql, $inparams) = $DB->get_in_or_equal($pageids);
-        $DB->delete_records_select('my_pages', "id $insql", $pageids);
+    if (!empty($pages)) {
+        $DB->delete_records_select('my_pages', $where, $params);
     }
 
     // We should be good to go now.
index 0d62887..4e5d02f 100644 (file)
@@ -81,21 +81,27 @@ if ($param->moveupcontext || $param->movedowncontext) {
     // The previous line does a redirect().
 }
 
-if ($param->delete && ($questionstomove = $DB->count_records("question", array("category" => $param->delete)))) {
-    if (!$category = $DB->get_record("question_categories", array("id" => $param->delete))) {  // security
+if ($param->delete) {
+    if (!$category = $DB->get_record("question_categories", array("id" => $param->delete))) {
         print_error('nocate', 'question', $thispageurl->out(), $param->delete);
     }
-    $categorycontext = context::instance_by_id($category->contextid);
-    $qcobject->moveform = new question_move_form($thispageurl,
-                array('contexts'=>array($categorycontext), 'currentcat'=>$param->delete));
-    if ($qcobject->moveform->is_cancelled()){
-        redirect($thispageurl);
-    }  elseif ($formdata = $qcobject->moveform->get_data()) {
-        /// 'confirm' is the category to move existing questions to
-        list($tocategoryid, $tocontextid) = explode(',', $formdata->category);
-        $qcobject->move_questions_and_delete_category($formdata->delete, $tocategoryid);
-        $thispageurl->remove_params('cat', 'category');
-        redirect($thispageurl);
+
+    question_remove_stale_questions_from_category($param->delete);
+    $questionstomove = $DB->count_records("question", array("category" => $param->delete));
+
+    // Second pass, if we still have questions to move, setup the form.
+    if ($questionstomove) {
+        $categorycontext = context::instance_by_id($category->contextid);
+        $qcobject->moveform = new question_move_form($thispageurl,
+            array('contexts' => array($categorycontext), 'currentcat' => $param->delete));
+        if ($qcobject->moveform->is_cancelled()) {
+            redirect($thispageurl);
+        } else if ($formdata = $qcobject->moveform->get_data()) {
+            list($tocategoryid, $tocontextid) = explode(',', $formdata->category);
+            $qcobject->move_questions_and_delete_category($formdata->delete, $tocategoryid);
+            $thispageurl->remove_params('cat', 'category');
+            redirect($thispageurl);
+        }
     }
 } else {
     $questionstomove = 0;
index ed4d4dc..022a5c3 100644 (file)
@@ -694,7 +694,7 @@ class qformat_gift extends qformat_default {
                 $expout .= $this->write_questiontext($question->questiontext, $question->questiontextformat);
                 $expout .= "{\n";
                 foreach ($question->options->answers as $answer) {
-                    if ($answer->fraction == 1) {
+                    if ($answer->fraction == 1 && $question->options->single) {
                         $answertext = '=';
                     } else if ($answer->fraction == 0) {
                         $answertext = '~';
index 9c7bc05..2d3b9ab 100644 (file)
@@ -448,6 +448,91 @@ class qformat_gift_test extends question_testcase {
         $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
     }
 
+    public function test_import_multichoice_multi_tricky() {
+        $gift = "
+// multiple choice, multiple response with specified feedback for right and wrong answers
+::colours:: What's between orange and green in the spectrum?
+{
+    ~%100%yellow # right; good!
+    ~%-50%red # wrong
+    ~%-50%blue # wrong
+}";
+        $lines = preg_split('/[\\n\\r]/', str_replace("\r\n", "\n", $gift));
+
+        $importer = new qformat_gift();
+        $q = $importer->readquestion($lines);
+
+        $expectedq = (object) array(
+                'name' => 'colours',
+                'questiontext' => "What's between orange and green in the spectrum?",
+                'questiontextformat' => FORMAT_MOODLE,
+                'generalfeedback' => '',
+                'generalfeedbackformat' => FORMAT_MOODLE,
+                'qtype' => 'multichoice',
+                'defaultmark' => 1,
+                'penalty' => 0.3333333,
+                'length' => 1,
+                'single' => 0,
+                'shuffleanswers' => '1',
+                'answernumbering' => 'abc',
+                'correctfeedback' => array(
+                        'text' => '',
+                        'format' => FORMAT_MOODLE,
+                        'files' => array(),
+                ),
+                'partiallycorrectfeedback' => array(
+                        'text' => '',
+                        'format' => FORMAT_MOODLE,
+                        'files' => array(),
+                ),
+                'incorrectfeedback' => array(
+                        'text' => '',
+                        'format' => FORMAT_MOODLE,
+                        'files' => array(),
+                ),
+                'answer' => array(
+                        0 => array(
+                                'text' => 'yellow',
+                                'format' => FORMAT_MOODLE,
+                                'files' => array(),
+                        ),
+                        1 => array(
+                                'text' => 'red',
+                                'format' => FORMAT_MOODLE,
+                                'files' => array(),
+                        ),
+                        2 => array(
+                                'text' => 'blue',
+                                'format' => FORMAT_MOODLE,
+                                'files' => array(),
+                        ),
+                ),
+                'fraction' => array(1, -0.5, -0.5),
+                'feedback' => array(
+                        0 => array(
+                                'text' => 'right; good!',
+                                'format' => FORMAT_MOODLE,
+                                'files' => array(),
+                        ),
+                        1 => array(
+                                'text' => "wrong",
+                                'format' => FORMAT_MOODLE,
+                                'files' => array(),
+                        ),
+                        2 => array(
+                                'text' => "wrong",
+                                'format' => FORMAT_MOODLE,
+                                'files' => array(),
+                        ),
+                ),
+        );
+
+        // Repeated test for better failure messages.
+        $this->assertEquals($expectedq->answer, $q->answer);
+        $this->assertEquals($expectedq->feedback, $q->feedback);
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
     public function test_export_multichoice() {
         $qdata = (object) array(
             'id' => 666 ,
@@ -509,6 +594,72 @@ class qformat_gift_test extends question_testcase {
 \t~[plain]blue#wrong, it's yellow
 }
 
+";
+
+        $this->assert_same_gift($expectedgift, $gift);
+    }
+
+    public function test_export_multichoice_multi_tricky() {
+        $qdata = (object) array(
+            'id' => 666 ,
+            'name' => 'Q8',
+            'questiontext' => "What's between orange and green in the spectrum?",
+            'questiontextformat' => FORMAT_MOODLE,
+            'generalfeedback' => '',
+            'generalfeedbackformat' => FORMAT_MOODLE,
+            'defaultmark' => 1,
+            'penalty' => 0.3333333,
+            'length' => 1,
+            'qtype' => 'multichoice',
+            'options' => (object) array(
+                'single' => 0,
+                'shuffleanswers' => '1',
+                'answernumbering' => 'abc',
+                'correctfeedback' => '',
+                'correctfeedbackformat' => FORMAT_MOODLE,
+                'partiallycorrectfeedback' => '',
+                'partiallycorrectfeedbackformat' => FORMAT_MOODLE,
+                'incorrectfeedback' => '',
+                'incorrectfeedbackformat' => FORMAT_MOODLE,
+                'answers' => array(
+                    123 => (object) array(
+                        'id' => 123,
+                        'answer' => 'yellow',
+                        'answerformat' => FORMAT_MOODLE,
+                        'fraction' => 1,
+                        'feedback' => 'right; good!',
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                    124 => (object) array(
+                        'id' => 124,
+                        'answer' => 'red',
+                        'answerformat' => FORMAT_MOODLE,
+                        'fraction' => -0.5,
+                        'feedback' => "wrong, it's yellow",
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                    125 => (object) array(
+                        'id' => 125,
+                        'answer' => 'blue',
+                        'answerformat' => FORMAT_MOODLE,
+                        'fraction' => -0.5,
+                        'feedback' => "wrong, it's yellow",
+                        'feedbackformat' => FORMAT_MOODLE,
+                    ),
+                ),
+            ),
+        );
+
+        $exporter = new qformat_gift();
+        $gift = $exporter->writequestion($qdata);
+
+        $expectedgift = "// question: 666  name: Q8
+::Q8::What's between orange and green in the spectrum?{
+\t~%100%yellow#right; good!
+\t~%-50%red#wrong, it's yellow
+\t~%-50%blue#wrong, it's yellow
+}
+
 ";
 
         $this->assert_same_gift($expectedgift, $gift);
index c61c2dd..80865b1 100644 (file)
@@ -102,7 +102,11 @@ class restore_qtype_multianswer_plugin extends restore_qtype_plugin {
                    AND bi.itemname = 'question_created'",
                 array($this->get_restoreid()));
         foreach ($rs as $rec) {
-            $sequencearr = explode(',', $rec->sequence);
+            $sequencearr = preg_split('/,/', $rec->sequence, -1, PREG_SPLIT_NO_EMPTY);
+            if (substr_count($rec->sequence, ',') + 1 != count($sequencearr)) {
+                $this->task->log('Invalid sequence found in restored multianswer question ' . $rec->id, backup::LOG_WARNING);
+            }
+
             foreach ($sequencearr as $key => $question) {
                 $sequencearr[$key] = $this->get_mappingid('question', $question);
             }
index 5cfef53..fe73037 100644 (file)
Binary files a/report/competency/amd/build/user_course_navigation.min.js and b/report/competency/amd/build/user_course_navigation.min.js differ
index 07a1595..c5bf1bf 100644 (file)
@@ -35,7 +35,6 @@ define(['jquery'], function($) {
         this._baseUrl = baseUrl;
         this._userId = userId + '';
         this._courseId = courseId;
-        this._ignoreFirstUser = true;
 
         $(userSelector).on('change', this._userChanged.bind(this));
     };
@@ -47,11 +46,6 @@ define(['jquery'], function($) {
      * @param {Event} e
      */
     UserCourseNavigation.prototype._userChanged = function(e) {
-        if (this._ignoreFirstUser) {
-            this._ignoreFirstUser = false;
-            return;
-        }
-
         var newUserId = $(e.target).val();
         var queryStr = '?user=' + newUserId + '&id=' + this._courseId;
         document.location = this._baseUrl + queryStr;
@@ -63,8 +57,6 @@ define(['jquery'], function($) {
     UserCourseNavigation.prototype._courseId = null;
     /** @type {String} Plugin base url. */
     UserCourseNavigation.prototype._baseUrl = null;
-    /** @type {Boolean} Ignore the first change event for users. */
-    UserCourseNavigation.prototype._ignoreFirstUser = null;
 
     return /** @alias module:report_competency/user_course_navigation */ UserCourseNavigation;
 
index 1dadf04..be4af37 100644 (file)
@@ -76,15 +76,18 @@ class profile_field_datetime extends profile_field_base {
      * @since Moodle 2.5
      */
     public function edit_save_data_preprocess($datetime, $datarecord) {
-        // If timestamp then explode it to check if year is within field limit.
-        $isstring = strpos($datetime, '-');
-        if (empty($isstring)) {
+        if (!$datetime) {
+            return 0;
+        }
+
+        if (is_numeric($datetime)) {
             $datetime = userdate($datetime, '%Y-%m-%d-%H-%M-%S');
         }
 
         $datetime = explode('-', $datetime);
         // Bound year with start and end year.
         $datetime[0] = min(max($datetime[0], $this->field->param1), $this->field->param2);
+
         if (!empty($this->field->param3) && count($datetime) == 6) {
             return make_timestamp($datetime[0], $datetime[1], $datetime[2], $datetime[3], $datetime[4], $datetime[5]);
         } else {
@@ -110,4 +113,13 @@ class profile_field_datetime extends profile_field_base {
             return userdate($this->data, $format);
         }
     }
+
+    /**
+     * Check if the field data is considered empty
+     *
+     * @return boolean
+     */
+    public function is_empty() {
+        return empty($this->data);
+    }
 }