Merge branch 'MDL-54739-master' of git://github.com/lameze/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 6 Jun 2016 22:04:59 +0000 (00:04 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 6 Jun 2016 22:04:59 +0000 (00:04 +0200)
98 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_qtype_plugin.class.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.expectation [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.test [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.expectation [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.test [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.expectation [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.test [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.expectation [new file with mode: 0644]
backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.test [new file with mode: 0644]
backup/moodle2/tests/restore_gradebook_structure_step_test.php [new file with mode: 0644]
blocks/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/course_overview/tests/behat/quiz_overview.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
course/externallib.php
course/format/lib.php
enrol/manual/ajax.php
enrol/manual/lib.php
enrol/manual/manage.php
enrol/manual/yui/quickenrolment/quickenrolment.js
grade/lib.php
install/lang/ca/admin.php
install/lang/ca/install.php
lang/en/error.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/externallib.php
message/lib.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/tests/base_test.php
mod/assign/tests/locallib_test.php
mod/chat/lib.php
mod/choice/lib.php
mod/data/data.js
mod/lti/locallib.php
mod/quiz/lib.php
mod/quiz/report/overview/report.php
mod/quiz/tests/lib_test.php
mod/workshop/lib.php
mod/workshop/mod_form.php
my/lib.php
portfolio/download/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
version.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 60f599a..7520aa3 100644 (file)
@@ -181,7 +181,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
                 $info = new stdClass();
                 $info->filequestionid = $oldquestionid;
                 $info->dbquestionid   = $newquestionid;
-                $info->answer         = $data->answertext;
+                $info->answer         = s($data->answertext);
                 throw new restore_step_exception('error_question_answers_missing_in_db', $info);
             }
             $newitemid = $this->questionanswercache[$data->answertext];
index 158249b..e7e1ee2 100644 (file)
@@ -121,6 +121,21 @@ class restore_gradebook_structure_step extends restore_structure_step {
             return false;
         }
 
+        // Identify the backup we're dealing with.
+        $backuprelease = floatval($this->get_task()->get_info()->backup_release); // The major version: 2.9, 3.0, ...
+        $backupbuild = 0;
+        preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
+        if (!empty($matches[1])) {
+            $backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
+        }
+
+        // On older versions the freeze value has to be converted.
+        // We do this from here as it is happening right before the file is read.
+        // This only targets the backup files that can contain the legacy freeze.
+        if ($backupbuild > 20150618 && ($backuprelease < 3.0 || $backupbuild < 20160527)) {
+            $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
+        }
+
         // Arrived here, execute the step
         return true;
      }
@@ -129,7 +144,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         $paths = array();
         $userinfo = $this->task->get_setting_value('users');
 
-        $paths[] = new restore_path_element('gradebook', '/gradebook');
+        $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
         $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
         $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
         if ($userinfo) {
@@ -141,7 +156,7 @@ class restore_gradebook_structure_step extends restore_structure_step {
         return $paths;
     }
 
-    protected function process_gradebook($data) {
+    protected function process_attributes($data) {
         // For non-merge restore types:
         // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
         $target = $this->get_task()->get_target();
@@ -581,6 +596,85 @@ class restore_gradebook_structure_step extends restore_structure_step {
             }
         }
     }
+
+    /**
+     * Rewrite step definition to handle the legacy freeze attribute.
+     *
+     * In previous backups the calculations_freeze property was stored as an attribute of the
+     * top level node <gradebook>. The backup API, however, do not process grandparent nodes.
+     * It only processes definitive children, and their parent attributes.
+     *
+     * We had:
+     *
+     * <gradebook calculations_freeze="20160511">
+     *   <grade_categories>
+     *     <grade_category id="10">
+     *       <depth>1</depth>
+     *       ...
+     *     </grade_category>
+     *   </grade_categories>
+     *   ...
+     * </gradebook>
+     *
+     * And this method will convert it to:
+     *
+     * <gradebook >
+     *   <attributes>
+     *     <calculations_freeze>20160511</calculations_freeze>
+     *   </attributes>
+     *   <grade_categories>
+     *     <grade_category id="10">
+     *       <depth>1</depth>
+     *       ...
+     *     </grade_category>
+     *   </grade_categories>
+     *   ...
+     * </gradebook>
+     *
+     * Note that we cannot just load the XML file in memory as it could potentially be huge.
+     * We can also completely ignore if the node <attributes> is already in the backup
+     * file as it never existed before.
+     *
+     * @param string $filepath The absolute path to the XML file.
+     * @return void
+     */
+    protected function rewrite_step_backup_file_for_legacy_freeze($filepath) {
+        $foundnode = false;
+        $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml';
+        $fr = fopen($filepath, 'r');
+        $fw = fopen($newfile, 'w');
+        if ($fr && $fw) {
+            while (($line = fgets($fr, 4096)) !== false) {
+                if (!$foundnode && strpos($line, '<gradebook ') === 0) {
+                    $foundnode = true;
+                    $matches = array();
+                    $pattern = '@calculations_freeze=.([0-9]+).@';
+                    if (preg_match($pattern, $line, $matches)) {
+                        $freeze = $matches[1];
+                        $line = preg_replace($pattern, '', $line);
+                        $line .= "  <attributes>\n    <calculations_freeze>$freeze</calculations_freeze>\n  </attributes>\n";
+                    }
+                }
+                fputs($fw, $line);
+            }
+            if (!feof($fr)) {
+                throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.');
+            }
+            fclose($fr);
+            fclose($fw);
+            if (!rename($newfile, $filepath)) {
+                throw new restore_step_exception('Error while attempting to rename the gradebook step file.');
+            }
+        } else {
+            if ($fr) {
+                fclose($fr);
+            }
+            if ($fw) {
+                fclose($fw);
+            }
+        }
+    }
+
 }
 
 /**
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.expectation b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.expectation
new file mode 100644 (file)
index 0000000..99e8d85
--- /dev/null
@@ -0,0 +1,10 @@
+<gradebook >
+  <attributes>
+    <calculations_freeze>20160511</calculations_freeze>
+  </attributes>
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.test b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/1.test
new file mode 100644 (file)
index 0000000..830eca6
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebook calculations_freeze="20160511">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.expectation b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.expectation
new file mode 100644 (file)
index 0000000..b4f21d4
--- /dev/null
@@ -0,0 +1,10 @@
+<gradebook some_other_value="false" >
+  <attributes>
+    <calculations_freeze>20160511</calculations_freeze>
+  </attributes>
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.test b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/2.test
new file mode 100644 (file)
index 0000000..04f3b63
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebook some_other_value="false" calculations_freeze="20160511">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.expectation b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.expectation
new file mode 100644 (file)
index 0000000..c61f19d
--- /dev/null
@@ -0,0 +1,10 @@
+<gradebook some_other_value="false"  and_another_value="42">
+  <attributes>
+    <calculations_freeze>20160511</calculations_freeze>
+  </attributes>
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.test b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/3.test
new file mode 100644 (file)
index 0000000..39c46bc
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebook some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebook>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.expectation b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.expectation
new file mode 100644 (file)
index 0000000..71070aa
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebookplugin some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebookplugin>
diff --git a/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.test b/backup/moodle2/tests/fixtures/rewrite_step_backup_file_for_legacy_freeze/4.test
new file mode 100644 (file)
index 0000000..71070aa
--- /dev/null
@@ -0,0 +1,7 @@
+<gradebookplugin some_other_value="false" calculations_freeze="20160511" and_another_value="42">
+  <grade_categories>
+    <grade_category id="10">
+      <depth>1</depth>
+    </grade_category>
+  </grade_categories>
+</gradebookplugin>
diff --git a/backup/moodle2/tests/restore_gradebook_structure_step_test.php b/backup/moodle2/tests/restore_gradebook_structure_step_test.php
new file mode 100644 (file)
index 0000000..26856de
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Test for restore_stepslib.
+ *
+ * @package core_backup
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->libdir . '/completionlib.php');
+
+/**
+ * Test for restore_stepslib.
+ *
+ * @package core_backup
+ * @copyright 2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_backup_restore_gradebook_structure_step_testcase extends advanced_testcase {
+
+    /**
+     * Provide tests for rewrite_step_backup_file_for_legacy_freeze based upon fixtures.
+     *
+     * @return array
+     */
+    public function rewrite_step_backup_file_for_legacy_freeze_provider() {
+        $fixturesdir = realpath(__DIR__ . '/fixtures/rewrite_step_backup_file_for_legacy_freeze/');
+        $tests = [];
+        $iterator = new \RecursiveIteratorIterator(
+                new \RecursiveDirectoryIterator($fixturesdir),
+                \RecursiveIteratorIterator::LEAVES_ONLY);
+
+        foreach ($iterator as $sourcefile) {
+            $pattern = '/\.test$/';
+            if (!preg_match($pattern, $sourcefile)) {
+                continue;
+            }
+
+            $expectfile = preg_replace($pattern, '.expectation', $sourcefile);
+            $test = array($sourcefile, $expectfile);
+            $tests[basename($sourcefile)] = $test;
+        }
+
+        return $tests;
+    }
+
+    /**
+     * @dataProvider rewrite_step_backup_file_for_legacy_freeze_provider
+     * @param   string  $source     The source file to test
+     * @param   string  $expected   The expected result of the transformation
+     */
+    public function test_rewrite_step_backup_file_for_legacy_freeze($source, $expected) {
+        $restore = $this->getMockBuilder('\restore_gradebook_structure_step')
+            ->setMethods(null)
+            ->disableOriginalConstructor()
+            ->getMock()
+            ;
+
+        // Copy the file somewhere as the rewrite_step_backup_file_for_legacy_freeze will write the file.
+        $dir = make_request_directory(true);
+        $filepath = $dir . DIRECTORY_SEPARATOR . 'file.xml';
+        copy($source, $filepath);
+
+        $rc = new \ReflectionClass('\restore_gradebook_structure_step');
+        $rcm = $rc->getMethod('rewrite_step_backup_file_for_legacy_freeze');
+        $rcm->setAccessible(true);
+        $rcm->invoke($restore, $filepath);
+
+        // Check the result.
+        $this->assertFileEquals($expected, $filepath);
+    }
+}
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"
diff --git a/blocks/course_overview/tests/behat/quiz_overview.feature b/blocks/course_overview/tests/behat/quiz_overview.feature
new file mode 100644 (file)
index 0000000..238591f
--- /dev/null
@@ -0,0 +1,94 @@
+@block @block_course_overview @mod_quiz
+Feature: View the quiz being due
+  In order to know what quizzes are due
+  As a student
+  I can visit my dashboard
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | student2 | Student | 2 | student2@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+      | Course 2 | C2        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | student1 | C1     | student        |
+      | student2 | C2     | student        |
+      | teacher1 | C1     | editingteacher |
+      | teacher1 | C2     | editingteacher |
+    And the following "activities" exist:
+      | activity | course | idnumber | name                    | timeclose  |
+      | quiz     | C1     | Q1A      | Quiz 1A No deadline     | 0          |
+      | quiz     | C1     | Q1B      | Quiz 1B Past deadline   | 1337       |
+      | quiz     | C1     | Q1C      | Quiz 1C Future deadline | 9000000000 |
+      | quiz     | C1     | Q1D      | Quiz 1D Future deadline | 9000000000 |
+      | quiz     | C1     | Q1E      | Quiz 1E Future deadline | 9000000000 |
+      | quiz     | C2     | Q2A      | Quiz 2A Future deadline | 9000000000 |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | qtype     | name           | questiontext              | questioncategory |
+      | truefalse | First question | Answer the first question | Test questions   |
+    And quiz "Quiz 1A No deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1B Past deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1C Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1D Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 1E Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And quiz "Quiz 2A Future deadline" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+
+  Scenario: View my quizzes that are due
+    Given I log in as "student1"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+    And I log out
+    And I log in as "student2"
+    And I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should not see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should see "Quiz 2A Future deadline" in the "Course overview" "block"
+
+  Scenario: View my quizzes that are due and never finished
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Quiz 1D Future deadline"
+    And I press "Attempt quiz now"
+    And I follow "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I follow "Course 1"
+    And I follow "Quiz 1E Future deadline"
+    And I press "Attempt quiz now"
+    When I am on homepage
+    Then I should see "You have quizzes that are due" in the "Course overview" "block"
+    And I should see "Quiz 1C Future deadline" in the "Course overview" "block"
+    And I should see "Quiz 1E Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1A No deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1B Past deadline" in the "Course overview" "block"
+    And I should not see "Quiz 1D Future deadline" in the "Course overview" "block"
+    And I should not see "Quiz 2A Future deadline" in the "Course overview" "block"
+
index 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 ea687c6..b348aa1 100644 (file)
@@ -2140,7 +2140,7 @@ class core_course_external extends external_api {
                 'perpage'       => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
                 'requiredcapabilities' => new external_multiple_structure(
                     new external_value(PARAM_CAPABILITY, 'Capability string used to filter courses by permission'),
-                    VALUE_OPTIONAL
+                    'Optional list of required capabilities (used to filter the list)', VALUE_DEFAULT, array()
                 ),
                 'limittoenrolled' => new external_value(PARAM_BOOL, 'limit to enrolled courses', VALUE_DEFAULT, 0),
             )
index 208c188..20f39a4 100644 (file)
@@ -984,14 +984,14 @@ abstract class format_base {
         }
         if (!is_object($section)) {
             $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section),
-                'id,section,sequence');
+                'id,section,sequence,summary');
         }
         if (!$section || !$section->section) {
             // Not possible to delete 0-section.
             return false;
         }
 
