Merge branch 'MDL-59171-master' of git://github.com/junpataleta/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 13 Jun 2017 13:40:06 +0000 (14:40 +0100)
committerDan Poltawski <dan@moodle.com>
Wed, 14 Jun 2017 11:36:39 +0000 (12:36 +0100)
62 files changed:
admin/settings/appearance.php
admin/settings/subsystems.php
auth/oauth2/classes/api.php
availability/classes/info_section.php
availability/condition/date/classes/condition.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
blocks/completionstatus/details.php
blocks/myoverview/amd/build/tab_preferences.min.js [new file with mode: 0644]
blocks/myoverview/amd/src/tab_preferences.js [new file with mode: 0644]
blocks/myoverview/block_myoverview.php
blocks/myoverview/classes/output/courses_view.php
blocks/myoverview/classes/output/main.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lib.php [new file with mode: 0644]
blocks/myoverview/settings.php [new file with mode: 0644]
blocks/myoverview/templates/main.mustache
blocks/myoverview/tests/behat/block_myoverview_progress.feature
blocks/myoverview/version.php
completion/completion_completion.php
course/lib.php
course/tests/courselib_test.php
grade/edit/tree/item_form.php
lang/en/admin.php
lang/en/cache.php
lang/en/moodle.php
lib/completionlib.php
lib/db/caches.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/navigationlib.php
lib/setuplib.php
mod/assign/db/upgrade.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/comments/tests/behat/feedback_comments.feature [new file with mode: 0644]
mod/assign/gradingtable.php
mod/assign/locallib.php
mod/assign/override_form.php
mod/assign/tests/markerallocation_test.php [new file with mode: 0644]
mod/assign/version.php
mod/feedback/classes/completion.php
mod/feedback/classes/external.php
mod/feedback/lib.php
mod/lesson/classes/external.php
mod/lesson/locallib.php
mod/lesson/tests/external_test.php
mod/lesson/upgrade.txt
mod/scorm/lib.php
mod/scorm/tests/lib_test.php
question/type/multichoice/classes/admin_setting_answernumbering.php [new file with mode: 0644]
question/type/multichoice/edit_multichoice_form.php
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/renderer.php
question/type/multichoice/settings.php [new file with mode: 0644]
report/stats/locallib.php
report/stats/user.php
theme/boost/templates/header.mustache
theme/clean/classes/core_renderer.php
theme/styles.php
user/tests/userlib_test.php
version.php

index 5a20af8..1a63f4f 100644 (file)
@@ -179,7 +179,8 @@ preferences,moodle|/user/preferences.php|preferences',
         'idnumber' => new lang_string('sort_idnumber', 'admin'),
     );
     $temp->add(new admin_setting_configselect('navsortmycoursessort', new lang_string('navsortmycoursessort', 'admin'), new lang_string('navsortmycoursessort_help', 'admin'), 'sortorder', $sortoptions));
-    $temp->add(new admin_setting_configtext('navcourselimit',new lang_string('navcourselimit','admin'),new lang_string('confignavcourselimit', 'admin'),20,PARAM_INT));
+    $temp->add(new admin_setting_configtext('navcourselimit', new lang_string('navcourselimit', 'admin'),
+        new lang_string('confignavcourselimit', 'admin'), 10, PARAM_INT));
     $temp->add(new admin_setting_configcheckbox('usesitenameforsitepages', new lang_string('usesitenameforsitepages', 'admin'), new lang_string('configusesitenameforsitepages', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('linkadmincategories', new lang_string('linkadmincategories', 'admin'), new lang_string('linkadmincategories_help', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('linkcoursesections', new lang_string('linkcoursesections', 'admin'), new lang_string('linkcoursesections_help', 'admin'), 0));
index 6f5ded5..f2b50d5 100644 (file)
@@ -55,9 +55,6 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $optionalsubsystems->add(new admin_setting_configcheckbox('enableglobalsearch', new lang_string('enableglobalsearch', 'admin'),
         new lang_string('enableglobalsearch_desc', 'admin'), 0, 1, 0));
 
-    $choices = array();
-    $choices[0] = new lang_string('no');
-    $choices[1] = new lang_string('yes');
-    $optionalsubsystems->add(new admin_setting_configselect('allowstealth', new lang_string('allowstealthmodules'),
-        new lang_string('allowstealthmodules_help'), 0, $choices));
+    $optionalsubsystems->add(new admin_setting_configcheckbox('allowstealth', new lang_string('allowstealthmodules'),
+        new lang_string('allowstealthmodules_help'), 0, 1, 0));
 }
index 689ad1c..9d678a8 100644 (file)
@@ -192,10 +192,10 @@ class api {
         ];
         $confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params);
 
-        // Remove data parameter just in case it was included in the confirmation so we can add it manually later.
-        $data->link = $confirmationurl->out();
+        $data->link = $confirmationurl->out(false);
+        $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
 
-        $message     = get_string('confirmlinkedloginemail', 'auth_oauth2', $data);
+        $data->link = $confirmationurl->out();
         $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false, true);
 
         $user->mailformat = 1;  // Always send HTML version as well.
@@ -303,9 +303,10 @@ class api {
         ];
         $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params);
 
-        $data->link = $confirmationurl->out();
+        $data->link = $confirmationurl->out(false);
+        $message = get_string('confirmaccountemail', 'auth_oauth2', $data);
 
-        $message     = get_string('confirmaccountemail', 'auth_oauth2', $data);
+        $data->link = $confirmationurl->out();
         $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false, true);
 
         $user->mailformat = 1;  // Always send HTML version as well.
index d65729a..6ef4fc9 100644 (file)
@@ -62,8 +62,12 @@ class info_section extends info {
 
     protected function set_in_database($availability) {
         global $DB;
-        $DB->set_field('course_sections', 'availability', $availability,
-                array('id' => $this->section->id));
+
+        $section = new \stdClass();
+        $section->id = $this->section->id;
+        $section->availability = $availability;
+        $section->timemodified = time();
+        $DB->update_record('course_sections', $section);
     }
 
     /**
index 722b024..63b61b4 100644 (file)
@@ -287,8 +287,12 @@ class condition extends \core_availability\condition {
 
             // Save the updated course module.
             if ($changed) {
-                $DB->set_field('course_sections', 'availability', json_encode($tree->save()),
-                        array('id' => $section->id));
+                $updatesection = new \stdClass();
+                $updatesection->id = $section->id;
+                $updatesection->availability = json_encode($tree->save());
+                $updatesection->timemodified = time();
+                $DB->update_record('course_sections', $updatesection);
+
                 $anychanged = true;
             }
         }
index acdf34b..2ea5248 100644 (file)
@@ -325,7 +325,7 @@ class backup_section_structure_step extends backup_structure_step {
 
         $section = new backup_nested_element('section', array('id'), array(
                 'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible',
-                'availabilityjson'));
+                'availabilityjson', 'timemodified'));
 
         // attach format plugin structure to $section element, only one allowed
         $this->add_plugin_structure('format', $section, false);
index 5d607ef..c788ac1 100644 (file)
@@ -795,7 +795,8 @@ class restore_rebuild_course_cache extends restore_execution_step {
             if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
                 $sectionrec = array(
                     'course' => $this->get_courseid(),
-                    'section' => $i);
+                    'section' => $i,
+                    'timemodified' => time());
                 $DB->insert_record('course_sections', $sectionrec); // missing section created
             }
         }
@@ -1575,8 +1576,9 @@ class restore_section_structure_step extends restore_structure_step {
         $section = new stdclass();
         $section->course  = $this->get_courseid();
         $section->section = $data->number;
+        $section->timemodified = isset($data->timemodified) ? $this->apply_date_offset($data->timemodified) : 0;
         // Section doesn't exist, create it with all the info from backup
-        if (!$secrec = $DB->get_record('course_sections', (array)$section)) {
+        if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
             $section->name = $data->name;
             $section->summary = $data->summary;
             $section->summaryformat = $data->summaryformat;
@@ -1721,8 +1723,12 @@ class restore_section_structure_step extends restore_structure_step {
                     array('id' => $availfield->coursesectionid), MUST_EXIST);
             $newvalue = \core_availability\info::add_legacy_availability_field_condition(
                     $currentvalue, $availfield, $show);
-            $DB->set_field('course_sections', 'availability', $newvalue,
-                    array('id' => $availfield->coursesectionid));
+
+            $section = new stdClass();
+            $section->id = $availfield->coursesectionid;
+            $section->availability = $newvalue;
+            $section->timemodified = time();
+            $DB->update_record('course_sections', $section);
         }
     }
 
@@ -4032,11 +4038,13 @@ class restore_module_structure_step extends restore_structure_step {
         if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
             $sectionrec = array(
                 'course' => $this->get_courseid(),
-                'section' => 0);
+                'section' => 0,
+                'timemodified' => time());
             $DB->insert_record('course_sections', $sectionrec); // section 0
             $sectionrec = array(
                 'course' => $this->get_courseid(),
-                'section' => 1);
+                'section' => 1,
+                'timemodified' => time());
             $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
         }
         $data->groupingid= $this->get_mappingid('grouping', $data->groupingid);      // grouping
@@ -4090,7 +4098,12 @@ class restore_module_structure_step extends restore_structure_step {
         } else {
             $sequence = $newitemid;
         }
-        $DB->set_field('course_sections', 'sequence', $sequence, array('id' => $data->section));
+
+        $updatesection = new \stdClass();
+        $updatesection->id = $data->section;
+        $updatesection->sequence = $sequence;
+        $updatesection->timemodified = time();
+        $DB->update_record('course_sections', $updatesection);
 
         // If there is the legacy showavailability data, store this for later use.
         // (This data is not present when restoring 'new' backups.)
index abb479b..33bbe77 100644 (file)
@@ -90,7 +90,7 @@ echo html_writer::start_tag('tbody');
 if ($USER->id != $user->id) {
     echo html_writer::start_tag('tr');
     echo html_writer::start_tag('td', array('colspan' => '2'));
-    echo html_writer::tag('b', get_string('showinguser', 'completion'));
+    echo html_writer::tag('b', get_string('showinguser', 'completion') . ' ');
     $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id));
     echo html_writer::link($url, fullname($user));
     echo html_writer::end_tag('td');
@@ -99,7 +99,7 @@ if ($USER->id != $user->id) {
 
 echo html_writer::start_tag('tr');
 echo html_writer::start_tag('td', array('colspan' => '2'));
-echo html_writer::tag('b', get_string('status'));
+echo html_writer::tag('b', get_string('status') . ' ');
 
 // Is course complete?
 $coursecomplete = $info->is_course_complete($user->id);
@@ -141,7 +141,7 @@ if (empty($completions)) {
 } else {
     echo html_writer::start_tag('tr');
     echo html_writer::start_tag('td', array('colspan' => '2'));
-    echo html_writer::tag('b', get_string('required'));
+    echo html_writer::tag('b', get_string('required') . ' ');
 
     // Get overall aggregation method.
     $overall = $info->get_aggregation_method();
@@ -214,7 +214,7 @@ if (empty($completions)) {
                     echo core_text::strtolower(get_string('any', 'completion'));
                 }
 
-                echo html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')';
+                echo ' ' . html_writer::end_tag('i') .core_text::strtolower(get_string('required')).')';
                 $agg_type = false;
             }
         }
diff --git a/blocks/myoverview/amd/build/tab_preferences.min.js b/blocks/myoverview/amd/build/tab_preferences.min.js
new file mode 100644 (file)
index 0000000..2bfb8fe
Binary files /dev/null and b/blocks/myoverview/amd/build/tab_preferences.min.js differ
diff --git a/blocks/myoverview/amd/src/tab_preferences.js b/blocks/myoverview/amd/src/tab_preferences.js
new file mode 100644 (file)
index 0000000..f6afc1b
--- /dev/null
@@ -0,0 +1,60 @@
+// 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/>.
+
+/**
+ * Javascript used to save the user's tab preference.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define(['jquery', 'core/ajax', 'core/custom_interaction_events'], function($, Ajax, CustomEvents) {
+
+    /**
+     * Registers an event that saves the user's tab preference when switching between them.
+     *
+     * @param {object} root The container element
+     */
+    var registerEventListeners = function(root) {
+        CustomEvents.define(root, [CustomEvents.events.activate]);
+        root.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
+            var tabname = $(e.currentTarget).data('tabname');
+            // Bootstrap does not change the URL when using BS tabs, so need to do this here.
+            // Also check to make sure the browser supports the history API.
+            if (typeof window.history.pushState === "function") {
+                window.history.pushState(null, null, '?myoverviewtab=' + tabname);
+            }
+            var request = {
+                methodname: 'core_user_update_user_preferences',
+                args: {
+                    preferences: [
+                        {
+                            type: 'block_myoverview_last_tab',
+                            value: tabname
+                        }
+                    ]
+                }
+            };
+
+            Ajax.call([request])[0]
+                .fail(Notification.exception);
+        });
+    };
+
+    return {
+        registerEventListeners: registerEventListeners
+    };
+});
index f22ce15..8afd4a1 100644 (file)
@@ -50,7 +50,16 @@ class block_myoverview extends block_base {
             return $this->content;
         }
 
