Merge branch 'MDL-34498-master' of git://github.com/damyon/moodle
authorJun Pataleta <jun@moodle.com>
Thu, 6 Jun 2019 02:50:56 +0000 (10:50 +0800)
committerJun Pataleta <jun@moodle.com>
Thu, 6 Jun 2019 02:50:56 +0000 (10:50 +0800)
52 files changed:
admin/tests/behat/filter_users.feature
admin/tool/task/cli/schedule_task.php
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/tests/cronhelper_test.php
badges/classes/form/collections.php
composer.json
composer.lock
lang/en/group.php
lib/filelib.php
lib/testing/generator/data_generator.php
message/classes/api.php
message/lib.php
message/tests/api_test.php
message/upgrade.txt
mod/assign/feedback/editpdf/styles.css
mod/assign/feedback/file/importzipform.php
mod/assign/locallib.php
mod/book/tests/behat/display_book_description.feature [new file with mode: 0644]
mod/book/view.php
mod/forum/amd/build/posts_list.min.js
mod/forum/amd/src/posts_list.js
mod/forum/classes/local/exporters/author.php
mod/forum/classes/local/exporters/discussion.php
mod/forum/classes/local/exporters/forum.php
mod/forum/classes/local/exporters/post.php
mod/forum/classes/local/factories/url.php
mod/forum/classes/local/managers/capability.php
mod/forum/classes/local/renderers/discussion.php
mod/forum/classes/local/renderers/discussion_list.php
mod/forum/lang/en/deprecated.txt [new file with mode: 0644]
mod/forum/lang/en/forum.php
mod/forum/styles.css
mod/forum/templates/discussion_list.mustache
mod/forum/templates/forum_discussion.mustache
mod/forum/templates/forum_discussion_post.mustache
mod/forum/templates/inpage_reply.mustache
mod/forum/tests/exporters_post_test.php
mod/forum/tests/externallib_test.php
mod/imscp/tests/behat/display_imscp_description.feature [new file with mode: 0644]
mod/imscp/view.php
mod/lesson/renderer.php
mod/lesson/tests/behat/display_lesson_description.feature [new file with mode: 0644]
mod/quiz/report/grading/report.php
mod/quiz/report/grading/tests/behat/grading.feature
mod/scorm/tests/behat/behat_mod_scorm.php [new file with mode: 0644]
pix/movehere.svg [new file with mode: 0644]
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/scss/moodle/icons.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/filters/lib.php

index 75e2b42..34088d5 100644 (file)
@@ -6,11 +6,11 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email | auth | confirmed |
-      | user1 | User | One | one@example.com | manual | 0 |
-      | user2 | User | Two | two@example.com | ldap | 1 |
-      | user3 | User | Three | three@example.com | manual | 1 |
-      | user4 | User | Four | four@example.com | ldap | 0 |
+      | username | firstname | lastname | email | auth | confirmed | lastip |
+      | user1 | User | One | one@example.com | manual | 0 | 127.0.1.1 |
+      | user2 | User | Two | two@example.com | ldap | 1 | 0.0.0.0 |
+      | user3 | User | Three | three@example.com | manual | 1 | 0.0.0.0 |
+      | user4 | User | Four | four@example.com | ldap | 0 | 127.0.1.2 |
     And the following "cohorts" exist:
       | name | idnumber |
       | Cohort 1 | CH1 |
@@ -82,3 +82,25 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
     And I should not see "User Two"
     And I should not see "User Three"
     And I should see "User Four"
+
+  Scenario: Filter user accounts by last IP address
+    When I set the following fields to these values:
+      | id_lastip | 127.0.1.1 |
+    And I press "Add filter"
+    Then I should see "User One"
+    And I should not see "User Two"
+    And I should not see "User Three"
+    And I should not see "User Four"
+    And I press "Remove all filters"
+    And I set the following fields to these values:
+      | id_lastip | 127.0.1.2 |
+    And I press "Add filter"
+    And I should not see "User One"
+    And I should not see "User Two"
+    And I should not see "User Three"
+    And I should see "User Four"
+    And I press "Remove all filters"
+    And I should see "User One"
+    And I should see "User Two"
+    And I should see "User Three"
+    And I should see "User Four"
index cf03700..85d7a4f 100644 (file)
@@ -50,7 +50,7 @@ Options:
 -h, --help            Print out this help
 
 Example:
-\$sudo -u www-data /usr/bin/php admin/tool/task/cli/scheduled_task.php --execute=\\\\core\\\\task\\\\session_cleanup_task
+\$sudo -u www-data /usr/bin/php admin/tool/task/cli/schedule_task.php --execute=\\\\core\\\\task\\\\session_cleanup_task
 
 ";
 
index 683c446..33917bd 100644 (file)
@@ -68,6 +68,40 @@ abstract class backup_cron_automated_helper {
     /** Automated backup storage in course backup filearea and specified directory */
     const STORAGE_COURSE_AND_DIRECTORY = 2;
 
+    /**
+     * Get the courses to backup.
+     *
+     * When there are multiple courses to backup enforce some order to the record set.
+     * The following is the preference order.
+     * First backup courses that do not have an entry in backup_courses first,
+     * as they are likely new and never been backed up. Do the oldest modified courses first.
+     * Then backup courses that have previously been backed up starting with the oldest next start time.
+     *
+     * @param null|int $now timestamp to use in course selection.
+     * @return moodle_recordset The recordset of matching courses.
+     */
+    protected static function get_courses($now = null) {
+        global $DB;
+        if ($now == null) {
+            $now = time();
+        }
+
+        $sql = 'SELECT c.*,
+                       COALESCE(bc.nextstarttime, 1) nextstarttime
+                  FROM {course} c
+             LEFT JOIN {backup_courses} bc ON bc.courseid = c.id
+                 WHERE bc.nextstarttime IS NULL OR bc.nextstarttime < ?
+              ORDER BY nextstarttime ASC,
+                       c.timemodified ASC';
+
+        $params = array(
+            $now,  // Only get courses where the backup start time is in the past.
+        );
+        $rs = $DB->get_recordset_sql($sql, $params);
+
+        return $rs;
+    }
+
     /**
      * Runs the automated backups if required
      *
@@ -127,7 +161,7 @@ abstract class backup_cron_automated_helper {
                 $showtime = date('r', $nextstarttime);
             }
 
-            $rs = $DB->get_recordset('course');
+            $rs = self::get_courses($now); // Get courses to backup.
             foreach ($rs as $course) {
                 $backupcourse = $DB->get_record('backup_courses', array('courseid' => $course->id));
                 if (!$backupcourse) {
index 274e6dd..c67efe0 100644 (file)
@@ -364,8 +364,107 @@ class backup_cron_helper_testcase extends advanced_testcase {
         // Updated courses should be backed up.
         $this->assertTrue(testable_backup_cron_automated_helper::testable_is_course_modified($course->id, $timepriortobackup));
     }
+
+    /**
+     * Create courses and backup records for tests.
+     *
+     * @return array Created courses.
+     */
+    private function course_setup() {
+        global $DB;
+
+        // Create test courses.
+        $course1 = $this->getDataGenerator()->create_course(array('timecreated' => 1551402000));
+        $course2 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600));
+        $course3 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600));
+        $course4 = $this->getDataGenerator()->create_course(array('timecreated' => 1552179600));
+
+        // Create backup course records for the courses that need them.
+        $backupcourse3 = new stdClass;
+        $backupcourse3->courseid = $course3->id;
+        $backupcourse3->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK;
+        $backupcourse3->nextstarttime = 1554858160;
+        $DB->insert_record('backup_courses', $backupcourse3);
+
+        $backupcourse4 = new stdClass;
+        $backupcourse4->courseid = $course4->id;
+        $backupcourse4->laststatus = testable_backup_cron_automated_helper::BACKUP_STATUS_OK;
+        $backupcourse4->nextstarttime = 1554858160;
+        $DB->insert_record('backup_courses', $backupcourse4);
+
+        return array($course1, $course2, $course3, $course4);
+    }
+
+    /**
+     * Test the selection and ordering of courses to be backed up.
+     */
+    public function test_get_courses() {
+        $this->resetAfterTest();
+
+        list($course1, $course2, $course3, $course4) = $this->course_setup();
+
+        $now = 1559215025;
+
+        // Get the courses in order.
+        $courseset = testable_backup_cron_automated_helper::testable_get_courses($now);
+
+        $coursearray = array();
+        foreach ($courseset as $course) {
+            if ($course->id != SITEID) { // Skip system course for test.
+                $coursearray[] = $course->id;
+            }
+
+        }
+        $courseset->close();
+
+        // First should be course 1, it is the oldest modified without a backup.
+        $this->assertEquals($course1->id, $coursearray[0]);
+
+        // Second should be course 2, it is the next oldest modified without a backup.
+        $this->assertEquals($course2->id, $coursearray[1]);
+
+        // Third should be course 3, it is the course with the oldest backup.
+        $this->assertEquals($course3->id, $coursearray[2]);
+
+        // Fourth should be course 4, it is the course with the newest backup.
+        $this->assertEquals($course4->id, $coursearray[3]);
+    }
+
+    /**
+     * Test the selection and ordering of courses to be backed up.
+     * Where it is not yet time to start backups for courses with existing backups.
+     */
+    public function test_get_courses_starttime() {
+        $this->resetAfterTest();
+
+        list($course1, $course2, $course3, $course4) = $this->course_setup();
+
+        $now = 1554858000;
+
+        // Get the courses in order.
+        $courseset = testable_backup_cron_automated_helper::testable_get_courses($now);
+
+        $coursearray = array();
+        foreach ($courseset as $course) {
+            if ($course->id != SITEID) { // Skip system course for test.
+                $coursearray[] = $course->id;
+            }
+
+        }
+        $courseset->close();
+
+        // Should only be two courses.
+        // First should be course 1, it is the oldest modified without a backup.
+        $this->assertEquals($course1->id, $coursearray[0]);
+
+        // Second should be course 2, it is the next oldest modified without a backup.
+        $this->assertEquals($course2->id, $coursearray[1]);
+    }
+
 }
 