-        if (!$forcedeleteifnotempty && !empty($section->sequence)) {
+        if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
             return false;
         }
 
index 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 b820f0e..05f7740 100644 (file)
@@ -42,3 +42,4 @@ L\'opció --help us orientarà.';
 $string['cliyesnoprompt'] = 'Escriu y (significa Sí) o n (significa No)';
 $string['environmentrequireinstall'] = 'cal instal·lar-lo i habilitar-lo';
 $string['environmentrequireversion'] = 'esteu executant la versió {$a->current} i es requereix la {$a->needed}';
+$string['upgradekeyset'] = 'Clau d\'actualització (deixeu-ho en blanc per no establir-ne cap)';
index 8e4178b..02b3354 100644 (file)
@@ -67,14 +67,11 @@ $string['pathserrcreatedataroot'] = 'L\'instal·lador no pot crear el directori
 $string['pathshead'] = 'Confirmeu els camins';
 $string['pathsrodataroot'] = 'No es pot escriure en el directori dataroot.';
 $string['pathsroparentdataroot'] = 'No es pot escriure en el directori pare ({$a->parent}). L\'instal·lador no pot crear el directori ({$a->dataroot}).';
-$string['pathssubadmindir'] = 'Alguns serveis d\'allotjament web (pocs) utilitzen un URL especial /admin p. ex. per a accedir a un tauler de control o quelcom semblant. Malauradament això entra en conflicte amb la ubicació estàndard de les pàgines d\'administració de Moodle. Podeu arreglar aquest problema canviant el nom del directori d\'administració de Moodle en la vostra instal·lació i posant el nou nom aquí. Per exemple <em>moodleadmin</em>. Això modificarà els enllaços d\'administració de Moodle.';
+$string['pathssubadmindir'] = 'Alguns serveis d\'allotjament web (pocs) utilitzen /admin com a URL especial perquè accediu a un tauler de control o quelcom semblant. Malauradament, això entra en conflicte amb la ubicació estàndard de les pàgines d\'administració de Moodle. Podeu arreglar aquest problema canviant el nom del directori d\'administració de Moodle en la vostra instal·lació i posant el nou nom aquí. Per exemple: <em>moodleadmin</em>. Això arreglarà els enllaços d\'administració de Moodle.';
 $string['pathssubdataroot'] = 'Necessiteu un espai on Moodle pugui desar els fitxers penjats. Aquest directori hauria de tenir permisos de lectura I ESCRIPTURA per a l\'usuari del servidor web (normalment \'nobody\' o \'apache\'), però no cal que sigui accessible directament via web. L\'instal·lador provarà de crear-lo si no existeix.';
 $string['pathssubdirroot'] = 'Camí complet del directori d\'instal·lació de Moodle.';