-        $renderable = new \block_myoverview\output\main();
+        // Check if the tab to select wasn't passed in the URL, if so see if the user has any preference.
+        if (!$tab = optional_param('myoverviewtab', null, PARAM_ALPHA)) {
+            // Check if the user has no preference, if so get the site setting.
+            if (!$tab = get_user_preferences('block_myoverview_last_tab')) {
+                $config = get_config('block_myoverview');
+                $tab = $config->defaulttab;
+            }
+        }
+
+        $renderable = new \block_myoverview\output\main($tab);
         $renderer = $this->page->get_renderer('block_myoverview');
 
         $this->content = new stdClass();
@@ -68,4 +77,13 @@ class block_myoverview extends block_base {
     public function applicable_formats() {
         return array('my' => true);
     }
+
+    /**
+     * This block does contain a configuration settings.
+     *
+     * @return boolean
+     */
+    public function has_config() {
+        return true;
+    }
 }
index 31dc8c6..798eb7b 100644 (file)
@@ -63,8 +63,6 @@ class courses_view implements renderable, templatable {
      * @return array
      */
     public function export_for_template(renderer_base $output) {
-        $today = time();
-
         // Build courses view data structure.
         $coursesview = [
             'hascourses' => !empty($this->courses)
@@ -73,8 +71,6 @@ class courses_view implements renderable, templatable {
         // How many courses we have per status?
         $coursesbystatus = ['past' => 0, 'inprogress' => 0, 'future' => 0];
         foreach ($this->courses as $course) {
-            $startdate = $course->startdate;
-            $enddate = $course->enddate;
             $courseid = $course->id;
             $context = \context_course::instance($courseid);
             $exporter = new course_summary_exporter($course, [
@@ -84,14 +80,17 @@ class courses_view implements renderable, templatable {
             // Convert summary to plain text.
             $exportedcourse->summary = content_to_text($exportedcourse->summary, $exportedcourse->summaryformat);
 
+            $courseprogress = null;
+
+            $classified = course_classify_for_timeline($course);
+
             if (isset($this->coursesprogress[$courseid])) {
-                $coursecompleted = $this->coursesprogress[$courseid]['completed'];
                 $courseprogress = $this->coursesprogress[$courseid]['progress'];
                 $exportedcourse->hasprogress = !is_null($courseprogress);
                 $exportedcourse->progress = $courseprogress;
             }
 
-            if ((isset($coursecompleted) && $coursecompleted) || (!empty($enddate) && $enddate < $today)) {
+            if ($classified == COURSE_TIMELINE_PAST) {
                 // Courses that have already ended.
                 $pastpages = floor($coursesbystatus['past'] / $this::COURSES_PER_PAGE);
 
@@ -100,7 +99,7 @@ class courses_view implements renderable, templatable {
                 $coursesview['past']['pages'][$pastpages]['page'] = $pastpages + 1;
                 $coursesview['past']['haspages'] = true;
                 $coursesbystatus['past']++;
-            } else if ($startdate > $today) {
+            } else if ($classified == COURSE_TIMELINE_FUTURE) {
                 // Courses that have not started yet.
                 $futurepages = floor($coursesbystatus['future'] / $this::COURSES_PER_PAGE);
 
index 6215a5a..2435f54 100644 (file)
@@ -29,6 +29,7 @@ use renderer_base;
 use templatable;
 use core_completion\progress;
 
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
 require_once($CFG->libdir . '/completionlib.php');
 
 /**
@@ -39,6 +40,20 @@ require_once($CFG->libdir . '/completionlib.php');
  */
 class main implements renderable, templatable {
 
+    /**
+     * @var string The tab to display.
+     */
+    public $tab;
+
+    /**
+     * Constructor.
+     *
+     * @param string $tab The tab to display.
+     */
+    public function __construct($tab) {
+        $this->tab = $tab;
+    }
+
     /**
      * Export this data so it can be used as the context for a mustache template.
      *
@@ -73,13 +88,24 @@ class main implements renderable, templatable {
         $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
         $noeventsurl = $output->image_url('activities', 'block_myoverview')->out();
 
+        // Now, set the tab we are going to be viewing.
+        $viewingtimeline = false;
+        $viewingcourses = false;
+        if ($this->tab == BLOCK_MYOVERVIEW_TIMELINE_VIEW) {
+            $viewingtimeline = true;
+        } else {
+            $viewingcourses = true;
+        }
+
         return [
             'midnight' => usergetmidnight(time()),
             'coursesview' => $coursesview->export_for_template($output),
             'urls' => [
                 'nocourses' => $nocoursesurl,
                 'noevents' => $noeventsurl
-            ]
+            ],
+            'viewingtimeline' => $viewingtimeline,
+            'viewingcourses' => $viewingcourses
         ];
     }
 }
index 4c464f5..99fb83f 100644 (file)
@@ -22,6 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['defaulttab'] = 'Default tab';
+$string['defaulttab_desc'] = 'This is the default tab that will be shown to a user.';
 $string['future'] = 'Future';
 $string['inprogress'] = 'In progress';
 $string['morecourses'] = 'More courses';
diff --git a/blocks/myoverview/lib.php b/blocks/myoverview/lib.php
new file mode 100644 (file)
index 0000000..a73db25
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Contains functions called by core.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * The timeline view.
+ */
+define('BLOCK_MYOVERVIEW_TIMELINE_VIEW', 'timeline');
+
+/**
+ * The courses view.
+ */
+define('BLOCK_MYOVERVIEW_COURSES_VIEW', 'courses');
+
+/**
+ * Returns the name of the user preferences as well as the details this plugin uses.
+ *
+ * @return array
+ */
+function block_myoverview_user_preferences() {
+    $preferences = array();
+    $preferences['block_myoverview_last_tab'] = array(
+        'type' => PARAM_ALPHA,
+        'null' => NULL_NOT_ALLOWED,
+        'default' => BLOCK_MYOVERVIEW_TIMELINE_VIEW,
+        'choices' => array(BLOCK_MYOVERVIEW_TIMELINE_VIEW, BLOCK_MYOVERVIEW_COURSES_VIEW)
+    );
+
+    return $preferences;
+}
diff --git a/blocks/myoverview/settings.php b/blocks/myoverview/settings.php
new file mode 100644 (file)
index 0000000..10f084d
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Settings for the overview block.
+ *
+ * @package    block_myoverview
+ * @copyright  2017 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
+
+if ($ADMIN->fulltree) {
+
+    $options = [
+        BLOCK_MYOVERVIEW_TIMELINE_VIEW => get_string('timeline', 'block_myoverview'),
+        BLOCK_MYOVERVIEW_COURSES_VIEW => get_string('courses')
+    ];
+
+    $settings->add(new admin_setting_configselect('block_myoverview/defaulttab',
+        get_string('defaulttab', 'block_myoverview'),
+        get_string('defaulttab_desc', 'block_myoverview'), 'timeline', $options));
+}
index 3a1a942..e9b21bd 100644 (file)
 }}
 
 <div id="block-myoverview-{{uniqid}}" class="block-myoverview" data-region="myoverview">
-    <ul class="nav nav-tabs" role="tablist">
+    <ul id="block-myoverview-view-choices-{{uniqid}}" class="nav nav-tabs" role="tablist">
         <li class="nav-item">
-            <a class="nav-link active" href="#myoverview_timeline_view" role="tab" data-toggle="tab">
+            <a class="nav-link {{#viewingtimeline}}active{{/viewingtimeline}}" href="#myoverview_timeline_view" role="tab" data-toggle="tab" data-tabname="timeline">
                 {{#str}} timeline, block_myoverview {{/str}}
             </a>
         </li>
         <li class="nav-item">
-            <a class="nav-link" href="#myoverview_courses_view" role="tab" data-toggle="tab">
+            <a class="nav-link {{#viewingcourses}}active{{/viewingcourses}}" href="#myoverview_courses_view" role="tab" data-toggle="tab" data-tabname="courses">
                 {{#str}} courses {{/str}}
             </a>
         </li>
     </ul>
     <div class="tab-content content-centred">
-        <div role="tabpanel" class="tab-pane fade in active" id="myoverview_timeline_view">
+        <div role="tabpanel" class="tab-pane fade {{#viewingtimeline}}in active{{/viewingtimeline}}" id="myoverview_timeline_view">
             {{> block_myoverview/timeline-view }}
         </div>
-        <div role="tabpanel" class="tab-pane fade" id="myoverview_courses_view">
+        <div role="tabpanel" class="tab-pane fade {{#viewingcourses}}in active{{/viewingcourses}}" id="myoverview_courses_view">
             {{#coursesview}}
                 {{> block_myoverview/courses-view }}
             {{/coursesview}}
         </div>
     </div>
 </div>
+{{#js}}
+require(['jquery', 'block_myoverview/tab_preferences'], function($, TabPreferences) {
+    var root = $('#block-myoverview-view-choices-{{uniqid}}');
+    TabPreferences.registerEventListeners(root);
+});
+{{/js}}
index 8bd3afe..e8d8692 100644 (file)
@@ -53,6 +53,7 @@ Feature: Course overview block show users their progress on courses
     And I am on "Course 1" course homepage
     And I follow "Test choice 1"
     And I follow "Dashboard" in the user menu
+    And I click on "Timeline" "link" in the "Course overview" "block"
     And I click on "Sort by courses" "link" in the "Course overview" "block"
     And I should see "100%" in the "Course overview" "block"
     And I click on "Courses" "link" in the "Course overview" "block"
index fd45f77..a637f48 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2017051500;         // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2017051502;         // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2017050500;         // Requires this Moodle version.
 $plugin->component = 'block_myoverview'; // Full name of the plugin (used for diagnostics).
index 4e0a33c..04b05c4 100644 (file)
@@ -74,7 +74,16 @@ class completion_completion extends data_object {
      * @return data_object instance of data_object or false if none found.
      */
     public static function fetch($params) {
-        return self::fetch_helper('course_completions', __CLASS__, $params);
+        $cache = cache::make('core', 'coursecompletion');
+
+        $key = $params['userid'] . '_' . $params['course'];
+        if ($hit = $cache->get($key)) {
+            return $hit['value'];
+        }
+
+        $tocache = self::fetch_helper('course_completions', __CLASS__, $params);
+        $cache->set($key, ['value' => $tocache]);
+        return $tocache;
     }
 
     /**
@@ -179,9 +188,10 @@ class completion_completion extends data_object {
             $this->timeenrolled = 0;
         }
 
+        $result = false;
         // Save record
         if ($this->id) {
-            return $this->update();
+            $result = $this->update();
         } else {
             // Make sure reaggregate field is not null
             if (!$this->reaggregate) {
@@ -193,7 +203,17 @@ class completion_completion extends data_object {
                 $this->timestarted = 0;
             }
 
-            return $this->insert();
+            $result = $this->insert();
+        }
+
+        if ($result) {
+            // Update the cached record.
+            $cache = cache::make('core', 'coursecompletion');
+            $data = $this->get_record_data();
+            $key = $data->userid . '_' . $data->course;
+            $cache->set($key, ['value' => $data]);
         }
+
+        return $result;
     }
 }
index e60bd2c..f079a74 100644 (file)
@@ -55,6 +55,10 @@ define('FIRSTUSEDEXCELROW', 3);
 define('MOD_CLASS_ACTIVITY', 0);
 define('MOD_CLASS_RESOURCE', 1);
 
+define('COURSE_TIMELINE_PAST', 'past');
+define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
+define('COURSE_TIMELINE_FUTURE', 'future');
+
 function make_log_url($module, $url) {
     switch ($module) {
         case 'course':
@@ -863,6 +867,7 @@ function course_create_section($courseorid, $position = 0, $skipcheck = false) {
     $cw->name = null;
     $cw->visible = 1;
     $cw->availability = null;
+    $cw->timemodified = time();
     $cw->id = $DB->insert_record("course_sections", $cw);
 
     // Now move it to the specified position.
@@ -1611,6 +1616,7 @@ function course_update_section($course, $section, $data) {
 
     // Update record in the DB and course format options.
     $data['id'] = $section->id;
+    $data['timemodified'] = time();
     $DB->update_record('course_sections', $data);
     rebuild_course_cache($courseid, true);
     course_get_format($courseid)->update_section_format_options($data);
@@ -4001,6 +4007,46 @@ function course_check_updates($course, $tocheck, $filter = array()) {
     return array($instances, $warnings);
 }
 
+/**
+ * This function classifies a course as past, in progress or future.
+ *
+ * This function may incur a DB hit to calculate course completion.
+ * @param stdClass $course Course record
+ * @param stdClass $user User record (optional - defaults to $USER).
+ * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
+ * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
+ */
+function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
+    global $USER;
+
+    if ($user == null) {
+        $user = $USER;
+    }
+
+    $today = time();
+    // End date past.
+    if (!empty($course->enddate) && $course->enddate < $today) {
+        return COURSE_TIMELINE_PAST;
+    }
+
+    if ($completioninfo == null) {
+        $completioninfo = new completion_info($course);
+    }
+
+    // Course was completed.
+    if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
+        return COURSE_TIMELINE_PAST;
+    }
+
+    // Start date not reached.
+    if (!empty($course->startdate) && $course->startdate > $today) {
+        return COURSE_TIMELINE_FUTURE;
+    }
+
+    // Everything else is in progress.
+    return COURSE_TIMELINE_INPROGRESS;
+}
+
 /**
  * Check module updates since a given time.
  * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
index 61169d5..5ce1ffa 100644 (file)
@@ -678,6 +678,29 @@ class core_course_courselib_testcase extends advanced_testcase {
         }
     }
 
+    public function test_update_course_section_time_modified() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Create the course with sections.
+        $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
+        $sections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Get the last section's time modified value.
+        $section = array_pop($sections);
+        $oldtimemodified = $section->timemodified;
+
+        // Update the section.
+        $this->waitForSecond(); // Ensuring that the section update occurs at a different timestamp.
+        course_update_section($course, $section, array());
+
+        // Check that the time has changed.
+        $section = $DB->get_record('course_sections', array('id' => $section->id));
+        $newtimemodified = $section->timemodified;
+        $this->assertGreaterThan($oldtimemodified, $newtimemodified);
+    }
+
     public function test_course_add_cm_to_section() {
         global $DB;
         $this->resetAfterTest(true);
@@ -3684,4 +3707,52 @@ class core_course_courselib_testcase extends advanced_testcase {
         }
         $this->assertEquals(2, $count);
     }
+
+    public function test_classify_course_for_timeline() {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
+
+        set_config('enablecompletion', COMPLETION_ENABLED);
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Create courses for testing.
+        $generator = $this->getDataGenerator();
+        $future = time() + 3600;
+        $past = time() - 3600;
+        $futurecourse = $generator->create_course(['startdate' => $future]);
+        $pastcourse = $generator->create_course(['startdate' => $past - 60, 'enddate' => $past]);
+        $completedcourse = $generator->create_course(['enablecompletion' => COMPLETION_ENABLED]);
+        $inprogresscourse = $generator->create_course();
+
+        // Set completion rules.
+        $criteriadata = new stdClass();
+        $criteriadata->id = $completedcourse->id;
+
+        // Self completion.
+        $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
+        $class = 'completion_criteria_self';
+        $criterion = new $class();
+        $criterion->update_config($criteriadata);
+
+        $user = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $futurecourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $pastcourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $completedcourse->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($user->id, $inprogresscourse->id, $studentrole->id);
+
+        $this->setUser($user);
+        core_completion_external::mark_course_self_completed($completedcourse->id);
+        $ccompletion = new completion_completion(array('course' => $completedcourse->id, 'userid' => $user->id));
+        $ccompletion->mark_complete();
+
+        // Aggregate the completions.
+        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($pastcourse));
+        $this->assertEquals(COURSE_TIMELINE_FUTURE, course_classify_for_timeline($futurecourse));
+        $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
+        $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
+    }
 }
index 2313c63..5dfb8e9 100644 (file)
@@ -264,142 +264,143 @@ class edit_item_form extends moodleform {
         $mform =& $this->_form;
 
         if ($id = $mform->getElementValue('id')) {
-            $grade_item = grade_item::fetch(array('id'=>$id));
+            $gradeitem = grade_item::fetch(array('id' => $id));
+            $parentcategory = $gradeitem->get_parent_category();
+        } else {
+            // If we do not have an id, we are creating a new grade item.
+            $gradeitem = new grade_item(array('courseid' => $COURSE->id, 'itemtype' => 'manual'), false);
+
+            // Assign the course category to this grade item.
+            $parentcategory = grade_category::fetch_course_category($COURSE->id);
+            $gradeitem->parent_category = $parentcategory;
+        }
+
+        if (!$gradeitem->is_raw_used()) {
+            $mform->removeElement('plusfactor');
+            $mform->removeElement('multfactor');
+        }
 
-            if (!$grade_item->is_raw_used()) {
-                $mform->removeElement('plusfactor');
-                $mform->removeElement('multfactor');
+        if ($gradeitem->is_outcome_item()) {
+            // We have to prevent incompatible modifications of outcomes if outcomes disabled.
+            $mform->removeElement('grademax');
+            if ($mform->elementExists('grademin')) {
+                $mform->removeElement('grademin');
             }
+            $mform->removeElement('gradetype');
+            $mform->removeElement('display');
+            $mform->removeElement('decimals');
+            $mform->hardFreeze('scaleid');
 
-            if ($grade_item->is_outcome_item()) {
-                // we have to prevent incompatible modifications of outcomes if outcomes disabled
-                $mform->removeElement('grademax');
+        } else {
+            if ($gradeitem->is_external_item()) {
+                // Following items are set up from modules and should not be overrided by user.
                 if ($mform->elementExists('grademin')) {
-                    $mform->removeElement('grademin');
+                    // The site setting grade_report_showmin may have prevented grademin being added to the form.
+                    $mform->hardFreeze('grademin');
+                }
+                $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
+                if ($gradeitem->itemnumber == 0) {
+                    // The idnumber of grade itemnumber 0 is synced with course_modules.
+                    $mform->hardFreeze('idnumber');
                 }
-                $mform->removeElement('gradetype');
-                $mform->removeElement('display');
-                $mform->removeElement('decimals');
-                $mform->hardFreeze('scaleid');
 
-            } else {
-                if ($grade_item->is_external_item()) {
-                    // following items are set up from modules and should not be overrided by user
+                // For external items we can not change the grade type, even if no grades exist, so if it is set to
+                // scale, then remove the grademax and grademin fields from the form - no point displaying them.
+                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
+                    $mform->removeElement('grademax');
                     if ($mform->elementExists('grademin')) {
-                        // The site setting grade_report_showmin may have prevented grademin being added to the form.
-                        $mform->hardFreeze('grademin');
-                    }
-                    $mform->hardFreeze('itemname,gradetype,grademax,scaleid');
-                    if ($grade_item->itemnumber == 0) {
-                        // the idnumber of grade itemnumber 0 is synced with course_modules
-                        $mform->hardFreeze('idnumber');
+                        $mform->removeElement('grademin');
                     }
+                } else { // Not using scale, so remove it.
+                    $mform->removeElement('scaleid');
+                }
 
-                    // For external items we can not change the grade type, even if no grades exist, so if it is set to
-                    // scale, then remove the grademax and grademin fields from the form - no point displaying them.
-                    if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
-                        $mform->removeElement('grademax');
-                        if ($mform->elementExists('grademin')) {
-                            $mform->removeElement('grademin');
-                        }
-                    } else { // Not using scale, so remove it.
-                        $mform->removeElement('scaleid');
-                    }
+                // Always remove the rescale grades element if it's an external item.
+                $mform->removeElement('rescalegrades');
+            } else if ($gradeitem->has_grades()) {
+                // Can't change the grade type or the scale if there are grades.
+                $mform->hardFreeze('gradetype, scaleid');
 
-                    // Always remove the rescale grades element if it's an external item.
+                // If we are using scales then remove the unnecessary rescale and grade fields.
+                if ($gradeitem->gradetype == GRADE_TYPE_SCALE) {
                     $mform->removeElement('rescalegrades');
-                } else if ($grade_item->has_grades()) {
-                    // Can't change the grade type or the scale if there are grades.
-                    $mform->hardFreeze('gradetype, scaleid');
-
-                    // If we are using scales then remove the unnecessary rescale and grade fields.
-                    if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
-                        $mform->removeElement('rescalegrades');
-                        $mform->removeElement('grademax');
-                        if ($mform->elementExists('grademin')) {
-                            $mform->removeElement('grademin');
-                        }
-                    } else { // Remove the scale field.
-                        $mform->removeElement('scaleid');
-                        // Set the maximum grade to disabled unless a grade is chosen.
-                        $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
+                    $mform->removeElement('grademax');
+                    if ($mform->elementExists('grademin')) {
+                        $mform->removeElement('grademin');
                     }
-                } else {
-                    // Remove the rescale element if there are no grades.
-                    $mform->removeElement('rescalegrades');
+                } else { // Remove the scale field.
+                    $mform->removeElement('scaleid');
+                    // Set the maximum grade to disabled unless a grade is chosen.
+                    $mform->disabledIf('grademax', 'rescalegrades', 'eq', '');
                 }
+            } else {
+                // Remove the rescale element if there are no grades.
+                $mform->removeElement('rescalegrades');
             }
+        }
+
+        // If we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
+        if ($id && $mform->elementExists('parentcategory')) {
+            $mform->hardFreeze('parentcategory');
+        }
+
+        $parentcategory->apply_forced_settings();
 
-            // if we wanted to change parent of existing item - we would have to verify there are no circular references in parents!!!
-            if ($mform->elementExists('parentcategory')) {
-                $mform->hardFreeze('parentcategory');
+        if (!$parentcategory->is_aggregationcoef_used()) {
+            if ($mform->elementExists('aggregationcoef')) {
+                $mform->removeElement('aggregationcoef');
             }
 
-            $parent_category = $grade_item->get_parent_category();
-            $parent_category->apply_forced_settings();
+        } else {
+            $coefstring = $gradeitem->get_coefstring();
 
-            if (!$parent_category->is_aggregationcoef_used()) {
-                if ($mform->elementExists('aggregationcoef')) {
-                    $mform->removeElement('aggregationcoef');
+            if ($coefstring !== '') {
+                if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
+                    // The advcheckbox is not compatible with disabledIf!
+                    $coefstring = 'aggregationcoefextrasum';
+                    $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
+                } else {
+                    $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
                 }
-
-            } else {
-                $coefstring = $grade_item->get_coefstring();
-
-                if ($coefstring !== '') {
-                    if ($coefstring == 'aggregationcoefextrasum' || $coefstring == 'aggregationcoefextraweightsum') {
-                        // advcheckbox is not compatible with disabledIf!
-                        $coefstring = 'aggregationcoefextrasum';
-                        $element =& $mform->createElement('checkbox', 'aggregationcoef', get_string($coefstring, 'grades'));
-                    } else {
-                        $element =& $mform->createElement('text', 'aggregationcoef', get_string($coefstring, 'grades'));
-                    }
-                    if ($mform->elementExists('parentcategory')) {
-                        $mform->insertElementBefore($element, 'parentcategory');
-                    } else {
-                        $mform->insertElementBefore($element, 'id');
-                    }
-                    $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
+                if ($mform->elementExists('parentcategory')) {
+                    $mform->insertElementBefore($element, 'parentcategory');
+                } else {
+                    $mform->insertElementBefore($element, 'id');
                 }
-                $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
-                $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
-                $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parent_category->id);
+                $mform->addHelpButton('aggregationcoef', $coefstring, 'grades');
             }
+            $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_NONE);
+            $mform->disabledIf('aggregationcoef', 'gradetype', 'eq', GRADE_TYPE_TEXT);
+            $mform->disabledIf('aggregationcoef', 'parentcategory', 'eq', $parentcategory->id);
+        }
 
-            // Remove fields used by natural weighting if the parent category is not using natural weighting.
-            // Or if the item is a scale and scales are not used in aggregation.
-            if ($parent_category->aggregation != GRADE_AGGREGATE_SUM
-                    || (empty($CFG->grade_includescalesinaggregation) && $grade_item->gradetype == GRADE_TYPE_SCALE)) {
-                if ($mform->elementExists('weightoverride')) {
-                    $mform->removeElement('weightoverride');
-                }
-                if ($mform->elementExists('aggregationcoef2')) {
-                    $mform->removeElement('aggregationcoef2');
-                }
+        // Remove fields used by natural weighting if the parent category is not using natural weighting.
+        // Or if the item is a scale and scales are not used in aggregation.
+        if ($parentcategory->aggregation != GRADE_AGGREGATE_SUM
+                || (empty($CFG->grade_includescalesinaggregation) && $gradeitem->gradetype == GRADE_TYPE_SCALE)) {
+            if ($mform->elementExists('weightoverride')) {
+                $mform->removeElement('weightoverride');
             }
+            if ($mform->elementExists('aggregationcoef2')) {
+                $mform->removeElement('aggregationcoef2');
+            }
+        }
 
-            if ($category = $grade_item->get_item_category()) {
-                if ($category->aggregation == GRADE_AGGREGATE_SUM) {
-                    if ($mform->elementExists('gradetype')) {
-                        $mform->hardFreeze('gradetype');
-                    }
-                    if ($mform->elementExists('grademin')) {
-                        $mform->hardFreeze('grademin');
-                    }
-                    if ($mform->elementExists('grademax')) {
-                        $mform->hardFreeze('grademax');
-                    }
-                    if ($mform->elementExists('scaleid')) {
-                        $mform->removeElement('scaleid');
-                    }
+        if ($category = $gradeitem->get_item_category()) {
+            if ($category->aggregation == GRADE_AGGREGATE_SUM) {
+                if ($mform->elementExists('gradetype')) {
+                    $mform->hardFreeze('gradetype');
+                }
+                if ($mform->elementExists('grademin')) {
+                    $mform->hardFreeze('grademin');
+                }
+                if ($mform->elementExists('grademax')) {
+                    $mform->hardFreeze('grademax');
+                }
+                if ($mform->elementExists('scaleid')) {
+                    $mform->removeElement('scaleid');
                 }
             }
-
-        } else {
-            // all new items are manual, children of course category
-            $mform->removeElement('plusfactor');
-            $mform->removeElement('multfactor');
-            $mform->removeElement('rescalegrades');
         }
 
         // no parent header for course category
index f0d5e4f..b9bb1e5 100644 (file)
@@ -274,7 +274,7 @@ $string['configmodchooserdefault'] = 'Should the activity chooser be presented t
 $string['configmycoursesperpage'] = 'Maximum number of courses to display in any list of a user\'s own courses';
 $string['configmymoodleredirect'] = 'This setting forces redirects to /my on login for non-admins and replaces the top level site navigation with /my';
 $string['configmypagelocked'] = 'This setting prevents the default page from being edited by any non-admins';
-$string['confignavcourselimit'] = 'Limits the number of courses shown to the user when they are either not logged in or are not enrolled in any courses.';
+$string['confignavcourselimit'] = 'Limits the number of courses shown to the user in the navigation.';
 $string['confignavshowallcourses'] = 'This setting determines whether users who are enrolled in courses can see Courses (listing all courses) in the navigation, in addition to My Courses (listing courses in which they are enrolled).';
 $string['confignavshowcategories'] = 'Show course categories in the navigation bar and navigation blocks. This does not occur with courses the user is currently enrolled in, they will still be listed under mycourses without categories.';
 $string['confignoreplyaddress'] = 'Emails are sometimes sent out on behalf of a user (eg forum posts). The email address you specify here will be used as the "From" address in those cases when the recipients should not be able to reply directly to the user (eg when a user chooses to keep their address private). This setting will also be used as the envelope sender when sending email.';
index e811f03..545a230 100644 (file)
@@ -41,8 +41,9 @@ $string['cachedef_capabilities'] = 'System capabilities list';
 $string['cachedef_config'] = 'Config settings';
 $string['cachedef_coursecat'] = 'Course categories lists for particular user';
 $string['cachedef_coursecatrecords'] = 'Course categories records';
-$string['cachedef_coursecontacts'] = 'List of course contacts';
 $string['cachedef_coursecattree'] = 'Course categories tree';
+$string['cachedef_coursecompletion'] = 'Course completion status';
+$string['cachedef_coursecontacts'] = 'List of course contacts';
 $string['cachedef_coursemodinfo'] = 'Accumulated information about modules and sections for each course';
 $string['cachedef_completion'] = 'Activity completion status';
 $string['cachedef_databasemeta'] = 'Database meta information';
index 07e154d..3387da5 100644 (file)
@@ -1741,6 +1741,7 @@ $string['showsettings'] = 'Show settings';
 $string['showtheselogs'] = 'Show these logs';
 $string['showthishelpinlanguage'] = 'Show this help in language: {$a}';
 $string['schedule'] = 'Schedule';
+$string['sidepanel'] = 'Side panel';
 $string['signoutofotherservices'] = 'Sign out everywhere';
 $string['signoutofotherservices_help'] = 'If ticked, the account will be signed out of all devices and systems which use web services, such as the mobile app.';
 $string['since'] = 'Since';
index 80c8542..c32ad4c 100644 (file)
@@ -769,6 +769,7 @@ class completion_info {
 
         // Difficult to find affected users, just purge all completion cache.
         cache::make('core', 'completion')->purge();
+        cache::make('core', 'coursecompletion')->purge();
     }
 
     /**
@@ -820,6 +821,7 @@ class completion_info {
 
         // Difficult to find affected users, just purge all completion cache.
         cache::make('core', 'completion')->purge();
+        cache::make('core', 'coursecompletion')->purge();
     }
 
     /**
index 42d98d4..f5fcff0 100644 (file)
@@ -229,6 +229,16 @@ $definitions = array(
         'staticaccelerationsize' => 2, // Should be current course and site course.
     ),
 
+    // Used to cache course completion status.
+    'coursecompletion' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'simpledata' => true,
+        'ttl' => 3600,
+        'staticacceleration' => true,
+        'staticaccelerationsize' => 30, // Will be users list of current courses in nav.
+    ),
+
     // A simple cache that stores whether a user can expand a course in the navigation.
     // The key is the course ID and the value will either be 1 or 0 (cast to bool).
     // The cache isn't always up to date, it should only ever be used to save a costly call to
index 39ab0c6..ce5dda2 100644 (file)
         <FIELD NAME="sequence" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="visible" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
         <FIELD NAME="availability" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Availability restrictions for viewing this section, in JSON format. Null if no restrictions."/>
+        <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Time at which the course section was last changed."/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index 002640e..bdc4019 100644 (file)
@@ -244,7 +244,7 @@ $functions = array(
         'classname' => 'core_course_external',
         'methodname' => 'duplicate_course',
         'classpath' => 'course/externallib.php',
-        'description' => 'Duplicate an existing course (creating a new one), without user data',
+        'description' => 'Duplicate an existing course (creating a new one).',
         'type' => 'write',
         'capabilities' => 'moodle/backup:backupcourse, moodle/restore:restorecourse, moodle/course:create'
     ),
index b130390..7798fec 100644 (file)
@@ -2865,5 +2865,27 @@ function xmldb_main_upgrade($oldversion) {
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2017061201.00) {
+        $table = new xmldb_table('course_sections');
+        $field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'availability');
+
+        // Define a field 'timemodified' in the 'course_sections' table.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        upgrade_main_savepoint(true, 2017061201.00);
+    }
+
+    if ($oldversion < 2017061301.00) {
+        // Check if the value of 'navcourselimit' is set to the old default value, if so, change it to the new default.
+        if ($CFG->navcourselimit == 20) {
+            set_config('navcourselimit', 10);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2017061301.00);
+    }
+
     return true;
 }
index aa29bd9..0a9b47c 100644 (file)
@@ -84,6 +84,8 @@ class navigation_node implements renderable {
     const COURSE_MY = 1;
     /** var int Course the current user is currently viewing */
     const COURSE_CURRENT = 2;
+    /** var string The course index page navigation node */
+    const COURSE_INDEX_PAGE = 'courseindexpage';
 
     /** @var int Parameter to aid the coder in tracking [optional] */
     public $id = null;
@@ -430,7 +432,7 @@ class navigation_node implements renderable {
     public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false) {
         if ($this->showinflatnavigation) {
             $indent = 0;
-            if ($this->type == self::TYPE_COURSE) {
+            if ($this->type == self::TYPE_COURSE || $this->key == self::COURSE_INDEX_PAGE) {
                 $indent = 1;
             }
             $flat = new flat_navigation_node($this, $indent);
@@ -2567,7 +2569,16 @@ class global_navigation extends navigation_node {
         }
 
         $coursenode = $parent->add($coursename, $url, self::TYPE_COURSE, $shortname, $course->id);
-        $coursenode->showinflatnavigation = $coursetype == self::COURSE_MY;
+
+        // Do some calculation to see if the course is past, current or future.
+        if ($coursetype == self::COURSE_MY) {
+            $classify = course_classify_for_timeline($course);
+
+            if ($classify == COURSE_TIMELINE_INPROGRESS) {
+                $coursenode->showinflatnavigation = true;
+            }
+        }
+
         $coursenode->hidden = (!$course->visible);
         $coursenode->title(format_string($course->fullname, true, array('context' => $coursecontext, 'escape' => false)));
         if ($canexpandcourse) {
@@ -2883,6 +2894,9 @@ class global_navigation extends navigation_node {
      */
     protected function load_courses_enrolled() {
         global $CFG;
+
+        $limit = (int) $CFG->navcourselimit;
+
         $sortorder = 'visible DESC';
         // Prevent undefined $CFG->navsortmycoursessort errors.
         if (empty($CFG->navsortmycoursessort)) {
@@ -2890,8 +2904,10 @@ class global_navigation extends navigation_node {
         }
         // Append the chosen sortorder.
         $sortorder = $sortorder . ',' . $CFG->navsortmycoursessort . ' ASC';
-        $courses = enrol_get_my_courses(null, $sortorder);
-        if (count($courses) && $this->show_my_categories()) {
+        $courses = enrol_get_my_courses('*', $sortorder);
+        $numcourses = count($courses);
+        $courses = array_slice($courses, 0, $limit);
+        if ($numcourses && $this->show_my_categories()) {
             // Generate an array containing unique values of all the courses' categories.
             $categoryids = array();
             foreach ($courses as $course) {
@@ -2947,6 +2963,14 @@ class global_navigation extends navigation_node {
         foreach ($courses as $course) {
             $this->add_course($course, false, self::COURSE_MY);
         }
+        // Show a link to the course page if there are more courses the user is enrolled in.
+        if ($numcourses > $limit) {
+            // Adding hash to URL so the link is not highlighted in the navigation when clicked.
+            $url = new moodle_url('/course/index.php#');
+            $parent = $this->rootnodes['mycourses'];
+            $coursenode = $parent->add(get_string('morenavigationlinks'), $url, self::TYPE_CUSTOM, null, self::COURSE_INDEX_PAGE);
+            $coursenode->showinflatnavigation = true;
+        }
     }
 }
 
@@ -3140,7 +3164,7 @@ class global_navigation_for_ajax extends global_navigation {
         // If category is shown in MyHome then only show enrolled courses and hide empty subcategories,
         // else show all courses.
         if ($nodetype === self::TYPE_MY_CATEGORY) {
-            $courses = enrol_get_my_courses();
+            $courses = enrol_get_my_courses('*');
             $categoryids = array();
 
             // Only search for categories if basecategory was found.
index 9759ced..7f2e3f8 100644 (file)
@@ -384,7 +384,14 @@ function default_exception_handler($ex) {
                 // If you enable db debugging and exception is thrown, the print footer prints a lot of rubbish
                 $DB->set_debug(0);
             }
-            echo $OUTPUT->fatal_error($info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo,
+            if (AJAX_SCRIPT) {
+                // If we are in an AJAX script we don't want to use PREFERRED_RENDERER_TARGET.
+                // Because we know we will want to use ajax format.
+                $renderer = $PAGE->get_renderer('core', null, 'ajax');
+            } else {
+                $renderer = $OUTPUT;
+            }
+            echo $renderer->fatal_error($info->message, $info->moreinfourl, $info->link, $info->backtrace, $info->debuginfo,
                 $info->errorcode);
         } catch (Exception $e) {
             $out_ex = $e;
@@ -897,7 +904,11 @@ function initialise_fullme() {
     // (That is, the Moodle server uses http, with an external box translating everything to https).
     if (empty($CFG->sslproxy)) {
         if ($rurl['scheme'] === 'http' and $wwwroot['scheme'] === 'https') {
-            print_error('sslonlyaccess', 'error');
+            if (defined('REQUIRE_CORRECT_ACCESS') && REQUIRE_CORRECT_ACCESS) {
+                print_error('sslonlyaccess', 'error');
+            } else {
+                redirect($CFG->wwwroot, get_string('wwwrootmismatch', 'error', $CFG->wwwroot), 3);
+            }
         }
     } else {
         if ($wwwroot['scheme'] !== 'https') {
index e197a6d..1439d45 100644 (file)
@@ -295,6 +295,28 @@ function xmldb_assign_upgrade($oldversion) {
 
     // Automatically generated Moodle v3.3.0 release upgrade line.
     // Put any upgrade step following this.
+    if ($oldversion < 2017061200) {
+        // Data fix any assign group override event priorities which may have been accidentally nulled due to a bug on the group
+        // overrides edit form.
+
+        // First, find all assign group override events having null priority (and join their corresponding assign_overrides entry).
+        $sql = "SELECT e.id AS id, o.sortorder AS priority
+                  FROM {assign_overrides} o
+                  JOIN {event} e ON (e.modulename = 'assign' AND o.assignid = e.instance AND e.groupid = o.groupid)
+                 WHERE o.groupid IS NOT NULL AND e.priority IS NULL
+              ORDER BY o.id";
+        $affectedrs = $DB->get_recordset_sql($sql);
+
+        // Now update the event's priority based on the assign_overrides sortorder we found. This uses similar logic to
+        // assign_refresh_events(), except we've restricted the set of assignments and overrides we're dealing with here.
+        foreach ($affectedrs as $record) {
+            $DB->set_field('event', 'priority', $record->priority, ['id' => $record->id]);
+        }
+        $affectedrs->close();
+
+        // Main savepoint reached.
+        upgrade_mod_savepoint(true, 2017061200, 'assign');
+    }
 
     return true;
 }
index db2deae..e6c0f4a 100644 (file)
@@ -200,7 +200,7 @@ class assign_feedback_comments extends assign_feedback_plugin {
         global $DB;
         $feedbackcomment = $this->get_feedback_comments($grade->id);
         $quickgradecomments = optional_param('quickgrade_comments_' . $userid, null, PARAM_RAW);
-        if (!$quickgradecomments) {
+        if (!$quickgradecomments && $quickgradecomments !== '') {
             return true;
         }
         if ($feedbackcomment) {
diff --git a/mod/assign/feedback/comments/tests/behat/feedback_comments.feature b/mod/assign/feedback/comments/tests/behat/feedback_comments.feature
new file mode 100644 (file)
index 0000000..245e53a
--- /dev/null
@@ -0,0 +1,47 @@
+@mod @mod_assign @assignfeedback @assignfeedback_comments
+Feature: In an assignment, teachers can provide feedback comments on student submissions
+  In order to provide feedback to students on their assignments
+  As a teacher,
+  I need to create feedback comments against their submissions.
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 0 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | teacher |
+      | student1 | C1 | student |
+
+  @javascript
+  Scenario: Teachers should be able to add and remove feedback comments via the quick grading interface
+    Given the following "activities" exist:
+      | activity | course | idnumber | name             | assignsubmission_onlinetext_enabled | assignfeedback_comments_enabled |
+      | assign   | C1     | assign1  | Test assignment1 | 1                                   | 1                               |
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment1"
+    And I press "Add submission"
+    And I set the following fields to these values:
+      | Online text | I'm the student1 submission |
+    And I press "Save changes"
+    And I log out
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment1"
+    And I navigate to "View all submissions" in current page administration
+    Then I click on "Quick grading" "checkbox"
+    And I set the field "Feedback comments" to "Feedback from teacher."
+    And I press "Save all quick grading changes"
+    And I should see "The grade changes were saved"
+    And I press "Continue"
+    And I should see "Feedback from teacher."
+    And I set the field "Feedback comments" to ""
+    And I press "Save all quick grading changes"
+    And I should see "The grade changes were saved"
+    And I press "Continue"
+    And I should not see "Feedback from teacher."
index 33caa81..47867bc 100644 (file)
@@ -646,8 +646,9 @@ class assign_grading_table extends table_sql implements renderable {
         static $markers = null;
         static $markerlist = array();
         if ($markers === null) {
-            list($sort, $params) = users_order_by_sql();
-            $markers = get_users_by_capability($this->assignment->get_context(), 'mod/assign:grade', '', $sort);
+            list($sort, $params) = users_order_by_sql('u');
+            // Only enrolled users could be assigned as potential markers.
+            $markers = get_enrolled_users($this->assignment->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
             $markerlist[0] = get_string('choosemarker', 'assign');
             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->assignment->get_context());
             foreach ($markers as $marker) {
index f992d36..a763f37 100644 (file)
@@ -4017,8 +4017,9 @@ class assign {
         // Get markers to use in drop lists.
         $markingallocationoptions = array();
         if ($markingallocation) {
-            list($sort, $params) = users_order_by_sql();
-            $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+            list($sort, $params) = users_order_by_sql('u');
+            // Only enrolled users could be assigned as potential markers.
+            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
             $markingallocationoptions[''] = get_string('filternone', 'assign');
             $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
@@ -4644,8 +4645,9 @@ class assign {
             'usershtml' => $usershtml,
         );
 
-        list($sort, $params) = users_order_by_sql();
-        $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
+        list($sort, $params) = users_order_by_sql('u');
+        // Only enrolled users could be assigned as potential markers.
+        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
         $markerlist = array();
         foreach ($markers as $marker) {
             $markerlist[$marker->id] = fullname($marker);
@@ -6532,8 +6534,9 @@ class assign {
         if ($markingallocation) {
             $markingallocationoptions[''] = get_string('filternone', 'assign');
             $markingallocationoptions[ASSIGN_MARKER_FILTER_NO_MARKER] = get_string('markerfilternomarker', 'assign');
-            list($sort, $params) = users_order_by_sql();
-            $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+            list($sort, $params) = users_order_by_sql('u');
+            // Only enrolled users could be assigned as potential markers.
+            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
             foreach ($markers as $marker) {
                 $markingallocationoptions[$marker->id] = fullname($marker);
             }
@@ -7167,8 +7170,9 @@ class assign {
             $this->get_instance()->markingallocation &&
             has_capability('mod/assign:manageallocations', $this->context)) {
 
-            list($sort, $params) = users_order_by_sql();
-            $markers = get_users_by_capability($this->context, 'mod/assign:grade', '', $sort);
+            list($sort, $params) = users_order_by_sql('u');
+            // Only enrolled users could be assigned as potential markers.
+            $markers = get_enrolled_users($this->context, 'mod/assign:grade', 0, 'u.*', $sort);
             $markerlist = array('' =>  get_string('choosemarker', 'assign'));
             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
             foreach ($markers as $marker) {
@@ -7587,8 +7591,9 @@ class assign {
             'usershtml' => ''   // initialise these parameters with real information.
         );
 
-        list($sort, $params) = users_order_by_sql();
-        $markers = get_users_by_capability($this->get_context(), 'mod/assign:grade', '', $sort);
+        list($sort, $params) = users_order_by_sql('u');
+        // Only enrolled users could be assigned as potential markers.
+        $markers = get_enrolled_users($this->get_context(), 'mod/assign:grade', 0, 'u.*', $sort);
         $markerlist = array();
         foreach ($markers as $marker) {
             $markerlist[$marker->id] = fullname($marker);
index b3cf8d4..0c68b77 100644 (file)
@@ -55,6 +55,9 @@ class assign_override_form extends moodleform {
     /** @var int userid, if provided. */
     protected $userid;
 
+    /** @var int sortorder, if provided. */
+    protected $sortorder;
+
     /**
      * Constructor.
      * @param moodle_url $submiturl the form action URL.
@@ -72,6 +75,7 @@ class assign_override_form extends moodleform {
         $this->groupmode = $groupmode;
         $this->groupid = empty($override->groupid) ? 0 : $override->groupid;
         $this->userid = empty($override->userid) ? 0 : $override->userid;
+        $this->sortorder = empty($override->sortorder) ? null : $override->sortorder;
 
         parent::__construct($submiturl, null, 'post');
 
@@ -97,6 +101,10 @@ class assign_override_form extends moodleform {
                 $mform->addElement('select', 'groupid',
                         get_string('overridegroup', 'assign'), $groupchoices);
                 $mform->freeze('groupid');
+                // Add a sortorder element.
+                $mform->addElement('hidden', 'sortorder', $this->sortorder);
+                $mform->setType('sortorder', PARAM_INT);
+                $mform->freeze('sortorder');
             } else {
                 // Prepare the list of groups.
                 $groups = groups_get_all_groups($cm->course);
diff --git a/mod/assign/tests/markerallocation_test.php b/mod/assign/tests/markerallocation_test.php
new file mode 100644 (file)
index 0000000..09b5591
--- /dev/null
@@ -0,0 +1,148 @@
+<?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/>.
+
+/**
+ * Unit tests for (some of) mod/assign/markerallocaion_test.php.
+ *
+ * @package    mod_assign
+ * @category   test
+ * @copyright  2017 Andrés Melo <andres.torres@blackboard.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/lib/accesslib.php');
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * This class tests some of marker allocation functionality.
+ *
+ * @package    mod_assign
+ * @copyright  2017 Andrés Melo <andres.torres@blackboard.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class mod_assign_markerallocation_testcase extends advanced_testcase {
+
+    /**
+     * Create all the needed elements to test the difference between both functions.
+     */
+    public function test_markerusers() {
+        $this->resetAfterTest();
+        global $DB;
+
+        // Create a course, by default it is created with 5 sections.
+        $this->course = $this->getDataGenerator()->create_course();
+
+        // Setting assing module, markingworkflow and markingallocation set to 1 to enable marker allocation.
+        $record = new stdClass();
+        $record->course = $this->course;
+
+        $modulesettings = array(
+            'alwaysshowdescription'             => 1,
+            'submissiondrafts'                  => 1,
+            'requiresubmissionstatement'        => 0,
+            'sendnotifications'                 => 0,
+            'sendstudentnotifications'          => 1,
+            'sendlatenotifications'             => 0,
+            'duedate'                           => 0,
+            'allowsubmissionsfromdate'          => 0,
+            'grade'                             => 100,
+            'cutoffdate'                        => 0,
+            'teamsubmission'                    => 0,
+            'requireallteammemberssubmit'       => 0,
+            'teamsubmissiongroupingid'          => 0,
+            'blindmarking'                      => 0,
+            'attemptreopenmethod'               => 'none',
+            'maxattempts'                       => -1,
+            'markingworkflow'                   => 1,
+            'markingallocation'                 => 1,
+        );
+
+        $assignelement = $this->getDataGenerator()->create_module('assign', $record, $modulesettings);
+
+        $coursesectionid = course_add_cm_to_section($this->course->id, $assignelement->id, 1);
+
+        // Adding users to the course.
+        $userdata = array();
+        $userdata['firstname'] = 'teacher1';
+        $userdata['lasttname'] = 'lastname_teacher1';
+
+        $user1 = $this->getDataGenerator()->create_user($userdata);
+
+        $this->getDataGenerator()->enrol_user($user1->id, $this->course->id, 'teacher');
+
+        $userdata = array();
+        $userdata['firstname'] = 'teacher2';
+        $userdata['lasttname'] = 'lastname_teacher2';
+
+        $user2 = $this->getDataGenerator()->create_user($userdata);
+
+        $this->getDataGenerator()->enrol_user($user2->id, $this->course->id, 'teacher');
+
+        $userdata = array();
+        $userdata['firstname'] = 'student';
+        $userdata['lasttname'] = 'lastname_student';
+
+        $user3 = $this->getDataGenerator()->create_user($userdata);
+
+        $this->getDataGenerator()->enrol_user($user3->id, $this->course->id, 'student');
+
+        // Adding manager to the system.
+        $userdata = array();
+        $userdata['firstname'] = 'Manager';
+        $userdata['lasttname'] = 'lastname_Manager';
+
+        $user4 = $this->getDataGenerator()->create_user($userdata);
+
+        // Getting id of manager role.
+        $managerrole = $DB->get_record('role', array('shortname' => 'manager'));
+        if (!empty($managerrole)) {
+            // By default the context of the system is assigned.
+            $idassignment = $this->getDataGenerator()->role_assign($managerrole->id, $user4->id);
+        }
+
+        $oldusers = array($user1, $user2, $user4);
+        $newusers = array($user1, $user2);
+
+        list($sort, $params) = users_order_by_sql('u');
+
+        // Old code, it must return 3 users: teacher1, teacher2 and Manger.
+        $oldmarkers = get_users_by_capability(context_course::instance($this->course->id), 'mod/assign:grade', '', $sort);
+        // New code, it must return 2 users: teacher1 and teacher2.
+        $newmarkers = get_enrolled_users(context_course::instance($this->course->id), 'mod/assign:grade', 0, 'u.*', $sort);
+
+        // Test result quantity.
+        $this->assertEquals(count($oldusers), count($oldmarkers));
+        $this->assertEquals(count($newusers), count($newmarkers));
+        $this->assertEquals(count($oldmarkers) > count($newmarkers), true);
+
+        // Elements expected with new code.
+        foreach ($newmarkers as $key => $nm) {
+            $this->assertEquals($nm, $newusers[array_search($nm, $newusers)]);
+        }
+
+        // Elements expected with old code.
+        foreach ($oldusers as $key => $os) {
+            $this->assertEquals($os->id, $oldmarkers[$os->id]->id);
+            unset($oldmarkers[$os->id]);
+        }
+
+        $this->assertEquals(count($oldmarkers), 0);
+
+    }
+}
index d75c94c..2c7e0e0 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version  = 2017051500;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version  = 2017061200;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires = 2017050500;    // Requires this Moodle version.
 $plugin->cron     = 60;
index 4cf34b1..2289840 100644 (file)
@@ -561,7 +561,8 @@ class mod_feedback_completion extends mod_feedback_structure {
 
         // Update completion state.
         $completion = new completion_info($this->cm->get_course());
-        if (isloggedin() && !isguestuser() && $completion->is_enabled($this->cm) && $this->feedback->completionsubmit) {
+        if (isloggedin() && !isguestuser() && $completion->is_enabled($this->cm) &&
+                $this->cm->completion == COMPLETION_TRACKING_AUTOMATIC && $this->feedback->completionsubmit) {
             $completion->update_state($this->cm, COMPLETION_COMPLETE);
         }
     }
index faa58c0..2d89f59 100644 (file)
@@ -1149,6 +1149,8 @@ class mod_feedback_external extends external_api {
 
         $feedbackstructure = new mod_feedback_structure($feedback, $cm, $course->id);
         $responsestable = new mod_feedback_responses_table($feedbackstructure, $groupid);
+        // Ensure responses number is correct prior returning them.
+        $feedbackstructure->shuffle_anonym_responses();
         $anonresponsestable = new mod_feedback_responses_anon_table($feedbackstructure, $groupid);
 
         $result = array(
index 5fdca5a..d242b19 100644 (file)
@@ -1372,11 +1372,12 @@ function feedback_items_from_template($feedback, $templateid, $deleteold = false
             if ($completeds = $DB->get_records('feedback_completed', $params)) {
                 $completion = new completion_info($course);
                 foreach ($completeds as $completed) {
+                    $DB->delete_records('feedback_completed', array('id' => $completed->id));
                     // Update completion state
-                    if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+                    if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC &&
+                            $feedback->completionsubmit) {
                         $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
                     }
-                    $DB->delete_records('feedback_completed', array('id'=>$completed->id));
                 }
             }
             $DB->delete_records('feedback_completedtmp', array('feedback'=>$feedback->id));
@@ -1730,11 +1731,12 @@ function feedback_delete_all_items($feedbackid) {
     if ($completeds = $DB->get_records('feedback_completed', array('feedback'=>$feedback->id))) {
         $completion = new completion_info($course);
         foreach ($completeds as $completed) {
+            $DB->delete_records('feedback_completed', array('id' => $completed->id));
             // Update completion state
-            if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+            if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC &&
+                    $feedback->completionsubmit) {
                 $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
             }
-            $DB->delete_records('feedback_completed', array('id'=>$completed->id));
         }
     }
 
@@ -2759,14 +2761,14 @@ function feedback_delete_completed($completed, $feedback = null, $cm = null, $co
     //first we delete all related values
     $DB->delete_records('feedback_value', array('completed' => $completed->id));
 
+    // Delete the completed record.
+    $return = $DB->delete_records('feedback_completed', array('id' => $completed->id));
+
     // Update completion state
     $completion = new completion_info($course);
-    if ($completion->is_enabled($cm) && $feedback->completionsubmit) {
+    if ($completion->is_enabled($cm) && $cm->completion == COMPLETION_TRACKING_AUTOMATIC && $feedback->completionsubmit) {
         $completion->update_state($cm, COMPLETION_INCOMPLETE, $completed->userid);
     }
-    // Last we delete the completed-record.
-    $return = $DB->delete_records('feedback_completed', array('id' => $completed->id));
-
     // Trigger event for the delete action we performed.
     $event = \mod_feedback\event\response_deleted::create_from_record($completed, $cm, $feedback);
     $event->trigger();
index 7763d6e..1e53c1c 100644 (file)
@@ -1814,6 +1814,10 @@ class mod_lesson_external extends external_api {
         }
 
         list($answerpages, $userstats) = lesson_get_user_detailed_report_data($lesson, $userid, $params['lessonattempt']);
+        // Convert page object to page record.
+        foreach ($answerpages as $answerp) {
+            $answerp->page = self::get_page_fields($answerp->page);
+        }
 
         $result = array(
             'answerpages' => $answerpages,
@@ -1835,6 +1839,7 @@ class mod_lesson_external extends external_api {
                 'answerpages' => new external_multiple_structure(
                     new external_single_structure(
                         array(
+                            'page' => self::get_page_structure(VALUE_OPTIONAL),
                             'title' => new external_value(PARAM_RAW, 'Page title.'),
                             'contents' => new external_value(PARAM_RAW, 'Page contents.'),
                             'qtype' => new external_value(PARAM_TEXT, 'Identifies the page type of this page.'),
index 8c32a2d..e0b143b 100644 (file)
@@ -1078,6 +1078,8 @@ function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt)
     while ($pageid != 0) { // EOL
         $page = $lessonpages[$pageid];
         $answerpage = new stdClass;
+        // Keep the original page object.
+        $answerpage->page = $page;
         $data ='';
 
         $answerdata = new stdClass;
index 3131639..64c5be9 100644 (file)
@@ -1217,6 +1217,14 @@ class mod_lesson_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals(1, $result['userstats']['gradeinfo']['total']);     // Total correct answers.
         $this->assertEquals(100, $result['userstats']['gradeinfo']['grade']);   // Correct answer.
 
+        // Check page object contains the lesson pages answered.
+        $pagesanswered = array();
+        foreach ($result['answerpages'] as $answerp) {
+            $pagesanswered[] = $answerp['page']['id'];
+        }
+        sort($pagesanswered);
+        $this->assertEquals(array($this->page1->id, $this->page2->id), $pagesanswered);
+
         // Test second attempt unfinished.
         $result = mod_lesson_external::get_user_attempt($this->lesson->id, $this->student->id, 1);
         $result = external_api::clean_returnvalue(mod_lesson_external::get_user_attempt_returns(), $result);
index 79cc01a..9372654 100644 (file)
@@ -1,5 +1,9 @@
 This files describes API changes in the lesson code.
 
+=== 3.4 ===
+
+* External function mod_lesson_external::get_user_attempt() now returns the full page object inside each answerpages.
+
 === 3.3 ===
 
 * lesson::callback_on_view() has an additional optional parameter $redirect default to true.
index c4a7c0f..d4bfce3 100644 (file)
@@ -1633,6 +1633,11 @@ function mod_scorm_core_calendar_provide_event_action(calendar_event $event,
 
     $cm = get_fast_modinfo($event->courseid)->instances['scorm'][$event->instance];
 
+    if (has_capability('mod/scorm:viewreport', $cm->context)) {
+        // Teachers do not need to be reminded to complete a scorm.
+        return null;
+    }
+
     if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
         // The scorm has closed so the user can no longer submit anything.
         return null;
index cd33822..0fcc38e 100644 (file)
@@ -207,6 +207,9 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         // Create a calendar event.
         $event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
 
+        // Only students see scorm events.
+        $this->setUser($this->student);
+
         // Create an action factory.
         $factory = new \core_calendar\action_factory();
 
@@ -261,6 +264,9 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         // Create a calendar event.
         $event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
 
+        // Only students see scorm events.
+        $this->setUser($this->student);
+
         // Create an action factory.
         $factory = new \core_calendar\action_factory();
 
@@ -289,6 +295,9 @@ class mod_scorm_lib_testcase extends externallib_advanced_testcase {
         // Create a calendar event.
         $event = $this->create_action_event($course->id, $scorm->id, SCORM_EVENT_TYPE_OPEN);
 
+        // Only students see scorm events.
+        $this->setUser($this->student);
+
         // Create an action factory.
         $factory = new \core_calendar\action_factory();
 
diff --git a/question/type/multichoice/classes/admin_setting_answernumbering.php b/question/type/multichoice/classes/admin_setting_answernumbering.php
new file mode 100644 (file)
index 0000000..80de03a
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * Admin settings for the multichoice question type.
+ *
+ * @package   qtype_multichoice
+ * @copyright  2015 onwards Nadav Kavalerchik
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Admin settings class for the multichoice question type method.
+ *
+ * Just so we can lazy-load the numbering style choices.
+ *
+ * @copyright  2015 onwards Nadav Kavalerchik
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qtype_multichoice_admin_setting_answernumbering extends admin_setting_configselect {
+
+    /**
+     * This function may be used in ancestors for lazy loading of choices
+     *
+     * Override this method if loading of choices is expensive, such
+     * as when it requires multiple db requests.
+     *
+     * @return bool true if loaded, false if error
+     */
+    public function load_choices() {
+        global $CFG;
+
+        if (is_array($this->choices)) {
+            return true;
+        }
+
+        require_once($CFG->dirroot . '/question/type/multichoice/questiontype.php');
+        $this->choices = qtype_multichoice::get_numbering_styles();
+
+        return true;
+    }
+}
index b0dc614..372dfca 100644 (file)
@@ -46,17 +46,17 @@ class qtype_multichoice_edit_form extends question_edit_form {
         );
         $mform->addElement('select', 'single',
                 get_string('answerhowmany', 'qtype_multichoice'), $menu);
-        $mform->setDefault('single', 1);
+        $mform->setDefault('single', get_config('qtype_multichoice', 'answerhowmany'));
 
         $mform->addElement('advcheckbox', 'shuffleanswers',
                 get_string('shuffleanswers', 'qtype_multichoice'), null, null, array(0, 1));
         $mform->addHelpButton('shuffleanswers', 'shuffleanswers', 'qtype_multichoice');
-        $mform->setDefault('shuffleanswers', 1);
+        $mform->setDefault('shuffleanswers', get_config('qtype_multichoice', 'shuffleanswers'));
 
         $mform->addElement('select', 'answernumbering',
                 get_string('answernumbering', 'qtype_multichoice'),
                 qtype_multichoice::get_numbering_styles());
-        $mform->setDefault('answernumbering', 'abc');
+        $mform->setDefault('answernumbering', get_config('qtype_multichoice', 'answernumbering'));
 
         $this->add_per_answer_fields($mform, get_string('choiceno', 'qtype_multichoice', '{no}'),
                 question_bank::fraction_options_full(), max(5, QUESTION_NUMANS_START));
index 7305552..18aacbd 100644 (file)
@@ -24,6 +24,7 @@
  */
 
 $string['answerhowmany'] = 'One or multiple answers?';
+$string['answerhowmany_desc'] = 'Should the default for new multichoice questions be to require single or multiple answers?';
 $string['answernumbering'] = 'Number the choices?';
 $string['answernumbering123'] = '1., 2., 3., ...';
 $string['answernumberingabc'] = 'a., b., c., ...';
@@ -31,6 +32,7 @@ $string['answernumberingABCD'] = 'A., B., C., ...';
 $string['answernumberingiii'] = 'i., ii., iii., ...';
 $string['answernumberingIIII'] = 'I., II., III., ...';
 $string['answernumberingnone'] = 'No numbering';
+$string['answernumbering_desc'] = 'Set the default numbering style for new multichoice questions.';
 $string['answersingleno'] = 'Multiple answers allowed';
 $string['answersingleyes'] = 'One answer only';
 $string['choiceno'] = 'Choice {$a}';
@@ -65,6 +67,7 @@ $string['pluginnamesummary'] = 'Allows the selection of a single or multiple res
 $string['selectmulti'] = 'Select one or more:';
 $string['selectone'] = 'Select one:';
 $string['shuffleanswers'] = 'Shuffle the choices?';
+$string['shuffleanswers_desc'] = 'Should the default for new nultichoice questions be to shuffle answers?';
 $string['shuffleanswers_help'] = 'If enabled, the order of the answers is randomly shuffled for each attempt, provided that "Shuffle within questions" in the activity settings is also enabled.';
 $string['singleanswer'] = 'Choose one answer.';
 $string['toomanyselected'] = 'You have selected too many options.';
index f333c1d..edc54a4 100644 (file)
@@ -93,11 +93,11 @@ abstract class qtype_multichoice_renderer_base extends qtype_with_combined_feedb
             }
             $radiobuttons[] = $hidden . html_writer::empty_tag('input', $inputattributes) .
                     html_writer::tag('label',
-                        $this->number_in_style($value, $question->answernumbering) .
+                        html_writer::span($this->number_in_style($value, $question->answernumbering), 'answernumber') .
                         $question->make_html_inline($question->format_text(
                                 $ans->answer, $ans->answerformat,
                                 $qa, 'question', 'answer', $ansid)),
-                    array('for' => $inputattributes['id'], 'class' => 'm-l-1'));
+                        array('for' => $inputattributes['id'], 'class' => 'm-l-1'));
 
             // Param $options->suppresschoicefeedback is a hack specific to the
             // oumultiresponse question type. It would be good to refactor to
diff --git a/question/type/multichoice/settings.php b/question/type/multichoice/settings.php
new file mode 100644 (file)
index 0000000..83697b3
--- /dev/null
@@ -0,0 +1,44 @@
+<?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/>.
+
+/**
+ * Admin settings for the multichoice question type.
+ *
+ * @package   qtype_multichoice
+ * @copyright  2015 onwards Nadav Kavalerchik
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+if ($ADMIN->fulltree) {
+    $menu = array(
+        new lang_string('answersingleno', 'qtype_multichoice'),
+        new lang_string('answersingleyes', 'qtype_multichoice'),
+    );
+    $settings->add(new admin_setting_configselect('qtype_multichoice/answerhowmany',
+    new lang_string('answerhowmany', 'qtype_multichoice'),
+    new lang_string('answerhowmany_desc', 'qtype_multichoice'), '1', $menu));
+
+    $settings->add(new admin_setting_configcheckbox('qtype_multichoice/shuffleanswers',
+    new lang_string('shuffleanswers', 'qtype_multichoice'),
+    new lang_string('shuffleanswers_desc', 'qtype_multichoice'), '1'));
+
+    $settings->add(new qtype_multichoice_admin_setting_answernumbering('qtype_multichoice/answernumbering',
+    new lang_string('answernumbering', 'qtype_multichoice'),
+    new lang_string('answernumbering_desc', 'qtype_multichoice'), 'abc', null ));
+
+}
index 7403e67..75b91b6 100644 (file)
@@ -264,7 +264,8 @@ function report_stats_report($course, $report, $mode, $user, $roleid, $time) {
                 // bad luck, we can not link other report
             } else if (empty($param->crosstab)) {
                 foreach  ($stats as $stat) {
-                    $a = array(userdate($stat->timeend-(60*60*24),get_string('strftimedate'),$CFG->timezone),$stat->line1);
+                    $a = array(userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone),
+                            $stat->line1);
                     if (isset($stat->line2)) {
                         $a[] = $stat->line2;
                     }
@@ -302,7 +303,8 @@ function report_stats_report($course, $report, $mode, $user, $roleid, $time) {
                         }
                     }
                     if (!array_key_exists($stat->timeend,$times)) {
-                        $times[$stat->timeend] = userdate($stat->timeend,get_string('strftimedate'),$CFG->timezone);
+                        $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'),
+                                $CFG->timezone);
                     }
                 }
 
@@ -395,7 +397,7 @@ function report_stats_print_chart($courseid, $report, $time, $mode, $userid = 0,
         foreach ($stats as $stat) {
             // Build the array of formatted times indexed by timestamp used as labels.
             if (!array_key_exists($stat->timeend, $times)) {
-                $times[$stat->timeend] = userdate($stat->timeend, get_string('strftimedate'), $CFG->timezone);
+                $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone);
 
                 // Just add the data if the time hasn't been added yet.
                 // The number of lines of data must match the number of labels.
@@ -436,7 +438,7 @@ function report_stats_print_chart($courseid, $report, $time, $mode, $userid = 0,
 
             // Build the array of formatted times indexed by timestamp used as labels.
             if (!array_key_exists($stat->timeend, $times)) {
-                $times[$stat->timeend] = userdate($stat->timeend, get_string('strftimedate'), $CFG->timezone);
+                $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone);
             }
         }
         // Fill empty days with zero to avoid chart errors.
index e36c2b4..2d5a5c8 100644 (file)
@@ -156,7 +156,7 @@ foreach ($stats as $stat) {
     if (!empty($stat->zerofixed)) {  // Don't know why this is necessary, see stats_fix_zeros above - MD
         continue;
     }
-    $a = array(userdate($stat->timeend,get_string('strftimedate'),$CFG->timezone),$stat->line1);
+    $a = array(userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone), $stat->line1);
     $a[] = $stat->line2;
     $a[] = $stat->line3;
     $table->data[] = $a;
index bf9bf9f..22526dc 100644 (file)
@@ -22,7 +22,7 @@
     <div class="container-fluid navbar-nav">
 
         <div data-region="drawer-toggle">
-            <button aria-expanded="{{#navdraweropen}}true{{/navdraweropen}}{{^navdraweropen}}false{{/navdraweropen}}" aria-controls="nav-drawer" type="button" class="btn pull-xs-left m-r-1 btn-secondary" data-action="toggle-drawer" data-side="left" data-preference="drawer-open-nav"><span aria-hidden="true">&#9776;</span><span class="sr-only">{{#str}}expand, core{{/str}}</span></button>
+            <button aria-expanded="{{#navdraweropen}}true{{/navdraweropen}}{{^navdraweropen}}false{{/navdraweropen}}" aria-controls="nav-drawer" type="button" class="btn pull-xs-left m-r-1 btn-secondary" data-action="toggle-drawer" data-side="left" data-preference="drawer-open-nav"><span aria-hidden="true">&#9776;</span><span class="sr-only">{{#str}}sidepanel, core{{/str}}</span></button>
         </div>
 
         <a href="{{{ config.wwwroot }}}" class="navbar-brand {{# output.should_display_navbar_logo }}has-logo{{/ output.should_display_navbar_logo }}
index ff09b25..b60d915 100644 (file)
@@ -130,8 +130,14 @@ class theme_clean_core_renderer extends theme_bootstrapbase_core_renderer {
      * @return moodle_url|false
      */
     public function get_logo_url($maxwidth = null, $maxheight = 100) {
+        global $CFG;
+
         if (!empty($this->page->theme->settings->logo)) {
-            return $this->page->theme->setting_file_url('logo', 'logo');
+            $url = $this->page->theme->setting_file_url('logo', 'logo');
+            // Get a URL suitable for moodle_url.
+            $relativebaseurl = preg_replace('|^https?://|i', '//', $CFG->wwwroot);
+            $url = str_replace($relativebaseurl, '', $url);
+            return new moodle_url($url);
         }
         return parent::get_logo_url($maxwidth, $maxheight);
     }
@@ -146,8 +152,14 @@ class theme_clean_core_renderer extends theme_bootstrapbase_core_renderer {
      * @return moodle_url|false
      */
     public function get_compact_logo_url($maxwidth = 100, $maxheight = 100) {
+        global $CFG;
+
         if (!empty($this->page->theme->settings->smalllogo)) {
-            return $this->page->theme->setting_file_url('smalllogo', 'smalllogo');
+            $url = $this->page->theme->setting_file_url('smalllogo', 'smalllogo');
+            // Get a URL suitable for moodle_url.
+            $relativebaseurl = preg_replace('|^https?://|i', '//', $CFG->wwwroot);
+            $url = str_replace($relativebaseurl, '', $url);
+            return new moodle_url($url);
         }
         return parent::get_compact_logo_url($maxwidth, $maxheight);
     }
index 2fe8ec1..fa6b0e9 100644 (file)
@@ -149,25 +149,26 @@ if ($type === 'editor') {
     css_store_css($theme, "$candidatedir/editor.css", $csscontent, false);
 
 } else {
+    // Fetch a lock whilst the CSS is fetched as this can be slow and CPU intensive.
+    // Each client should wait for one to finish the compilation before starting the compiler.
+    $lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
+    $lock = $lockfactory->get_lock($themename, rand(90, 120));
+
+    if (file_exists($candidatesheet)) {
+        // The file was built while we waited for the lock, we release the lock and serve the file.
+        if ($lock) {
+            $lock->release();
+        }
 
-    $lock = null;
-    // Lock system to prevent concurrent requests to compile LESS/SCSS, which is really slow and CPU intensive.
-    // Each client should wait for one to finish the compilation before starting a new compiling process.
-    // We only do this when the file will be cached...
-    if (in_array($type, ['less', 'scss']) && $cache) {
-        $lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
-        // We wait for the lock to be acquired, the timeout does not need to be strict here.
-        $lock = $lockfactory->get_lock($themename, rand(15, 30));
-        if (file_exists($candidatesheet)) {
-            // The file was built while we waited for the lock, we release the lock and serve the file.
-            if ($lock) {
-                $lock->release();
-            }
+        if ($cache) {
             css_send_cached_css($candidatesheet, $etag);
+        } else {
+            css_send_uncached_css(file_get_contents($candidatesheet));
         }
     }
 
-    // Older IEs require smaller chunks.
+    // The lock is still held, and the sheet still does not exist.
+    // Compile the CSS content.
     $csscontent = $theme->get_css_content();
 
     $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot);
@@ -187,8 +188,9 @@ if ($type === 'editor') {
 
     css_store_css($theme, "$candidatedir/$type.css", $csscontent, true, $chunkurl);
 
-    // Release the lock.
     if ($lock) {
+        // Now that the CSS has been generated and/or stored, release the lock.
+        // This will allow waiting clients to use the newly generated and stored CSS.
         $lock->release();
     }
 }
index d64d48d..73e86bb 100644 (file)
@@ -581,6 +581,7 @@ class core_userliblib_testcase extends advanced_testcase {
         // Visitor (Not a guest user, userid=0).
         $CFG->forceloginforprofiles = 1;
         $this->setUser($user8);
+        $this->assertFalse(user_can_view_profile($user1));
 
         $allroles = $DB->get_records_menu('role', array(), 'id', 'archetype, id');
         // Let us test with guest user.
@@ -591,7 +592,8 @@ class core_userliblib_testcase extends advanced_testcase {
         }
 
         // Even with cap, still guests should not be allowed in.
-        assign_capability('moodle/user:viewdetails', CAP_ALLOW, $allroles['guest'], context_system::instance()->id, true);
+        $guestrole = $DB->get_records_menu('role', array('shortname' => 'guest'), 'id', 'archetype, id');
+        assign_capability('moodle/user:viewdetails', CAP_ALLOW, $guestrole['guest'], context_system::instance()->id, true);
         reload_all_capabilities();
         foreach ($users as $user) {
             $this->assertFalse(user_can_view_profile($user));
index e90db34..b36d5e2 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017060800.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017061301.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.