Merge branch 'wip-MDL-59377-master-3' of git://github.com/marinaglancy/moodle
authorDavid Monllao <david.monllao@gmail.com>
Mon, 17 Jul 2017 09:04:02 +0000 (11:04 +0200)
committerDavid Monllao <david.monllao@gmail.com>
Mon, 17 Jul 2017 09:04:02 +0000 (11:04 +0200)
39 files changed:
blocks/comments/tests/behat/behat_block_comments.php
calendar/export_execute.php
calendar/lib.php
course/templates/defaultactivitycompletion.mustache
course/tests/behat/behat_course.php
course/yui/build/moodle-course-management/moodle-course-management-debug.js
course/yui/build/moodle-course-management/moodle-course-management-min.js
course/yui/build/moodle-course-management/moodle-course-management.js
course/yui/src/management/js/console.js
grade/report/overview/db/access.php
grade/report/overview/db/upgrade.php [new file with mode: 0644]
grade/report/overview/lib.php
grade/report/overview/version.php
group/tests/behat/behat_groups.php
lib/behat/behat_base.php
lib/behat/form_field/behat_form_checkbox.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_select.php
lib/bennu/iCalendar_components.php
lib/bennu/readme_moodle.txt
lib/classes/oauth2/api.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
message/output/popup/amd/build/message_popover_controller.min.js
message/output/popup/amd/build/notification_area_control_area.min.js
message/output/popup/amd/build/notification_popover_controller.min.js
message/output/popup/amd/src/message_popover_controller.js
message/output/popup/amd/src/notification_area_control_area.js
message/output/popup/amd/src/notification_popover_controller.js
mod/assign/locallib.php
mod/lesson/continue.php
mod/lesson/db/upgrade.php
mod/lesson/locallib.php
mod/lesson/tests/behat/lesson_question_attempts.feature
mod/lesson/version.php
mod/quiz/classes/structure.php
mod/quiz/tests/structure_test.php

index 587695b..26f7495 100644 (file)
@@ -66,7 +66,7 @@ class behat_block_comments extends behat_base {
             $this->find_link(get_string('savecomment'))->click();
             // Delay after clicking so that additional comments will have unique time stamps.
             // We delay 1 second which is all we need.
-            $this->getSession()->wait(1000, false);
+            $this->getSession()->wait(1000);
 
         } else {
 
@@ -104,7 +104,7 @@ class behat_block_comments extends behat_base {
         );
 
         // Wait for the animation to finish, in theory is just 1 sec, adding 4 just in case.
-        $this->getSession()->wait(4 * 1000, false);
+        $this->getSession()->wait(4 * 1000);
     }
 
 }
index 9952dee..ca162a2 100644 (file)
@@ -216,10 +216,14 @@ foreach($events as $event) {
         //dtend is better than duration, because it works in Microsoft Outlook and works better in Korganizer
         $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart)); // when event starts.
         $ev->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart + $event->timeduration));