-$string['pathssubwwwroot'] = 'L\'adreça web completa on s\'accedirà a Moodle.
-No és possible accedir a Moodle en diferents adreces.
-Si el vostre lloc té múltiples adreces públiques haureu de configurar redireccions permanents per a totes excepte aquesta.
-Si el vostre lloc és accessible tant des d\'Internet com des d\'una intranet, utilitzeu aquí l\'adreça pública i configureu el DNS de manera que els usuaris de la intranet puguin utilitzar també l\'adreça pública.
-Si l\'adreça no és correcta, canvieu l\'URL en el vostre navegador per reiniciar la instal·lació amb un altre valor.';
+$string['pathssubwwwroot'] = '<p>L\'adreça web completa on s\'accedirà a Moodle; per exemple, l\'adreça que els usuaris introduiran a la barra d\'adreces del navegador per accedir a Moodle.</p> <p> No és possible accedir a Moodle utilitzant diferents adreces. Si el vostre lloc és accessible a través de diferents adreces, trieu-ne la més fàcil i configureu una redirecció permanent per a cadascuna de les altres adreces.</p> <p>
+Si el vostre lloc és accessible tant des d\'Internet com des d\'una xarxa interna (anomenada de vegades intranet), utilitzeu l\'adreça pública aquí.</p> <p>Si l\'adreça actual no és correcta, canvieu l\'URL a la barra d\'adreces del navegador i reinicieu la instal·lació.';
 $string['pathsunsecuredataroot'] = 'La ubicació del dataroot no és segura.';
 $string['pathswrongadmindir'] = 'No existeix el directori d\'administració';
 $string['phpextension'] = 'Extensió PHP {$a}';