+
+
 /**
  * Provides access to protected methods we want to explicitly test
  *
@@ -397,4 +496,15 @@ class testable_backup_cron_automated_helper extends backup_cron_automated_helper
     public static function testable_is_course_modified($courseid, $since) {
         return parent::is_course_modified($courseid, $since);
     }
+
+    /**
+     * Provides access to protected method get_courses.
+     *
+     * @param int $now Timestamp to use.
+     * @return moodle_recordset The returned courses as a Moodle recordest.
+     */
+    public static function testable_get_courses($now) {
+        return parent::get_courses($now);
+    }
+
 }
index 6f221b7..5a57ba2 100644 (file)
@@ -79,24 +79,25 @@ class collections extends moodleform {
         $hasgroups = false;
         if (!empty($groups)) {
             foreach ($groups as $group) {
-                // Assertions or badges.
                 $count = 0;
-
+                // Handle attributes based on backpack's supported version.
                 if ($sitebackpack->apiversion == OPEN_BADGES_V2) {
+                    // OpenBadges v2 data attributes.
                     if (empty($group->published)) {
                         // Only public collections.
                         continue;
                     }
-                }
-                if (!empty($group->assertions)) {
+
+                    // Get the number of badges associated with this collection from the assertions array returned.
                     $count = count($group->assertions);
-                }
-                if (!empty($group->badges)) {
-                    $count = count($group->badges);
-                }
-                if (!empty($group->groupId)) {
+                } else {
+                    // OpenBadges v1 data attributes.
                     $group->entityId = $group->groupId;
+
+                    // Get the number of badges associated with this collection. In that case, the number is returned directly.
+                    $count = $group->badges;
                 }
+
                 if (!$hasgroups) {
                     $mform->addElement('static', 'selectgroup', '', get_string('selectgroup_start', 'badges'));
                 }
index 6dc963d..02de25a 100644 (file)
@@ -7,7 +7,7 @@
     "require-dev": {
         "phpunit/phpunit": "7.5.*",
         "phpunit/dbunit": "4.0.*",
-        "moodlehq/behat-extension": "3.37.0",
+        "moodlehq/behat-extension": "3.38.0",
         "mikey179/vfsstream": "^1.6"
     }
 }
index 948ff21..69a3ccd 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "3517a4473544055cd8523bb076cad8f6",
+    "content-hash": "c6d083d8c30f80b245ed03f8f064d4ec",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.37.0",
+            "version": "v3.38.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "7.5.9",
+            "version": "7.5.12",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160"
+                "reference": "9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/134669cf0eeac3f79bc7f0c793efbc158bffc160",
-                "reference": "134669cf0eeac3f79bc7f0c793efbc158bffc160",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c",
+                "reference": "9ba59817745b0fe0c1a5a3032dfd4a6d2994ad1c",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2019-04-19T15:50:46+00:00"
+            "time": "2019-05-28T11:59:40+00:00"
         },
         {
             "name": "psr/container",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v4.2.8",
+            "version": "v4.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "c09c18cca96d7067152f78956faf55346c338283"
+                "reference": "3fa7d8cbd2e5006038a09b8ef93f3859a89b627e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/c09c18cca96d7067152f78956faf55346c338283",
-                "reference": "c09c18cca96d7067152f78956faf55346c338283",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3fa7d8cbd2e5006038a09b8ef93f3859a89b627e",
+                "reference": "3fa7d8cbd2e5006038a09b8ef93f3859a89b627e",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "symfony/css-selector": "~3.4|~4.0",
+                "symfony/http-client": "^4.3",
+                "symfony/mime": "^4.3",
                 "symfony/process": "~3.4|~4.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.2-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2019-04-07T09:56:43+00:00"
+            "time": "2019-04-15T20:15:25+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
         },
         {
             "name": "symfony/config",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
         },
         {
             "name": "symfony/debug",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "681afbb26488903c5ac15e63734f1d8ac430c9b9"
+                "reference": "671fc55bd14800668b1d0a3708c3714940e30a8c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/681afbb26488903c5ac15e63734f1d8ac430c9b9",
-                "reference": "681afbb26488903c5ac15e63734f1d8ac430c9b9",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/671fc55bd14800668b1d0a3708c3714940e30a8c",
+                "reference": "671fc55bd14800668b1d0a3708c3714940e30a8c",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2019-04-11T09:48:14+00:00"
+            "time": "2019-05-18T13:32:47+00:00"
         },
         {
             "name": "symfony/dependency-injection",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v4.2.8",
+            "version": "v4.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb"
+                "reference": "28edb1d371640654fbfb9df53d70fa03fdf69fb6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/53c97769814c80a84a8403efcf3ae7ae966d53bb",
-                "reference": "53c97769814c80a84a8403efcf3ae7ae966d53bb",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/28edb1d371640654fbfb9df53d70fa03fdf69fb6",
+                "reference": "28edb1d371640654fbfb9df53d70fa03fdf69fb6",
                 "shasum": ""
             },
             "require": {
                 "symfony/polyfill-ctype": "~1.8",
                 "symfony/polyfill-mbstring": "~1.0"
             },
+            "conflict": {
+                "masterminds/html5": "<2.6"
+            },
             "require-dev": {
+                "masterminds/html5": "^2.6",
                 "symfony/css-selector": "~3.4|~4.0"
             },
             "suggest": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.2-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2019-02-23T15:17:42+00:00"
+            "time": "2019-04-26T05:53:56+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.4.27",
+            "version": "v3.4.28",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
         },
         {
             "name": "symfony/filesystem",
-            "version": "v4.2.8",
+            "version": "v4.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601"
+                "reference": "988ab7d70c267c34efa85772ca20de3fad11c74b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/e16b9e471703b2c60b95f14d31c1239f68f11601",
-                "reference": "e16b9e471703b2c60b95f14d31c1239f68f11601",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/988ab7d70c267c34efa85772ca20de3fad11c74b",
+                "reference": "988ab7d70c267c34efa85772ca20de3fad11c74b",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "4.2-dev"
+                    "dev-master": "4.3-dev"
                 }
             },
             "autoload": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2019-02-07T11:40:08+00:00"
