Merge branch 'wip-mdl-48117' of https://github.com/rajeshtaneja/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 19 Oct 2015 11:33:10 +0000 (13:33 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 19 Oct 2015 14:59:05 +0000 (16:59 +0200)
18 files changed:
blocks/news_items/block_news_items.php
grade/report/user/index.php
grade/report/user/styles.css
grade/report/user/tests/behat/view_usereport.feature [new file with mode: 0644]
lib/amd/src/form-autocomplete.js
lib/myprofilelib.php
lib/setuplib.php
lib/templates/form_autocomplete_selection.mustache
message/lib.php
message/tests/events_test.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/tests/behat/submit_without_group.feature
mod/forum/lib.php
mod/forum/tests/lib_test.php
question/engine/datalib.php
theme/image.php

index 092ae4b..de57a2d 100644 (file)
@@ -89,7 +89,12 @@ class block_news_items extends block_base {
 
         /// Get all the recent discussions we're allowed to see
 
-            if (! $discussions = forum_get_discussions($cm, 'p.modified DESC', false,
+            // This block displays the most recent posts in a forum in
+            // descending order. The call to default sort order here will use
+            // that unless the discussion that post is in has a timestart set
+            // in the future.
+            $sort = forum_get_default_sort_order(true, 'p.modified');
+            if (! $discussions = forum_get_discussions($cm, $sort, false,
                                                        $currentgroup, $this->page->course->newsitems) ) {
                 $text .= '('.get_string('nonews', 'forum').')';
                 $this->content->text = $text;
index 3136c56..149879c 100644 (file)
@@ -166,7 +166,12 @@ if (has_capability('moodle/grade:viewall', $context)) { //Teachers will see all
     }
 }
 
-// Trigger report viewed event.
-$report->viewed();
+if (isset($report)) {
+    // Trigger report viewed event.
+    $report->viewed();
+} else {
+    echo html_writer::tag('div', '', array('class' => 'clearfix'));
+    echo $OUTPUT->notification(get_string('nostudentsyet'));
+}
 
 echo $OUTPUT->footer();
index 7695995..64ee46a 100644 (file)
@@ -1,5 +1,6 @@
 .path-grade-report-user #graded_users_selector {
     float: right;
+    margin-bottom: 5px;
 }
 .path-grade-report-user #graded_users_selector .singleselect label {
     display: inline-block;
diff --git a/grade/report/user/tests/behat/view_usereport.feature b/grade/report/user/tests/behat/view_usereport.feature
new file mode 100644 (file)
index 0000000..b4051b8
--- /dev/null
@@ -0,0 +1,20 @@
+@core @core_grades @gradereport_user
+Feature: We can use the user report
+  As a user
+  I browse to the User report
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+
+    Scenario: Verify we can view a user grade report with no users enrolled.
+      Given I log in as "admin"
+      And I am on site homepage
+      And I follow "Course 1"
+      And I navigate to "Grades" node in "Course administration"
+      And I select "User report" from the "Grade report" singleselect
+      And I press "Go"
+      And I select "All users (0)" from the "Select all or one user" singleselect
+      And I click on "Go" "button" in the "#choosegradeuser" "css_element"
+      Then I should see "No students enrolled in this course yet"
index 47f0aaa..599a3af 100644 (file)
@@ -73,36 +73,28 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
     };
 
     /**
-     * Remove the current item from the list of selected things.
+     * Remove the given item from the list of selected things.
      *
-     * @method deselectCurrentSelection
+     * @method deselectItem
      * @private
      * @param {String} inputId The id of the input element for this instance of the autocomplete.
-     * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
      * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
+     * @param {Element} The item to be deselected.
      * @param {Element} originalSelect The original select list.
      * @param {Boolean} multiple Is this a multi select.
-     * @param {Boolean} tags Is this a tags select.
      */
-    var deselectCurrentSelection = function(inputId, suggestionsId, selectionId, originalSelect, multiple, tags) {
-        var selectionElement = $(document.getElementById(selectionId));
-        var selectedItemValue = selectionElement.children('[data-active-selection=true]').attr('data-value');
-        // The select will either be a single or multi select, so the following will either
-        // select one or more items correctly.
-        // Take care to use 'prop' and not 'attr' for selected properties.
-        // If only one can be selected at a time, start by deselecting everything.
-        if (!multiple) {
-            originalSelect.children('option').prop('selected', false);
-        }
-        // Look for a match, and toggle the selected property if there is a match.
-        originalSelect.children('option').each(function(index, ele) {
-            if ($(ele).attr('value') == selectedItemValue) {
-                $(ele).prop('selected', false);
-                if (tags) {
-                    $(ele).remove();
+    var deselectItem = function(inputId, selectionId, item, originalSelect, multiple) {
+        var selectedItemValue = $(item).attr('data-value');
+
+        // We can only deselect items if this is a multi-select field.
+        if (multiple) {
+            // Look for a match, and toggle the selected property if there is a match.
+            originalSelect.children('option').each(function(index, ele) {
+                if ($(ele).attr('value') == selectedItemValue) {
+                    $(ele).prop('selected', false);
                 }
-            }
-        });
+            });
+        }
         // Rerender the selection list.
         updateSelectionList(selectionId, inputId, originalSelect, multiple);
     };
@@ -612,20 +604,10 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         var selectionElement = $(document.getElementById(selectionId));
         // Handle clicks on the selected items (will unselect an item).
         selectionElement.parent().on('click', '[role=listitem]', function(e) {
-            var value = $(e.currentTarget).attr('data-value');
-
-            // Only allow deselect if we allow multiple selections.
-            if (multiple) {
-                // Find the matching element and deselect it.
-                originalSelect.children('option').each(function(index, ele) {
-                    if ($(ele).attr('value') == value) {
-                        $(ele).prop('selected', !$(ele).prop('selected'));
-                    }
-                });
-            }
-
-            // Re-render the selection list.
-            updateSelectionList(selectionId, inputId, originalSelect, multiple);
+            // Get the item that was clicked.
+            var item = $(e.currentTarget);
+            // Remove it from the selection.
+            deselectItem(inputId, selectionId, item, originalSelect, multiple);
         });
         // Keyboard navigation for the selection list.
         selectionElement.parent().on('keydown', function(e) {
@@ -644,10 +626,14 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
                     return false;
                 case KEYS.SPACE:
                 case KEYS.ENTER:
-                    // Unselect this item.
-                    deselectCurrentSelection(inputId, suggestionsId, selectionId, originalSelect, multiple, tags);
-                    // We handled this event, so prevent it.
-                    e.preventDefault();
+                    // Get the item that is currently selected.
+                    var selectedItem = $(document.getElementById(selectionId)).children('[data-active-selection=true]');
+                    if (selectedItem) {
+                        // Unselect this item.
+                        deselectItem(inputId, selectionId, selectedItem, originalSelect, multiple);
+                        // We handled this event, so prevent it.
+                        e.preventDefault();
+                    }
                     return false;
             }
             return true;
index d04ac8c..6b4f719 100644 (file)
@@ -39,7 +39,8 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
 
     $usercontext = context_user::instance($user->id, MUST_EXIST);
     $systemcontext = context_system::instance();
-    $context = !empty($course) ? context_course::instance($course->id) : $systemcontext;
+    $courseorusercontext = !empty($course) ? context_course::instance($course->id) : $usercontext;
+    $courseorsystemcontext = !empty($course) ? context_course::instance($course->id) : $systemcontext;
     $courseid = !empty($course) ? $course->id : SITEID;
 
     $contactcategory = new core_user\output\myprofile\category('contact', get_string('userdetails'));
@@ -62,7 +63,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
     // Full profile node.
     if (!empty($course)) {
         if (empty($CFG->forceloginforprofiles) || $iscurrentuser ||
-            has_capability('moodle/user:viewdetails', context_user::instance($user->id))
+            has_capability('moodle/user:viewdetails', $usercontext)
             || has_coursecontact_role($user->id)) {
             $url = new moodle_url('/user/profile.php', array('id' => $user->id));
             $node = new core_user\output\myprofile\node('miscellaneous', 'fullprofile', get_string('fullprofile'), null, $url);
@@ -113,7 +114,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
     // Login as ...
     if (!$user->deleted && !$iscurrentuser &&
                 !\core\session\manager::is_loggedinas() && has_capability('moodle/user:loginas',
-                $context) && !is_siteadmin($user->id)) {
+                $courseorsystemcontext) && !is_siteadmin($user->id)) {
         $url = new moodle_url('/course/loginas.php',
                 array('id' => $courseid, 'user' => $user->id, 'sesskey' => sesskey()));
         $node = new  core_user\output\myprofile\node('administration', 'loginas', get_string('loginas'), null, $url);
@@ -121,12 +122,12 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
     }
 
     // Contact details.
-    if (has_capability('moodle/user:viewhiddendetails', $usercontext)) {
+    if (has_capability('moodle/user:viewhiddendetails', $courseorusercontext)) {
         $hiddenfields = array();
     } else {
         $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
     }
-    if (has_capability('moodle/site:viewuseridentity', $context)) {
+    if (has_capability('moodle/site:viewuseridentity', $courseorusercontext)) {
         $identityfields = array_flip(explode(',', $CFG->showuseridentity));
     } else {
         $identityfields = array();
@@ -152,7 +153,8 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
 
     if (isset($identityfields['email']) and ($iscurrentuser
                                              or $user->maildisplay == 1
-                                             or has_capability('moodle/course:useremail', $usercontext)
+                                             or has_capability('moodle/course:useremail', $courseorusercontext)
+                                             or has_capability('moodle/site:viewuseridentity', $courseorusercontext)
                                              or ($user->maildisplay == 2 and enrol_sharing_course($user, $USER)))) {
         $node = new core_user\output\myprofile\node('contact', 'email', get_string('email'), null, null,
             obfuscate_mailto($user->email, ''));
@@ -284,7 +286,7 @@ function core_myprofile_navigation(core_user\output\myprofile\tree $tree, $user,
 
         // Show groups this user is in.
         if (!isset($hiddenfields['groups']) && !empty($course)) {
-            $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
+            $accessallgroups = has_capability('moodle/site:accessallgroups', $courseorsystemcontext);
             if ($usergroups = groups_get_all_groups($course->id, $user->id)) {
                 $groupstr = '';
                 foreach ($usergroups as $group) {
index 4b165b5..145c4a4 100644 (file)
@@ -951,6 +951,29 @@ function setup_get_remote_url() {
         //Apache server
         $rurl['fullpath'] = $_SERVER['REQUEST_URI'];
 
+        // Fixing a known issue with:
+        // - Apache versions lesser than 2.4.11
+        // - PHP deployed in Apache as PHP-FPM via mod_proxy_fcgi
+        // - PHP versions lesser than 5.6.3 and 5.5.18.
+        if (isset($_SERVER['PATH_INFO']) && (php_sapi_name() === 'fpm-fcgi') && isset($_SERVER['SCRIPT_NAME'])) {
+            $pathinfodec = rawurldecode($_SERVER['PATH_INFO']);
+            $lenneedle = strlen($pathinfodec);
+            // Checks whether SCRIPT_NAME ends with PATH_INFO, URL-decoded.
+            if (substr($_SERVER['SCRIPT_NAME'], -$lenneedle) === $pathinfodec) {
+                // This is the "Apache 2.4.10- running PHP-FPM via mod_proxy_fcgi" fingerprint,
+                // at least on CentOS 7 (Apache/2.4.6 PHP/5.4.16) and Ubuntu 14.04 (Apache/2.4.7 PHP/5.5.9)
+                // => SCRIPT_NAME contains 'slash arguments' data too, which is wrongly exposed via PATH_INFO as URL-encoded.
+                // Fix both $_SERVER['PATH_INFO'] and $_SERVER['SCRIPT_NAME'].
+                $lenhaystack = strlen($_SERVER['SCRIPT_NAME']);
+                $pos = $lenhaystack - $lenneedle;
+                // Here $pos is greater than 0 but let's double check it.
+                if ($pos > 0) {
+                    $_SERVER['PATH_INFO'] = $pathinfodec;
+                    $_SERVER['SCRIPT_NAME'] = substr($_SERVER['SCRIPT_NAME'], 0, $pos);
+                }
+            }
+        }
+
     } else if (stripos($_SERVER['SERVER_SOFTWARE'], 'iis') !== false) {
         //IIS - needs a lot of tweaking to make it work
         $rurl['fullpath'] = $_SERVER['SCRIPT_NAME'];
index 77a699e..5d907e9 100644 (file)
@@ -39,7 +39,9 @@
 <div class="form-autocomplete-selection {{#multiple}}form-autocomplete-multiple{{/multiple}}" id="{{selectionId}}" role="list" aria-atomic="true" tabindex="0" aria-multiselectable="true">
 <span class="accesshide">{{#str}}selecteditems, form{{/str}}</span>
     {{#items}}
-        <span role="listitem" data-value="{{value}}" aria-selected="true" class="label label-info"><span aria-hidden="true">×</span> {{{label}}}</span>
+        <span role="listitem" data-value="{{value}}" aria-selected="true" class="label label-info">
+            {{#multiple}}<span aria-hidden="true">× </span>{{/multiple}}{{{label}}}
+        </span>
     {{/items}}
     {{^items}}
         <span>{{#str}}noselection,form{{/str}}</span>
index 09c834f..d384496 100644 (file)
@@ -1038,7 +1038,7 @@ function message_add_contact($contactid, $blocked=0) {
     // Check if a record already exists as we may be changing blocking status.
     if (($contact = $DB->get_record('message_contacts', array('userid' => $USER->id, 'contactid' => $contactid))) !== false) {
         // Check if blocking status has been changed.
-        if ($contact->blocked !== $blocked) {
+        if ($contact->blocked != $blocked) {
             $contact->blocked = $blocked;
             $DB->update_record('message_contacts', $contact);
 
index cd92c44..5a6843d 100644 (file)
@@ -119,6 +119,15 @@ class core_message_events_testcase extends advanced_testcase {
         $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
         $this->assertEquals($url, $event->get_url());
 
+        // Make sure that the contact blocked event is not triggered again.
+        $sink->clear();
+        message_block_contact($user->id);
+        $events = $sink->get_events();
+        $event = reset($events);
+        $this->assertEmpty($event);
+        // Make sure that we still have 1 blocked user.
+        $this->assertEquals(1, message_count_blocked_users());
+
         // Now blocking a user that is not a contact.
         $sink->clear();
         message_block_contact($user2->id);
@@ -147,6 +156,11 @@ class core_message_events_testcase extends advanced_testcase {
         // Add the user to the admin's contact list.
         message_add_contact($user->id);
 
+        // Block the user.
+        message_block_contact($user->id);
+        // Make sure that we have 1 blocked user.
+        $this->assertEquals(1, message_count_blocked_users());
+
         // Trigger and capture the event when unblocking a contact.
         $sink = $this->redirectEvents();
         message_unblock_contact($user->id);
@@ -160,6 +174,19 @@ class core_message_events_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($expected, $event);
         $url = new moodle_url('/message/index.php', array('user1' => $event->userid, 'user2' => $event->relateduserid));
         $this->assertEquals($url, $event->get_url());
+
+        // Make sure that we have no blocked users.
+        $this->assertEmpty(message_count_blocked_users());
+
+        // Make sure that the contact unblocked event is not triggered again.
+        $sink->clear();
+        message_unblock_contact($user->id);
+        $events = $sink->get_events();
+        $event = reset($events);
+        $this->assertEmpty($event);
+
+        // Make sure that we still have no blocked users.
+        $this->assertEmpty(message_count_blocked_users());
     }
 
     /**
index 8e512ae..96481a7 100644 (file)
@@ -637,6 +637,13 @@ class assign_grading_table extends table_sql implements renderable {
         $this->get_group_and_submission($row->id, $group, $submission, -1);
         if ($group) {
             return $group->name;
+        } else if ($this->assignment->get_instance()->preventsubmissionnotingroup) {
+            $usergroups = $this->assignment->get_all_groups($row->id);
+            if (count($usergroups) > 1) {
+                return get_string('multipleteamsgrader', 'assign');
+            } else {
+                return get_string('noteamgrader', 'assign');
+            }
         }
         return get_string('defaultteam', 'assign');
     }
index a5476f2..fea73ea 100644 (file)
@@ -278,7 +278,8 @@ When reviewing assignments, teachers can leave feedback comments and upload file
 $string['modulename_link'] = 'mod/assignment/view';
 $string['modulenameplural'] = 'Assignments';
 $string['moreusers'] = '{$a} more...';
-$string['multipleteams'] = 'You\'re a member of multiple groups, please contact your teacher.';
+$string['multipleteams'] = 'Member of more than one group';
+$string['multipleteamsgrader'] = 'Member of more than one group, so unable to make submissions.';
 $string['mysubmission'] = 'My submission: ';
 $string['newsubmissions'] = 'Assignments submitted';
 $string['noattempt'] = 'No attempt';
@@ -290,7 +291,8 @@ $string['noonlinesubmissions'] = 'This assignment does not require you to submit
 $string['nosavebutnext'] = 'Next';
 $string['nosubmission'] = 'Nothing has been submitted for this assignment';
 $string['nosubmissionsacceptedafter'] = 'No submissions accepted after ';
-$string['noteam'] = 'You\'re not a member of any group, please contact your teacher.';
+$string['noteam'] = 'Not a member of any group';
+$string['noteamgrader'] = 'Not a member of any group, so unable to make submissions.';
 $string['notgraded'] = 'Not graded';
 $string['notgradedyet'] = 'Not graded yet';
 $string['notsubmittedyet'] = 'Not submitted yet';
@@ -435,7 +437,7 @@ $string['teamsubmissiongroupingid_help'] = 'This is the grouping that the assign
 $string['textinstructions'] = 'Assignment instructions';
 $string['timemodified'] = 'Last modified';
 $string['timeremaining'] = 'Time remaining';
-$string['ungroupedusers'] = 'The setting \'Require group to make submission\' is turned on and some users are not allocated to groups or are allocated to multiple groups, this will prevent them from submitting assignments.';
+$string['ungroupedusers'] = 'The setting \'Require group to make submission\' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.';
 $string['unlocksubmissionforstudent'] = 'Allow submissions for student: (id={$a->id}, fullname={$a->fullname}).';
 $string['unlocksubmissions'] = 'Unlock submissions';
 $string['unlimitedattempts'] = 'Unlimited';
index f5bbfaf..d6ba382 100644 (file)
@@ -2323,7 +2323,7 @@ class assign {
      * @param int $userid Teh id of the user who's groups we are checking
      * @return array The group objects
      */
-    protected function get_all_groups($userid) {
+    public function get_all_groups($userid) {
         if (isset($this->usergroups[$userid])) {
             return $this->usergroups[$userid];
         }
index 459ea31..cf76865 100644 (file)
@@ -47,7 +47,7 @@ Feature: Submit assignment without group
     When I log in as "student1"
     And I follow "Course 1"
     And I follow "Allow default group"
-    Then I should not see "You're not a member of any group, please contact your teacher."
+    Then I should not see "Not a member of any group"
     And I should see "Nothing has been submitted for this assignment"
     And I press "Add submission"
     And I set the following fields to these values:
@@ -58,13 +58,13 @@ Feature: Submit assignment without group
     And I should see "Submitted for grading"
     And I follow "Course 1"
     And I follow "Require group membership"
-    And I should see "You're not a member of any group, please contact your teacher."
+    And I should see "Not a member of any group"
     And I should see "Nothing has been submitted for this assignment"
     And I should not see "Add submission"
     And I am on homepage
     And I follow "Course 2"
     And I follow "Require group membership"
-    And I should not see "You're not a member of any group, please contact your teacher."
+    And I should not see "Not a member of any group"
     And I should see "Nothing has been submitted for this assignment"
     And I press "Add submission"
     And I set the following fields to these values:
@@ -89,7 +89,7 @@ Feature: Submit assignment without group
     And I follow "Course 1"
     And I follow "Allow default group"
     And I should see "1" in the "Groups" "table_row"
-    And I should not see "The setting 'Require group to make submission' is turned on and some users are not allocated to groups or are allocated to multiple groups, this will prevent them from submitting assignments."
+    And I should not see "The setting 'Require group to make submission\' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions."
     And I follow "View/grade all submissions"
     And I should see "Default group" in the "Student 1" "table_row"
     And I should see "Default group" in the "Student 2" "table_row"
@@ -99,17 +99,17 @@ Feature: Submit assignment without group
     And I follow "Course 1"
     And I follow "Require group membership"
     And I should see "0" in the "Groups" "table_row"
-    And I should see "The setting 'Require group to make submission' is turned on and some users are not allocated to groups or are allocated to multiple groups, this will prevent them from submitting assignments."
+    And I should see "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions."
     And I follow "View/grade all submissions"
-    And I should see "Default group" in the "Student 1" "table_row"
-    And I should see "Default group" in the "Student 2" "table_row"
+    And I should see "Not a member of any group, so unable to make submissions." in the "Student 1" "table_row"
+    And I should see "Not a member of any group, so unable to make submissions." in the "Student 2" "table_row"
     And I should not see "Submitted for grading" in the "Student 1" "table_row"
     And I should not see "Submitted for grading" in the "Student 2" "table_row"
     And I am on homepage
     And I follow "Course 2"
     And I follow "Require group membership"
     And I should see "1" in the "Groups" "table_row"
-    And I should not see "The setting 'Require group to make submission' is turned on and some users are not allocated to groups or are allocated to multiple groups, this will prevent them from submitting assignments."
+    And I should not see "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions."
     And I follow "View/grade all submissions"
     And I should see "Group 1" in the "Student 1" "table_row"
     And I should see "Group 1" in the "Student 2" "table_row"
@@ -120,11 +120,13 @@ Feature: Submit assignment without group
     And I log in as "student3"
     And I follow "Course 3"
     And I follow "Require group membership"
-    And I should see "You're a member of multiple groups, please contact your teacher."
+    And I should see "Member of more than one group"
     And I should see "Nothing has been submitted for this assignment"
     And I should not see "Add submission"
     And I log out
     And I log in as "teacher1"
-    And I follow "Course 1"
+    And I follow "Course 3"
     And I follow "Require group membership"
-    And I should see "The setting 'Require group to make submission' is turned on and some users are not allocated to groups or are allocated to multiple groups, this will prevent them from submitting assignments."
\ No newline at end of file
+    And I should see "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions."
+    And I follow "View/grade all submissions"
+    And I should see "Member of more than one group, so unable to make submissions." in the "Student 3" "table_row"
index 1d032e1..3eab24b 100644 (file)
@@ -2473,7 +2473,7 @@ function forum_count_discussions($forum, $cm, $course) {
  *                     Use FORUM_POSTS_ALL_USER_GROUPS for all the user groups
  * @return array
  */
-function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=true, $unused=-1, $limit=-1,
+function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $limit=-1,
                                 $userlastmodified=false, $page=-1, $perpage=0, $groupid = -1) {
     global $CFG, $DB, $USER;
 
@@ -2568,7 +2568,7 @@ function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=
         $groupselect = "";
     }
     if (empty($forumsort)) {
-        $forumsort = "d.timemodified DESC";
+        $forumsort = forum_get_default_sort_order();
     }
     if (empty($fullpost)) {
         $postdata = "p.id,p.subject,p.modified,p.discussion,p.userid";
@@ -2707,11 +2707,37 @@ function forum_get_discussion_neighbours($cm, $discussion, $forum) {
                        $timelimit
                        $groupselect";
 
-        $prevsql = $sql . " AND d.timemodified < :disctimemodified
-                       ORDER BY d.timemodified DESC";
+        if (empty($CFG->forum_enabletimedposts)) {
+            $prevsql = $sql . " AND d.timemodified < :disctimemodified";
+            $nextsql = $sql . " AND d.timemodified > :disctimemodified";
 
-        $nextsql = $sql . " AND d.timemodified > :disctimemodified
-                       ORDER BY d.timemodified ASC";
+        } else {
+            // Normally we would just use the timemodified for sorting
+            // discussion posts. However, when timed discussions are enabled,
+            // then posts need to be sorted base on the later of timemodified
+            // or the release date of the post (timestart).
+            $params['disctimecompare'] = $discussion->timemodified;
+            if ($discussion->timemodified < $discussion->timestart) {
+                $params['disctimecompare'] = $discussion->timestart;
+            }
+            $params['disctimecompare2'] = $params['disctimecompare'];
+
+            // Here we need to take into account the release time (timestart)
+            // if one is set, of the neighbouring posts and compare it to the
+            // timestart or timemodified of *this* post depending on if the
+            // release date of this post is in the future or not.
+            // This stops discussions that appear later because of the
+            // timestart value from being buried under discussions that were
+            // made afterwards.
+            $prevsql = $sql . " AND CASE WHEN d.timemodified < d.timestart
+                                    THEN d.timestart < :disctimecompare
+                                    ELSE d.timemodified < :disctimecompare2 END";
+            $nextsql = $sql . " AND CASE WHEN d.timemodified < d.timestart
+                                    THEN d.timestart > :disctimecompare
+                                    ELSE d.timemodified > :disctimecompare2 END";
+        }
+        $prevsql .= ' ORDER BY '.forum_get_default_sort_order();
+        $nextsql .= ' ORDER BY '.forum_get_default_sort_order(false);
 
         $neighbours['prev'] = $DB->get_record_sql($prevsql, $params, IGNORE_MULTIPLE);
         $neighbours['next'] = $DB->get_record_sql($nextsql, $params, IGNORE_MULTIPLE);
@@ -2720,6 +2746,35 @@ function forum_get_discussion_neighbours($cm, $discussion, $forum) {
     return $neighbours;
 }
 
+/**
+ * Get the sql to use in the ORDER BY clause for forum discussions.
+ *
+ * This has the ordering take timed discussion windows into account.
+ *
+ * @param bool $desc True for DESC, False for ASC.
+ * @param string $compare The field in the SQL to compare to normally sort by.
+ * @param string $prefix The prefix being used for the discussion table.
+ * @return string
+ */
+function forum_get_default_sort_order($desc = true, $compare = 'd.timemodified', $prefix = 'd') {
+    global $CFG;
+
+    if (!empty($prefix)) {
+        $prefix .= '.';
+    }
+
+    $dir = $desc ? 'DESC' : 'ASC';
+
+    $sort = "{$prefix}timemodified";
+    if (!empty($CFG->forum_enabletimedposts)) {
+        $sort = "CASE WHEN {$compare} < {$prefix}timestart
+                 THEN {$prefix}timestart
+                 ELSE {$compare}
+                 END";
+    }
+    return "$sort $dir";
+}
+
 /**
  *
  * @global object
@@ -5133,7 +5188,7 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $
     $context = context_module::instance($cm->id);
 
     if (empty($sort)) {
-        $sort = "d.timemodified DESC";
+        $sort = forum_get_default_sort_order();
     }
 
     $olddiscussionlink = false;
index 6ccdbc2..b57c320 100644 (file)
@@ -903,19 +903,30 @@ class mod_forum_lib_testcase extends advanced_testcase {
         $record->timestart = $past;
         $record->timeend = $future;
         $disc12 = $forumgen->create_discussion($record);
+        sleep(1);
+        $record->timestart = $future + 1; // Should be last post for those that can see it.
+        $record->timeend = 0;
+        $disc13 = $forumgen->create_discussion($record);
 
         // Admin user ignores the timed settings of discussions.
+        // Post ordering taking into account timestart:
+        //  8 = t
+        // 10 = t+3
+        // 11 = t+4
+        // 12 = t+5
+        //  9 = t+60
+        // 13 = t+61.
         $this->setAdminUser();
         $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum);
         $this->assertEquals($disc7->id, $neighbours['prev']->id);
-        $this->assertEquals($disc9->id, $neighbours['next']->id);
+        $this->assertEquals($disc10->id, $neighbours['next']->id);
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc9, $forum);
-        $this->assertEquals($disc8->id, $neighbours['prev']->id);
-        $this->assertEquals($disc10->id, $neighbours['next']->id);
+        $this->assertEquals($disc12->id, $neighbours['prev']->id);
+        $this->assertEquals($disc13->id, $neighbours['next']->id);
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc10, $forum);
-        $this->assertEquals($disc9->id, $neighbours['prev']->id);
+        $this->assertEquals($disc8->id, $neighbours['prev']->id);
         $this->assertEquals($disc11->id, $neighbours['next']->id);
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc11, $forum);
@@ -924,20 +935,24 @@ class mod_forum_lib_testcase extends advanced_testcase {
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc12, $forum);
         $this->assertEquals($disc11->id, $neighbours['prev']->id);
+        $this->assertEquals($disc9->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc13, $forum);
+        $this->assertEquals($disc9->id, $neighbours['prev']->id);
         $this->assertEmpty($neighbours['next']);
 
         // Normal user can see their own timed discussions.
         $this->setUser($user);
         $neighbours = forum_get_discussion_neighbours($cm, $disc8, $forum);
         $this->assertEquals($disc7->id, $neighbours['prev']->id);
-        $this->assertEquals($disc9->id, $neighbours['next']->id);
+        $this->assertEquals($disc10->id, $neighbours['next']->id);
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc9, $forum);
-        $this->assertEquals($disc8->id, $neighbours['prev']->id);
-        $this->assertEquals($disc10->id, $neighbours['next']->id);
+        $this->assertEquals($disc12->id, $neighbours['prev']->id);
+        $this->assertEquals($disc13->id, $neighbours['next']->id);
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc10, $forum);
-        $this->assertEquals($disc9->id, $neighbours['prev']->id);
+        $this->assertEquals($disc8->id, $neighbours['prev']->id);
         $this->assertEquals($disc11->id, $neighbours['next']->id);
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc11, $forum);
@@ -946,6 +961,10 @@ class mod_forum_lib_testcase extends advanced_testcase {
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc12, $forum);
         $this->assertEquals($disc11->id, $neighbours['prev']->id);
+        $this->assertEquals($disc9->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc13, $forum);
+        $this->assertEquals($disc9->id, $neighbours['prev']->id);
         $this->assertEmpty($neighbours['next']);
 
         // Normal user does not ignore timed settings.
@@ -975,11 +994,11 @@ class mod_forum_lib_testcase extends advanced_testcase {
         $disc3 = $DB->get_record('forum_discussions', array('id' => $disc3->id));
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc2, $forum);
-        $this->assertEquals($disc12->id, $neighbours['prev']->id);
+        $this->assertEquals($disc13->id, $neighbours['prev']->id);
         $this->assertEmpty($neighbours['next']);
 
         $neighbours = forum_get_discussion_neighbours($cm, $disc3, $forum);
-        $this->assertEquals($disc12->id, $neighbours['prev']->id);
+        $this->assertEquals($disc13->id, $neighbours['prev']->id);
         $this->assertEmpty($neighbours['next']);
     }
 
index 55be766..c9e1884 100644 (file)
@@ -306,7 +306,7 @@ class question_engine_data_mapper {
         $records = $this->db->get_recordset_sql("
 SELECT
     quba.contextid,
-    COALLESCE(q.qtype, 'missingtype') AS qtype,
+    COALESCE(q.qtype, 'missingtype') AS qtype,
     qas.id AS attemptstepid,
     qas.questionattemptid,
     qas.sequencenumber,
index 94ca801..ed5080f 100644 (file)
@@ -144,40 +144,21 @@ if ($themerev <= 0 or $rev != $themerev) {
 
 make_localcache_directory('theme', false);
 
-// We're not using SVG and there is no cached version of this file (in any format).
-// As we're going to be caching a format other than svg, and because svg use is conditional we need to ensure that at the same
-// time we cache a version of the SVG if it exists. If we don't do this other users who ask for SVG would not ever get it as
-// there is a cached image already of another format.
-// Remember this only gets run once before any candidate exists, and only if we want a cached revision.
-if (!$usesvg) {
-    $imagefile = $theme->resolve_image_location($image, $component, true);
-    if (!empty($imagefile) && is_readable($imagefile)) {
-        $cacheimage = cache_image($image, $imagefile, $candidatelocation);
-        $pathinfo = pathinfo($imagefile);
-        // There is no SVG equivalent, we've just successfully cached an image of another format.
-        if ($pathinfo['extension'] !== 'svg') {
-            // Serve the file as we would in a normal request.
-            if (connection_aborted()) {
-                die;
-            }
-            // Make sure nothing failed.
-            clearstatcache();
-            if (file_exists($cacheimage)) {
-                send_cached_image($cacheimage, $etag);
-            }
-            send_uncached_image($imagefile);
-            exit;
-        } else {
-            // We cannot serve the SVG file this time and there is no alternative, so prevent future
-            // requests from failing to find the image when they can support SVG.
-            image_not_found();
-        }
-    }
-}
+// At this stage caching is enabled, and either:
+// * we have no cached copy of the image in any format (either SVG, or non-SVG); or
+// * we have a cached copy of the SVG, but the non-SVG was requested by the browser.
+//
+// Because of the way in which the cache return code works above:
+// * if we are allowed to return SVG, we do not need to cache the non-SVG version; however
+// * if the browser has requested the non-SVG version, we *must* cache _both_ the SVG, and the non-SVG versions.
 
-// Either SVG was requested or we've cached a SVG version and are ready to serve a regular format.
-$imagefile = $theme->resolve_image_location($image, $component, $usesvg);
-if (empty($imagefile) or !is_readable($imagefile)) {
+// First get all copies - including, potentially, the SVG version.
+$imagefile = $theme->resolve_image_location($image, $component, true);
+
+if (empty($imagefile) || !is_readable($imagefile)) {
+    // Unable to find a copy of the image file in any format.
+    // We write a .error file for the image now - this will be used above when searching for cached copies to prevent
+    // trying to find the image in the future.
     if (!file_exists($candidatelocation)) {
         @mkdir($candidatelocation, $CFG->directorypermissions, true);
     }
@@ -188,19 +169,47 @@ if (empty($imagefile) or !is_readable($imagefile)) {
     image_not_found();
 }
 
-$cacheimage = cache_image($image, $imagefile, $candidatelocation);
+// The image was found, and it is readable.
+$pathinfo = pathinfo($imagefile);
+
+// Attempt to cache it if necessary.
+// We don't really want to overwrite any existing cache items just for the sake of it.
+$cacheimage = "$candidatelocation/$image.{$pathinfo['extension']}";
+if (!file_exists($cacheimage)) {
+    // We don't already hold a cached copy of this image. Cache it now.
+    $cacheimage = cache_image($image, $imagefile, $candidatelocation);
+}
+
+if (!$usesvg && $pathinfo['extension'] === 'svg') {
+    // The browser has requested that a non-SVG version be returned.
+    // The version found so far is the SVG version - try and find the non-SVG version.
+    $imagefile = $theme->resolve_image_location($image, $component, false);
+    if (empty($imagefile) || !is_readable($imagefile)) {
+        // A non-SVG file could not be found at all.
+        // The browser has requested a non-SVG version, so we must return image_not_found().
+        // We must *not* write an .error file because the SVG is available.
+        image_not_found();
+    }
+
+    // An non-SVG version of image was found - cache it.
+    // This will be used below in the image serving code.
+    $cacheimage = cache_image($image, $imagefile, $candidatelocation);
+}
+
 if (connection_aborted()) {
+    // Request was cancelled - do not send anything.
     die;
 }
+
 // Make sure nothing failed.
 clearstatcache();
 if (file_exists($cacheimage)) {
+    // The cached copy was found, and is accessible. Serve it.
     send_cached_image($cacheimage, $etag);
 }
 
 send_uncached_image($imagefile);
 
-
 //=================================================================================
 //=== utility functions ==
 // we are not using filelib because we need to fine tune all header