@@ -84,7 +81,7 @@ $string['phpversionhelp'] = '<p>Moodle necessita una versió de PHP 4.3.0 o 5.1.
 <p>Us cal actualitzar el PHP o traslladar Moodle a un ordinador amb una versió de PHP més recent.<br />(Si esteu utilitzant la versió 5.0.x, alternativament també podríeu tornar enrere a la 4.4.x)</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Esteu veient aquesta pàgina perquè heu instal·lat amb èxit i heu executat el paquet <strong>{$a->packname} {$a->packversion}</strong>. Felicitacions.';
-$string['welcomep30'] = 'Aquesta versió de <strong>{$a->installername}</strong> inclou les aplicacions necessàries per crear un entorn en el qual funcioni <strong>Moodle</strong>:';
+$string['welcomep30'] = 'Aquesta versió de <strong>{$a->installername}</strong> inclou les aplicacions necessàries per crear un entorn en el qual funcioni <strong>Moodle</strong>, concretament:';
 $string['welcomep40'] = 'El paquet inclou també <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'L\'ús de totes les aplicacions d\'aquest paquet és governat per les seves llicències respectives. El paquet <strong>{$a->installername}</strong> complet és
 <a href="http://www.opensource.org/docs/definition_plain.html">codi font obert</a> i es distribueix
index 6e8f415..93db2e5 100644 (file)
@@ -224,6 +224,7 @@ $string['duplicateroleshortname'] = 'There is already a role with this short nam
 $string['duplicateusername'] = 'Duplicate username - skipping record';
 $string['emailfail'] = 'Emailing failed';
 $string['error'] = 'Error occurred';
+$string['error_question_answers_missing_in_db'] = 'Failed to find an answer matching "{$a->answer}" in the question_answers database table. This occurred while restoring the question with id {$a->filequestionid} in the backup file, which has been matched to the existing question with id {$a->dbquestionid} in the database.';
 $string['errorprocessingarchive'] = 'Error processing archive file';
 $string['errorcleaningdirectory'] = 'Error cleaning directory "{$a}"';
 $string['errorcopyingfiles'] = 'Error copying files';
index 766167f..d245288 100644 (file)
@@ -65,7 +65,7 @@ $string['categorycurrent'] = 'Current category';
 $string['categorycurrentuse'] = 'Use this category';
 $string['categorydoesnotexist'] = 'This category does not exist';
 $string['categoryinfo'] = 'Category info';
-$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of them may be old, hidden, questions that are still in use in some existing quizzes). Please choose another category to move them to.';
+$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of them may be old, hidden, questions, or Random questions that are still in use in some existing quizzes). Please choose another category to move them to.';
 $string['categorymoveto'] = 'Save in category';
 $string['categorynamecantbeblank'] = 'The category name cannot be blank.';
 $string['clickflag'] = 'Flag question';
