Merge branch 'MDL-57490-master' of git://github.com/danpoltawski/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 18 Jan 2017 04:09:20 +0000 (12:09 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 18 Jan 2017 04:09:20 +0000 (12:09 +0800)
82 files changed:
.eslintignore
.stylelintignore
admin/tool/mobile/classes/api.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/version.php
auth/classes/external.php
auth/email/classes/external.php
badges/classes/external.php
cache/stores/redis/settings.php
cache/stores/static/lib.php
completion/classes/external.php
completion/tests/behat/behat_completion.php
completion/tests/behat/enable_manual_complete_mark.feature
course/externallib.php
enrol/lti/db/install.xml
enrol/lti/db/upgrade.php
enrol/lti/version.php
grade/report/grader/lib.php
grade/report/overview/classes/external.php
grade/report/user/externallib.php
grade/tests/report_graderlib_test.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/clean.js
lib/filestorage/file_storage.php
lib/filestorage/tests/file_storage_test.php
lib/javascript-static.js
lib/moodlelib.php
lib/templates/login.mustache
lib/tests/behat/behat_hooks.php
lib/upgrade.txt
media/player/videojs/amd/build/Youtube-lazy.min.js [new file with mode: 0644]
media/player/videojs/amd/build/Youtube.min.js [deleted file]
media/player/videojs/amd/build/loader.min.js
media/player/videojs/amd/build/video-lazy.min.js [moved from media/player/videojs/amd/build/video.min.js with 100% similarity]
media/player/videojs/amd/src/Youtube-lazy.js [moved from media/player/videojs/amd/src/Youtube.js with 99% similarity]
media/player/videojs/amd/src/loader.js
media/player/videojs/amd/src/video-lazy.js [moved from media/player/videojs/amd/src/video.js with 100% similarity]
media/player/videojs/classes/plugin.php
media/player/videojs/readme_moodle.txt
media/player/videojs/thirdpartylibs.xml
mod/assign/amd/build/participant_selector.min.js
mod/assign/amd/src/participant_selector.js
mod/assign/externallib.php
mod/book/classes/external.php
mod/chat/classes/external.php
mod/choice/classes/external.php
mod/data/classes/external.php
mod/feedback/item/captcha/lib.php
mod/feedback/item/feedback_item_class.php
mod/feedback/item/label/lib.php
mod/feedback/item/multichoice/lib.php
mod/feedback/item/multichoicerated/lib.php
mod/feedback/yui/dragdrop/dragdrop.js
mod/forum/classes/output/big_search_form.php
mod/forum/externallib.php
mod/forum/templates/big_search_form.mustache
mod/forum/tests/externallib_test.php
mod/forum/upgrade.txt
mod/glossary/classes/external.php
mod/imscp/classes/external.php
mod/label/classes/external.php [new file with mode: 0644]
mod/label/db/services.php [new file with mode: 0644]
mod/label/tests/externallib_test.php [new file with mode: 0644]
mod/label/version.php
mod/lesson/report.php
mod/quiz/classes/external.php
mod/resource/lib.php
mod/resource/tests/lib_test.php
mod/scorm/lib.php
mod/scorm/tests/behat/multisco_review_mode.feature [new file with mode: 0644]
mod/survey/classes/external.php
mod/url/classes/external.php
mod/url/db/services.php
mod/url/tests/externallib_test.php
mod/url/version.php
theme/boost/templates/core/login.mustache
theme/boost/templates/core_form/element-template.mustache
theme/boost/templates/mod_forum/big_search_form.mustache

index fe5da7b..452b0df 100644 (file)
@@ -56,7 +56,8 @@ lib/amd/src/chartjs-lazy.js
 lib/maxmind/GeoIp2/
 lib/maxmind/MaxMind/
 lib/ltiprovider/
-media/player/videojs/amd/src/
+media/player/videojs/amd/src/video-lazy.js
+media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
index cafa933..48f1959 100644 (file)
@@ -57,7 +57,8 @@ lib/amd/src/chartjs-lazy.js
 lib/maxmind/GeoIp2/
 lib/maxmind/MaxMind/
 lib/ltiprovider/
-media/player/videojs/amd/src/
+media/player/videojs/amd/src/video-lazy.js
+media/player/videojs/amd/src/Youtube-lazy.js
 media/player/videojs/videojs/
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
index f10fb60..4366823 100644 (file)
@@ -202,6 +202,10 @@ class api {
             $settings->mygradesurl = user_mygrades_url()->out(false);
         }
 
+        if (empty($section) or $section == 'mobileapp') {
+            $settings->tool_mobile_forcelogout = get_config('tool_mobile', 'forcelogout');
+        }
+
         return $settings;
     }
 
index 53081b0..5493521 100644 (file)
@@ -30,6 +30,8 @@ $string['enablesmartappbanners'] = 'Enable Smart App Banners';
 $string['enablesmartappbanners_desc'] = 'This will display a banner promoting the Moodle Mobile app when visiting the site in Mobile Safari.';
 $string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here; otherwise leave the field empty.';
 $string['forcedurlscheme_key'] = 'URL scheme';
+$string['forcelogout'] = 'Force log out';
+$string['forcelogout_desc'] = 'If enabled, the app option \'Change site\' is replaced by \'Log out\'. This results in the user being completely logged out. They must then re-enter their password the next time they wish to access the site.';
 $string['httpsrequired'] = 'HTTPS required';
 $string['invalidprivatetoken'] = 'Invalid private token. Token should not be empty or passed via GET parameter.';
 $string['iosappid'] = 'App\'s unique identifier';
@@ -41,6 +43,7 @@ $string['mobileapp'] = 'Mobile app';
 $string['mobileappearance'] = 'Mobile appearance';
 $string['mobileauthentication'] = 'Mobile authentication';
 $string['mobilecssurl'] = 'CSS';
+$string['mobilefeatures'] = 'Mobile features';
 $string['mobilesettings'] = 'Mobile settings';
 $string['pluginname'] = 'Moodle Mobile tools';
 $string['smartappbanners'] = 'Smart App Banners (iOS only)';
index 9cc8d90..45b4799 100644 (file)
@@ -81,5 +81,17 @@ if ($hassiteconfig) {
                     new lang_string('iosappid_desc', 'tool_mobile'), '633359593', PARAM_ALPHANUM));
 
         $ADMIN->add('mobileapp', $temp);
+
+        // Features related settings.
+        $temp = new admin_settingpage('mobilefeatures', new lang_string('mobilefeatures', 'tool_mobile'));
+
+        $temp->add(new admin_setting_heading('tool_mobile/logout',
+                    new lang_string('logout'), ''));
+
+        $temp->add(new admin_setting_configcheckbox('tool_mobile/forcelogout',
+                    new lang_string('forcelogout', 'tool_mobile'),
+                    new lang_string('forcelogout_desc', 'tool_mobile'), 0));
+
+        $ADMIN->add('mobileapp', $temp);
     }
 }
index f145cd0..bed4eb0 100644 (file)
@@ -144,6 +144,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             array('name' => 'commentsperpage', 'value' => $CFG->commentsperpage),
             array('name' => 'disableuserimages', 'value' => $CFG->disableuserimages),
             array('name' => 'mygradesurl', 'value' => user_mygrades_url()->out(false)),
+            array('name' => 'tool_mobile_forcelogout', 'value' => 0),
         );
         $this->assertCount(0, $result['warnings']);
         $this->assertEquals($expected, $result['settings']);
@@ -151,8 +152,8 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         // Change a value and retrieve filtering by section.
         set_config('commentsperpage', 1);
         $expected[10]['value'] = 1;
-        unset($expected[11]);
-        unset($expected[12]);
+        // Remove not expected elements.
+        array_splice($expected, 11);
 
         $result = external::get_config('frontpagesettings');
         $result = external_api::clean_returnvalue(external::get_config_returns(), $result);