+    } else if ($event->timeduration == 0) {
+        // When no duration is present, the event is instantaneous event, ex - Due date of a module.
+        // Moodle doesn't support all day events yet. See MDL-56227.
+        $ev->add_property('dtstart', Bennu::timestamp_to_datetime($event->timestart));
+        $ev->add_property('dtend', Bennu::timestamp_to_datetime($event->timestart));
     } else {
-        // When no duration is present, ie an all day event, VALUE should be date instead of time and dtend = dtstart + 1 day.
-        $ev->add_property('dtstart', Bennu::timestamp_to_date($event->timestart), array('value' => 'DATE')); // All day event.
-        $ev->add_property('dtend', Bennu::timestamp_to_date($event->timestart + DAYSECS), array('value' => 'DATE')); // All day event.
+        // This can be used to represent all day events in future.
+        throw new coding_exception("Negative duration is not supported yet.");
     }
     if ($event->courseid != 0) {
         $coursecontext = context_course::instance($event->courseid);
index f6f1e4a..94e617b 100644 (file)
@@ -2944,7 +2944,8 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
         // Check to see if the event started at Midnight on the imported calendar.
         date_default_timezone_set($timezone);
         if (date('H:i:s', $eventrecord->timestart) === "00:00:00") {
-            // This event should be an all day event.
+            // This event should be an all day event. This is not correct, we don't do anything differently for all day events.
+            // See MDL-56227.
             $eventrecord->timeduration = 0;
         }
         \core_date::set_default_server_timezone();
index cfd0676..db10908 100644 (file)
@@ -64,7 +64,7 @@
                     <div class="col-xs-6 span6">
                         <label class="accesshide" for="modtype_{{id}}">{{#str}}select, core_completion{{/str}} {{formattedname}}</label>
                         <input id="modtype_{{id}}" type="checkbox" class="m-r-1" name="modids[]" value="{{id}}" aria-label="{{#str}}checkactivity, completion, {{formattedname}}{{/str}}">
-                        <img src="{{icon}}" alt=" " role="presentation" />
+                        <img class="iconlarge activityicon" src="{{icon}}" alt=" " role="presentation" />
                         <span>{{formattedname}}</span>
                     </div>
                     <div class="activity-completionstatus col-xs-6 span6">
index 8cdeef6..4b20f93 100644 (file)
@@ -1830,7 +1830,7 @@ class behat_course extends behat_base {
         $node->click();
 
         // Smooth expansion.
-        $this->getSession()->wait(1000, false);
+        $this->getSession()->wait(1000);
     }
 
     /**
index abda207..0bef68f 100644 (file)
Binary files a/course/yui/build/moodle-course-management/moodle-course-management-debug.js and b/course/yui/build/moodle-course-management/moodle-course-management-debug.js differ
index 1c994ed..2e524a0 100644 (file)
Binary files a/course/yui/build/moodle-course-management/moodle-course-management-min.js and b/course/yui/build/moodle-course-management/moodle-course-management-min.js differ
index 0c71169..10220c9 100644 (file)
Binary files a/course/yui/build/moodle-course-management/moodle-course-management.js and b/course/yui/build/moodle-course-management/moodle-course-management.js differ
index 633fd47..4c7c034 100644 (file)
@@ -277,7 +277,7 @@ Console.prototype = {
         if (!this.categoriesinit) {
             this.get('categorylisting').delegate('click', this.handleCategoryDelegation, 'a[data-action]', this);
             this.get('categorylisting').delegate('click', this.handleCategoryDelegation, 'input[name="bcat[]"]', this);
-            this.get('categorylisting').delegate('click', this.handleBulkSortByaction, '#menuselectsortby', this);
+            this.get('categorylisting').delegate('change', this.handleBulkSortByaction, '#menuselectsortby', this);
             this.categoriesinit = true;
             Y.log(count + ' categories being managed', 'info', 'moodle-course-management');
         } else {
index b33412c..0676152 100644 (file)
@@ -31,8 +31,7 @@ $capabilities = array(
         'captype' => 'read',
         'contextlevel' => CONTEXT_COURSE,
         'archetypes' => array(
-            'student' => CAP_ALLOW,
-            'manager' => CAP_ALLOW
+            'user' => CAP_ALLOW
         )
     )
 
diff --git a/grade/report/overview/db/upgrade.php b/grade/report/overview/db/upgrade.php
new file mode 100644 (file)
index 0000000..a331896
--- /dev/null
@@ -0,0 +1,48 @@
+<?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/>.
+
+/**
+ * Grade overview report upgrade steps.
+ *
+ * @package    gradereport_overview
+ * @copyright  2017 Simey Lameze <simey@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Function to upgrade grade overview report.
+ *
+ * @param int $oldversion the version we are upgrading from
+ * @return bool result
+ */
+function xmldb_gradereport_overview_upgrade($oldversion) {
+
+    if ($oldversion < 2017051501) {
+        $context = context_system::instance();
+        $capability = 'gradereport/overview:view';
+
+        // Now allow authenticated user role to access that report.
+        $authenticateduserroles = get_archetype_roles('user');
+        foreach ($authenticateduserroles as $roleid => $notused) {
+            assign_capability($capability, CAP_ALLOW, $roleid, $context->id);
+        }
+
+        upgrade_plugin_savepoint(true, 2017051501, 'gradereport', 'overview');
+    }
+    return true;
+}
index 6ab529e..117e7fd 100644 (file)
@@ -468,7 +468,7 @@ function gradereport_overview_myprofile_navigation(core_user\output\myprofile\tr
     $usercontext = context_user::instance($user->id);
     $coursecontext = context_course::instance($course->id);
     if (grade_report_overview::check_access($systemcontext, $coursecontext, $usercontext, $course, $user->id)) {
-        $url = new moodle_url('/grade/report/overview/index.php', array('userid' => $user->id));
+        $url = new moodle_url('/grade/report/overview/index.php', array('userid' => $user->id, 'id' => $course->id));
         $node = new core_user\output\myprofile\node('reports', 'grades', get_string('gradesoverview', 'gradereport_overview'),
                 null, $url);
         $tree->add_node($node);
index d93b45a..630f614 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051500;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2017051501;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2017050500;        // Requires this Moodle version
 $plugin->component = 'gradereport_overview'; // Full name of the plugin (used for diagnostics)
index d17b225..e314e68 100644 (file)
@@ -62,8 +62,11 @@ class behat_groups extends behat_base {
         $select->selectOption($fulloption);
 
         // This is needed by some drivers to ensure relevant event is triggred and button is enabled.
-        $script = "Syn.trigger('change', {}, {{ELEMENT}})";
-        $this->getSession()->getDriver()->triggerSynScript($select->getXpath(), $script);
+        $driver = $this->getSession()->getDriver();
+        if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) {
+            $script = "Syn.trigger('change', {}, {{ELEMENT}})";
+            $driver->triggerSynScript($select->getXpath(), $script);
+        }
         $this->getSession()->wait(self::TIMEOUT * 1000, self::PAGE_READY_JS);
 
         // Here we don't need to wait for the AJAX response.
index d01213b..e7c631f 100644 (file)
@@ -28,7 +28,8 @@
 
 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
 
-use Behat\Mink\Exception\ExpectationException as ExpectationException,
+use Behat\Mink\Exception\DriverException,
+    Behat\Mink\Exception\ExpectationException as ExpectationException,
     Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
     Behat\Mink\Element\NodeElement as NodeElement;
 
@@ -335,8 +336,6 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
                 if (!$exception) {
                     $exception = $e;
                 }
-                // We wait until no exception is thrown or timeout expires.
-                continue;
             }
 
             if ($this->running_javascript()) {
@@ -727,7 +726,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
             $pending = '';
             try {
                 $jscode = '
-                    return function() {
+                    return (function() {
                         if (typeof M === "undefined") {
                             if (document.readyState === "complete") {
                                 return "";
@@ -741,7 +740,7 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
                         } else {
                             return "incomplete"
                         }
-                    }();';
+                    }());';
                 $pending = $this->getSession()->evaluateScript($jscode);
             } catch (NoSuchWindow $nsw) {
                 // We catch an exception here, in case we just closed the window we were interacting with.
@@ -883,6 +882,8 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
 
         } catch (NoSuchWindow $e) {
             // If we were interacting with a popup window it will not exists after closing it.
+        } catch (DriverException $e) {
+            // Same reason as above.
         }
     }
 
@@ -959,7 +960,12 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         }
         $this->ensure_node_is_visible($node); // Ensures hidden elements can't be clicked.
         $xpath = $node->getXpath();
-        $script = "Syn.click({{ELEMENT}})";
-        $this->getSession()->getDriver()->triggerSynScript($xpath, $script);
+        $driver = $this->getSession()->getDriver();
+        if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) {
+            $script = "Syn.click({{ELEMENT}})";
+            $driver->triggerSynScript($xpath, $script);
+        } else {
+            $driver->click($xpath);
+        }
     }
 }
index dc568b0..5cd624a 100644 (file)
@@ -108,9 +108,12 @@ class behat_form_checkbox extends behat_form_field {
      * Trigger on change event.
      */
     protected function trigger_on_change() {
-        $this->session->getDriver()->triggerSynScript(
-            $this->field->getXPath(),
-            "Syn.trigger('change', {}, {{ELEMENT}})"
-        );
+        $driver = $this->session->getDriver();
+        if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) {
+            $driver->triggerSynScript(
+                $this->field->getXPath(),
+                "Syn.trigger('change', {}, {{ELEMENT}})"
+            );
+        }
     }
 }