index 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 88d0a5f..6702c3b 100644 (file)
@@ -137,7 +137,8 @@ class core_message_external extends external_api {
             if ($success && empty($contactlist[$message['touserid']]) && !empty($blocknoncontacts)) {
                 // The user isn't a contact and they have selected to block non contacts so this message won't be sent.
                 $success = false;
-                $errormessage = get_string('userisblockingyounoncontact', 'message');
+                $errormessage = get_string('userisblockingyounoncontact', 'message',
+                        fullname(core_user::get_user($message['touserid'])));
             }
 
             //now we can send the message (at least try)
index 282d7e9..3904025 100644 (file)
@@ -983,6 +983,7 @@ function message_format_message_text($message, $forcetexttohtml = false) {
 
     $options = new stdClass();
     $options->para = false;
+    $options->blanktarget = true;
 
     $format = $message->fullmessageformat;
 
index c4ce567..cd23c8e 100644 (file)
@@ -1411,7 +1411,11 @@ function assign_get_completion_state($course, $cm, $userid, $type) {
 
     // If completion option is enabled, evaluate it and return true/false.
     if ($assign->get_instance()->completionsubmit) {
-        $submission = $assign->get_user_submission($userid, false);
+        if ($assign->get_instance()->teamsubmission) {
+            $submission = $assign->get_group_submission($userid, 0, false);
+        } else {
+            $submission = $assign->get_user_submission($userid, false);
+        }
         return $submission && $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED;
     } else {
         // Completion option is not enabled so just return $type.
index c5a2b1e..b506521 100644 (file)
@@ -5633,7 +5633,12 @@ class assign {
             $this->update_submission($submission, $userid, true, $instance->teamsubmission);
             $completion = new completion_info($this->get_course());
             if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-                $completion->update_state($this->get_course_module(), COMPLETION_COMPLETE, $userid);
+                $this->update_activity_completion_records($instance->teamsubmission,
+                                                          $instance->requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          COMPLETION_COMPLETE,
+                                                          $completion);
             }
 
             if (!empty($data->submissionstatement) && $USER->id == $userid) {
@@ -6325,7 +6330,12 @@ class assign {
         }
         $completion = new completion_info($this->get_course());
         if ($completion->is_enabled($this->get_course_module()) && $instance->completionsubmit) {
-            $completion->update_state($this->get_course_module(), $complete, $USER->id);
+            $this->update_activity_completion_records($instance->teamsubmission,
+                                                      $instance->requireallteammemberssubmit,
+                                                      $submission,
+                                                      $USER->id,
+                                                      $complete,
+                                                      $completion);
         }
 
         if (!$instance->submissiondrafts) {
@@ -7991,6 +8001,42 @@ class assign {
         }
         return $this->get_course_module()->id . '_' . $id;
     }
+
+    /**
+     * Updates and creates the completion records in mdl_course_modules_completion.
+     *
+     * @param int $teamsubmission value of 0 or 1 to indicate whether this is a group activity
+     * @param int $requireallteammemberssubmit value of 0 or 1 to indicate whether all group members must click Submit
+     * @param obj $submission the submission
+     * @param int $userid the user id
+     * @param int $complete
+     * @param obj $completion
+     *
+     * @return null
+     */
+    protected function update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+
+        if (($teamsubmission && $submission->groupid > 0 && !$requireallteammemberssubmit) ||
+            ($teamsubmission && $submission->groupid > 0 && $requireallteammemberssubmit &&
+             $submission->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED)) {
+
+            $members = groups_get_members($submission->groupid);
+
+            foreach ($members as $member) {
+                $completion->update_state($this->get_course_module(), $complete, $member->id);
+            }
+        } else {
+            $completion->update_state($this->get_course_module(), $complete, $userid);
+        }
+
+        return;
+    }
+
 }
 
 /**
index 412f72f..be6166d 100644 (file)
@@ -209,32 +209,6 @@ class mod_assign_mod_form extends moodleform_mod {
         $this->apply_admin_defaults();
 
         $this->add_action_buttons();
-
-        // Add warning popup/noscript tag, if grades are changed by user.
-        $hasgrade = false;
-        if (!empty($this->_instance)) {
-            $hasgrade = $DB->record_exists_select('assign_grades',
-                                                  'assignment = ? AND grade <> -1',
-                                                  array($this->_instance));
-        }
-
-        if ($mform->elementExists('grade') && $hasgrade) {
-            $module = array(
-                'name' => 'mod_assign',
-                'fullpath' => '/mod/assign/module.js',
-                'requires' => array('node', 'event'),
-                'strings' => array(array('changegradewarning', 'mod_assign'))
-                );
-            $PAGE->requires->js_init_call('M.mod_assign.init_grade_change', null, false, $module);
-
-            // Add noscript tag in case.
-            $noscriptwarning = $mform->createElement('static',
-                                                     'warning',
-                                                     null,
-                                                     html_writer::tag('noscript',
-                                                     get_string('changegradewarning', 'mod_assign')));
-            $mform->insertElementBefore($noscriptwarning, 'grade');
-        }
     }
 
     /**
index 57c234b..5c92b96 100644 (file)
@@ -90,7 +90,7 @@ class mod_assign_base_testcase extends advanced_testcase {
 
         $this->resetAfterTest(true);
 
-        $this->course = $this->getDataGenerator()->create_course();
+        $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
         $this->teachers = array();
         for ($i = 0; $i < self::DEFAULT_TEACHER_COUNT; $i++) {
             array_push($this->teachers, $this->getDataGenerator()->create_user());
@@ -350,4 +350,18 @@ class testable_assign extends assign {
 
         return $mform;
     }
+
+    public function testable_update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion) {
+        return parent::update_activity_completion_records($teamsubmission,
+                                                          $requireallteammemberssubmit,
+                                                          $submission,
+                                                          $userid,
+                                                          $complete,
+                                                          $completion);
+    }
 }
index 2062696..65670a5 100644 (file)
@@ -2631,4 +2631,94 @@ Anchor link 2:<a title=\"bananas\" href=\"../logo-240x60.gif\">Link text</a>
         $grade = $assign->get_user_grade($this->students[0]->id, false);
         $this->assertEquals('30.0', $grade->grade);
     }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_solitary_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                'requireallteammemberssubmit' => 0));
+
+        $cm = $assign->get_course_module();
+
+        $student = $this->students[0];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        $this->setUser($student);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student->id, true);
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(0, 0, $submission,
+                $student->id, COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
+
+    /**
+     * Test updating activity completion when submitting an assessment.
+     */
+    public function test_update_activity_completion_records_team_submission() {
+        $assign = $this->create_instance(array('grade' => 100,
+                'completion' => COMPLETION_TRACKING_AUTOMATIC,
+                 'teamsubmission' => 1));
+
+        $cm = $assign->get_course_module();
+
+        $student1 = $this->students[0];
+        $student2 = $this->students[1];
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+
+        // Put both users into a group.
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $this->course->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $student2->id));
+
+        $this->setUser($student1);
+
+        // Simulate a submission.
+        $data = new stdClass();
+        $data->onlinetext_editor = array(
+            'itemid' => file_get_unused_draft_itemid(),
+            'text' => 'Student submission text',
+            'format' => FORMAT_MOODLE
+        );
+        $completion = new completion_info($this->course);
+
+        $notices = array();
+        $assign->save_submission($data, $notices);
+
+        $submission = $assign->get_user_submission($student1->id, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $submission->groupid = $group1->id;
+
+        // Check that completion is not met yet.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(0, $completiondata->completionstate);
+        $assign->testable_update_activity_completion_records(1, 0, $submission, $student1->id,
+                COMPLETION_COMPLETE, $completion);
+        // Completion should now be met.
+        $completiondata = $completion->get_data($cm, false, $student1->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+        $completiondata = $completion->get_data($cm, false, $student2->id);
+        $this->assertEquals(1, $completiondata->completionstate);
+    }
 }
index 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 75f7d8e..e8709c6 100644 (file)
@@ -2596,17 +2596,31 @@ function lti_load_type_from_cartridge($url, $type) {
         array(
             "title" => "lti_typename",
             "launch_url" => "lti_toolurl",
-            "description" => "lti_description"
+            "description" => "lti_description",
+            "icon" => "lti_icon",
+            "secure_icon" => "lti_secureicon"
         ),
         array(
-            "icon_url" => "lti_icon",
-            "secure_icon_url" => "lti_secureicon"
+            "icon_url" => "lti_extension_icon",
+            "secure_icon_url" => "lti_extension_secureicon"
         )
     );
     // If an activity name exists, unset the cartridge name so we don't override it.
     if (isset($type->lti_typename)) {
         unset($toolinfo['lti_typename']);
     }
+
+    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
+    if (empty($toolinfo['lti_icon']) && !empty($toolinfo['lti_extension_icon'])) {
+        $toolinfo['lti_icon'] = $toolinfo['lti_extension_icon'];
+    }
+    unset($toolinfo['lti_extension_icon']);
+
+    if (empty($toolinfo['lti_secureicon']) && !empty($toolinfo['lti_extension_secureicon'])) {
+        $toolinfo['lti_secureicon'] = $toolinfo['lti_extension_secureicon'];
+    }
+    unset($toolinfo['lti_extension_secureicon']);
+
     foreach ($toolinfo as $property => $value) {
         $type->$property = $value;
     }
@@ -2626,17 +2640,31 @@ function lti_load_tool_from_cartridge($url, $lti) {
             "title" => "name",
             "launch_url" => "toolurl",
             "secure_launch_url" => "securetoolurl",
-            "description" => "intro"
+            "description" => "intro",
+            "icon" => "icon",
+            "secure_icon" => "secureicon"
         ),
         array(
-            "icon_url" => "icon",
-            "secure_icon_url" => "secureicon"
+            "icon_url" => "extension_icon",
+            "secure_icon_url" => "extension_secureicon"
         )
     );
     // If an activity name exists, unset the cartridge name so we don't override it.
     if (isset($lti->name)) {
         unset($toolinfo['name']);
     }
+
+    // Always prefer cartridge core icons first, then, if none are found, look at the extension icons.
+    if (empty($toolinfo['icon']) && !empty($toolinfo['extension_icon'])) {
+        $toolinfo['icon'] = $toolinfo['extension_icon'];
+    }
+    unset($toolinfo['extension_icon']);
+
+    if (empty($toolinfo['secureicon']) && !empty($toolinfo['extension_secureicon'])) {
+        $toolinfo['secureicon'] = $toolinfo['extension_secureicon'];
+    }
+    unset($toolinfo['extension_secureicon']);
+
     foreach ($toolinfo as $property => $value) {
         $lti->$property = $value;
     }
index cd021e3..f4b9175 100644 (file)
@@ -544,14 +544,14 @@ function quiz_cron() {
 }
 
 /**
- * @param int $quizid the quiz id.
+ * @param int|array $quizids A quiz ID, or an array of quiz IDs.
  * @param int $userid the userid.
  * @param string $status 'all', 'finished' or 'unfinished' to control
  * @param bool $includepreviews
  * @return an array of all the user's attempts at this quiz. Returns an empty
  *      array if there are none.
  */
-function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $includepreviews = false) {
+function quiz_get_user_attempts($quizids, $userid, $status = 'finished', $includepreviews = false) {
     global $DB, $CFG;
     // TODO MDL-33071 it is very annoying to have to included all of locallib.php
     // just to get the quiz_attempt::FINISHED constants, but I will try to sort
@@ -578,15 +578,18 @@ function quiz_get_user_attempts($quizid, $userid, $status = 'finished', $include
             break;
     }
 
+    $quizids = (array) $quizids;
+    list($insql, $inparams) = $DB->get_in_or_equal($quizids, SQL_PARAMS_NAMED);
+    $params += $inparams;
+    $params['userid'] = $userid;
+
     $previewclause = '';
     if (!$includepreviews) {
         $previewclause = ' AND preview = 0';
     }
 
-    $params['quizid'] = $quizid;
-    $params['userid'] = $userid;
     return $DB->get_records_select('quiz_attempts',
-            'quiz = :quizid AND userid = :userid' . $previewclause . $statuscondition,
+            "quiz $insql AND userid = :userid" . $previewclause . $statuscondition,
             $params, 'attempt ASC');
 }
 
@@ -1465,6 +1468,20 @@ function quiz_print_overview($courses, &$htmlarray) {
         return;
     }
 
+    // Get the quizzes attempts.
+    $attemptsinfo = [];
+    $quizids = [];
+    foreach ($quizzes as $quiz) {
+        $quizids[] = $quiz->id;
+        $attemptsinfo[$quiz->id] = ['count' => 0, 'hasfinished' => false];
+    }
+    $attempts = quiz_get_user_attempts($quizids, $USER->id);
+    foreach ($attempts as $attempt) {
+        $attemptsinfo[$attempt->quiz]['count']++;
+        $attemptsinfo[$attempt->quiz]['hasfinished'] = true;
+    }
+    unset($attempts);
+
     // Fetch some language strings outside the main loop.
     $strquiz = get_string('modulename', 'quiz');
     $strnoattempts = get_string('noattempts', 'quiz');
@@ -1474,15 +1491,7 @@ function quiz_print_overview($courses, &$htmlarray) {
     $now = time();
     foreach ($quizzes as $quiz) {
         if ($quiz->timeclose >= $now && $quiz->timeopen < $now) {
-            // Give a link to the quiz, and the deadline.
-            $str = '<div class="quiz overview">' .
-                    '<div class="name">' . $strquiz . ': <a ' .
-                    ($quiz->visible ? '' : ' class="dimmed"') .
-                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
-                    $quiz->coursemodule . '">' .
-                    $quiz->name . '</a></div>';
-            $str .= '<div class="info">' . get_string('quizcloseson', 'quiz',
-                    userdate($quiz->timeclose)) . '</div>';
+            $str = '';
 
             // Now provide more information depending on the uers's role.
             $context = context_module::instance($quiz->coursemodule);
@@ -1490,30 +1499,48 @@ function quiz_print_overview($courses, &$htmlarray) {
                 // For teacher-like people, show a summary of the number of student attempts.
                 // The $quiz objects returned by get_all_instances_in_course have the necessary $cm
                 // fields set to make the following call work.
-                $str .= '<div class="info">' .
-                        quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
-            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
-                    $context)) { // Student
+                $str .= '<div class="info">' . quiz_num_attempt_summary($quiz, $quiz, true) . '</div>';
+
+            } else if (has_any_capability(array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $context)) { // Student
                 // For student-like people, tell them how many attempts they have made.
-                if (isset($USER->id) &&
-                        ($attempts = quiz_get_user_attempts($quiz->id, $USER->id))) {
-                    $numattempts = count($attempts);
-                    $str .= '<div class="info">' .
-                            get_string('numattemptsmade', 'quiz', $numattempts) . '</div>';
+
+                if (isset($USER->id)) {
+                    if ($attemptsinfo[$quiz->id]['hasfinished']) {
+                        // The student's last attempt is finished.
+                        continue;
+                    }
+
+                    if ($attemptsinfo[$quiz->id]['count'] > 0) {
+                        $str .= '<div class="info">' .
+                            get_string('numattemptsmade', 'quiz', $attemptsinfo[$quiz->id]['count']) . '</div>';
+                    } else {
+                        $str .= '<div class="info">' . $strnoattempts . '</div>';
+                    }
+
                 } else {
                     $str .= '<div class="info">' . $strnoattempts . '</div>';
                 }
+
             } else {
                 // For ayone else, there is no point listing this quiz, so stop processing.
                 continue;
             }
 
-            // Add the output for this quiz to the rest.
-            $str .= '</div>';
+            // Give a link to the quiz, and the deadline.
+            $html = '<div class="quiz overview">' .
+                    '<div class="name">' . $strquiz . ': <a ' .
+                    ($quiz->visible ? '' : ' class="dimmed"') .
+                    ' href="' . $CFG->wwwroot . '/mod/quiz/view.php?id=' .
+                    $quiz->coursemodule . '">' .
+                    $quiz->name . '</a></div>';
+            $html .= '<div class="info">' . get_string('quizcloseson', 'quiz',
+                    userdate($quiz->timeclose)) . '</div>';
+            $html .= $str;
+            $html .= '</div>';
             if (empty($htmlarray[$quiz->course]['quiz'])) {
-                $htmlarray[$quiz->course]['quiz'] = $str;
+                $htmlarray[$quiz->course]['quiz'] = $html;
             } else {
-                $htmlarray[$quiz->course]['quiz'] .= $str;
+                $htmlarray[$quiz->course]['quiz'] .= $html;
             }
         }
     }
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 75edda7..22353ff 100644 (file)
@@ -227,4 +227,226 @@ class mod_quiz_lib_testcase extends advanced_testcase {
         $this->assertTrue(quiz_get_completion_state($course, $cm, $passstudent->id, 'return'));
         $this->assertFalse(quiz_get_completion_state($course, $cm, $failstudent->id, 'return'));
     }
+
+    public function test_quiz_get_user_attempts() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $dg = $this->getDataGenerator();
+        $quizgen = $dg->get_plugin_generator('mod_quiz');
+        $course = $dg->create_course();
+        $u1 = $dg->create_user();
+        $u2 = $dg->create_user();
+        $u3 = $dg->create_user();
+        $u4 = $dg->create_user();
+        $role = $DB->get_record('role', ['shortname' => 'student']);
+
+        $dg->enrol_user($u1->id, $course->id, $role->id);
+        $dg->enrol_user($u2->id, $course->id, $role->id);
+        $dg->enrol_user($u3->id, $course->id, $role->id);
+        $dg->enrol_user($u4->id, $course->id, $role->id);
+
+        $quiz1 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
+        $quiz2 = $quizgen->create_instance(['course' => $course->id, 'sumgrades' => 2]);
+
+        // Questions.
+        $questgen = $dg->get_plugin_generator('core_question');
+        $quizcat = $questgen->create_question_category();
+        $question = $questgen->create_question('numerical', null, ['category' => $quizcat->id]);
+        quiz_add_quiz_question($question->id, $quiz1);
+        quiz_add_quiz_question($question->id, $quiz2);
+
+        $quizobj1a = quiz::create($quiz1->id, $u1->id);
+        $quizobj1b = quiz::create($quiz1->id, $u2->id);
+        $quizobj1c = quiz::create($quiz1->id, $u3->id);
+        $quizobj1d = quiz::create($quiz1->id, $u4->id);
+        $quizobj2a = quiz::create($quiz2->id, $u1->id);
+
+        // Set attempts.
+        $quba1a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1a->get_context());
+        $quba1a->set_preferred_behaviour($quizobj1a->get_quiz()->preferredbehaviour);
+        $quba1b = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1b->get_context());
+        $quba1b->set_preferred_behaviour($quizobj1b->get_quiz()->preferredbehaviour);
+        $quba1c = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1c->get_context());
+        $quba1c->set_preferred_behaviour($quizobj1c->get_quiz()->preferredbehaviour);
+        $quba1d = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj1d->get_context());
+        $quba1d->set_preferred_behaviour($quizobj1d->get_quiz()->preferredbehaviour);
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $timenow = time();
+
+        // User 1 passes quiz 1.
+        $attempt = quiz_create_attempt($quizobj1a, 1, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj1a, $quba1a, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1a, $quba1a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_submitted_actions($timenow, false, [1 => ['answer' => '3.14']]);
+        $attemptobj->process_finish($timenow, false);
+
+        // User 2 goes overdue in quiz 1.
+        $attempt = quiz_create_attempt($quizobj1b, 1, false, $timenow, false, $u2->id);
+        quiz_start_new_attempt($quizobj1b, $quba1b, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1b, $quba1b, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_going_overdue($timenow, true);
+
+        // User 3 does not finish quiz 1.
+        $attempt = quiz_create_attempt($quizobj1c, 1, false, $timenow, false, $u3->id);
+        quiz_start_new_attempt($quizobj1c, $quba1c, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1c, $quba1c, $attempt);
+
+        // User 4 abandons the quiz 1.
+        $attempt = quiz_create_attempt($quizobj1d, 1, false, $timenow, false, $u4->id);
+        quiz_start_new_attempt($quizobj1d, $quba1d, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj1d, $quba1d, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_abandon($timenow, true);
+
+        // User 1 attempts the quiz three times (abandon, finish, in progress).
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 1, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 1, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_abandon($timenow, true);
+
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 2, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 2, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+        $attemptobj = quiz_attempt::create($attempt->id);
+        $attemptobj->process_finish($timenow, false);
+
+        $quba2a = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj2a->get_context());
+        $quba2a->set_preferred_behaviour($quizobj2a->get_quiz()->preferredbehaviour);
+
+        $attempt = quiz_create_attempt($quizobj2a, 3, false, $timenow, false, $u1->id);
+        quiz_start_new_attempt($quizobj2a, $quba2a, $attempt, 3, $timenow);
+        quiz_attempt_save_started($quizobj2a, $quba2a, $attempt);
+
+        // Check for user 1.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'finished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u1->id, 'unfinished');
+        $this->assertCount(0, $attempts);
+
+        // Check for user 2.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
+        $this->assertEquals($u2->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'finished');
+        $this->assertCount(0, $attempts);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u2->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::OVERDUE, $attempt->state);
+        $this->assertEquals($u2->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        // Check for user 3.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u3->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'finished');
+        $this->assertCount(0, $attempts);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u3->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u3->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        // Check for user 4.
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'all');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u4->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'finished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u4->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz1->id, $u4->id, 'unfinished');
+        $this->assertCount(0, $attempts);
+
+        // Multiple attempts for user 1 in quiz 2.
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'all');
+        $this->assertCount(3, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'finished');
+        $this->assertCount(2, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+
+        $attempts = quiz_get_user_attempts($quiz2->id, $u1->id, 'unfinished');
+        $this->assertCount(1, $attempts);
+        $attempt = array_shift($attempts);
+
+        // Multiple quiz attempts fetched at once.
+        $attempts = quiz_get_user_attempts([$quiz1->id, $quiz2->id], $u1->id, 'all');
+        $this->assertCount(4, $attempts);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz1->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::ABANDONED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::FINISHED, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+        $attempt = array_shift($attempts);
+        $this->assertEquals(quiz_attempt::IN_PROGRESS, $attempt->state);
+        $this->assertEquals($u1->id, $attempt->userid);
+        $this->assertEquals($quiz2->id, $attempt->quiz);
+    }
+
 }
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 3959382..081db48 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 require_once($CFG->libdir . '/portfoliolib.php');
+require_once($CFG->libdir . '/portfolio/plugin.php');
 
 class portfolio_plugin_download extends portfolio_plugin_pull_base {
 
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);
+    }
 }
index 2370153..09a8aaf 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016052300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016052300.02;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.2dev (Build: 20160526)'; // Human-friendly version name
+$release  = '3.2dev (Build: 20160603)'; // Human-friendly version name
 
 $branch   = '32';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.