index 87915c0..b6a3b7b 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2016120500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2016120501; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2016112900; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
index 5e0693c..3bf9979 100644 (file)
@@ -43,7 +43,7 @@ class core_auth_external extends external_api {
     /**
      * Describes the parameters for confirm_user.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.2
      */
     public static function confirm_user_parameters() {
index 1f4f7cf..007a7e5 100644 (file)
@@ -59,7 +59,7 @@ class auth_email_external extends external_api {
     /**
      * Describes the parameters for get_signup_settings.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.2
      */
     public static function get_signup_settings_parameters() {
@@ -179,7 +179,7 @@ class auth_email_external extends external_api {
     /**
      * Describes the parameters for signup_user.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.2
      */
     public static function signup_user_parameters() {
index 2a98264..25081bf 100644 (file)
@@ -43,7 +43,7 @@ class core_badges_external extends external_api {
     /**
      * Describes the parameters for get_user_badges.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_user_badges_parameters() {
index 3300b23..0787815 100644 (file)
@@ -47,7 +47,7 @@ if (class_exists('Redis')) { // Only if Redis is available.
             'cachestore_redis/test_serializer',
             get_string('test_serializer', 'cachestore_redis'),
             get_string('test_serializer_desc', 'cachestore_redis'),
-            0,
+            Redis::SERIALIZER_PHP,
             $options
         )
     );
index ecf06dd..72a0c80 100644 (file)
@@ -117,12 +117,19 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
      * @var bool
      */
     protected $simpledata = false;
+
     /**
      * The number of items currently being stored.
      * @var int
      */
     protected $storecount = 0;
 
+    /**
+     * igbinary extension available.
+     * @var bool
+     */
+    protected $igbinaryfound = false;
+
     /**
      * Constructs the store instance.
      *
@@ -203,6 +210,7 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
         $this->store = &self::register_store_id($this->storeid);
         $maxsize = $definition->get_maxsize();
         $this->simpledata = $definition->uses_simple_data();
+        $this->igbinaryfound = extension_loaded('igbinary');
         if ($maxsize !== null) {
             // Must be a positive int.
             $this->maxsize = abs((int)$maxsize);
@@ -219,6 +227,41 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
         return (is_array($this->store));
     }
 
+    /**
+     * Uses igbinary serializer if igbinary extension is loaded.
+     * Fallback to PHP serializer.
+     *
+     * @param mixed $data
+     * The value to be serialized.
+     * @return string a string containing a byte-stream representation of
+     * value that can be stored anywhere.
+     */
+    protected function serialize($data) {
+        if ($this->igbinaryfound) {
+            return igbinary_serialize($data);
+        } else {
+            return serialize($data);
+        }
+    }
+
+    /**
+     * Uses igbinary unserializer if igbinary extension is loaded.
+     * Fallback to PHP unserializer.
+     *
+     * @param string $str
+     * The serialized string.
+     * @return mixed The converted value is returned, and can be a boolean,
+     * integer, float, string,
+     * array or object.
+     */
+    protected function unserialize($str) {
+        if ($this->igbinaryfound) {
+            return igbinary_unserialize($str);
+        } else {
+            return unserialize($str);
+        }
+    }
+
     /**
      * Retrieves an item from the cache store given its key.
      *
@@ -233,7 +276,7 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
         $key = $key['key'];
         if (isset($this->store[$key])) {
             if ($this->store[$key]['serialized']) {
-                return unserialize($this->store[$key]['data']);
+                return $this->unserialize($this->store[$key]['data']);
             } else {
                 return $this->store[$key]['data'];
             }
@@ -261,7 +304,7 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
             $return[$key] = false;
             if (isset($this->store[$key])) {
                 if ($this->store[$key]['serialized']) {
-                    $return[$key] = unserialize($this->store[$key]['data']);
+                    $return[$key] = $this->unserialize($this->store[$key]['data']);
                 } else {
                     $return[$key] = $this->store[$key]['data'];
                 }
@@ -292,7 +335,7 @@ class cachestore_static extends static_data_store implements cache_is_key_aware,
             $this->store[$key]['data'] = $data;
             $this->store[$key]['serialized'] = false;
         } else {
-            $this->store[$key]['data'] = serialize($data);
+            $this->store[$key]['data'] = $this->serialize($data);
             $this->store[$key]['serialized'] = true;
         }
 
index c32b0b1..7979e61 100644 (file)
@@ -43,7 +43,7 @@ class core_completion_external extends external_api {
     /**
      * Describes the parameters for update_activity_completion_status_manually.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 2.9
      */
     public static function update_activity_completion_status_manually_parameters() {
@@ -383,7 +383,7 @@ class core_completion_external extends external_api {
     /**
      * Describes the parameters for mark_course_self_completed.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function mark_course_self_completed_parameters() {
index 5d80be0..c85f83d 100644 (file)
@@ -121,13 +121,14 @@ class behat_completion extends behat_base {
     public function activity_marked_as_complete($activityname, $activitytype, $completiontype) {
         if ($completiontype == "manual") {
             $imgalttext = get_string("completion-alt-manual-y", 'core_completion', $activityname);
+            $xpathtocheck = "//input[@type='image'][contains(@alt, '$imgalttext')]";
         } else {
             $imgalttext = get_string("completion-alt-auto-y", 'core_completion', $activityname);
+            $xpathtocheck = "//img[contains(@alt, '$imgalttext')]";
         }
         $activityxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]";
         $activityxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]";
 
-        $xpathtocheck = "//img[contains(@alt, '$imgalttext')]";
         $this->execute("behat_general::should_exist_in_the",
             array($xpathtocheck, "xpath_element", $activityxpath, "xpath_element")
         );
@@ -142,13 +143,14 @@ class behat_completion extends behat_base {
     public function activity_marked_as_not_complete($activityname, $activitytype, $completiontype) {
         if ($completiontype == "manual") {
             $imgalttext = get_string("completion-alt-manual-n", 'core_completion', $activityname);
+            $xpathtocheck = "//input[@type='image'][contains(@alt, '$imgalttext')]";
         } else {
             $imgalttext = get_string("completion-alt-auto-n", 'core_completion', $activityname);
+            $xpathtocheck = "//img[contains(@alt, '$imgalttext')]";
         }
         $activityxpath = "//li[contains(concat(' ', @class, ' '), ' modtype_" . strtolower($activitytype) . " ')]";
         $activityxpath .= "[descendant::*[contains(text(), '" . $activityname . "')]]";
 
-        $xpathtocheck = "//img[contains(@alt, '$imgalttext')]";
         $this->execute("behat_general::should_exist_in_the",
             array($xpathtocheck, "xpath_element", $activityxpath, "xpath_element")
         );
index c7aeb27..443dc22 100644 (file)
@@ -25,15 +25,16 @@ Feature: Allow students to manually mark an activity as complete
     And I set the following fields to these values:
       | Enable completion tracking | Yes |
     And I press "Save and display"
-    When I add a "Forum" to section "1" and I fill the form with:
+    And I add a "Forum" to section "1" and I fill the form with:
       | Forum name | Test forum name |
       | Description | Test forum description |
-    Then "Student First" user has not completed "Test forum name" activity
+    And "Student First" user has not completed "Test forum name" activity
     And I log out
     And I log in as "student1"
     And I am on site homepage
     And I follow "Course 1"
-    And I press "Mark as complete: Test forum name"
+    When I press "Mark as complete: Test forum name"
+    Then the "Test forum name" "forum" activity with "manual" completion should be marked as complete
     And I log out
     And I log in as "teacher1"
     And I am on site homepage
index 4a57ae5..d509033 100644 (file)
@@ -2012,7 +2012,7 @@ class core_course_external extends external_api {
     /**
      * Describes the parameters for delete_modules.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 2.5
      */
     public static function delete_modules_parameters() {
index e2d1f9e..c724a4b 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="enrol/lti/db" VERSION="20160809" COMMENT="XMLDB file for Moodle enrol/lti"
+<XMLDB PATH="enrol/lti/db" VERSION="20170113" COMMENT="XMLDB file for Moodle enrol/lti"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
 >
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="consumerid" TYPE="int" LENGTH="11" NOTNULL="true" SEQUENCE="false"/>
-        <FIELD NAME="value" TYPE="char" LENGTH="32" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="value" TYPE="char" LENGTH="64" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="expires" TYPE="int" LENGTH="10"  NOTNULL="true" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
index 94a90cc..6023fac 100644 (file)
@@ -242,6 +242,19 @@ function xmldb_enrol_lti_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2016052304, 'enrol', 'lti');
     }
 
+    if ($oldversion < 2017011300) {
+
+        // Changing precision of field value on table enrol_lti_lti2_nonce to (64).
+        $table = new xmldb_table('enrol_lti_lti2_nonce');
+        $field = new xmldb_field('value', XMLDB_TYPE_CHAR, '64', null, XMLDB_NOTNULL, null, null, 'consumerid');
+
+        // Launch change of precision for field value.
+        $dbman->change_field_precision($table, $field);
+
+        // Lti savepoint reached.
+        upgrade_plugin_savepoint(true, 2017011300, 'enrol', 'lti');
+    }
+
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 366f925..11167f1 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version = 2017011300; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires = 2016112900; // Requires this Moodle version (3.1)
 $plugin->component = 'enrol_lti'; // Full name of the plugin (used for diagnostics).
index 2d9378b..d154df1 100644 (file)
@@ -919,6 +919,11 @@ class grade_report_grader extends grade_report {
         // user.
         if (!$this->canviewhidden) {
             $allgradeitems = grade_item::fetch_all(array('courseid' => $this->courseid));
+
+            // But hang on - don't include ones which are set to not show the grade at all.
+            $allgradeitems = array_filter($allgradeitems, function($item) {
+                return $item->gradetype != GRADE_TYPE_NONE;
+            });
         }
 
         foreach ($this->users as $userid => $user) {
index abdb080..8b73f27 100644 (file)
@@ -42,7 +42,7 @@ class gradereport_overview_external extends external_api {
     /**
      * Describes the parameters for get_course_grades.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.2
      */
     public static function get_course_grades_parameters() {
index 9ed53bd..d002d00 100644 (file)
@@ -208,7 +208,7 @@ class gradereport_user_external extends external_api {
     /**
      * Describes the parameters for get_grades_table.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 2.9
      */
     public static function get_grades_table_parameters() {
@@ -424,7 +424,7 @@ class gradereport_user_external extends external_api {
     /**
      * Describes the parameters for get_grade_items.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.2
      */
     public static function get_grade_items_parameters() {
index 8282265..d65e285 100644 (file)
@@ -230,6 +230,74 @@ class core_grade_report_graderlib_testcase extends advanced_testcase {
         $this->assertEquals(count($toobigvalue['gradesonly']) - 1, count($report1->collapsed['gradesonly']));
     }
 
+    /**
+     * Tests the get_right_rows function with one 'normal' and one 'ungraded' quiz.
+     *
+     * Previously, with an ungraded quiz (which results in a grade item with type GRADETYPE_NONE)
+     * there was a bug in get_right_rows in some situations.
+     */
+    public function test_get_right_rows() {
+        global $USER, $DB;
+        $this->resetAfterTest(true);
+
+        // Create manager and student on a course.
+        $generator = $this->getDataGenerator();
+        $manager = $generator->create_user();
+        $student = $generator->create_user();
+        $course = $generator->create_course();
+        $generator->enrol_user($manager->id, $course->id, 'manager');
+        $generator->enrol_user($student->id, $course->id, 'student');
+
+        // Create a couple of quizzes on the course.
+        $normalquiz = $generator->create_module('quiz', array('course' => $course->id,
+                'name' => 'NormalQuiz'));
+        $ungradedquiz = $generator->create_module('quiz', array('course' => $course->id,
+                'name' => 'UngradedQuiz'));
+
+        // Set the grade for the second one to 0 (note, you have to do this after creating it,
+        // otherwise it doesn't create an ungraded grade item).
+        $ungradedquiz->instance = $ungradedquiz->id;
+        quiz_set_grade(0, $ungradedquiz);
+
+        // Set current user.
+        $this->setUser($manager);
+        $USER->gradeediting[$course->id] = false;
+
+        // Get the report.
+        $report = $this->create_report($course);
+        $report->load_users();
+        $report->load_final_grades();
+        $result = $report->get_right_rows(false);
+
+        // There should be 3 rows (2 header rows, plus the grades for the first user).
+        $this->assertCount(3, $result);
+
+        // The second row should contain 2 cells - one for the graded quiz and course total.
+        $this->assertCount(2, $result[1]->cells);
+        $this->assertContains('NormalQuiz', $result[1]->cells[0]->text);
+        $this->assertContains('Course total', $result[1]->cells[1]->text);
+
+        // User row should contain grade values '-'.
+        $this->assertCount(2, $result[2]->cells);
+        $this->assertContains('>-<', $result[2]->cells[0]->text);
+        $this->assertContains('>-<', $result[2]->cells[1]->text);
+
+        // Supposing the user cannot view hidden grades, this shouldn't make any difference (due
+        // to a bug, it previously did).
+        $context = context_course::instance($course->id);
+        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
+        assign_capability('moodle/grade:viewhidden', CAP_PROHIBIT, $managerroleid, $context->id, true);
+        $context->mark_dirty();
+        $this->assertFalse(has_capability('moodle/grade:viewhidden', $context));
+
+        // Recreate the report. Confirm it returns successfully still.
+        $report = $this->create_report($course);
+        $report->load_users();
+        $report->load_final_grades();
+        $result = $report->get_right_rows(false);
+        $this->assertCount(3, $result);
+    }
+
     private function create_grade_category($course) {
         static $cnt = 0;
         $cnt++;
index edab4a1..90e0996 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index b706ced..22df82b 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 9d68afe..1b88fea 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index bd167c5..1763547 100644 (file)
@@ -147,9 +147,9 @@ EditorClean.prototype = {
                     isHTML = (types.indexOf('text/html') > -1);
                 }
 
+                var content;
                 if (isHTML) {
                     // Get the clipboard content.
-                    var content;
                     try {
                         content = event.clipboardData.getData('text/html');
                     } catch (error) {
@@ -179,10 +179,16 @@ EditorClean.prototype = {
                     this.updateOriginal();
                     return false;
                 } else {
-                    // Due to poor cross browser clipboard compatibility, the failure to find html doesn't mean it isn't there.
-                    // Wait for the clipboard event to finish then fallback clean the entire editor.
-                    this.fallbackPasteCleanupDelayed();
-                    return true;
+                    try {
+                        // Plaintext clipboard content can only be retrieved this way.
+                        content = event.clipboardData.getData('text');
+                    } catch (error) {
+                        // Something went wrong. Fallback.
+                        // Due to poor cross browser clipboard compatibility, the failure to find html doesn't mean it isn't there.
+                        // Wait for the clipboard event to finish then fallback clean the entire editor.
+                        this.fallbackPasteCleanupDelayed();
+                        return true;
+                    }
                 }
             } else {
                 // If we reached a here, this probably means the browser has limited (or no) clipboard support.
index 0fb9922..c481ebe 100644 (file)
@@ -799,10 +799,12 @@ class file_storage {
      * @param string $sort A fragment of SQL to use for sorting
      * @param bool $includedirs whether or not include directories
      * @param int $updatedsince return files updated since this time
+     * @param int $limitfrom return a subset of records, starting at this point (optional).
+     * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set).
      * @return stored_file[] array of stored_files indexed by pathanmehash
      */
     public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename",
-                                    $includedirs = true, $updatedsince = 0) {
+            $includedirs = true, $updatedsince = 0, $limitfrom = 0, $limitnum = 0) {
         global $DB;
 
         list($areasql, $conditions) = $DB->get_in_or_equal($filearea, SQL_PARAMS_NAMED);
@@ -824,6 +826,16 @@ class file_storage {
             $updatedsincesql = 'AND f.timemodified > :time';
         }
 
+        $includedirssql = '';
+        if (!$includedirs) {
+            $includedirssql = 'AND f.filename != :dot';
+            $conditions['dot'] = '.';
+        }
+
+        if ($limitfrom && !$limitnum) {
+            throw new coding_exception('If specifying $limitfrom you must also specify $limitnum');
+        }
+
         $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
                   FROM {files} f
              LEFT JOIN {files_reference} r
@@ -831,6 +843,7 @@ class file_storage {
                  WHERE f.contextid = :contextid
                        AND f.component = :component
                        AND f.filearea $areasql
+                       $includedirssql
                        $updatedsincesql
                        $itemidsql";
         if (!empty($sort)) {
@@ -838,11 +851,8 @@ class file_storage {
         }
 
         $result = array();
-        $filerecords = $DB->get_records_sql($sql, $conditions);
+        $filerecords = $DB->get_records_sql($sql, $conditions, $limitfrom, $limitnum);
         foreach ($filerecords as $filerecord) {
-            if (!$includedirs and $filerecord->filename === '.') {
-                continue;
-            }
             $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
         }
         return $result;
index 45b89cb..f16ada7 100644 (file)
@@ -466,6 +466,17 @@ class core_files_file_storage_testcase extends advanced_testcase {
             $this->assertEquals($key, $file->get_pathnamehash());
         }
 
+        // Test the limit feature to retrieve each individual file.
+        $limited = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'filename', false,
+                0, 0, 1);
+        $mapfunc = function($f) {
+            return $f->get_filename();
+        };
+        $this->assertEquals(array('1.txt'), array_values(array_map($mapfunc, $limited)));
+        $limited = $fs->get_area_files($user->ctxid, 'user', 'private', false, 'filename', false,
+                0, 1, 50);
+        $this->assertEquals(array('2.txt', '3.txt'), array_values(array_map($mapfunc, $limited)));
+
         // Test with an itemid with no files.
         $areafiles = $fs->get_area_files($user->ctxid, 'user', 'private', 666, 'sortorder', false);
         // Should be none.
index a9df26a..d2ce894 100644 (file)
@@ -812,42 +812,21 @@ M.util.get_string = function(identifier, component, a) {
 };
 
 /**
- * Set focus on username or password field of the login form
+ * Set focus on username or password field of the login form.
+ * @deprecated since Moodle 3.3.
  */
 M.util.focus_login_form = function(Y) {
-    var username = Y.one('#username');
-    var password = Y.one('#password');
-
-    if (username == null || password == null) {
-        // something is wrong here
-        return;
-    }
-
-    var curElement = document.activeElement
-    if (curElement == 'undefined') {
-        // legacy browser - skip refocus protection
-    } else if (curElement.tagName == 'INPUT') {
-        // user was probably faster to focus something, do not mess with focus
-        return;
-    }
-
-    if (username.get('value') == '') {
-        username.focus();
-    } else {
-        password.focus();
-    }
-}
+    Y.log('M.util.focus_login_form no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
+};
 
 /**
- * Set focus on login error message
+ * Set focus on login error message.
+ * @deprecated since Moodle 3.3.
  */
 M.util.focus_login_error = function(Y) {
-    var errorlog = Y.one('#loginerrormessage');
+    Y.log('M.util.focus_login_error no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
+};
 
-    if (errorlog) {
-        errorlog.focus();
-    }
-}
 /**
  * Adds lightbox hidden element that covers the whole node.
  *
index 2eefa09..966112f 100644 (file)
@@ -6589,7 +6589,9 @@ function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0
 
     $filesize = array();
     $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
-                      5242880, 10485760, 20971520, 52428800, 104857600);
+                      5242880, 10485760, 20971520, 52428800, 104857600,
+                      262144000, 524288000, 786432000, 1073741824,
+                      2147483648, 4294967296, 8589934592);
 
     // If custombytes is given and is valid then add it to the list.
     if (is_number($custombytes) and $custombytes > 0) {
index 3ee5e2a..fa5836f 100644 (file)
 </div>
 
 {{#js}}
-    require(['jquery', 'core/yui'], function($, Y) {
     {{#error}}
-        $(function() {
-            M.util.focus_login_error(Y);
+        require(['jquery'], function($) {
+            $('#loginerrormessage').focus();
         });
     {{/error}}
     {{^error}}
         {{#autofocusform}}
-            $(function() {
-                M.util.focus_login_form(Y);
+            require(['jquery'], function($) {
+                if ($('#username').val()) {
+                    $('#password').focus();
+                } else {
+                    $('#username').focus();
+                }
             });
         {{/autofocusform}}
     {{/error}}
-    });
 {{/js}}
index 239dd99..d5254b1 100644 (file)
@@ -429,6 +429,11 @@ class behat_hooks extends behat_base {
     public function after_step_javascript(AfterStepScope $scope) {
         global $CFG, $DB;
 
+        // If step is undefined then throw exception, to get failed exit code.
+        if ($scope->getTestResult()->getResultCode() === Behat\Behat\Tester\Result\StepResult::UNDEFINED) {
+            throw new coding_exception("Step '" . $scope->getStep()->getText() . "'' is undefined.");
+        }
+
         // Save the page content if the step failed.
         if (!empty($CFG->behat_faildump_path) &&
             $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
index c856437..2462ad7 100644 (file)
@@ -8,6 +8,8 @@ information provided here is intended especially for developers.
 * $mform->init_javascript_enhancement() is deprecated and no longer does anything. Existing uses of smartselect enhancement
   should be switched to the searchableselector form element or other solutions.
 * Return value of the validate_email() is now proper boolean as documented. Previously the function could return 1, 0 or false.
+* M.util.focus_login_form and M.util.focus_login_error no longer do anything. Please use jquery instead. See
+  lib/templates/login.mustache for an example.
 * Some outdated global JS functions have been removed and should be replaced with calls to jquery or alternative approaches:
     checkall, checknone, select_all_in_element_with_id, select_all_in, deselect_all_in, confirm_if, findParentNode,
     filterByParent, stripHTML
diff --git a/media/player/videojs/amd/build/Youtube-lazy.min.js b/media/player/videojs/amd/build/Youtube-lazy.min.js
new file mode 100644 (file)
index 0000000..7e010ea
Binary files /dev/null and b/media/player/videojs/amd/build/Youtube-lazy.min.js differ
diff --git a/media/player/videojs/amd/build/Youtube.min.js b/media/player/videojs/amd/build/Youtube.min.js
deleted file mode 100644 (file)
index 785b901..0000000
Binary files a/media/player/videojs/amd/build/Youtube.min.js and /dev/null differ
index 436b2da..6c77c7f 100644 (file)
Binary files a/media/player/videojs/amd/build/loader.min.js and b/media/player/videojs/amd/build/loader.min.js differ
similarity index 99%
rename from media/player/videojs/amd/src/Youtube.js
rename to media/player/videojs/amd/src/Youtube-lazy.js
index c2391da..571c602 100644 (file)
@@ -24,7 +24,7 @@ THE SOFTWARE. */
   if(typeof exports==='object' && typeof module!=='undefined') {
     module.exports = factory(require('video.js'));
   } else if(typeof define === 'function' && define.amd) {
-    define(['media_videojs/video'], function(videojs){
+    define(['media_videojs/video-lazy'], function(videojs){
       return (root.Youtube = factory(videojs));
     });
   } else {
index be7a1f0..2601cf8 100644 (file)
  * @copyright  2016 Frédéric Massart - FMCorz.net
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'media_videojs/video', 'core/event'], function($, videojs, Event) {
+define(['jquery', 'core/event'], function($, Event) {
+
+    /**
+     * Stores the method we need to execute on the first load of videojs module.
+     */
+    var onload;
 
     /**
      * Set-up.
      *
      * Adds the listener for the event to then notify video.js.
+     * @param {Function} executeonload function to execute when media_videojs/video is loaded
      */
-    var setUp = function() {
+    var setUp = function(executeonload) {
+        onload = executeonload;
+        // Notify Video.js about the nodes already present on the page.
+        notifyVideoJS(null, $('body'));
         // We need to call popover automatically if nodes are added to the page later.
         Event.getLegacyEvents().done(function(events) {
             $(document).on(events.FILTER_CONTENT_UPDATED, notifyVideoJS);
@@ -53,9 +62,20 @@ define(['jquery', 'media_videojs/video', 'core/event'], function($, videojs, Eve
             .addBack(selector)
             .find('audio, video').each(function() {
                 var id = $(this).attr('id'),
-                    config = $(this).data('setup');
+                    config = $(this).data('setup'),
+                    modules = ['media_videojs/video-lazy'];
 
-                videojs(id, config);
+                if (config.techOrder && config.techOrder.indexOf('youtube') !== -1) {
+                    // Add YouTube to the list of modules we require.
+                    modules.push('media_videojs/Youtube-lazy');
+                }
+                require(modules, function(videojs) {
+                    if (onload) {
+                        onload(videojs);
+                        onload = null;
+                    }
+                    videojs(id, config);
+                });
             });
     };
 
index ff82f98..f13352f 100644 (file)
@@ -334,21 +334,19 @@ class media_videojs_plugin extends core_media_player_native {
      */
     public function setup($page) {
 
-        // Load core video JS.
+        // Load dynamic loader. It will scan page for videojs media and load necessary modules.
+        // Loader will be loaded on absolutely every page, however the videojs will only be loaded
+        // when video is present on the page or added later to it in AJAX.
         $path = new moodle_url('/media/player/videojs/videojs/video-js.swf');
         $contents = 'videojs.options.flash.swf = "' . $path . '";' . "\n";
         $contents .= $this->find_language(current_language());
         $page->requires->js_amd_inline(<<<EOT
-require(["media_videojs/video"], function(videojs) {
-$contents
+require(["media_videojs/loader"], function(loader) {
+    loader.setUp(function(videojs) {
+        $contents
+    });
 });
 EOT
         );
-
-        // Load Youtube JS.
-        $page->requires->js_amd_inline('require(["media_videojs/Youtube"])');
-
-        // Load dynamic loader.
-        $page->requires->js_call_amd('media_videojs/loader', 'setUp');
     }
 }
index ce84109..4799afd 100644 (file)
@@ -5,7 +5,7 @@ https://github.com/videojs/video.js
 Instructions to import VideoJS player into Moodle:
 
 1. Download the latest release from https://github.com/videojs/video.js/releases in a separate directory
-2. copy 'dist/video.js' into 'amd/src/video.js'
+2. copy 'dist/video.js' into 'amd/src/video-lazy.js'
 3. copy 'dist/font/' into 'fonts/' folder
 4. copy 'dist/video-js.css' into 'styles.css'
    Replace
@@ -21,8 +21,8 @@ Instructions to import VideoJS player into Moodle:
 
 Import plugins:
 
-1. Copy https://github.com/videojs/videojs-youtube/blob/master/dist/Youtube.js into 'amd/src/Youtube.js'
+1. Copy https://github.com/videojs/videojs-youtube/blob/master/dist/Youtube.js into 'amd/src/Youtube-lazy.js'
    In the beginning of the js file replace
      define(['videojs']
    with
-     define(['media_videojs/video']
+     define(['media_videojs/video-lazy']
index de0b9bc..ff41fe9 100644 (file)
@@ -1,12 +1,19 @@
 <?xml version="1.0"?>
 <libraries>
     <library>
-        <location>amd/src</location>
+        <location>amd/src/video-lazy.js</location>
         <name>VideoJS</name>
         <license>Apache</license>
         <version>5.12.6</version>
         <licenseversion></licenseversion>
     </library>
+    <library>
+        <location>amd/src/Youtube-lazy.js</location>
+        <name>YouTube Playback Technology for Video.js</name>
+        <license>MIT</license>
+        <version>2.1.1</version>
+        <licenseversion></licenseversion>
+    </library>
     <library>
         <location>videojs</location>
         <name>VideoJS support files</name>
index c5b0452..671a70a 100644 (file)
Binary files a/mod/assign/amd/build/participant_selector.min.js and b/mod/assign/amd/build/participant_selector.min.js differ
index f03466c..60b6816 100644 (file)
@@ -35,12 +35,7 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
          * @return {Array}
          */
         processResults: function(selector, data) {
-            var results = [];
-            var i = 0;
-            for (i = 0; i < data.length; i++) {
-                results[i] = {value: data[i].id, label: data[i].label};
-            }
-            return results;
+            return data;
         },
 
         /**
@@ -62,12 +57,10 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
                 filterstrings[$(element).attr('name')] = $(element).prop('checked');
             });
 
-            var promise = ajax.call([{
+            ajax.call([{
                 methodname: 'mod_assign_list_participants',
                 args: {assignid: assignmentid, groupid: groupid, filter: query, limit: 30, includeenrolments: false}
-            }]);
-
-            promise[0].then(function(results) {
+            }])[0].then(function(results) {
                 var promises = [];
                 var identityfields = $('[data-showuseridentity]').data('showuseridentity').split(',');
 
@@ -94,23 +87,18 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
                             }
                         });
                         ctx.identity = identity.join(', ');
-                        promises.push(templates.render('mod_assign/list_participant_user_summary', ctx));
+                        promises.push(templates.render('mod_assign/list_participant_user_summary', ctx).then(function(html) {
+                            return {value: user.id, label: html};
+                        }));
                     }
                 });
-
-                // When all the templates have been rendered, call the success handler.
-                $.when.apply($.when, promises).then(function() {
-                    var args = arguments,
-                        i = 0;
-
-                    $.each(results, function(index, user) {
-                        user.label = args[i];
-                        i++;
-                    });
-
-                    success(results);
-                });
-            }, failure);
+                // Do the dance for $.when()
+                return $.when.apply($, promises);
+            }).then(function() {
+                // Undo the $.when() dance from arguments object into an array..
+                var users = Array.prototype.slice.call(arguments);
+                success(users);
+            }).catch(failure);
         }
     };
 });
index 41f74c3..76614d3 100644 (file)
@@ -70,7 +70,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for get_grades
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.4
      */
     public static function get_grades_parameters() {
@@ -660,7 +660,7 @@ class mod_assign_external extends external_api {
     /**
      * Describes the parameters for get_submissions
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 2.5
      */
     public static function get_submissions_parameters() {
@@ -1317,7 +1317,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for lock_submissions
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.6
      */
     public static function lock_submissions_parameters() {
@@ -1374,7 +1374,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for revert_submissions_to_draft
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.6
      */
     public static function revert_submissions_to_draft_parameters() {
@@ -1431,7 +1431,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for unlock_submissions
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.6
      */
     public static function unlock_submissions_parameters() {
@@ -1488,7 +1488,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for submit_grading_form webservice.
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 3.1
      */
     public static function submit_grading_form_parameters() {
@@ -1566,7 +1566,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the return for submit_grading_form
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 3.1
      */
     public static function submit_grading_form_returns() {
@@ -1575,7 +1575,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for submit_for_grading
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.6
      */
     public static function submit_for_grading_parameters() {
@@ -1630,7 +1630,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for save_user_extensions
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.6
      */
     public static function save_user_extensions_parameters() {
@@ -1703,7 +1703,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for reveal_identities
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.6
      */
     public static function reveal_identities_parameters() {
@@ -1752,7 +1752,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for save_submission
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.6
      */
     public static function save_submission_parameters() {
@@ -1828,7 +1828,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for save_grade
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.6
      */
     public static function save_grade_parameters() {
@@ -1964,7 +1964,7 @@ class mod_assign_external extends external_api {
 
     /**
      * Describes the parameters for save_grades
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since  Moodle 2.7
      */
     public static function save_grades_parameters() {
@@ -2220,7 +2220,7 @@ class mod_assign_external extends external_api {
     /**
      * Describes the parameters for view_submission_status.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function view_submission_status_parameters() {
@@ -2274,7 +2274,7 @@ class mod_assign_external extends external_api {
     /**
      * Describes the parameters for get_submission_status.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_submission_status_parameters() {
@@ -2827,7 +2827,7 @@ class mod_assign_external extends external_api {
     /**
      * Describes the parameters for view_assign.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.2
      */
     public static function view_assign_parameters() {
index 3fde48d..189d2c6 100644 (file)
@@ -152,7 +152,7 @@ class mod_book_external extends external_api {
     /**
      * Describes the parameters for get_books_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function get_books_by_courses_parameters() {
index cd08786..50a962e 100644 (file)
@@ -494,7 +494,7 @@ class mod_chat_external extends external_api {
     /**
      * Describes the parameters for get_chats_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function get_chats_by_courses_parameters() {
index 48053d3..06d30eb 100644 (file)
@@ -42,7 +42,7 @@ class mod_choice_external extends external_api {
     /**
      * Describes the parameters for get_choices_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function get_choice_results_parameters() {
@@ -167,7 +167,7 @@ class mod_choice_external extends external_api {
     /**
      * Describes the parameters for mod_choice_get_choice_options.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function get_choice_options_parameters() {
@@ -288,7 +288,7 @@ class mod_choice_external extends external_api {
     /**
      * Describes the parameters for submit_choice_response.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function submit_choice_response_parameters() {
@@ -449,7 +449,7 @@ class mod_choice_external extends external_api {
     /**
      * Describes the parameters for get_choices_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function get_choices_by_courses_parameters() {
@@ -587,7 +587,7 @@ class mod_choice_external extends external_api {
     /**
      * Describes the parameters for delete_choice_responses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function delete_choice_responses_parameters() {
index 8a53280..0afbe03 100644 (file)
@@ -42,7 +42,7 @@ class mod_data_external extends external_api {
     /**
      * Describes the parameters for get_databases_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 2.9
      */
     public static function get_databases_by_courses_parameters() {
index 3e19008..8ff8716 100644 (file)
@@ -120,20 +120,25 @@ class feedback_item_captcha extends feedback_item_base {
         $inputname = $item->typ . '_' . $item->id;
 
         if ($form->get_mode() != mod_feedback_complete_form::MODE_COMPLETE) {
+            // Span to hold the element id. The id is used for drag and drop reordering.
             $form->add_form_element($item,
-                    ['static', $inputname, $name],
+                    ['static', $inputname, $name, html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])],
                     false,
                     false);
         } else {
+            // Add recaptcha element that is used during the form validation.
             $form->add_form_element($item,
-                    ['recaptcha', $inputname, $name],
+                    ['recaptcha', $inputname . 'recaptcha', $name],
                     false,
                     false);
+            // Add hidden element with value "1" that will be saved in the values table after completion.
+            $form->add_form_element($item, ['hidden', $inputname, 1], false);
+            $form->set_element_type($inputname, PARAM_INT);
         }
 
         // Add recaptcha validation to the form.
         $form->add_validation_rule(function($values, $files) use ($item, $form) {
-            $elementname = $item->typ . '_' . $item->id;
+            $elementname = $item->typ . '_' . $item->id . 'recaptcha';
             $recaptchaelement = $form->get_form_element($elementname);
             if (empty($values['recaptcha_response_field'])) {
                 return array($elementname => get_string('required'));
index ddb3c43..92eb10a 100644 (file)
@@ -294,7 +294,11 @@ class feedback_item_pagebreak extends feedback_item_base {
      */
     public function complete_form_element($item, $form) {
         $form->add_form_element($item,
-                ['static', $item->typ.'_'.$item->id, '', '<hr class="feedback_pagebreak">']);
+            ['static',
+                $item->typ.'_'.$item->id,
+                '',
+                html_writer::empty_tag('hr', ['class' => 'feedback_pagebreak', 'id' => 'feedback_item_' . $item->id])
+            ]);
     }
 
     /**
index 4ea3ae4..6fa83bf 100644 (file)
@@ -194,6 +194,7 @@ class feedback_item_label extends feedback_item_base {
                 $context->id, 'mod_feedback', $filearea, $item->id);
         $formatoptions = array('overflowdiv' => true, 'noclean' => true);
         $output = format_text($output, FORMAT_HTML, $formatoptions);
+        $output = html_writer::div($output, '', ['id' => 'feedback_item_' . $item->id]);
 
         $inputname = $item->typ . '_' . $item->id;
 
index 8e47ca0..d3b628b 100644 (file)
@@ -342,6 +342,8 @@ class feedback_item_multichoice extends feedback_item_base {
                     $objs[] = ['advcheckbox', $inputname.'['.$idx.']', '', $label, null, array(0, $idx)];
                     $form->set_element_type($inputname.'['.$idx.']', PARAM_INT);
                 }
+                // Span to hold the element id. The id is used for drag and drop reordering.
+                $objs[] = ['static', '', '', html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])];
                 $element = $form->add_form_group_element($item, 'group_'.$inputname, $name, $objs, $separator, $class);
                 if ($tmpvalue) {
                     foreach (explode(FEEDBACK_MULTICHOICE_LINE_SEP, $tmpvalue) as $v) {
@@ -357,6 +359,8 @@ class feedback_item_multichoice extends feedback_item_base {
                 foreach ($options as $idx => $label) {
                     $objs[] = ['radio', $inputname.'[0]', '', $label, $idx];
                 }
+                // Span to hold the element id. The id is used for drag and drop reordering.
+                $objs[] = ['static', '', '', html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])];
                 $element = $form->add_form_group_element($item, 'group_'.$inputname, $name, $objs, $separator, $class);
                 $form->set_element_default($inputname.'[0]', $tmpvalue);
                 $form->set_element_type($inputname.'[0]', PARAM_INT);
index 5eb16e3..8be2faa 100644 (file)
@@ -312,6 +312,8 @@ class feedback_item_multichoicerated extends feedback_item_base {
             foreach ($options as $idx => $label) {
                 $objs[] = ['radio', $inputname, '', $label, $idx];
             }
+            // Span to hold the element id. The id is used for drag and drop reordering.
+            $objs[] = ['static', '', '', html_writer::span('', '', ['id' => 'feedback_item_' . $item->id])];
             $separator = $info->horizontal ? ' ' : '<br>';
             $class .= ' multichoicerated-' . ($info->horizontal ? 'horizontal' : 'vertical');
             $el = $form->add_form_group_element($item, 'group_'.$inputname, $name, $objs, $separator, $class);
index 31f117f..ac4af8f 100644 (file)
@@ -167,12 +167,20 @@ YUI.add('moodle-mod_feedback-dragdrop', function(Y) {
                 if (!drop.contains(drag)) {
                     drop.appendChild(drag);
                 }
-                myElements = '';
+                var childElement;
+                var elementId;
+                var elements = [];
                 drop.all(CSS.DRAGITEM).each(function(v) {
-                    myElements = myElements + ',' + this.get_node_id(v.get('id'));
+                    childElement = v.one('.felement').one('[id^="feedback_item_"]');
+                    if (childElement) {
+                        elementId = this.get_node_id(childElement.get('id'));
+                        if (elements.indexOf(elementId) == -1) {
+                            elements.push(elementId);
+                        }
+                    }
                 }, this);
                 var spinner = M.util.add_spinner(Y, dragnode);
-                this.save_item_order(this.cmid, myElements, spinner);
+                this.save_item_order(this.cmid, elements.toString(), spinner);
            }
         },
 
index 6a983d6..e2bd7ce 100644 (file)
@@ -53,6 +53,8 @@ class big_search_form implements renderable, templatable {
     public $subject;
     public $user;
     public $words;
+    /** @var string The URL of the search form. */
+    public $actionurl;
 
     /**
      * Constructor.
@@ -65,6 +67,7 @@ class big_search_form implements renderable, templatable {
         $this->course = $course;
         $this->scripturl = new moodle_url('/mod/forum/forum.js');
         $this->showfullwords = $DB->get_dbfamily() == 'mysql' || $DB->get_dbfamily() == 'postgres';
+        $this->actionurl = new moodle_url('/mod/forum/search.php');
 
         $forumoptions = ['' => get_string('allforums', 'forum')] + forum_menu_list($course);
         $this->forumoptions = array_map(function($option) use ($forumoptions) {
@@ -161,6 +164,7 @@ class big_search_form implements renderable, templatable {
         $data->subject = $this->subject;
         $data->user = $this->user;
         $data->showfullwords = $this->showfullwords;
+        $data->actionurl = $this->actionurl->out(false);
 
         $datefrom = $this->datefrom;
         if (empty($datefrom)) {
index a6f246c..84351fc 100644 (file)
@@ -32,7 +32,7 @@ class mod_forum_external extends external_api {
     /**
      * Describes the parameters for get_forum.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 2.5
      */
     public static function get_forums_by_courses_parameters() {
@@ -97,6 +97,7 @@ class mod_forum_external extends external_api {
                 $forum->numdiscussions = forum_count_discussions($forum, $cm, $course);
                 $forum->cmid = $forum->coursemodule;
                 $forum->cancreatediscussions = forum_user_can_post_discussion($forum, null, -1, $cm, $context);
+                $forum->istracked = forum_tp_is_tracked($forum);
 
                 // Add the forum to the array to return.
                 $arrforums[$forum->id] = $forum;
@@ -144,6 +145,7 @@ class mod_forum_external extends external_api {
                     'numdiscussions' => new external_value(PARAM_INT, 'Number of discussions in the forum', VALUE_OPTIONAL),
                     'cancreatediscussions' => new external_value(PARAM_BOOL, 'If the user can create discussions', VALUE_OPTIONAL),
                     'lockdiscussionafter' => new external_value(PARAM_INT, 'After what period a discussion is locked', VALUE_OPTIONAL),
+                    'istracked' => new external_value(PARAM_BOOL, 'If the user is tracking the forum', VALUE_OPTIONAL),
                 ), 'forum'
             )
         );
@@ -152,7 +154,7 @@ class mod_forum_external extends external_api {
     /**
      * Describes the parameters for get_forum_discussion_posts.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 2.7
      */
     public static function get_forum_discussion_posts_parameters() {
@@ -350,7 +352,7 @@ class mod_forum_external extends external_api {
     /**
      * Describes the parameters for get_forum_discussions_paginated.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 2.8
      */
     public static function get_forum_discussions_paginated_parameters() {
@@ -678,7 +680,7 @@ class mod_forum_external extends external_api {
      * @throws moodle_exception
      */
     public static function view_forum_discussion($discussionid) {
-        global $DB, $CFG;
+        global $DB, $CFG, $USER;
         require_once($CFG->dirroot . "/mod/forum/lib.php");
 
         $params = self::validate_parameters(self::view_forum_discussion_parameters(),
@@ -700,6 +702,11 @@ class mod_forum_external extends external_api {
         // Call the forum/lib API.
         forum_discussion_view($modcontext, $forum, $discussion);
 
+        // Mark as read if required.
+        if (!$CFG->forum_usermarksread && forum_tp_is_tracked($forum)) {
+            forum_tp_mark_discussion_read($USER, $discussion->id);
+        }
+
         $result = array();
         $result['status'] = true;
         $result['warnings'] = $warnings;
index 373694e..ffdc388 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template mod_forum/big_search_form
+
     Big search form.
+
+    Example context (json):
+    {
+        "scripturl": "https://example.com/mod/forum/forum.js",
+        "actionurl": "https://example.com/mod/forum/search.php",
+        "courseid": "2",
+        "words": "apples",
+        "phrase": "Lorem ipsum dolor",
+        "notwords": "Not these words",
+        "showfullwords": [
+            {
+                "fullwords": "Exactly"
+            }
+        ],
+        "datefromchecked": 1,
+        "datetochecked": "",
+        "forumoptions": [
+            {
+                "name": "Forum One",
+                "value": "23"
+            },
+            {
+                "name": "Forum Two",
+                "value": "34"
+            }
+        ],
+        "subject": "Help me please",
+        "user": "Helpy McUser"
+    }
 }}
 <div id="intro" class="box searchbox boxaligncenter">
     {{#str}}searchforumintro, forum{{/str}}
                     <label for="menuforumid">{{#str}}searchwhichforums, forum{{/str}}</label>
                 </td>
                 <td class="c1">
-                    <select name="menuforumid" id="menuforumid">
+                    <select name="forumid" id="menuforumid">
                         {{#forumoptions}}
                             <option value="{{value}}">{{name}}</option>
                         {{/forumoptions}}
index d5ed265..3a02d73 100644 (file)
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die();
 global $CFG;
 
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once($CFG->dirroot . '/mod/forum/lib.php');
 
 class mod_forum_external_testcase extends externallib_advanced_testcase {
 
@@ -59,7 +60,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $this->resetAfterTest(true);
 
         // Create a user.
-        $user = self::getDataGenerator()->create_user();
+        $user = self::getDataGenerator()->create_user(array('trackforums' => 1));
 
         // Set to the user.
         self::setUser($user);
@@ -72,12 +73,14 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $record = new stdClass();
         $record->introformat = FORMAT_HTML;
         $record->course = $course1->id;
+        $record->trackingtype = FORUM_TRACKING_FORCED;
         $forum1 = self::getDataGenerator()->create_module('forum', $record);
 
         // Second forum.
         $record = new stdClass();
         $record->introformat = FORMAT_HTML;
         $record->course = $course2->id;
+        $record->trackingtype = FORUM_TRACKING_OFF;
         $forum2 = self::getDataGenerator()->create_module('forum', $record);
         $forum2->introfiles = [];
 
@@ -90,6 +93,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         // Expect one discussion.
         $forum1->numdiscussions = 1;
         $forum1->cancreatediscussions = true;
+        $forum1->istracked = true;
         $forum1->introfiles = [];
 
         $record = new stdClass();
@@ -102,6 +106,7 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $forum2->numdiscussions = 2;
         // Default limited role, no create discussion capability enabled.
         $forum2->cancreatediscussions = false;
+        $forum2->istracked = false;
 
         // Check the forum was correctly created.
         $this->assertEquals(2, $DB->count_records_select('forum', 'id = :forum1 OR id = :forum2',
@@ -206,6 +211,12 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $forum1 = self::getDataGenerator()->create_module('forum', $record);
         $forum1context = context_module::instance($forum1->cmid);
 
+        // Forum with tracking enabled.
+        $record = new stdClass();
+        $record->course = $course1->id;
+        $forum2 = self::getDataGenerator()->create_module('forum', $record);
+        $forum2context = context_module::instance($forum2->cmid);
+
         // Add discussions to the forums.
         $record = new stdClass();
         $record->course = $course1->id;
@@ -219,6 +230,12 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $record->forum = $forum1->id;
         $discussion2 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
 
+        $record = new stdClass();
+        $record->course = $course1->id;
+        $record->userid = $user2->id;
+        $record->forum = $forum2->id;
+        $discussion3 = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+
         // Add 2 replies to the discussion 1 from different users.
         $record = new stdClass();
         $record->discussion = $discussion1->id;
@@ -340,6 +357,31 @@ class mod_forum_external_testcase extends externallib_advanced_testcase {
         $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
         $this->assertEquals(1, count($posts['posts']));
 
+        // Test discussion tracking on not tracked forum.
+        $result = mod_forum_external::view_forum_discussion($discussion1->id);
+        $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
+        $this->assertTrue($result['status']);
+        $this->assertEmpty($result['warnings']);
+
+        // Test posts have not been marked as read.
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion1->id, 'modified', 'DESC');
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        foreach ($posts['posts'] as $post) {
+            $this->assertFalse($post['postread']);
+        }
+
+        // Test discussion tracking on tracked forum.
+        $result = mod_forum_external::view_forum_discussion($discussion3->id);
+        $result = external_api::clean_returnvalue(mod_forum_external::view_forum_discussion_returns(), $result);
+        $this->assertTrue($result['status']);
+        $this->assertEmpty($result['warnings']);
+
+        // Test posts have been marked as read.
+        $posts = mod_forum_external::get_forum_discussion_posts($discussion3->id, 'modified', 'DESC');
+        $posts = external_api::clean_returnvalue(mod_forum_external::get_forum_discussion_posts_returns(), $posts);
+        foreach ($posts['posts'] as $post) {
+            $this->assertTrue($post['postread']);
+        }
     }
 
     /**
index 287838e..7cd1a70 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /mod/forum/*,
 information provided here is intended especially for developers.
 
+=== 3.3 ===
+  * External function get_forums_by_courses now returns and additional field "istracked" that indicates if the user
+   is tracking the related forum.
+
 === 3.2 ===
  * The setting $CFG->forum_replytouser has been removed in favour of a centralized noreplyaddress setting.
    Please use $CFG->noreplyaddress setting instead.
index 245afca..41a3233 100644 (file)
@@ -169,7 +169,7 @@ class mod_glossary_external extends external_api {
     /**
      * Describes the parameters for get_glossaries_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_glossaries_by_courses_parameters() {
index a5f0b37..85292af 100644 (file)
@@ -107,7 +107,7 @@ class mod_imscp_external extends external_api {
     /**
      * Describes the parameters for get_imscps_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function get_imscps_by_courses_parameters() {
diff --git a/mod/label/classes/external.php b/mod/label/classes/external.php
new file mode 100644 (file)
index 0000000..cd43576
--- /dev/null
@@ -0,0 +1,140 @@
+<?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/>.
+
+/**
+ * Label external API
+ *
+ * @package    mod_label
+ * @category   external
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.3
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once("$CFG->libdir/externallib.php");
+
+/**
+ * Label external functions
+ *
+ * @package    mod_label
+ * @category   external
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.3
+ */
+class mod_label_external extends external_api {
+
+    /**
+     * Describes the parameters for get_labels_by_courses.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_labels_by_courses_parameters() {
+        return new external_function_parameters (
+            array(
+                'courseids' => new external_multiple_structure(
+                    new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, array()
+                ),
+            )
+        );
+    }
+
+    /**
+     * Returns a list of labels in a provided list of courses.
+     * If no list is provided all labels that the user can view will be returned.
+     *
+     * @param array $courseids course ids
+     * @return array of warnings and labels
+     * @since Moodle 3.3
+     */
+    public static function get_labels_by_courses($courseids = array()) {
+
+        $warnings = array();
+        $returnedlabels = array();
+
+        $params = array(
+            'courseids' => $courseids,
+        );
+        $params = self::validate_parameters(self::get_labels_by_courses_parameters(), $params);
+
+        $mycourses = array();
+        if (empty($params['courseids'])) {
+            $mycourses = enrol_get_my_courses();
+            $params['courseids'] = array_keys($mycourses);
+        }
+
+        // Ensure there are courseids to loop through.
+        if (!empty($params['courseids'])) {
+
+            list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
+
+            // Get the labels in this course, this function checks users visibility permissions.
+            // We can avoid then additional validate_context calls.
+            $labels = get_all_instances_in_courses("label", $courses);
+            foreach ($labels as $label) {
+                $context = context_module::instance($label->coursemodule);
+                // Entry to return.
+                $label->name = external_format_string($label->name, $context->id);
+
+                list($label->intro, $label->introformat) = external_format_text($label->intro,
+                                                                $label->introformat, $context->id, 'mod_label', 'intro', null);
+                $label->introfiles = external_util::get_area_files($context->id, 'mod_label', 'intro', false, false);
+
+                $returnedlabels[] = $label;
+            }
+        }
+
+        $result = array(
+            'labels' => $returnedlabels,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_labels_by_courses return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_labels_by_courses_returns() {
+        return new external_single_structure(
+            array(
+                'labels' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'Module id'),
+                            'course' => new external_value(PARAM_INT, 'Course id'),
+                            'name' => new external_value(PARAM_RAW, 'Label name'),
+                            'intro' => new external_value(PARAM_RAW, 'Label contents'),
+                            'introformat' => new external_format_value('intro', 'Content format'),
+                            'introfiles' => new external_files('Files in the introduction text'),
+                            'timemodified' => new external_value(PARAM_INT, 'Last time the label was modified'),
+                            'section' => new external_value(PARAM_INT, 'Course section id'),
+                            'visible' => new external_value(PARAM_INT, 'Module visibility'),
+                            'groupmode' => new external_value(PARAM_INT, 'Group mode'),
+                            'groupingid' => new external_value(PARAM_INT, 'Grouping id'),
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+}
diff --git a/mod/label/db/services.php b/mod/label/db/services.php
new file mode 100644 (file)
index 0000000..e6cf14b
--- /dev/null
@@ -0,0 +1,40 @@
+<?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/>.
+
+/**
+ * Label external functions and service definitions.
+ *
+ * @package    mod_label
+ * @category   external
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.3
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$functions = array(
+
+    'mod_label_get_labels_by_courses' => array(
+        'classname'     => 'mod_label_external',
+        'methodname'    => 'get_labels_by_courses',
+        'description'   => 'Returns a list of labels in a provided list of courses, if no list is provided all labels that the user
+                            can view will be returned.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/label:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+);
diff --git a/mod/label/tests/externallib_test.php b/mod/label/tests/externallib_test.php
new file mode 100644 (file)
index 0000000..c1f5b37
--- /dev/null
@@ -0,0 +1,160 @@
+<?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/>.
+
+/**
+ * External mod_label functions unit tests
+ *
+ * @package    mod_label
+ * @category   external
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.3
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * External mod_label functions unit tests
+ *
+ * @package    mod_label
+ * @category   external
+ * @copyright  2017 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.3
+ */
+class mod_label_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Test test_mod_label_get_labels_by_courses
+     */
+    public function test_mod_label_get_labels_by_courses() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+
+        $student = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id);
+
+        // First label.
+        $record = new stdClass();
+        $record->course = $course1->id;
+        $label1 = self::getDataGenerator()->create_module('label', $record);
+
+        // Second label.
+        $record = new stdClass();
+        $record->course = $course2->id;
+        $label2 = self::getDataGenerator()->create_module('label', $record);
+
+        // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
+        $enrol = enrol_get_plugin('manual');
+        $enrolinstances = enrol_get_instances($course2->id, true);
+        foreach ($enrolinstances as $courseenrolinstance) {
+            if ($courseenrolinstance->enrol == "manual") {
+                $instance2 = $courseenrolinstance;
+                break;
+            }
+        }
+        $enrol->enrol_user($instance2, $student->id, $studentrole->id);
+
+        self::setUser($student);
+
+        $returndescription = mod_label_external::get_labels_by_courses_returns();
+
+        // Create what we expect to be returned when querying the two courses.
+        $expectedfields = array('id', 'course', 'name', 'intro', 'introformat', 'introfiles', 'timemodified', 'section',
+                                    'visible', 'groupmode', 'groupingid');
+
+        // Add expected coursemodule and data.
+        $label1->coursemodule = $label1->cmid;
+        $label1->introformat = 1;
+        $label1->section = 0;
+        $label1->visible = true;
+        $label1->groupmode = 0;
+        $label1->groupingid = 0;
+        $label1->introfiles = [];
+
+        $label2->coursemodule = $label2->cmid;
+        $label2->introformat = 1;
+        $label2->section = 0;
+        $label2->visible = true;
+        $label2->groupmode = 0;
+        $label2->groupingid = 0;
+        $label2->introfiles = [];
+
+        foreach ($expectedfields as $field) {
+            $expected1[$field] = $label1->{$field};
+            $expected2[$field] = $label2->{$field};
+        }
+
+        $expectedlabels = array($expected2, $expected1);
+
+        // Call the external function passing course ids.
+        $result = mod_label_external::get_labels_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertEquals($expectedlabels, $result['labels']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Call the external function without passing course id.
+        $result = mod_label_external::get_labels_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+        $this->assertEquals($expectedlabels, $result['labels']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Add a file to the intro.
+        $filename = "file.txt";
+        $filerecordinline = array(
+            'contextid' => context_module::instance($label2->cmid)->id,
+            'component' => 'mod_label',
+            'filearea'  => 'intro',
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => $filename,
+        );
+        $fs = get_file_storage();
+        $timepost = time();
+        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
+
+        $result = mod_label_external::get_labels_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertCount(1, $result['labels'][0]['introfiles']);
+        $this->assertEquals($filename, $result['labels'][0]['introfiles'][0]['filename']);
+
+        // Unenrol user from second course.
+        $enrol->unenrol_user($instance2, $student->id);
+        array_shift($expectedlabels);
+
+        // Call the external function without passing course id.
+        $result = mod_label_external::get_labels_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+        $this->assertEquals($expectedlabels, $result['labels']);
+
+        // Call for the second course we unenrolled the user from, expected warning.
+        $result = mod_label_external::get_labels_by_courses(array($course2->id));
+        $this->assertCount(1, $result['warnings']);
+        $this->assertEquals('1', $result['warnings'][0]['warningcode']);
+        $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
+    }
+}
index 230a5e5..95d7ab8 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120500;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016120501;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;    // Requires this Moodle version
 $plugin->component = 'mod_label'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 2cb2e9f..f61a708 100644 (file)
@@ -459,11 +459,11 @@ if ($action === 'delete') {
             $('#actionid').change(function() {
                 $('#mod-lesson-report-form').submit();
             });
-            $('#checkall').click(function() {
+            $('#checkall').click(function(e) {
                 $('#mod-lesson-report-form').find('input:checkbox').prop('checked', true);
                 e.preventDefault();
             });
-            $('#checknone').click(function() {
+            $('#checknone').click(function(e) {
                 $('#mod-lesson-report-form').find('input:checkbox').prop('checked', false);
                 e.preventDefault();
             });
index 8ba95f9..3b28b45 100644 (file)
@@ -43,7 +43,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_quizzes_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_quizzes_by_courses_parameters() {
@@ -301,7 +301,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for view_quiz.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function view_quiz_parameters() {
@@ -355,7 +355,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_user_attempts.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_user_attempts_parameters() {
@@ -474,7 +474,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_user_best_grade.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_user_best_grade_parameters() {
@@ -552,7 +552,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_combined_review_options.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_combined_review_options_parameters() {
@@ -653,7 +653,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for start_attempt.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function start_attempt_parameters() {
@@ -944,7 +944,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_attempt_data.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_attempt_data_parameters() {
@@ -1027,7 +1027,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_attempt_summary.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_attempt_summary_parameters() {
@@ -1091,7 +1091,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for save_attempt.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function save_attempt_parameters() {
@@ -1178,7 +1178,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for process_attempt.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function process_attempt_parameters() {
@@ -1300,7 +1300,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_attempt_review.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_attempt_review_parameters() {
@@ -1405,7 +1405,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for view_attempt.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function view_attempt_parameters() {
@@ -1478,7 +1478,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for view_attempt_summary.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function view_attempt_summary_parameters() {
@@ -1543,7 +1543,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for view_attempt_review.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function view_attempt_review_parameters() {
@@ -1598,7 +1598,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for view_quiz.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_quiz_feedback_for_grade_parameters() {
@@ -1671,7 +1671,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_quiz_access_information.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_quiz_access_information_parameters() {
@@ -1753,7 +1753,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_attempt_access_information.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_attempt_access_information_parameters() {
@@ -1860,7 +1860,7 @@ class mod_quiz_external extends external_api {
     /**
      * Describes the parameters for get_quiz_required_qtypes.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.1
      */
     public static function get_quiz_required_qtypes_parameters() {
index 6100a5c..468e838 100644 (file)
@@ -213,8 +213,10 @@ function resource_get_coursemodule_info($coursemodule) {
         $info->icon ='i/invalid';
         return $info;
     }
+
+    // See if there is at least one file.
     $fs = get_file_storage();
-    $files = $fs->get_area_files($context->id, 'mod_resource', 'content', 0, 'sortorder DESC, id ASC', false); // TODO: this is not very efficient!!
+    $files = $fs->get_area_files($context->id, 'mod_resource', 'content', 0, 'sortorder DESC, id ASC', false, 0, 0, 1);
     if (count($files) >= 1) {
         $mainfile = reset($files);
         $info->icon = file_file_icon($mainfile, 24);
index 13855c3..0c507ad 100644 (file)
@@ -89,4 +89,66 @@ class mod_resource_lib_testcase extends advanced_testcase {
         $this->assertEquals(1, $completiondata->completionstate);
 
     }
+
+    /**
+     * Tests the resource_get_coursemodule_info function.
+     *
+     * Note: This currently doesn't test every aspect of the function, mainly focusing on the icon.
+     */
+    public function test_get_coursemodule_info() {
+        global $DB, $USER;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create course.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        // Create a resource with no files.
+        $draftid = file_get_unused_draft_itemid();
+        $resource1 = $generator->create_module('resource', array('course' => $course->id,
+                'name' => 'R1', 'files' => $draftid));
+
+        // Create a resource with one file.
+        $draftid = file_get_unused_draft_itemid();
+        $contextid = context_user::instance($USER->id)->id;
+        $filerecord = array('component' => 'user', 'filearea' => 'draft', 'contextid' => $contextid,
+                'itemid' => $draftid, 'filename' => 'r2.txt', 'filepath' => '/');
+        $fs = get_file_storage();
+        $fs->create_file_from_string($filerecord, 'Test');
+        $resource2 = $generator->create_module('resource', array('course' => $course->id,
+                'name' => 'R2', 'files' => $draftid));
+
+        // Create a resource with two files.
+        $draftid = file_get_unused_draft_itemid();
+        $filerecord = array('component' => 'user', 'filearea' => 'draft', 'contextid' => $contextid,
+                'itemid' => $draftid, 'filename' => 'r3.txt', 'filepath' => '/', 'sortorder' => 1);
+        $fs->create_file_from_string($filerecord, 'Test');
+        $filerecord['filename'] = 'r3.doc';
+        $filerecord['sortorder'] = 2;
+        $fs->create_file_from_string($filerecord, 'Test');
+        $resource3 = $generator->create_module('resource', array('course' => $course->id,
+                'name' => 'R3', 'files' => $draftid));
+
+        // Try get_coursemodule_info for first one.
+        $info = resource_get_coursemodule_info(
+                $DB->get_record('course_modules', array('id' => $resource1->cmid)));
+
+        // The name should be set. There is no overridden icon.
+        $this->assertEquals('R1', $info->name);
+        $this->assertEmpty($info->icon);
+
+        // For second one, there should be an overridden icon.
+        $info = resource_get_coursemodule_info(
+                $DB->get_record('course_modules', array('id' => $resource2->cmid)));
+        $this->assertEquals('R2', $info->name);
+        $this->assertEquals('f/text-24', $info->icon);
+
+        // For third one, it should use the highest sortorder icon.
+        $info = resource_get_coursemodule_info(
+                $DB->get_record('course_modules', array('id' => $resource3->cmid)));
+        $this->assertEquals('R3', $info->name);
+        $this->assertEquals('f/document-24', $info->icon);
+    }
 }
index 2f740e5..0f2b752 100644 (file)
@@ -1438,8 +1438,13 @@ function scorm_check_mode($scorm, &$newattempt, &$attempt, $userid, &$mode) {
     }
     // Check if the scorm module is incomplete (used to validate user request to start a new attempt).
     $incomplete = true;
-    $tracks = $DB->get_recordset('scorm_scoes_track', array('scormid' => $scorm->id, 'userid' => $userid,
-        'attempt' => $attempt, 'element' => 'cmi.core.lesson_status'));
+    $sql = "SELECT sc.id, t.value
+              FROM {scorm_scoes} sc
+         LEFT JOIN {scorm_scoes_track} t ON sc.scorm = t.scormid AND sc.id = t.scoid
+                   AND t.element = 'cmi.core.lesson_status' AND t.userid = ? AND t.attempt = ?
+             WHERE sc.scormtype = 'sco' AND sc.scorm = ?";
+    $tracks = $DB->get_recordset_sql($sql, array($userid, $attempt, $scorm->id));
+
     foreach ($tracks as $track) {
         if (($track->value == 'completed') || ($track->value == 'passed') || ($track->value == 'failed')) {
             $incomplete = false;
diff --git a/mod/scorm/tests/behat/multisco_review_mode.feature b/mod/scorm/tests/behat/multisco_review_mode.feature
new file mode 100644 (file)
index 0000000..4195df1
--- /dev/null
@@ -0,0 +1,172 @@
+@mod @mod_scorm @_file_upload @_switch_iframe
+Feature: Scorm multi-sco review mode.
+  In order to let students access a scorm package
+  As a teacher
+  I need to add scorm activity to a course
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+
+  @javascript
+  Scenario: Test review mode with a single sco completion.
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | Enable completion tracking | Yes |
+    And I press "Save and display"
+    And I add a "SCORM package" to section "1"
+    And I set the following fields to these values:
+      | Name | Basic Multi-sco SCORM package |
+      | Description | Description |
+    And I set the field "Completed" to "1"
+    And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I should see "Basic Multi-sco SCORM package"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Basic Multi-sco SCORM package"
+    And I should see "Normal"
+    And I press "Enter"
+    And I switch to "scorm_object" iframe
+    And I should see "Play of the game"
+    And I switch to the main frame
+    And I follow "Exit activity"
+    And I wait until the page is ready
+    And I should see "Basic Multi-sco SCORM package"
+    And I follow "Basic Multi-sco SCORM package"
+    And I should see "Normal"
+    And I press "Enter"
+    Then I should not see "Review mode"
+
+  @javascript
+  Scenario: Test review mode with all scos completed.
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I navigate to "Edit settings" in current page administration
+    And I set the following fields to these values:
+      | Enable completion tracking | Yes |
+    And I press "Save and display"
+    And I add a "SCORM package" to section "1"
+    And I set the following fields to these values:
+      | Name | ADV Multi-sco SCORM package |
+      | Description | Description |
+      | Completion tracking | Show activity as complete when conditions are met |
+      | Require all scos to return completion status | 1 |
+    And I set the field "Completed" to "1"
+    And I upload "mod/scorm/tests/packages/RuntimeMinimumCalls_SCORM12.zip" file to "Package file" filemanager
+    And I click on "Save and display" "button"
+    And I should see "ADV Multi-sco SCORM package"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "ADV Multi-sco SCORM package"
+    And I should see "Normal"
+    And I press "Enter"
+    And I switch to "scorm_object" iframe
+    And I should see "Play of the game"
+
+    And I switch to the main frame
+    And I click on "Par?" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Par"
+
+    And I switch to the main frame
+    And I click on "Keeping Score" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Scoring"
+
+    And I switch to the main frame
+    And I click on "Other Scoring Systems" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Other Scoring Systems"
+
+    And I switch to the main frame
+    And I click on "The Rules of Golf" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "The Rules of Golf"
+
+    And I switch to the main frame
+    And I click on "Playing Golf Quiz" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Knowledge Check"
+
+    And I switch to the main frame
+    And I click on "Taking Care of the Course" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Etiquette - Care For the Course"
+
+    And I switch to the main frame
+    And I click on "Avoiding Distraction" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Etiquette - Avoiding Distraction"
+
+    And I switch to the main frame
+    And I click on "Playing Politely" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Etiquette - Playing the Game"
+
+    And I switch to the main frame
+    And I click on "Etiquette Quiz" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Knowledge Check"
+
+    And I switch to the main frame
+    And I click on "Handicapping Overview" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Handicapping"
+
+    And I switch to the main frame
+    And I click on "Calculating a Handicap" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Calculating a Handicap"
+
+    And I switch to the main frame
+    And I click on "Calculating a Handicapped Score" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Calculating a Score"
+
+    And I switch to the main frame
+    And I click on "Handicapping Example" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Calculating a Score"
+
+    And I switch to the main frame
+    And I click on "Handicapping Quiz" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Knowledge Check"
+
+    And I switch to the main frame
+    And I click on "How to Have Fun Playing Golf" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "How to Have Fun Golfing"
+
+    And I switch to the main frame
+    And I click on "How to Make Friends Playing Golf" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "How to Make Friends on the Golf Course"
+
+    And I switch to the main frame
+    And I click on "Having Fun Quiz" "list_item"
+    And I switch to "scorm_object" iframe
+    And I should see "Knowledge Check"
+    And I switch to the main frame
+    And I follow "Exit activity"
+    And I wait until the page is ready
+    And I should see "ADV Multi-sco SCORM package"
+    And I follow "ADV Multi-sco SCORM package"
+    And I should see "Normal"
+    And I press "Enter"
+    Then I should see "Review mode"
index d232b90..0d53c41 100644 (file)
@@ -43,7 +43,7 @@ class mod_survey_external extends external_api {
     /**
      * Describes the parameters for get_surveys_by_courses.
      *
-     * @return external_external_function_parameters
+     * @return external_function_parameters
      * @since Moodle 3.0
      */
     public static function get_surveys_by_courses_parameters() {
index 7a09cca..59e28c2 100644 (file)
@@ -104,4 +104,106 @@ class mod_url_external extends external_api {
         );
     }
 
+    /**
+     * Describes the parameters for get_urls_by_courses.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_urls_by_courses_parameters() {
+        return new external_function_parameters (
+            array(
+                'courseids' => new external_multiple_structure(
+                    new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, array()
+                ),
+            )
+        );
+    }
+
+    /**
+     * Returns a list of urls in a provided list of courses.
+     * If no list is provided all urls that the user can view will be returned.
+     *
+     * @param array $courseids course ids
+     * @return array of warnings and urls
+     * @since Moodle 3.3
+     */
+    public static function get_urls_by_courses($courseids = array()) {
+
+        $warnings = array();
+        $returnedurls = array();
+
+        $params = array(
+            'courseids' => $courseids,
+        );
+        $params = self::validate_parameters(self::get_urls_by_courses_parameters(), $params);
+
+        $mycourses = array();
+        if (empty($params['courseids'])) {
+            $mycourses = enrol_get_my_courses();
+            $params['courseids'] = array_keys($mycourses);
+        }
+
+        // Ensure there are courseids to loop through.
+        if (!empty($params['courseids'])) {
+
+            list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
+
+            // Get the urls in this course, this function checks users visibility permissions.
+            // We can avoid then additional validate_context calls.
+            $urls = get_all_instances_in_courses("url", $courses);
+            foreach ($urls as $url) {
+                $context = context_module::instance($url->coursemodule);
+                // Entry to return.
+                $url->name = external_format_string($url->name, $context->id);
+
+                list($url->intro, $url->introformat) = external_format_text($url->intro,
+                                                                $url->introformat, $context->id, 'mod_url', 'intro', null);
+                $url->introfiles = external_util::get_area_files($context->id, 'mod_url', 'intro', false, false);
+
+                $returnedurls[] = $url;
+            }
+        }
+
+        $result = array(
+            'urls' => $returnedurls,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_urls_by_courses return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_urls_by_courses_returns() {
+        return new external_single_structure(
+            array(
+                'urls' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'Module id'),
+                            'course' => new external_value(PARAM_INT, 'Course id'),
+                            'name' => new external_value(PARAM_RAW, 'URL name'),
+                            'intro' => new external_value(PARAM_RAW, 'Summary'),
+                            'introformat' => new external_format_value('intro', 'Summary format'),
+                            'introfiles' => new external_files('Files in the introduction text'),
+                            'externalurl' => new external_value(PARAM_RAW_TRIMMED, 'External URL'),
+                            'display' => new external_value(PARAM_INT, 'How to display the url'),
+                            'displayoptions' => new external_value(PARAM_RAW, 'Display options (width, height)'),
+                            'parameters' => new external_value(PARAM_RAW, 'Parameters to append to the URL'),
+                            'timemodified' => new external_value(PARAM_INT, 'Last time the url was modified'),
+                            'section' => new external_value(PARAM_INT, 'Course section id'),
+                            'visible' => new external_value(PARAM_INT, 'Module visibility'),
+                            'groupmode' => new external_value(PARAM_INT, 'Group mode'),
+                            'groupingid' => new external_value(PARAM_INT, 'Grouping id'),
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
 }
index 0481d94..987533a 100644 (file)
@@ -36,5 +36,13 @@ $functions = array(
         'capabilities'  => 'mod/url:view',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
-
+    'mod_url_get_urls_by_courses' => array(
+        'classname'     => 'mod_url_external',
+        'methodname'    => 'get_urls_by_courses',
+        'description'   => 'Returns a list of urls in a provided list of courses, if no list is provided all urls that the user
+                            can view will be returned.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/url:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 );
index 686c422..eea0bf4 100644 (file)
@@ -110,4 +110,121 @@ class mod_url_external_testcase extends externallib_advanced_testcase {
         }
 
     }
+
+    /**
+     * Test test_mod_url_get_urls_by_courses
+     */
+    public function test_mod_url_get_urls_by_courses() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+
+        $student = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id);
+
+        // First url.
+        $record = new stdClass();
+        $record->course = $course1->id;
+        $url1 = self::getDataGenerator()->create_module('url', $record);
+
+        // Second url.
+        $record = new stdClass();
+        $record->course = $course2->id;
+        $url2 = self::getDataGenerator()->create_module('url', $record);
+
+        // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
+        $enrol = enrol_get_plugin('manual');
+        $enrolinstances = enrol_get_instances($course2->id, true);
+        foreach ($enrolinstances as $courseenrolinstance) {
+            if ($courseenrolinstance->enrol == "manual") {
+                $instance2 = $courseenrolinstance;
+                break;
+            }
+        }
+        $enrol->enrol_user($instance2, $student->id, $studentrole->id);
+
+        self::setUser($student);
+
+        $returndescription = mod_url_external::get_urls_by_courses_returns();
+
+        // Create what we expect to be returned when querying the two courses.
+        $expectedfields = array('id', 'course', 'name', 'intro', 'introformat', 'introfiles', 'externalurl', 'display',
+                                'displayoptions', 'parameters', 'timemodified', 'section', 'visible', 'groupmode', 'groupingid');
+
+        // Add expected coursemodule and data.
+        $url1->coursemodule = $url1->cmid;
+        $url1->introformat = 1;
+        $url1->section = 0;
+        $url1->visible = true;
+        $url1->groupmode = 0;
+        $url1->groupingid = 0;
+        $url1->introfiles = [];
+
+        $url2->coursemodule = $url2->cmid;
+        $url2->introformat = 1;
+        $url2->section = 0;
+        $url2->visible = true;
+        $url2->groupmode = 0;
+        $url2->groupingid = 0;
+        $url2->introfiles = [];
+
+        foreach ($expectedfields as $field) {
+            $expected1[$field] = $url1->{$field};
+            $expected2[$field] = $url2->{$field};
+        }
+
+        $expectedurls = array($expected2, $expected1);
+
+        // Call the external function passing course ids.
+        $result = mod_url_external::get_urls_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertEquals($expectedurls, $result['urls']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Call the external function without passing course id.
+        $result = mod_url_external::get_urls_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+        $this->assertEquals($expectedurls, $result['urls']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Add a file to the intro.
+        $filename = "file.txt";
+        $filerecordinline = array(
+            'contextid' => context_module::instance($url2->cmid)->id,
+            'component' => 'mod_url',
+            'filearea'  => 'intro',
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => $filename,
+        );
+        $fs = get_file_storage();
+        $timepost = time();
+        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
+
+        $result = mod_url_external::get_urls_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertCount(1, $result['urls'][0]['introfiles']);
+        $this->assertEquals($filename, $result['urls'][0]['introfiles'][0]['filename']);
+
+        // Unenrol user from second course and alter expected urls.
+        $enrol->unenrol_user($instance2, $student->id);
+        array_shift($expectedurls);
+
+        // Call the external function without passing course id.
+        $result = mod_url_external::get_urls_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+        $this->assertEquals($expectedurls, $result['urls']);
+
+        // Call for the second course we unenrolled the user from, expected warning.
+        $result = mod_url_external::get_urls_by_courses(array($course2->id));
+        $this->assertCount(1, $result['warnings']);
+        $this->assertEquals('1', $result['warnings'][0]['warningcode']);
+        $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
+    }
 }
index 64d8f34..d0d7b8e 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120500;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016120501;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;    // Requires this Moodle version
 $plugin->component = 'mod_url';        // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index ad8f240..22e0a23 100644 (file)
 </div>
 {{/hasinstructions}}
 
-
 {{#js}}
-    require(['jquery', 'core/yui'], function($, Y) {
     {{#error}}
-        $(function() {
-            M.util.focus_login_error(Y);
+        require(['jquery'], function($) {
+            $('#loginerrormessage').focus();
         });
     {{/error}}
     {{^error}}
         {{#autofocusform}}
-            $(function() {
-                M.util.focus_login_form(Y);
+            require(['jquery'], function($) {
+                if ($('#username').val()) {
+                    $('#password').focus();
+                } else {
+                    $('#username').focus();
+                }
             });
         {{/autofocusform}}
     {{/error}}
-    })
 {{/js}}
index 1af0c25..e51b1a1 100644 (file)
@@ -1,3 +1,46 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_form/element-template
+
+    Template for the form element wrapper template.
+
+    Context variables required for this template:
+    * label
+    * required
+    * advanced
+    * helpbutton
+    * error
+    * element
+        * id
+        * name
+
+    Example context (json):
+    {
+        "label": "Course full name",
+        "required": true,
+        "advanced": false,
+        "error": null,
+        "element": {
+            "id": "id_fullname",
+            "name": "fullname"
+        }
+    }
+}}
 <div class="form-group row {{#error}}has-danger{{/error}} fitem {{#advanced}}advanced{{/advanced}} {{{element.extraclasses}}}">
     <div class="col-md-3">
         <span class="pull-xs-right text-nowrap">
index 18ddbc6..3e96080 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template mod_forum/big_search_form
+
     Big search form.
+
+    Example context (json):
+    {
+        "scripturl": "https://example.com/mod/forum/forum.js",
+        "actionurl": "https://example.com/mod/forum/search.php",
+        "courseid": "2",
+        "words": "apples",
+        "phrase": "Lorem ipsum dolor",
+        "notwords": "Not these words",
+        "showfullwords": [
+            {
+                "fullwords": "Exactly"
+            }
+        ],
+        "datefromchecked": 1,
+        "datetochecked": "",
+        "forumoptions": [
+            {
+                "name": "Forum One",
+                "value": "23"
+            },
+            {
+                "name": "Forum Two",
+                "value": "34"
+            }
+        ],
+        "subject": "Help me please",
+        "user": "Helpy McUser"
+    }
 }}
 <div id="intro" class="box searchbox boxaligncenter">
     {{#str}}searchforumintro, forum{{/str}}
                     <label for="menuforumid">{{#str}}searchwhichforums, forum{{/str}}</label>
                 </td>
                 <td class="c1">
-                    <select name="menuforumid" id="menuforumid" class="form-control">
+                    <select name="forumid" id="menuforumid" class="form-control">
                         {{#forumoptions}}
                             <option value="{{value}}">{{name}}</option>
                         {{/forumoptions}}