+            "time": "2019-05-24T12:50:04+00:00"
         },
         {
             "name": "symfony/polyfill-ctype",
index 9d448ec..9f32f85 100644 (file)
@@ -196,3 +196,4 @@ $string['toomanygroups'] = 'Insufficient users to populate this number of groups
 $string['usercount'] = 'User count';
 $string['usercounttotal'] = 'User count ({$a})';
 $string['usergroupmembership'] = 'Selected user\'s membership:';
+$string['memberofgroup'] = 'Group member of: {$a}';
index 16d17fb..6512933 100644 (file)
@@ -4237,7 +4237,7 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null, $offlin
 
         } else if ($filearea == GRADE_FEEDBACK_FILEAREA || $filearea == GRADE_HISTORY_FEEDBACK_FILEAREA) {
             if ($context->contextlevel != CONTEXT_MODULE) {
-                send_file_not_found;
+                send_file_not_found();
             }
 
             require_login($course, false);
index 314ae0c..cda6868 100644 (file)
@@ -253,7 +253,10 @@ EOD;
         }
 
         $record['timemodified'] = $record['timecreated'];
-        $record['lastip'] = '0.0.0.0';
+
+        if (!isset($record['lastip'])) {
+            $record['lastip'] = '0.0.0.0';
+        }
 
         if ($record['deleted']) {
             $delname = $record['email'].'.'.time();
index a47abea..661c571 100644 (file)
@@ -2386,10 +2386,14 @@ class api {
     public static function get_conversation_between_users(array $userids) {
         global $DB;
 
-        $conversations = self::get_individual_conversations_between_users([$userids]);
-        $conversation = $conversations[0];
+        if (empty($userids)) {
+            return false;
+        }
+
+        $hash = helper::get_conversation_hash($userids);
 
-        if ($conversation) {
+        if ($conversation = $DB->get_record('message_conversations', ['type' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+                'convhash' => $hash])) {
             return $conversation->id;
         }
 
@@ -2415,12 +2419,16 @@ class api {
      *
      * Where null is returned for the pairing of [3, 4] since no record exists.
      *
+     * @deprecated since 3.8
      * @param array $useridsets An array of arrays where the inner array is the set of user ids
      * @return stdClass[] Array of conversation records
      */
     public static function get_individual_conversations_between_users(array $useridsets) : array {
         global $DB;
 
+        debugging('\core_message\api::get_individual_conversations_between_users is deprecated and no longer used',
+            DEBUG_DEVELOPER);
+
         if (empty($useridsets)) {
             return [];
         }
index 9493f9e..83dd501 100644 (file)
@@ -487,15 +487,15 @@ function get_message_output_default_preferences() {
 function translate_message_default_setting($plugindefault, $processorname) {
     // Preset translation arrays
     $permittedvalues = array(
-        0x04 => 'disallowed',
-        0x08 => 'permitted',
-        0x0c => 'forced',
+        MESSAGE_DISALLOWED => 'disallowed',
+        MESSAGE_PERMITTED  => 'permitted',
+        MESSAGE_FORCED     => 'forced',
     );
 
     $loggedinstatusvalues = array(
         0x00 => null, // use null if loggedin/loggedoff is not defined
-        0x01 => 'loggedin',
-        0x02 => 'loggedoff',
+        MESSAGE_DEFAULT_LOGGEDIN  => 'loggedin',
+        MESSAGE_DEFAULT_LOGGEDOFF => 'loggedoff',
     );
 
     // define the default setting
index bc63342..5d13640 100644 (file)
@@ -5749,6 +5749,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      */
     public function test_get_individual_conversations_between_users_no_user_sets() {
         $this->assertEmpty(\core_message\api::get_individual_conversations_between_users([]));
+        $this->assertDebuggingCalled();
     }
 
     /**
@@ -5763,6 +5764,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [null],
             \core_message\api::get_individual_conversations_between_users([[$user1->id, $user2->id]])
         );
+        $this->assertDebuggingCalled();
     }
 
     /**
@@ -5783,6 +5785,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [$user2->id, $user3->id],
             [$user1->id, $user3->id]
         ]);
+        $this->assertDebuggingCalled();
 
         $result = array_map(function($result) {
             if ($result) {
@@ -5817,6 +5820,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [$user2->id, $user3->id],
             [$user1->id, $user3->id]
         ]);
+        $this->assertDebuggingCalled();
 
         $result = array_map(function($result) {
             if ($result) {
@@ -5851,6 +5855,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [$user2->id, $user3->id],
             [$user1->id, $user3->id]
         ]);
+        $this->assertDebuggingCalled();
 
         $result = array_map(function($result) {
             if ($result) {
@@ -5870,6 +5875,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             [$user1->id, $user2->id],
             [$user1->id, $user3->id]
         ]);
+        $this->assertDebuggingCalled();
 
         $result = array_map(function($result) {
             if ($result) {
index b0142f1..041fa20 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /message/ messaging system,
 information provided here is intended especially for developers.
 
+=== 3.8 ===
+
+* The following methods have been deprecated and should not be used any more:
+  - \core_message\api::get_individual_conversations_between_users()
+
 === 3.7 ===
 
 * The message/index.php page used to support viewing another user's messages (if you had the right capabilities) by
index 48d87f8..573fab2 100644 (file)
@@ -358,6 +358,7 @@ ul.assignfeedback_editpdf_menu {
 
 .assignfeedback_editpdf_widget .commentdrawable.commentcollapsed {
     z-index: auto;
+    width: 24px;
 }
 
 .assignfeedback_editpdf_widget .commentdrawable.commentcollapsed textarea,
index 433d205..2e6840e 100644 (file)
@@ -80,7 +80,7 @@ class assignfeedback_file_import_zip_form extends moodleform implements renderab
             if ($importer->is_valid_filename_for_import($assignment, $unzippedfile, $participants, $user, $plugin, $filename)) {
                 if ($importer->is_file_modified($assignment, $user, $plugin, $filename, $unzippedfile)) {
                     // Get a string we can show to identify this user.
-                    $userdesc = fullname($user);
+                    $userdesc = fullname($user, has_capability('moodle/site:viewfullnames', $assignment->get_context()));
                     $path = pathinfo($filename);
                     if ($assignment->is_blind_marking()) {
                         $userdesc = get_string('hiddenuser', 'assign') .
index a9f0174..bef787c 100644 (file)
@@ -3517,7 +3517,8 @@ class assign {
                     $prefix = str_replace('_', ' ', $groupname . get_string('participant', 'assign'));
                     $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
                 } else {
-                    $prefix = str_replace('_', ' ', $groupname . fullname($student));
+                    $fullname = fullname($student, has_capability('moodle/site:viewfullnames', $this->get_context()));
+                    $prefix = str_replace('_', ' ', $groupname . $fullname);
                     $prefix = clean_filename($prefix . '_' . $this->get_uniqueid_for_user($userid));
                 }
 
diff --git a/mod/book/tests/behat/display_book_description.feature b/mod/book/tests/behat/display_book_description.feature
new file mode 100644 (file)
index 0000000..08f977b
--- /dev/null
@@ -0,0 +1,54 @@
+@mod @mod_book
+Feature: Display the book description in the book and optionally in the course
+  In order to display the the book description in the course
+  As a teacher
+  I need to enable the 'Display description on course page' setting.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Book" to section "1" and I fill the form with:
+      | Name | Test book |
+      | Description | A book about dreams! |
+    And I follow "Test book"
+    And I should see "Add new chapter"
+    And I set the following fields to these values:
+      | Chapter title | Dummy first chapter |
+      | Content | Dream is the start of a journey |
+    And I press "Save changes"
+
+  Scenario: Description is displayed in the book
+    Given I am on "Course 1" course homepage
+    When I follow "Test book"
+    Then I should see "A book about dreams!"
+
+  Scenario: Show book description in the course homepage
+    Given I am on "Course 1" course homepage
+    And I follow "Test book"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I set the following fields to these values:
+      | Display description on course page | 1 |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should see "A book about dreams!"
+
+  Scenario: Hide book description in the course homepage
+    Given I am on "Course 1" course homepage
+    And I follow "Test book"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should not see "A book about dreams!"
index f0ad1b9..2b2c2d1 100644 (file)
@@ -213,6 +213,11 @@ book_view($book, $chapter, $islastchapter, $course, $cm, $context);
 echo $OUTPUT->header();
 echo $OUTPUT->heading(format_string($book->name));
 
+// Info box.
+if ($book->intro) {
+    echo $OUTPUT->box(format_module_intro('book', $book, $cm->id), 'generalbox', 'intro');
+}
+
 $navclasses = book_get_nav_classes();
 
 if ($book->navstyle) {
index df43413..b888a7f 100644 (file)
Binary files a/mod/forum/amd/build/posts_list.min.js and b/mod/forum/amd/build/posts_list.min.js differ
index 5e24d7a..3fbc593 100644 (file)
@@ -59,7 +59,7 @@ define([
                 postid: $(currentRoot).data('post-id'),
                 "reply_url": $(e.currentTarget).attr('href'),
                 sesskey: M.cfg.sesskey,
-                parentsubject: currentSubject.html(),
+                parentsubject: currentSubject.data('replySubject'),
                 canreplyprivately: $(e.currentTarget).data('can-reply-privately'),
                 postformat: InPageReply.CONTENT_FORMATS.MOODLE
             };
index 7ac4822..7beb5d5 100644 (file)
@@ -140,15 +140,26 @@ class author extends exporter {
         $author = $this->author;
         $authorcontextid = $this->authorcontextid;
         $urlfactory = $this->related['urlfactory'];
+        $context = $this->related['context'];
 
         if ($this->canview) {
-            $groups = array_map(function($group) {
-                $imageurl = get_group_picture_url($group, $group->courseid);
+            $groups = array_map(function($group) use ($urlfactory, $context) {
+                $imageurl = null;
+                $groupurl = null;
+                if (!$group->hidepicture) {
+                    $imageurl = get_group_picture_url($group, $group->courseid, true);
+                }
+                if (course_can_view_participants($context)) {
+                    $groupurl = $urlfactory->get_author_group_url($group);
+                }
+
                 return [
                     'id' => $group->id,
                     'name' => $group->name,
                     'urls' => [
-                        'image' => $imageurl ? $imageurl->out(false) : null
+                        'image' => $imageurl ? $imageurl->out(false) : null,
+                        'group' => $groupurl ? $groupurl->out(false) : null
+
                     ]
                 ];
             }, $this->authorgroups);
index 16d682a..a008de4 100644 (file)
@@ -179,7 +179,7 @@ class discussion extends exporter {
                 ];
 
                 if (!$group->hidepicture) {
-                    $url = get_group_picture_url($group, $forum->get_course_id());
+                    $url = get_group_picture_url($group, $forum->get_course_id(), true);
                     if (!empty($url)) {
                         $groupdata['urls']['picture'] = $url;
                     }
index 40d4dd6..ad39615 100644 (file)
@@ -119,6 +119,7 @@ class forum extends exporter {
             'capabilities' => [
                 'viewdiscussions' => $capabilitymanager->can_view_discussions($user),
                 'create' => $capabilitymanager->can_create_discussions($user, $currentgroup),
+                'selfenrol' => $capabilitymanager->can_self_enrol($user),
                 'subscribe' => $capabilitymanager->can_subscribe_to_forum($user),
             ],
             'urls' => [
index 4528c60..0d23672 100644 (file)
@@ -92,6 +92,7 @@ class post extends exporter {
         return [
             'id' => ['type' => PARAM_INT],
             'subject' => ['type' => PARAM_TEXT],
+            'replysubject' => ['type' => PARAM_TEXT],
             'message' => ['type' => PARAM_RAW],
             'messageformat' => ['type' => PARAM_INT],
             'author' => ['type' => author_exporter::read_properties_definition()],
@@ -146,6 +147,11 @@ class post extends exporter {
                         'null' => NULL_ALLOWED,
                         'description' => 'Whether the user can reply to the post',
                     ],
+                    'selfenrol' => [
+                        'type' => PARAM_BOOL,
+                        'null' => NULL_ALLOWED,
+                        'description' => 'Whether the user can self enrol into the course',
+                    ],
                     'export' => [
                         'type' => PARAM_BOOL,
                         'null' => NULL_ALLOWED,
@@ -360,6 +366,7 @@ class post extends exporter {
         $canreply = $capabilitymanager->can_reply_to_post($user, $discussion, $post);
         $canexport = $capabilitymanager->can_export_post($user, $post);
         $cancontrolreadstatus = $capabilitymanager->can_manually_control_post_read_status($user);
+        $canselfenrol = $capabilitymanager->can_self_enrol($user);
         $canreplyprivately = $capabilitymanager->can_reply_privately_to_post($user, $post);
 
         $urlfactory = $this->related['urlfactory'];
@@ -369,7 +376,7 @@ class post extends exporter {
         $editurl = $canedit ? $urlfactory->get_edit_post_url_from_post($forum, $post) : null;
         $deleteurl = $candelete ? $urlfactory->get_delete_post_url_from_post($post) : null;
         $spliturl = $cansplit ? $urlfactory->get_split_discussion_at_post_url_from_post($post) : null;
-        $replyurl = $canreply ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
+        $replyurl = $canreply || $canselfenrol ? $urlfactory->get_reply_to_post_url_from_post($post) : null;
         $exporturl = $canexport ? $urlfactory->get_export_post_url_from_post($post) : null;
         $markasreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_read_url_from_post($post) : null;
         $markasunreadurl = $cancontrolreadstatus ? $urlfactory->get_mark_post_as_unread_url_from_post($post) : null;
@@ -401,9 +408,16 @@ class post extends exporter {
             }
         }
 
+        $replysubject = $subject;
+        $strre = get_string('re', 'forum');
+        if (!(substr($replysubject, 0, strlen($strre)) == $strre)) {
+            $replysubject = "{$strre} {$replysubject}";
+        }
+
         return [
             'id' => $post->get_id(),
             'subject' => $subject,
+            'replysubject' => $replysubject,
             'message' => $message,
             'messageformat' => $post->get_message_format(),
             'author' => $exportedauthor,
@@ -424,7 +438,8 @@ class post extends exporter {
                 'reply' => $canreply,
                 'export' => $canexport,
                 'controlreadstatus' => $cancontrolreadstatus,
-                'canreplyprivately' => $canreplyprivately
+                'canreplyprivately' => $canreplyprivately,
+                'selfenrol' => $canselfenrol
             ],
             'urls' => [
                 'view' => $viewurl ? $viewurl->out(false) : null,
index ab0fc75..cfb6b4b 100644 (file)
@@ -427,6 +427,18 @@ class url {
         return $userpicture->get_url($PAGE);
     }
 
+    /**
+     * Get the url to view an author's group.
+     *
+     * @param \stdClass $group The group
+     * @return moodle_url
+     */
+    public function get_author_group_url(\stdClass $group) : moodle_url {
+        return new moodle_url('/user/index.php', [
+                'id' => $group->courseid,
+                'group' => $group->id
+        ]);
+    }
     /**
      * Get the url to mark a discussion as read.
      *
index 76ba7c1..ece049e 100644 (file)
@@ -607,4 +607,30 @@ class capability {
     public function can_manage_tags(stdClass $user) : bool {
         return has_capability('moodle/tag:manage', context_system::instance(), $user);
     }
+
+    /**
+     * Checks whether the user can self enrol into the course.
+     * Mimics the checks on the add button in deprecatedlib/forum_print_latest_discussions
+     *
+     * @param stdClass $user
+     * @return bool
+     */
+    public function can_self_enrol(stdClass $user) : bool {
+        $canstart = false;
+
+        if ($this->forum->get_type() != 'news') {
+            if (isguestuser($user) or !isloggedin()) {
+                $canstart = true;
+            }
+
+            if (!is_enrolled($this->context) and !is_viewing($this->context)) {
+                 // Allow guests and not-logged-in to see the button - they are prompted to log in after clicking the link,
+                 // Normal users with temporary guest access see this button too, they are asked to enrol instead,
+                 // Do not show the button to users with suspended enrolments here.
+                $canstart = enrol_selfenrol_available($this->forum->get_course_id());
+            }
+        }
+
+        return $canstart;
+    }
 }
index 084122a..7559b2b 100644 (file)
@@ -183,9 +183,15 @@ class discussion {
             $exporteddiscussion = $this->get_exported_discussion($user);
         }
 
+        $hasanyactions = false;
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_favourite_discussion($user);
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_pin_discussions($user);
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_manage_forum($user);
+
         $exporteddiscussion = array_merge($exporteddiscussion, [
             'notifications' => $this->get_notifications($user),
             'html' => [
+                'hasanyactions' => $hasanyactions,
                 'posts' => $this->postsrenderer->render($user, [$this->forum], [$this->discussion], $posts),
                 'modeselectorform' => $this->get_display_mode_selector_html($displaymode),
                 'subscribe' => null,
index ef136a3..24879b1 100644 (file)
@@ -160,8 +160,15 @@ class discussion_list {
         // Get all forum discussion summaries.
         $discussions = mod_forum_get_discussion_summaries($forum, $user, $groupid, $sortorder, $pageno, $pagesize);
 
+        $capabilitymanager = $this->capabilitymanager;
+        $hasanyactions = false;
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_favourite_discussion($user);
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_pin_discussions($user);
+        $hasanyactions = $hasanyactions || $capabilitymanager->can_manage_forum($user);
+
         $forumview = [
             'forum' => (array) $forumexporter->export($this->renderer),
+            'hasanyactions' => $hasanyactions,
             'groupchangemenu' => groups_print_activity_menu(
                 $cm,
                 $this->urlfactory->get_forum_view_url_from_forum($forum),
diff --git a/mod/forum/lang/en/deprecated.txt b/mod/forum/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..dd59859
--- /dev/null
@@ -0,0 +1 @@
+inpagereplysubject,mod_forum
index 12b7824..89a544f 100644 (file)
@@ -323,7 +323,6 @@ $string['invalidforcesubscribe'] = 'Invalid force subscription mode';
 $string['invalidforumid'] = 'Forum ID was incorrect';
 $string['invalidparentpostid'] = 'Parent post ID was incorrect';
 $string['invalidpostid'] = 'Invalid post ID - {$a}';
-$string['inpagereplysubject'] = 'Re: {$a}';
 $string['lastpost'] = 'Last post';
 $string['learningforums'] = 'Learning forums';
 $string['lockdiscussionafter'] = 'Lock discussions after period of inactivity';
@@ -673,3 +672,6 @@ $string['yournewtopic'] = 'Your new discussion topic';
 $string['yourreply'] = 'Your reply';
 $string['forumsubjectdeleted'] = 'This forum post has been removed';
 $string['forumbodydeleted'] = 'The content of this forum post has been removed and can no longer be accessed.';
+
+// Deprecated since Moodle 3.8.
+$string['inpagereplysubject'] = 'Re: {$a}';
index c7f31dd..6343e1d 100644 (file)
     margin: 5px 0;
 }
 
-.discussion-list .userpicture {
+.discussion-list .userpicture,
+.discussion-list .grouppicture {
     height: 35px;
     width: 35px;
 }
index 60e3704..93632b4 100644 (file)
             </div>
         </div>
     {{/forum.capabilities.create}}
+    {{^forum.capabilities.create}}
+        {{#forum.capabilities.selfenrol}}
+            <div class="p-t-1 p-b-1">
+                <a class="btn btn-primary" href="{{forum.urls.create}}">
+                    {{$discussion_create_text}}
+                        {{#str}}addanewdiscussion, forum{{/str}}
+                    {{/discussion_create_text}}
+                </a>
+            </div>
+        {{/forum.capabilities.selfenrol}}
+    {{/forum.capabilities.create}}
+
 
     {{#state.hasdiscussions}}
         {{$discussion_top_pagination}}
                             {{/state.sortorder.iscreateddesc}}
                         </th>
                         <th scope="col">&nbsp;</th>
-                        {{#forum.capabilities.subscribe}}
-                            <th scope="col" class="discussionsubscription"></th>
-                        {{/forum.capabilities.subscribe}}
+                        <th scope="col" class="discussionsubscription"></th>
                     </tr>
                 </thead>
                 {{/discussion_list_header}}
                                                 class="rounded-circle userpicture"
                                                 src="{{urls.profileimage}}"
                                                 alt="{{#str}}pictureof, moodle, {{fullname}}{{/str}}"
+                                                title="{{#str}}pictureof, moodle, {{fullname}}{{/str}}"
                                             >
                                         </div>
                                         <div class="align-middle p-2">
                                 {{/firstpostauthor}}
                             </td>
                             {{#forum.state.groupmode}}
-                                <td scope="col" class="group">
+                                <td scope="col" class="group align-middle">
                                     {{#discussion.group}}
                                         {{#urls.picture}}
                                             {{#urls.userlist}}
-                                                <a href="{{{urls.userlist}}}">
-                                                    <img class="border rounded h-auto rounded-circle" src="{{{urls.picture}}}">
+                                                <a href="{{{urls.userlist}}}" role="button" aria-label='{{#str}} memberofgroup, group, {{name}}{{/str}}'>
+                                                    <img alt="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                                         aria-hidden="true"
+                                                         class="border rounded h-auto rounded-circle grouppicture"
+                                                         src="{{{urls.picture}}}"
+                                                         title="{{#str}} pictureof, core, {{name}} {{/str}}">
                                                 </a>
                                             {{/urls.userlist}}
                                             {{^urls.userlist}}
-                                                <img class="border rounded h-auto rounded-circle" src="{{{urls.picture}}}">
+                                                <img alt="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                                     class="border rounded h-auto rounded-circle grouppicture"
+                                                     src="{{{urls.picture}}}"
+                                                     title="{{#str}} pictureof, core, {{name}} {{/str}}">
                                             {{/urls.userlist}}
                                         {{/urls.picture}}
                                         {{^urls.picture}}
                                             {{#urls.userlist}}
-                                                <a href="{{{urls.userlist}}}">{{name}}</a>
+                                                <a href="{{{urls.userlist}}}" aria-label='{{#str}} memberofgroup, group, {{name}}{{/str}}'>{{name}}</a>
                                             {{/urls.userlist}}
                                             {{^urls.userlist}}
                                                 {{name}}
                                                     class="rounded-circle userpicture"
                                                     src="{{latestpostauthor.urls.profileimage}}"
                                                     alt="{{#str}}pictureof, moodle, {{latestpostauthor.fullname}}{{/str}}"
+                                                    title="{{#str}}pictureof, moodle, {{latestpostauthor.fullname}}{{/str}}"
                                                 >
                                             </a>
                                         </div>
                                         <div class="pt-1 mt-2  {{^discussion.locked}}hidden{{/discussion.locked}}" data-region="locked-icon">
                                             <span class="btn" >{{#pix}}i/lock, core, {{#str}}locked, forum{{/str}}{{/pix}}</span>
                                         </div>
+                                        {{#forum.capabilities.subscribe}}
                                         <div>
                                             {{> mod_forum/discussion_subscription_toggle}}
                                         </div>
+                                        {{/forum.capabilities.subscribe}}
+                                        {{#hasanyactions}}
                                         <div class="mt-3" data-container='discussion-tools'>
                                             {{> mod_forum/forum_action_menu}}
                                         </div>
+                                        {{/hasanyactions}}
                                     </div>
                                 {{/discussion}}
                             </td>
index f4d37ff..8d661ff 100644 (file)
@@ -32,6 +32,7 @@
 
 <div id="discussion-container-{{uniqid}}" data-content="forum-discussion">
 {{#html}}
+    {{#hasanyactions}}
     <div class="d-flex flex-wrap flex-row-reverse m-b-1 text-right" data-container="discussion-tools">
 
         <div class="pl-1">
@@ -41,6 +42,7 @@
         </div>
         <div class="pl-1">{{{subscribe}}}</div>
     </div>
+    {{/hasanyactions}}
     {{{neighbourlinks}}}
 
     <div class="d-flex flex-wrap mb-1">
index e5a8e1d..5023af8 100644 (file)
@@ -58,6 +58,7 @@
                                     src="{{{.}}}"
                                     alt="{{#str}} pictureof, core, {{author.fullname}} {{/str}}"
                                     aria-hidden="true"
+                                    title="{{#str}} pictureof, core, {{author.fullname}} {{/str}}"
                                 >
                             {{/urls.profileimage}}
                         </div>
                     {{#parentauthorname}}
                         <span class="sr-only">{{#str}} inreplyto, mod_forum, {{.}} {{/str}}</span>
                     {{/parentauthorname}}
-                    <h3 class="h6 font-weight-bold mb-0" data-region-content="forum-post-core-subject">{{$subject}}{{{subject}}}{{/subject}}</h3>
+                    <h3 {{!
+                        }}class="h6 font-weight-bold mb-0" {{!
+                        }}data-region-content="forum-post-core-subject" {{!
+                        }}data-reply-subject="{{replysubject}}" {{!
+                        }}>{{$subject}}{{{subject}}}{{/subject}}</h3>
                     {{^isdeleted}}
                         <address tabindex="-1">
                             {{#html.authorsubheading}}{{{.}}}{{/html.authorsubheading}}
                     {{#author}}
                         <div class="mr-2 author-groups-container" style="width: 45px; flex-shrink: 0">
                             {{#groups}}
-                                {{#urls.image}}
-                                    <img
-                                        class="rounded-circle w-100"
-                                        src="{{{.}}}"
-                                        alt="{{#str}} pictureof, core, {{name}} {{/str}}"
-                                        aria-hidden="true"
-                                    >
-                                {{/urls.image}}
+                                {{#urls.group}}
+                                    {{#urls.image}}
+                                        <a href="{{urls.group}}" role="button" aria-label='{{#str}} memberofgroup, group, {{name}}{{/str}}'>
+                                            <img
+                                                 class="rounded-circle w-100"
+                                                 src="{{{.}}}"
+                                                 alt="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                                 aria-hidden="true"
+                                                 title="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                            >
+                                        </a>
+                                    {{/urls.image}}
+                                {{/urls.group}}
+                                {{^urls.group}}
+                                    {{#urls.image}}
+                                        <img class="rounded-circle w-100"
+                                            src="{{{.}}}"
+                                            alt="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                            title="{{#str}} pictureof, core, {{name}} {{/str}}"
+                                        >
+                                    {{/urls.image}}
+                                {{/urls.group}}
                             {{/groups}}
                         </div>
                     {{/author}}
                                                     </a>
                                                 {{/replyoutput}}
                                             {{/reply}}
+                                            {{^reply}}
+                                                {{#selfenrol}}
+                                                    {{$replyoutput}}
+                                                        <a
+                                                                href="{{{urls.reply}}}"
+                                                                class="btn btn-link"
+                                                                data-post-id="{{id}}"
+                                                                data-can-reply-privately="{{canreplyprivately}}"
+                                                                title="{{#str}} reply, mod_forum {{/str}}"
+                                                        >
+                                                            {{#str}} reply, mod_forum {{/str}}
+                                                        </a>
+                                                    {{/replyoutput}}
+                                                {{/selfenrol}}
+                                            {{/reply}}
                                             {{#export}}
                                                 <a
                                                     data-region="post-action"
index 8a99d1d..5ceb5c9 100644 (file)
@@ -41,7 +41,7 @@
                     <textarea rows="5" name="post" title="post" class="w-100" placeholder="{{#str}} replyplaceholder, forum {{/str}}"></textarea>
                     <input type="hidden" name="postformat" value="{{postformat}}"/>
                 </span>
-                <input type="hidden" name="subject" value="{{#str}} inpagereplysubject, forum, {{parentsubject}} {{/str}}"/>
+                <input type="hidden" name="subject" value="{{parentsubject}}"/>
                 <input type="hidden" name="reply" value="{{postid}}"/>
                 <input type="hidden" name="sesskey" value="{{sesskey}}"/>
             </div>
index 425abc7..67cbcdf 100644 (file)
@@ -104,6 +104,7 @@ class mod_forum_exporters_post_testcase extends advanced_testcase {
         $canexport = true;
         $cancontrolreadstatus = true;
         $canreplyprivately = true;
+        $canenrol = true;
         $capabilitymanager = new test_capability_manager(
             $canview,
             $canedit,
@@ -112,7 +113,8 @@ class mod_forum_exporters_post_testcase extends advanced_testcase {
             $canreply,
             $canexport,
             $cancontrolreadstatus,
-            $canreplyprivately
+            $canreplyprivately,
+            $canenrol
         );
         $managerfactory = \mod_forum\local\container::get_manager_factory();
         $entityfactory = \mod_forum\local\container::get_entity_factory();
@@ -157,6 +159,7 @@ class mod_forum_exporters_post_testcase extends advanced_testcase {
         $this->assertEquals($cansplit, $exportedpost->capabilities['split']);
         $this->assertEquals($canreply, $exportedpost->capabilities['reply']);
         $this->assertEquals($canexport, $exportedpost->capabilities['export']);
+        $this->assertEquals($canenrol, $exportedpost->capabilities['selfenrol']);
         $this->assertEquals($cancontrolreadstatus, $exportedpost->capabilities['controlreadstatus']);
         $this->assertNotEmpty($exportedpost->urls['view']);
         $this->assertNotEmpty($exportedpost->urls['viewisolated']);
@@ -416,6 +419,8 @@ class test_capability_manager extends capability_manager {
     private $controlreadstatus;
     /** @var bool $controlreadstatus Value for can_reply_privately_to_post */
     private $canreplyprivatelytopost;
+    /** @var bool $canenrol Value for can_self_enrol */
+    private $canenrol;
 
     /**
      * Constructor.
@@ -436,7 +441,8 @@ class test_capability_manager extends capability_manager {
         bool $reply = true,
         bool $export = true,
         bool $controlreadstatus = true,
-        bool $canreplyprivatelytopost = true
+        bool $canreplyprivatelytopost = true,
+        bool $canenrol = true
     ) {
         $this->view = $view;
         $this->edit = $edit;
@@ -446,6 +452,7 @@ class test_capability_manager extends capability_manager {
         $this->export = $export;
         $this->controlreadstatus = $controlreadstatus;
         $this->canreplyprivatelytopost = $canreplyprivatelytopost;
+        $this->canenrol = $canenrol;
     }
 
     /**
@@ -538,4 +545,13 @@ class test_capability_manager extends capability_manager {
     public function can_reply_privately_to_post(stdClass $user, post_entity $post) : bool {
         return $this->canreplyprivatelytopost;
     }
+
+    /**
+     * Override can_self_enrol
+     * @param stdClass $user
+     * @return bool
+     */
+    public function can_self_enrol(stdClass $user) : bool {
+        return $this->canenrol;
+    }
 }
index 5e903ac..8b6fd97 100644 (file)
@@ -668,6 +668,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
             'hasparent' => true,
             'timecreated' => $discussion1reply2->created,
             'subject' => $discussion1reply2->subject,
+            'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply2->subject}",
             'message' => file_rewrite_pluginfile_urls($discussion1reply2->message, 'pluginfile.php',
                     $forum1context->id, 'mod_forum', 'post', $discussion1reply2->id),
             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
@@ -692,7 +693,8 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                 'reply' => 1,
                 'export' => 0,
                 'controlreadstatus' => 0,
-                'canreplyprivately' => 0
+                'canreplyprivately' => 0,
+                'selfenrol' => 0
             ],
             'urls' => [
                 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply2->discussion, $discussion1reply2->id),
@@ -721,6 +723,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
             'hasparent' => true,
             'timecreated' => $discussion1reply1->created,
             'subject' => $discussion1reply1->subject,
+            'replysubject' => get_string('re', 'mod_forum') . " {$discussion1reply1->subject}",
             'message' => file_rewrite_pluginfile_urls($discussion1reply1->message, 'pluginfile.php',
                     $forum1context->id, 'mod_forum', 'post', $discussion1reply1->id),
             'messageformat' => 1,   // This value is usually changed by external_format_text() function.
@@ -745,7 +748,8 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
                 'reply' => 1,
                 'export' => 0,
                 'controlreadstatus' => 0,
-                'canreplyprivately' => 0
+                'canreplyprivately' => 0,
+                'selfenrol' => 0
             ],
             'urls' => [
                 'view' => $urlfactory->get_view_post_url_from_post_id($discussion1reply1->discussion, $discussion1reply1->id),
diff --git a/mod/imscp/tests/behat/display_imscp_description.feature b/mod/imscp/tests/behat/display_imscp_description.feature
new file mode 100644 (file)
index 0000000..c0024bb
--- /dev/null
@@ -0,0 +1,71 @@
+@mod @mod_imscp
+Feature: Display the IMS content package description in the IMSCP and optionally in the course
+  In order to display the the IMS content package description description in the course
+  As a teacher
+  I need to enable the 'Display description on course page' setting.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+
+  @javascript
+  Scenario: Description is displayed in the IMS content package
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "IMS content package" to section "1"
+    And I set the following fields to these values:
+      | Name | Test IMS content package |
+      | Description | Test IMS content package description |
+    And I upload "mod/imscp/tests/packages/singlescobasic.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I am on "Course 1" course homepage
+    And I should see "Test IMS content package"
+    When I follow "Test IMS content package"
+    Then I should see "Test IMS content package description"
+
+  @javascript
+  Scenario: Show IMS description in the course homepage
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "IMS content package" to section "1"
+    And I set the following fields to these values:
+      | Name | Test IMS content package |
+      | Description | Test IMS content package description |
+    And I upload "mod/imscp/tests/packages/singlescobasic.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I am on "Course 1" course homepage
+    And I follow "Test IMS content package"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I set the following fields to these values:
+      | Display description on course page | 1 |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should see "Test IMS content package description"
+
+  @javascript
+  Scenario: Hide IMS description in the course homepage
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "IMS content package" to section "1"
+    And I set the following fields to these values:
+      | Name | Test IMS content package |
+      | Description | Test IMS content package description |
+    And I upload "mod/imscp/tests/packages/singlescobasic.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I am on "Course 1" course homepage
+    And I follow "Test IMS content package"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should not see "Test IMS content package description"
index f47f6cc..87aefe6 100644 (file)
@@ -70,6 +70,10 @@ if (!$imscp->structure) {
 
 echo $OUTPUT->header();
 echo $OUTPUT->heading(format_string($imscp->name));
+// Info box.
+if ($imscp->intro) {
+    echo $OUTPUT->box(format_module_intro('imscp', $imscp, $cm->id), 'generalbox', 'intro');
+}
 
 imscp_print_content($imscp, $cm, $course);
 
index 212fcb8..2ee7d14 100644 (file)
@@ -37,7 +37,7 @@ class mod_lesson_renderer extends plugin_renderer_base {
      * @return string
      */
     public function header($lesson, $cm, $currenttab = '', $extraeditbuttons = false, $lessonpageid = null, $extrapagetitle = null) {
-        global $CFG;
+        global $CFG, $OUTPUT;
 
         $activityname = format_string($lesson->name, true, $lesson->course);
         if (empty($extrapagetitle)) {
@@ -49,7 +49,7 @@ class mod_lesson_renderer extends plugin_renderer_base {
         // Build the buttons
         $context = context_module::instance($cm->id);
 
-    /// Header setup
+        // Header setup.
         $this->page->set_title($title);
         $this->page->set_heading($this->page->course->fullname);
         lesson_add_header_buttons($cm, $context, $extraeditbuttons, $lessonpageid);
@@ -57,7 +57,10 @@ class mod_lesson_renderer extends plugin_renderer_base {
 
         if (has_capability('mod/lesson:manage', $context)) {
             $output .= $this->output->heading_with_help($activityname, 'overview', 'lesson');
-
+            // Info box.
+            if ($lesson->intro) {
+                $output .= $OUTPUT->box(format_module_intro('lesson', $lesson, $cm->id), 'generalbox', 'intro');
+            }
             if (!empty($currenttab)) {
                 ob_start();
                 include($CFG->dirroot.'/mod/lesson/tabs.php');
@@ -66,6 +69,10 @@ class mod_lesson_renderer extends plugin_renderer_base {
             }
         } else {
             $output .= $this->output->heading($activityname);
+            // Info box.
+            if ($lesson->intro) {
+                $output .= $OUTPUT->box(format_module_intro('lesson', $lesson, $cm->id), 'generalbox', 'intro');
+            }
         }
 
         foreach ($lesson->messages as $message) {
diff --git a/mod/lesson/tests/behat/display_lesson_description.feature b/mod/lesson/tests/behat/display_lesson_description.feature
new file mode 100644 (file)
index 0000000..07b3540
--- /dev/null
@@ -0,0 +1,50 @@
+@mod @mod_lesson
+Feature: Display the lesson description in the lesson and optionally in the course
+  In order to display the the lesson description description in the course
+  As a teacher
+  I need to enable the 'Display description on course page' setting.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add a "Lesson" to section "1"
+    And I set the following fields to these values:
+      | Name | Test lesson |
+      | Description | Test lesson description |
+    And I click on "Save and display" "button"
+
+  Scenario: Description is displayed in the Lesson
+    Given I am on "Course 1" course homepage
+    When I follow "Test lesson"
+    Then I should see "Test lesson description"
+
+  Scenario: Show lesson description in the course homepage
+    Given I am on "Course 1" course homepage
+    And I follow "Test lesson"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I set the following fields to these values:
+      | Display description on course page | 1 |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should see "Test lesson description"
+
+  Scenario: Hide lesson description in the course homepage
+    Given I am on "Course 1" course homepage
+    And I follow "Test lesson"
+    And I navigate to "Edit settings" in current page administration
+    And the following fields match these values:
+      | Display description on course page | |
+    And I press "Save and return to course"
+    When I am on "Course 1" course homepage
+    Then I should not see "Test lesson description"
index c4e843f..39c0c40 100644 (file)
@@ -551,6 +551,9 @@ class quiz_grading_report extends quiz_default_report {
                 case "studentfirstname":
                     $orderby = "u.firstname, u.lastname";
                     break;
+                case "idnumber":
+                    $orderby = "u.idnumber";
+                    break;
             }
         }
 
index 840902b..e2d793b 100644 (file)
@@ -1,4 +1,4 @@
-@mod @mod_quiz
+@mod @mod_quiz @quiz @quiz_grading
 Feature: Basic use of the Manual grading report
   In order to easily find students attempts that need manual grading
   As a teacher
@@ -50,8 +50,15 @@ Feature: Basic use of the Manual grading report
     And "Short answer 001" row "To grade" column of "questionstograde" table should contain "0"
     And "Short answer 001" row "Already graded" column of "questionstograde" table should contain "0"
 
-    # Adjust the mark for Student1.
+    # Go to the grading page.
     And I click on "update grades" "link" in the "Short answer 001" "table_row"
+    And I should see "Grading attempts 1 to 1 of 1"
+
+    # Test the display options.
+    And I set the field "Order attempts" to "By student id number"
+    And I press "Change options"
+
+    # Adjust the mark for Student1.
     And I set the field "Comment" to "I have adjusted your mark to 0.6"
     And I set the field "Mark" to "0.6"
     And I press "Save and go to next page"
diff --git a/mod/scorm/tests/behat/behat_mod_scorm.php b/mod/scorm/tests/behat/behat_mod_scorm.php
new file mode 100644 (file)
index 0000000..ed7e16f
--- /dev/null
@@ -0,0 +1,54 @@
+<?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/>.
+
+/**
+ * Steps definitions related to the SCORM activity module.
+ *
+ * @package    mod_scorm
+ * @category   test
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
+
+use Behat\Behat\Hook\Scope\AfterScenarioScope;
+
+/**
+ * Steps definitions related to the SCORM activity module.
+ *
+ * @package    mod_scorm
+ * @category   test
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_mod_scorm extends behat_base {
+
+    /**
+     * Restart the Seleium Session after each mod_scorm Scenario.
+     *
+     * This prevents issues with the scorm player's onbeforeunload event, and cached SCORM content being served to the
+     * browser in subsequent tests.
+     *
+     * @AfterScenario @mod_scorm
+     * @param AfterScenarioScope $scope The scenario scope
+     */
+    public function reset_after_scorm(AfterScenarioScope $scope) {
+        $this->getSession()->stop();
+    }
+}
diff --git a/pix/movehere.svg b/pix/movehere.svg
new file mode 100644 (file)
index 0000000..c3fde66
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="16" viewBox="0 0 80 16"><style>.st0{fill:none;stroke:#000;stroke-width:2;stroke-miterlimit:10}</style><path d="M25.4 1.5h3.5v1h-3.5zM24.4 1.5h1v3h-1zM25.4 14.5v-4h-1v4h.5zM30.4 1.5h3v1h-3zM34.9 1.5h3v1h-3zM24.4 6h1v3h-1zM66.4 1.5h3v1h-3zM57.4 1.5h3v1h-3zM61.9 1.5h3v1h-3zM70.9 1.5h3v1h-3zM48.4 1.5h3v1h-3zM52.9 1.5h3v1h-3zM43.9 1.5h3v1h-3zM39.4 1.5h3v1h-3zM33.9 13.5h3v1h-3zM69.9 13.5h3v1h-3zM77.4 13.5h-3v1h3V14zM65.4 13.5h3v1h-3zM75.4 1.5v1h2V5h1V1.5zM60.9 13.5h3v1h-3z"/><path d="M25 13.5h-.5v1h3.4v-1zM77.4 6.5h1v3h-1zM77.4 13.9v.6h1V11h-1zM38.4 13.5h3v1h-3zM29.4 13.5h3v1h-3zM42.9 13.5h3v1h-3zM56.4 13.5h3v1h-3zM47.4 13.5h3v1h-3zM51.9 13.5h3v1h-3z"/><path class="st0" d="M13 4l4 4-4 4m4-4H2.8"/></svg>
index e1c8960..9bf51f7 100644 (file)
@@ -500,11 +500,6 @@ li.section.hidden span.commands a.editing_show {
     clear: both;
 }
 
-.section img.movetarget {
-    height: 16px;
-    width: 80px;
-}
-
 input.titleeditor {
     width: 330px;
     vertical-align: text-bottom;
index 90030e2..88a924d 100644 (file)
@@ -47,6 +47,17 @@ $drawer-bg: $gray-lighter !default;
 #nav-drawer {
     right: auto;
     left: 0;
+
+    /* Override the z-indexes defined in bootstrap/_list-group.scss that
+       lead to side effects on the user tours positioning. */
+    .list-group-item-action.active,
+    .list-group-item.active {
+        z-index: inherit;
+    }
+    .list-group-item-action.active + .list-group-item,
+    .list-group-item.active + .list-group-item {
+        border-top: none;
+    }
 }
 #page {
     margin-top: $fixed-header-y;
index a5a2748..da9a574 100644 (file)
@@ -39,6 +39,10 @@ $iconsizes: map-merge((
         height: $icon-big-height;
         font-size: $icon-big-height;
     }
+
+    &.movetarget {
+        width: 80px;
+    }
 }
 
 .navbar-dark a .icon {
index 9d35115..c236c9e 100644 (file)
@@ -10567,6 +10567,8 @@ div.editor_atto_toolbar button .icon {
     width: 64px;
     height: 64px;
     font-size: 64px; }
+  .icon.movetarget {
+    width: 80px; }
 
 .navbar-dark a .icon {
   color: rgba(255, 255, 255, 0.5) !important;
@@ -12040,10 +12042,6 @@ li.section.hidden span.commands a.editing_show {
   text-align: center;
   clear: both; }
 
-.section img.movetarget {
-  height: 16px;
-  width: 80px; }
-
 input.titleeditor {
   width: 330px;
   vertical-align: text-bottom; }
@@ -12602,7 +12600,15 @@ span.editinstructions {
 
 #nav-drawer {
   right: auto;
-  left: 0; }
+  left: 0;
+  /* Override the z-indexes defined in bootstrap/_list-group.scss that
+       lead to side effects on the user tours positioning. */ }
+  #nav-drawer .list-group-item-action.active,
+  #nav-drawer .list-group-item.active {
+    z-index: inherit; }
+  #nav-drawer .list-group-item-action.active + .list-group-item,
+  #nav-drawer .list-group-item.active + .list-group-item {
+    border-top: none; }
 
 #page {
   margin-top: 50px; }
index c28589f..dd8a733 100644 (file)
@@ -10808,6 +10808,8 @@ div.editor_atto_toolbar button .icon {
     width: 64px;
     height: 64px;
     font-size: 64px; }
+  .icon.movetarget {
+    width: 80px; }
 
 .navbar-dark a .icon {
   color: rgba(255, 255, 255, 0.5) !important;
@@ -12282,10 +12284,6 @@ li.section.hidden span.commands a.editing_show {
   text-align: center;
   clear: both; }
 
-.section img.movetarget {
-  height: 16px;
-  width: 80px; }
-
 input.titleeditor {
   width: 330px;
   vertical-align: text-bottom; }
@@ -12849,7 +12847,15 @@ span.editinstructions {
 
 #nav-drawer {
   right: auto;
-  left: 0; }
+  left: 0;
+  /* Override the z-indexes defined in bootstrap/_list-group.scss that
+       lead to side effects on the user tours positioning. */ }
+  #nav-drawer .list-group-item-action.active,
+  #nav-drawer .list-group-item.active {
+    z-index: inherit; }
+  #nav-drawer .list-group-item-action.active + .list-group-item,
+  #nav-drawer .list-group-item.active + .list-group-item {
+    border-top: none; }
 
 #page {
   margin-top: 50px; }
index 2264671..45d92fa 100644 (file)
@@ -68,7 +68,7 @@ class user_filtering {
                                 'country' => 1, 'confirmed' => 1, 'suspended' => 1, 'profile' => 1, 'courserole' => 1,
                                 'anycourses' => 1, 'systemrole' => 1, 'cohort' => 1, 'firstaccess' => 1, 'lastaccess' => 1,
                                 'neveraccessed' => 1, 'timemodified' => 1, 'nevermodified' => 1, 'auth' => 1, 'mnethostid' => 1,
-                                'idnumber' => 1);
+                                'idnumber' => 1, 'lastip' => 1);
         }
 
         $this->_fields  = array();
@@ -154,6 +154,7 @@ class user_filtering {
             case 'nevermodified': return new user_filter_checkbox('nevermodified', get_string('nevermodified', 'filters'), $advanced, array('timemodified', 'timecreated'), array('timemodified_sck', 'timemodified_eck'));
             case 'cohort':      return new user_filter_cohort($advanced);
             case 'idnumber':    return new user_filter_text('idnumber', get_string('idnumber'), $advanced, 'idnumber');
+            case 'lastip':    return new user_filter_text('lastip', get_string('lastip'), $advanced, 'lastip');
             case 'auth':
                 $plugins = core_component::get_plugin_list('auth');
                 $choices = array();