index f7f5b9e..56d7124 100644 (file)
@@ -114,6 +114,8 @@ class behat_form_field {
             // If the JS handler attached to keydown or keypress destroys the element
             // the later events may trigger errors because form element no longer exist
             // or is not visible. Ignore such exceptions here.
+        } catch (\Behat\Mink\Exception\ElementNotFoundException $e) {
+            // Other Mink drivers can throw this for the same reason as above.
         }
     }
 
index 0b16552..6e359fd 100644 (file)
@@ -78,8 +78,11 @@ class behat_form_select extends behat_form_field {
                 if (!$node = $this->session->getDriver()->find($dialoguexpath)) {
                     $script = "Syn.trigger('change', {}, {{ELEMENT}})";
                     try {
-                        $this->session->getDriver()->triggerSynScript($this->field->getXpath(), $script);
-                        $this->session->getDriver()->click('//body//div[@class="skiplinks"]');
+                        $driver = $this->session->getDriver();
+                        if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) {
+                            $driver->triggerSynScript($this->field->getXpath(), $script);
+                        }
+                        $driver->click('//body//div[@class="skiplinks"]');
                     } catch (\Exception $e) {
                         return;
                     }
index 9b9e313..83a1e5f 100644 (file)
@@ -411,7 +411,7 @@ class iCalendar_event extends iCalendar_component {
             // DTEND must be later than DTSTART
             // The standard is not clear on how to hande different value types though
             // TODO: handle this correctly even if the value types are different
-            if($this->properties['DTEND'][0]->value <= $this->properties['DTSTART'][0]->value) {
+            if($this->properties['DTEND'][0]->value < $this->properties['DTSTART'][0]->value) {
                 return false;
             }
 
index e33a956..845664c 100644 (file)
@@ -8,3 +8,4 @@ modifications:
 5/ updated DTEND;TZID and DTSTAR;TZID values to support quotations (7 Nov 2014)
 6/ MDL-49032: fixed rfc2445_fold() to fix incorrect RFC2445_WSP definition (16 Sep 2015)
 7/ added timestamp_to_date function to support zero duration events (16 Sept 2015)
+8/ Updated \iCalendar_event::invariant_holds() to allow for same dtstart and dtend timestamps (13 July 2017)
index d7c0cad..966ea3f 100644 (file)
@@ -349,7 +349,7 @@ class api {
      */
     protected static function guess_image($issuer) {
         if (empty($issuer->get('image'))) {
-            $baseurl = parse_url($issuer->get('discoveryurl'));
+            $baseurl = parse_url($issuer->get('baseurl'));
             $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico';
             $issuer->set('image', $imageurl);
             $issuer->update();
index f42b6e5..4e878f8 100644 (file)
@@ -1020,6 +1020,27 @@ class file_storage {
         return $dir_info;
     }
 
+    /**
+     * Add new file record to database and handle callbacks.
+     *
+     * @param stdClass $newrecord
+     */
+    protected function create_file($newrecord) {
+        global $DB;
+        $newrecord->id = $DB->insert_record('files', $newrecord);
+
+        if ($newrecord->filename !== '.') {
+            // Callback for file created.
+            if ($pluginsfunction = get_plugins_with_function('after_file_created')) {
+                foreach ($pluginsfunction as $plugintype => $plugins) {
+                    foreach ($plugins as $pluginfunction) {
+                        $pluginfunction($newrecord);
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Add new local file based on existing local file.
      *
@@ -1134,7 +1155,7 @@ class file_storage {
         }
 
         try {
-            $newrecord->id = $DB->insert_record('files', $newrecord);
+            $this->create_file($newrecord);
         } catch (dml_exception $e) {
             throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
                                                      $newrecord->filepath, $newrecord->filename, $e->debuginfo);
@@ -1302,7 +1323,7 @@ class file_storage {
         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
 
         try {
-            $newrecord->id = $DB->insert_record('files', $newrecord);
+            $this->create_file($newrecord);
         } catch (dml_exception $e) {
             if ($newfile) {
                 $this->move_to_trash($newrecord->contenthash);
@@ -1421,7 +1442,7 @@ class file_storage {
         $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
 
         try {
-            $newrecord->id = $DB->insert_record('files', $newrecord);
+            $this->create_file($newrecord);
         } catch (dml_exception $e) {
             if ($newfile) {
                 $this->move_to_trash($newrecord->contenthash);
index 9655d01..e1c3e73 100644 (file)
@@ -139,6 +139,7 @@ class stored_file {
         global $DB;
         $updatereferencesneeded = false;
         $keys = array_keys((array)$this->file_record);
+        $filepreupdate = clone($this->file_record);
         foreach ($dataobject as $field => $value) {
             if (in_array($field, $keys)) {
                 if ($field == 'contextid' and (!is_number($value) or $value < 1)) {
@@ -216,6 +217,17 @@ class stored_file {
             // Either filesize or contenthash of this file have changed. Update all files that reference to it.
             $this->fs->update_references_to_storedfile($this);
         }
+
+        // Callback for file update.
+        if (!$this->is_directory()) {
+            if ($pluginsfunction = get_plugins_with_function('after_file_updated')) {
+                foreach ($pluginsfunction as $plugintype => $plugins) {
+                    foreach ($plugins as $pluginfunction) {
+                        $pluginfunction($this->file_record, $filepreupdate);
+                    }
+                }
+            }
+        }
     }
 
     /**
@@ -375,6 +387,17 @@ class stored_file {
             $DB->delete_records('files', array('id'=>$this->file_record->id));
 
             $transaction->allow_commit();
+
+            if (!$this->is_directory()) {
+                // Callback for file deletion.
+                if ($pluginsfunction = get_plugins_with_function('after_file_deleted')) {
+                    foreach ($pluginsfunction as $plugintype => $plugins) {
+                        foreach ($plugins as $pluginfunction) {
+                            $pluginfunction($this->file_record);
+                        }
+                    }
+                }
+            }
         }
 
         // Move pool file to trash if content not needed any more.
@@ -815,9 +838,20 @@ class stored_file {
      * @return int
      */
     public function set_sortorder($sortorder) {
+        $oldorder = $this->file_record->sortorder;
         $filerecord = new stdClass;
         $filerecord->sortorder = $sortorder;
         $this->update($filerecord);
+        if (!$this->is_directory()) {
+            // Callback for file sort order change.
+            if ($pluginsfunction = get_plugins_with_function('after_file_sorted')) {
+                foreach ($pluginsfunction as $plugintype => $plugins) {
+                    foreach ($plugins as $pluginfunction) {
+                        $pluginfunction($this->file_record, $oldorder, $sortorder);
+                    }
+                }
+            }
+        }
     }
 
     /**
index 229b5c7..f7e09b3 100644 (file)
@@ -148,13 +148,20 @@ class behat_forms extends behat_base {
                 return;
             }
 
-            // Funny thing about this, with findAll() we specify a pattern and each element matching the pattern is added to the array
-            // with of xpaths with a [0], [1]... sufix, but when we click on an element it does not matches the specified xpath
-            // anymore (now is a "Show less..." link) so [1] becomes [0], that's why we always click on the first XPath match,
-            // will be always the next one.
-            $iterations = count($showmores);
-            for ($i = 0; $i < $iterations; $i++) {
-                $showmores[0]->click();
+            if ($this->getSession()->getDriver() instanceof \DMore\ChromeDriver\ChromeDriver) {
+                // Chrome Driver produces unique xpaths for each element.
+                foreach ($showmores as $showmore) {
+                    $showmore->click();
+                }
+            } else {
+                // Funny thing about this, with findAll() we specify a pattern and each element matching the pattern
+                // is added to the array with of xpaths with a [0], [1]... sufix, but when we click on an element it
+                // does not matches the specified xpath anymore (now is a "Show less..." link) so [1] becomes [0],
+                // that's why we always click on the first XPath match, will be always the next one.
+                $iterations = count($showmores);
+                for ($i = 0; $i < $iterations; $i++) {
+                    $showmores[0]->click();
+                }
             }
 
         } catch (ElementNotFoundException $e) {
index 0adb30f..749540d 100644 (file)
@@ -143,7 +143,7 @@ class behat_general extends behat_base {
 
         // Wait until the URL change is executed.
         if ($this->running_javascript()) {
-            $this->getSession()->wait($waittime * 1000, false);
+            $this->getSession()->wait($waittime * 1000);
 
         } else if (!empty($url)) {
             // We redirect directly as we can not wait for an automatic redirection.
@@ -253,7 +253,7 @@ class behat_general extends behat_base {
      */
     public function i_wait_seconds($seconds) {
         if ($this->running_javascript()) {
-            $this->getSession()->wait($seconds * 1000, false);
+            $this->getSession()->wait($seconds * 1000);
         } else {
             sleep($seconds);
         }
@@ -634,6 +634,9 @@ class behat_general extends behat_base {
                     } catch (WebDriver\Exception\NoSuchElement $e) {
                         // Do nothing just return, as element is no more on page.
                         return true;
+                    } catch (ElementNotFoundException $e) {
+                        // Do nothing just return, as element is no more on page.
+                        return true;
                     }
                 }
 
@@ -1610,7 +1613,12 @@ class behat_general extends behat_base {
         }
         // Gets the node based on the requested selector type and locator.
         $node = $this->get_selected_node($selectortype, $element);
-        $this->getSession()->getDriver()->post_key("\xEE\x80\x84", $node->getXpath());
+        $driver = $this->getSession()->getDriver();
+        if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) {
+            $driver->post_key("\xEE\x80\x84", $node->getXpath());
+        } else {
+            $driver->keyDown($node->getXpath(), "\t");
+        }
     }
 
     /**
index 313d911..dd61230 100644 (file)
Binary files a/message/output/popup/amd/build/message_popover_controller.min.js and b/message/output/popup/amd/build/message_popover_controller.min.js differ
index 0361c24..f9b3794 100644 (file)
Binary files a/message/output/popup/amd/build/notification_area_control_area.min.js and b/message/output/popup/amd/build/notification_area_control_area.min.js differ
index dadaa7f..d0fadcf 100644 (file)
Binary files a/message/output/popup/amd/build/notification_popover_controller.min.js and b/message/output/popup/amd/build/notification_popover_controller.min.js differ
index 5699698..d731c0f 100644 (file)
@@ -176,16 +176,23 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str',
                 id: message.userid,
             });
 
-            var promise = Templates.render('message_popup/message_content_item', message)
-            .then(function(html, js) {
-                container.append(html);
-                Templates.runTemplateJS(js);
-                return;
-            });
+            var promise = Templates.render('message_popup/message_content_item', message);
             promises.push(promise);
         }.bind(this));
 
-        return $.when.apply($, promises);
+        return $.when.apply($, promises).then(function() {
+            // Each of the promises in the when will pass its results as an argument to the function.
+            // The order of the arguments will be the order that the promises are passed to when()
+            // i.e. the first promise's results will be in the first argument.
+            $.each(arguments, function(index, argument) {
+                // The promises will return an array containing two values.
+                // The first value is the html that should be attached to the page.
+                // The second will be any JavaScript that needs to be run.
+                container.append(argument[0]);
+                Templates.runTemplateJS(argument[1]);
+            });
+            return;
+        });
     };
 
     /**
index 5feea45..ac94b17 100644 (file)
@@ -317,17 +317,28 @@ define(['jquery', 'core/templates', 'core/notification', 'core/custom_interactio
 
             var promise = Templates.render(TEMPLATES.NOTIFICATION, notification)
             .then(function(html, js) {
-                container.append(html);
-                Templates.runTemplateJS(js);
                 // Restore it for the cache.
                 notification.contexturl = contextUrl;
                 this.setCacheNotification(notification);
-                return;
+                // Pass the Rendered content out.
+                return [html, js];
             }.bind(this));
             promises.push(promise);
         }.bind(this));
 
-        return $.when.apply($, promises);
+        return $.when.apply($, promises).then(function() {
+            // Each of the promises in the when will pass its results as an argument to the function.
+            // The order of the arguments will be the order that the promises are passed to when()
+            // i.e. the first promise's results will be in the first argument.
+            $.each(arguments, function(index, argument) {
+                // The promises will return an array containing two values.
+                // The first value is the html that should be attached to the page.
+                // The second will be any JavaScript that needs to be run.
+                container.append(argument[0]);
+                Templates.runTemplateJS(argument[1]);
+            });
+            return;
+        });
     };
 
     /**
index 2d5a2c4..7507d9c 100644 (file)
@@ -237,16 +237,23 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',
                 offset: offset,
             });
 
-            var promise = Templates.render('message_popup/notification_content_item', notification)
-            .then(function(html, js) {
-                container.append(html);
-                Templates.runTemplateJS(js);
-                return;
-            });
+            var promise = Templates.render('message_popup/notification_content_item', notification);
             promises.push(promise);
         }.bind(this));
 
-        return $.when.apply($, promises);
+        return $.when.apply($, promises).then(function() {
+            // Each of the promises in the when will pass its results as an argument to the function.
+            // The order of the arguments will be the order that the promises are passed to when()
+            // i.e. the first promise's results will be in the first argument.
+            $.each(arguments, function(index, argument) {
+                // The promises will return an array containing two values.
+                // The first value is the html that should be attached to the page.
+                // The second will be any JavaScript that needs to be run.
+                container.append(argument[0]);
+                Templates.runTemplateJS(argument[1]);
+            });
+            return;
+        });
     };
 
     /**
index cebdddc..5a16442 100644 (file)
@@ -2329,7 +2329,7 @@ class assign {
                 $uniqueid = 0;
                 if ($submission->blindmarking && !$submission->revealidentities) {
                     if (empty($submission->recordid)) {
-                        $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $user->id);
+                        $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $grader->id);
                     } else {
                         $uniqueid = $submission->recordid;
                     }
index 11be7d0..a743dc7 100644 (file)
@@ -118,9 +118,7 @@ if (!$result->correctanswer && !$result->noanswer && !$result->isessayquestion &
 
 $url = new moodle_url('/mod/lesson/view.php', array('id'=>$cm->id, 'pageid'=>$result->newpageid));
 if ($lesson->review && !$result->correctanswer && !$result->noanswer && !$result->isessayquestion && !$result->maxattemptsreached) {
-    // When the answer is wrong - the result->newpageid points back to the current question.
-    $newpageid = $lesson->calculate_new_page_on_jump($page, $page->nextpageid);
-    $url = new moodle_url('/mod/lesson/view.php', array('id'=>$cm->id, 'pageid'=>$newpageid));
+    // Button to continue the lesson (the page to go is configured by the teacher).
     echo $OUTPUT->single_button($url, get_string('reviewquestioncontinue', 'lesson'));
 } else {
     // Normal continue button
index 03d5fdd..6c9baf7 100644 (file)
@@ -141,5 +141,24 @@ function xmldb_lesson_upgrade($oldversion) {
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017051501) {
+
+        // Delete orphaned lesson answer and response files.
+        $sql = "SELECT DISTINCT f.*
+                  FROM {files} f
+             LEFT JOIN {lesson_answers} la ON f.itemid = la.id
+                 WHERE component = :component
+                   AND la.id IS NULL";
+
+        $orphanedfiles = $DB->get_recordset_sql($sql, array('component' => 'mod_lesson'));
+        $fs = get_file_storage();
+        foreach ($orphanedfiles as $file) {
+            $fs->delete_area_files($file->contextid, $file->component, $file->filearea, $file->itemid);
+        }
+        $orphanedfiles->close();
+
+        upgrade_mod_savepoint(true, 2017051501, 'lesson');
+    }
+
     return true;
 }
index a62b189..7c7c379 100644 (file)
@@ -3831,6 +3831,15 @@ abstract class lesson_page extends lesson_base {
         $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id));
 
         $DB->delete_records("lesson_branch", array("pageid" => $this->properties->id));
+
+        // Delete files related to answers and responses.
+        if ($answers = $DB->get_records("lesson_answers", array("pageid" => $this->properties->id))) {
+            foreach ($answers as $answer) {
+                $fs->delete_area_files($context->id, 'mod_lesson', 'page_answers', $answer->id);
+                $fs->delete_area_files($context->id, 'mod_lesson', 'page_responses', $answer->id);
+            }
+        }
+
         // ...now delete the answers...
         $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id));
         // ..and the page itself
@@ -3850,8 +3859,6 @@ abstract class lesson_page extends lesson_base {
 
         // Delete files associated with this page.
         $fs->delete_area_files($context->id, 'mod_lesson', 'page_contents', $this->properties->id);
-        $fs->delete_area_files($context->id, 'mod_lesson', 'page_answers', $this->properties->id);
-        $fs->delete_area_files($context->id, 'mod_lesson', 'page_responses', $this->properties->id);
 
         // repair the hole in the linkage
         if (!$this->properties->prevpageid && !$this->properties->nextpageid) {
index 610c6a6..9abc8c0 100644 (file)
@@ -114,47 +114,6 @@ Feature: In a lesson activity, students can not re-attempt a question more than
     When I press "Submit"
     Then I should see "Maximum number of attempts reached - Moving to next page"
 
-  Scenario: Check that we can move past a question we don't want to re-attempt
-    Given I log in as "teacher1"
-    And I am on "Course 1" course homepage
-    And I follow "Test lesson name"
-    And I navigate to "Edit settings" in current page administration
-    And I expand all fieldsets
-    And I set the field "Provide option to try a question again" to "Yes"
-    And I set the field "Maximum number of attempts" to "3"
-    And I press "Save and display"
-    And I log out
-    And I log in as "student1"
-    And I am on "Course 1" course homepage
-    And I follow "Test lesson name"
-    And I should see "First page contents"
-    And I press "Next page"
-    And I should see "The earth is round"
-    And I set the following fields to these values:
-      | False| 1 |
-    And I press "Submit"
-    And I should see "Wrong"
-    When I press "No, I just want to go on to the next question"
-    Then I should not see "The earth is round"
-    And I should see "Kermit is a frog"
-    And I set the following fields to these values:
-      | False | 1 |
-    And I press "Submit"
-    And I should see "Wrong"
-    And I press "Yes, I'd like to try again"
-    And I should see "Kermit is a frog"
-    And I set the following fields to these values:
-      | False | 1 |
-    And I press "Submit"
-    And I should see "Wrong"
-    And I press "Yes, I'd like to try again"
-    And I should see "Kermit is a frog"
-    And I set the following fields to these values:
-      | False | 1 |
-    And I press "Submit"
-    And I should not see "Yes, I'd like to try again"
-    And I should see "Continue"
-
   @javascript @_bug_phantomjs
   Scenario: Check that we can not click back on the browser at the last quiz result page and re-attempt the last question to get full marks
     Given I log in as "student1"
index 12ca11c..78dd9d6 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051500;     // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2017051501;     // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2017050500;    // Requires this Moodle version
 $plugin->component = 'mod_lesson'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index ba7f3a3..aae5c24 100644 (file)
@@ -715,7 +715,8 @@ class structure {
         }
 
         $followingslotnumber = $moveafterslotnumber + 1;
-        if ($followingslotnumber == $movingslotnumber) {
+        // Prevent checking against non-existance slot when already at the last slot.
+        if ($followingslotnumber == $movingslotnumber && !$this->is_last_slot_in_quiz($followingslotnumber)) {
             $followingslotnumber += 1;
         }
 
index dd13daa..9874635 100644 (file)
@@ -315,6 +315,24 @@ class mod_quiz_structure_testcase extends advanced_testcase {
             ), $structure);
     }
 
+    public function test_move_last_slot_to_previous_page_emptying_the_last_page() {
+        $quizobj = $this->create_test_quiz(array(
+                array('TF1', 1, 'truefalse'),
+                array('TF2', 2, 'truefalse'),
+            ));
+        $structure = \mod_quiz\structure::create_for_quiz($quizobj);
+
+        $idtomove = $structure->get_question_in_slot(2)->slotid;
+        $idmoveafter = $structure->get_question_in_slot(1)->slotid;
+        $structure->move_slot($idtomove, $idmoveafter, '1');
+
+        $structure = \mod_quiz\structure::create_for_quiz($quizobj);
+        $this->assert_quiz_layout(array(
+                array('TF1', 1, 'truefalse'),
+                array('TF2', 1, 'truefalse'),
+            ), $structure);
+    }
+
     public function test_end_of_one_section_to_start_of_next() {
         $quizobj = $this->create_test_quiz(array(
                 array('TF1', 1, 'truefalse'),