Merge branch 'MDL-51535-master' of git://github.com/junpataleta/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 6 Oct 2015 11:14:02 +0000 (12:14 +0100)
committerDan Poltawski <dan@moodle.com>
Tue, 6 Oct 2015 15:32:02 +0000 (16:32 +0100)
87 files changed:
admin/settings/plugins.php
auth/radius/config.html
backup/moodle2/tests/moodle2_course_format_test.php
badges/criteria/award_criteria_profile.php
badges/tests/badgeslib_test.php
blocks/html/block_html.php
blocks/tags/tests/behat/tagcloud.feature
course/editsection.php
course/editsection_form.php
course/format/lib.php
course/format/topics/lib.php
course/format/topics/tests/behat/edit_delete_sections.feature
course/format/topics/tests/format_topics_test.php
course/format/upgrade.txt
course/format/weeks/lib.php
course/format/weeks/tests/behat/edit_delete_sections.feature
course/format/weeks/tests/format_weeks_test.php
enrol/self/lang/en/enrol_self.php
enrol/self/lib.php
index.php
lib/adminlib.php
lib/amd/build/localstorage.min.js
lib/amd/build/loglevel.min.js
lib/amd/src/localstorage.js
lib/amd/src/loglevel.js
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-debug.js
lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-min.js
lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button.js
lib/editor/atto/plugins/backcolor/yui/src/button/js/button.js
lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-debug.js
lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-min.js
lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button.js
lib/editor/atto/plugins/fontcolor/yui/src/button/js/button.js
lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button-debug.js
lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button-min.js
lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button.js
lib/editor/atto/plugins/noautolink/yui/src/button/js/button.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js
lib/editor/atto/yui/src/editor/js/editor-plugin-buttons.js
lib/grade/grade_category.php
lib/javascript-static.js
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/requirejs/readme_moodle.txt [new file with mode: 0644]
lib/requirejs/require.js
lib/requirejs/require.min.js
lib/testing/generator/data_generator.php
lib/testing/tests/generator_test.php
lib/thirdpartylibs.xml
mod/choice/lang/en/deprecated.txt [new file with mode: 0644]
mod/choice/renderer.php
mod/data/field/file/field.class.php
mod/data/field/picture/field.class.php
mod/lti/classes/external.php [new file with mode: 0644]
mod/lti/db/services.php [new file with mode: 0644]
mod/lti/locallib.php
mod/lti/tests/externallib_test.php [new file with mode: 0644]
mod/lti/version.php
mod/scorm/classes/external.php
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/scorm/settings.php
mod/scorm/tests/externallib_test.php
mod/scorm/version.php
mod/wiki/lang/en/wiki.php
mod/wiki/lib.php
tag/tests/behat/delete_tag.feature
tag/tests/behat/edit_tag.feature
tag/tests/behat/flag_tags.feature
user/editlib.php
version.php
webservice/amf/db/access.php
webservice/amf/version.php
webservice/pluginfile.php
webservice/rest/db/access.php
webservice/rest/version.php
webservice/soap/db/access.php
webservice/soap/version.php
webservice/upgrade.txt
webservice/xmlrpc/db/access.php
webservice/xmlrpc/version.php

index 820d25b..f5ea9e3 100644 (file)
@@ -277,11 +277,18 @@ if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('webservicesettings', new lang_string('webservices', 'webservice')));
     // Mobile
     $temp = new admin_settingpage('mobile', new lang_string('mobile','admin'), 'moodle/site:config', false);
-    $enablemobiledocurl = new moodle_url(get_docs_url('Enable_mobile_web_services'));
-    $enablemobiledoclink = html_writer::link($enablemobiledocurl, new lang_string('documentation'));
-    $temp->add(new admin_setting_enablemobileservice('enablemobilewebservice',
-            new lang_string('enablemobilewebservice', 'admin'),
-            new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), 0));
+
+    // We should wait to the installation to finish since we depend on some configuration values that are set once
+    // the admin user profile is configured.
+    if (!during_initial_install()) {
+        $enablemobiledocurl = new moodle_url(get_docs_url('Enable_mobile_web_services'));
+        $enablemobiledoclink = html_writer::link($enablemobiledocurl, new lang_string('documentation'));
+        $default = is_https() ? 1 : 0;
+        $temp->add(new admin_setting_enablemobileservice('enablemobilewebservice',
+                new lang_string('enablemobilewebservice', 'admin'),
+                new lang_string('configenablemobilewebservice', 'admin', $enablemobiledoclink), $default));
+    }
+
     $temp->add(new admin_setting_configtext('mobilecssurl', new lang_string('mobilecssurl', 'admin'), new lang_string('configmobilecssurl','admin'), '', PARAM_URL));
     $ADMIN->add('webservicesettings', $temp);
     /// overview page
index 103bb20..3ae3f26 100644 (file)
@@ -60,7 +60,7 @@ if (!isset($config->changepasswordurl)) {
 </tr>
 
 <tr valign="top" >
-    <td align="right"><?php echo html_writer::label(get_string('auth_radiustype_key', 'auth_radius'), 'menuradiustype'); ?>: </td>
+    <td align="right"><?php echo html_writer::label(get_string('auth_radiustype_key', 'auth_radius') . ':', 'menuradiustype'); ?> </td>
     <td>
 <?php
 
index 787db96..47b9971 100644 (file)
@@ -220,6 +220,21 @@ class core_backup_moodle2_course_format_testcase extends advanced_testcase {
  * Test course format that has 1 option.
  */
 class format_test_cs_options extends format_topics {
+    /**
+     * Override method format_topics::get_default_section_name to prevent PHPUnit errors related to the nonexistent
+     * format_test_cs_options lang file.
+     *
+     * @param stdClass $section The section in question.
+     * @return string The section's name for display.
+     */
+    public function get_default_section_name($section) {
+        if ($section->section == 0) {
+            return parent::get_default_section_name($section);
+        } else {
+            return get_string('sectionname', 'format_topics') . ' ' . $section->section;
+        }
+    }
+
     public function section_format_options($foreditform = false) {
         return array(
             'numdaystocomplete' => array(
index 18915a0..65c2dda 100644 (file)
@@ -170,33 +170,31 @@ class award_criteria_profile extends award_criteria {
         }
 
         $join = '';
-        $where = '';
+        $whereparts = array();
         $sqlparams = array();
         $rule = ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) ? ' OR ' : ' AND ';
 
         foreach ($this->params as $param) {
             if (is_numeric($param['field'])) {
-                $infodata[] = " uid.fieldid = :fieldid{$param['field']} ";
-                $sqlparams["fieldid{$param['field']}"] = $param['field'];
+                // This is a custom field.
+                $idx = count($whereparts) + 1;
+                $join .= " LEFT JOIN {user_info_data} uid{$idx} ON uid{$idx}.userid = u.id AND uid{$idx}.fieldid = :fieldid{$idx} ";
+                $sqlparams["fieldid{$idx}"] = $param['field'];
+                $whereparts[] = "uid{$idx}.id IS NOT NULL";
             } else {
-                $userdata[] = $DB->sql_isnotempty('u', "u.{$param['field']}", false, true);
+                // This is a field from {user} table.
+                $whereparts[] = $DB->sql_isnotempty('u', "u.{$param['field']}", false, true);
             }
         }
 
-        // Add user custom field parameters if there are any.
-        if (!empty($infodata)) {
-            $extraon = implode($rule, $infodata);
-            $join = " LEFT JOIN {user_info_data} uid ON uid.userid = u.id AND ({$extraon})";
-        }
+        $sqlparams['userid'] = $userid;
 
-        // Add user table field parameters if there are any.
-        if (!empty($userdata)) {
-            $extraon = implode($rule, $userdata);
-            $where = " AND ({$extraon})";
+        if ($whereparts) {
+            $where = " AND (" . implode($rule, $whereparts) . ")";
+        } else {
+            $where = '';
         }
-
-        $sqlparams['userid'] = $userid;
-        $sql = "SELECT u.* FROM {user} u " . $join . " WHERE u.id = :userid " . $where;
+        $sql = "SELECT 1 FROM {user} u " . $join . " WHERE u.id = :userid $where";
         $overall = $DB->record_exists_sql($sql, $sqlparams);
 
         return $overall;
@@ -212,29 +210,26 @@ class award_criteria_profile extends award_criteria {
         global $DB;
 
         $join = '';
-        $where = '';
+        $whereparts = array();
         $params = array();
         $rule = ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) ? ' OR ' : ' AND ';
 
         foreach ($this->params as $param) {
             if (is_numeric($param['field'])) {
-                $infodata[] = " uid.fieldid = :fieldid{$param['field']} ";
-                $params["fieldid{$param['field']}"] = $param['field'];
+                // This is a custom field.
+                $idx = count($whereparts);
+                $join .= " LEFT JOIN {user_info_data} uid{$idx} ON uid{$idx}.userid = u.id AND uid{$idx}.fieldid = :fieldid{$idx} ";
+                $params["fieldid{$idx}"] = $param['field'];
+                $whereparts[] = "uid{$idx}.id IS NOT NULL";
             } else {
-                $userdata[] = $DB->sql_isnotempty('u', "u.{$param['field']}", false, true);
+                $whereparts[] = $DB->sql_isnotempty('u', "u.{$param['field']}", false, true);
             }
         }
 
-        // Add user custom fields if there are any.
-        if (!empty($infodata)) {
-            $extraon = implode($rule, $infodata);
-            $join = " LEFT JOIN {user_info_data} uid ON uid.userid = u.id AND ({$extraon})";
-        }
-
-        // Add user table fields if there are any.
-        if (!empty($userdata)) {
-            $extraon = implode($rule, $userdata);
-            $where = " AND ({$extraon})";
+        if ($whereparts) {
+            $where = " AND (" . implode($rule, $whereparts) . ")";
+        } else {
+            $where = '';
         }
         return array($join, $where, $params);
     }
index b628431..2de1122 100644 (file)
@@ -344,6 +344,11 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id));
         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->cmid => $this->module->cmid));
 
+        // Assert the badge will not be issued to the user as is.
+        $badge = new badge($this->coursebadge);
+        $badge->review_all_criteria();
+        $this->assertFalse($badge->is_issued($this->user->id));
+
         // Set completion for forum activity.
         $c = new completion_info($this->course);
         $activities = $c->get_activities();
@@ -379,6 +384,11 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
 
         $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
 
+        // Assert the badge will not be issued to the user as is.
+        $badge = new badge($this->coursebadge);
+        $badge->review_all_criteria();
+        $this->assertFalse($badge->is_issued($this->user->id));
+
         // Mark course as complete.
         $sink = $this->redirectEmails();
         $ccompletion->mark_complete();
@@ -394,18 +404,33 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
      * Test badges observer when user_updated event is fired.
      */
     public function test_badges_observer_profile_criteria_review() {
+        global $CFG, $DB;
+        require_once($CFG->dirroot.'/user/profile/lib.php');
+
+        // Add a custom field of textarea type.
+        $customprofileid = $DB->insert_record('user_info_field', array(
+            'shortname' => 'newfield', 'name' => 'Description of new field', 'categoryid' => 1,
+            'datatype' => 'textarea'));
+
         $this->preventResetByRollback(); // Messaging is not compatible with transactions.
         $badge = new badge($this->coursebadge);
-        $this->assertFalse($badge->is_issued($this->user->id));
 
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
         $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
-        $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'field_aim' => 'aim'));
+        $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'field_aim' => 'aim',
+            'field_' . $customprofileid => $customprofileid));
+
+        // Assert the badge will not be issued to the user as is.
+        $badge = new badge($this->coursebadge);
+        $badge->review_all_criteria();
+        $this->assertFalse($badge->is_issued($this->user->id));
 
+        // Set the required fields and make sure the badge got issued.
         $this->user->address = 'Test address';
         $this->user->aim = '999999999';
         $sink = $this->redirectEmails();
+        profile_save_data((object)array('id' => $this->user->id, 'profile_field_newfield' => 'X'));
         user_update_user($this->user, false);
         $this->assertCount(1, $sink->get_messages());
         $sink->close();
index 9fe64c9..2183370 100644 (file)
@@ -104,6 +104,23 @@ class block_html extends block_base {
         return true;
     }
 
+    /**
+     * Copy any block-specific data when copying to a new block instance.
+     * @param int $fromid the id number of the block instance to copy from
+     * @return boolean
+     */
+    public function instance_copy($fromid) {
+        $fromcontext = context_block::instance($fromid);
+        $fs = get_file_storage();
+        // This extra check if file area is empty adds one query if it is not empty but saves several if it is.
+        if (!$fs->is_area_empty($fromcontext->id, 'block_html', 'content', 0, false)) {
+            $draftitemid = 0;
+            file_prepare_draft_area($draftitemid, $fromcontext->id, 'block_html', 'content', 0, array('subdirs' => true));
+            file_save_draft_area_files($draftitemid, $this->context->id, 'block_html', 'content', 0, array('subdirs' => true));
+        }
+        return true;
+    }
+
     function content_is_trusted() {
         global $SCRIPT;
 
index 2851ae7..ed80f7b 100644 (file)
@@ -6,9 +6,9 @@ Feature: Block tags displaying tag cloud
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email |
-      | teacher1 | Teacher | 1 | teacher1@example.com |
-      | student1 | Student | 1 | student1@example.com |
+      | username | firstname | lastname | email | interests |
+      | teacher1 | Teacher | 1 | teacher1@example.com | Dogs, Cats |
+      | student1 | Student | 1 | student1@example.com | |
     And the following "courses" exist:
       | fullname  | shortname |
       | Course 1  | c1        |
@@ -19,13 +19,6 @@ Feature: Block tags displaying tag cloud
       | user     | course | role           |
       | teacher1 | c1     | editingteacher |
       | student1 | c1     | student        |
-    And I log in as "teacher1"
-    And I follow "Preferences" in the user menu
-    And I follow "Edit profile"
-    And I expand all fieldsets
-    And I set the field "Enter tags separated by commas" to "Dogs, Cats"
-    And I press "Update profile"
-    And I log out
 
   Scenario: Add Tags block on a front page
     When I log in as "admin"
index ea4724a..1aff731 100644 (file)
@@ -80,8 +80,17 @@ if ($deletesection) {
 }
 
 $editoroptions = array('context'=>$context ,'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true);
-$mform = course_get_format($course->id)->editsection_form($PAGE->url,
-        array('cs' => $sectioninfo, 'editoroptions' => $editoroptions));
+
+$courseformat = course_get_format($course);
+$defaultsectionname = $courseformat->get_default_section_name($section);
+
+$customdata = array(
+    'cs' => $sectioninfo,
+    'editoroptions' => $editoroptions,
+    'defaultsectionname' => $defaultsectionname
+);
+$mform = $courseformat->editsection_form($PAGE->url, $customdata);
+
 // set current value, make an editable copy of section_info object
 // this will retrieve all format-specific options as well
 $initialdata = convert_to_array($sectioninfo);
index c614470..f4a9df4 100644 (file)
@@ -25,7 +25,16 @@ class editsection_form extends moodleform {
 
         $elementgroup = array();
         $elementgroup[] = $mform->createElement('text', 'name', '', array('size' => '30', 'maxlength' => '255'));
-        $elementgroup[] = $mform->createElement('checkbox', 'usedefaultname', '', get_string('sectionusedefaultname'));
+
+        // Get default section name.
+        $defaultsectionname = $this->_customdata['defaultsectionname'];
+        if ($defaultsectionname) {
+            $defaultsectionname = ' [' . $defaultsectionname . ']';
+        }
+
+        $elementgroup[] = $mform->createElement('checkbox', 'usedefaultname', '',
+                                                get_string('sectionusedefaultname') . $defaultsectionname);
+
         $mform->addGroup($elementgroup, 'name_group', get_string('sectionname'), ' ', false);
         $mform->addGroupRule('name_group', array('name' => array(array(get_string('maximumchars', '', 255), 'maxlength', 255))));
 
@@ -103,7 +112,7 @@ class editsection_form extends moodleform {
         $data = parent::get_data();
         if ($data !== null) {
             $editoroptions = $this->_customdata['editoroptions'];
-            if (!empty($data->usedefaultname)) {
+            if (!empty($data->usedefaultname) || empty(trim($data->name))) {
                 $data->name = null;
             }
             $data = file_postupdate_standard_editor($data, 'summary', $editoroptions,
index 8d65610..d5423f7 100644 (file)
@@ -350,7 +350,23 @@ abstract class format_base {
         } else {
             $sectionnum = $section;
         }
-        return get_string('sectionname', 'format_'.$this->format) . ' ' . $sectionnum;
+
+        if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) {
+            return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum;
+        }
+
+        // Return an empty string if there's no available section name string for the given format.
+        return '';
+    }
+
+    /**
+     * Returns the default section using format_base's implementation of get_section_name.
+     *
+     * @param int|stdClass $section Section object from database or just field course_sections section
+     * @return string The default value for the section name based on the given course format.
+     */
+    public function get_default_section_name($section) {
+        return self::get_section_name($section);
     }
 
     /**
index 034b58e..228b122 100644 (file)
@@ -57,10 +57,29 @@ class format_topics extends format_base {
         if ((string)$section->name !== '') {
             return format_string($section->name, true,
                     array('context' => context_course::instance($this->courseid)));
-        } else if ($section->section == 0) {
+        } else {
+            return $this->get_default_section_name($section);
+        }
+    }
+
+    /**
+     * Returns the default section name for the topics course format.
+     *
+     * If the section number is 0, it will use the string with key = section0name from the course format's lang file.
+     * If the section number is not 0, the base implementation of format_base::get_default_section_name which uses
+     * the string with the key = 'sectionname' from the course format's lang file + the section number will be used.
+     *
+     * @param stdClass $section Section object from database or just field course_sections section
+     * @return string The default value for the section name.
+     */
+    public function get_default_section_name($section) {
+        if ($section->section == 0) {
+            // Return the general section.
             return get_string('section0name', 'format_topics');
         } else {
-            return get_string('topic').' '.$section->section;
+            // Use format_base::get_default_section_name implementation which
+            // will display the section name in "Topic n" format.
+            return parent::get_default_section_name($section);
         }
     }
 
index 6973dea..150c5e3 100644 (file)
@@ -24,6 +24,22 @@ Feature: Sections can be edited and deleted in topics format
     And I follow "Course 1"
     And I turn editing mode on
 
+  Scenario: View the default name of the general section in topics format
+    When I click on "Edit section" "link" in the "li#section-0" "css_element"
+    Then I should see "Use default section name [General]"
+
+  Scenario: Edit the default name of the general section in topics format
+    When I click on "Edit section" "link" in the "li#section-0" "css_element"
+    And I set the following fields to these values:
+      | Use default section name | 0                           |
+      | name                     | This is the general section |
+    And I press "Save changes"
+    Then I should see "This is the general section" in the "li#section-0" "css_element"
+
+  Scenario: View the default name of the second section in topics format
+    When I click on "Edit topic" "link" in the "li#section-2" "css_element"
+    Then I should see "Use default section name [Topic 2]"
+
   Scenario: Edit section summary in topics format
     When I edit the section "2"
     And I set the following fields to these values:
index 09dea51..3253e45 100644 (file)
@@ -61,4 +61,89 @@ class format_topics_testcase extends advanced_testcase {
         $this->assertEquals(8, count(get_fast_modinfo($course)->get_section_info_all()));
         $this->assertEquals(6, course_get_format($course)->get_course()->numsections);
     }
+
+    /**
+     * Tests for format_topics::get_section_name method with default section names.
+     */
+    public function test_get_section_name() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Generate a course with 5 sections.
+        $generator = $this->getDataGenerator();
+        $numsections = 5;
+        $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
+            array('createsections' => true));
+
+        // Get section names for course.
+        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Test get_section_name with default section names.
+        $courseformat = course_get_format($course);
+        foreach ($coursesections as $section) {
+            // Assert that with unmodified section names, get_section_name returns the same result as get_default_section_name.
+            $this->assertEquals($courseformat->get_default_section_name($section), $courseformat->get_section_name($section));
+        }
+    }
+
+    /**
+     * Tests for format_topics::get_section_name method with modified section names.
+     */
+    public function test_get_section_name_customised() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Generate a course with 5 sections.
+        $generator = $this->getDataGenerator();
+        $numsections = 5;
+        $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
+            array('createsections' => true));
+
+        // Get section names for course.
+        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Modify section names.
+        $customname = "Custom Section";
+        foreach ($coursesections as $section) {
+            $section->name = "$customname $section->section";
+            $DB->update_record('course_sections', $section);
+        }
+
+        // Requery updated section names then test get_section_name.
+        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+        $courseformat = course_get_format($course);
+        foreach ($coursesections as $section) {
+            // Assert that with modified section names, get_section_name returns the modified section name.
+            $this->assertEquals($section->name, $courseformat->get_section_name($section));
+        }
+    }
+
+    /**
+     * Tests for format_topics::get_default_section_name.
+     */
+    public function test_get_default_section_name() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Generate a course with 5 sections.
+        $generator = $this->getDataGenerator();
+        $numsections = 5;
+        $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
+            array('createsections' => true));
+
+        // Get section names for course.
+        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Test get_default_section_name with default section names.
+        $courseformat = course_get_format($course);
+        foreach ($coursesections as $section) {
+            if ($section->section == 0) {
+                $sectionname = get_string('section0name', 'format_topics');
+                $this->assertEquals($sectionname, $courseformat->get_default_section_name($section));
+            } else {
+                $sectionname = get_string('sectionname', 'format_topics') . ' ' . $section->section;
+                $this->assertEquals($sectionname, $courseformat->get_default_section_name($section));
+            }
+        }
+    }
 }
index 2f216c2..fce6e1c 100644 (file)
@@ -7,6 +7,10 @@ Overview of this plugin type at http://docs.moodle.org/dev/Course_formats
   renderable menu or array of links.  Plugin calls to section_edit_controls will now include the section edit control in the returned array.
 * The section name is now wrapped in a new span (.sectionname > span), process_sections method in format.js should be updated so .sectionname
   DOM node's wraps the section title in a span. You can look at how to implement the change in course/format/topics/format.js or MDL-48947.
+* New method format_base::get_default_section_name retrieves the default section name for the given course format. The base
+  implementation basically uses the implementation of format_base::get_section_name. The method can be overridden in
+  format_base subclasses that use sections (i.e. format_topics, format_weeks). In relation to the changes made for the default
+  section name, the default section name is now being shown when editing the section information.
 
 === 2.9 ===
 * Course formats may support deleting sections, see MDL-10405 for more details.
index 59a58b3..7bcf1f4 100644 (file)
@@ -55,7 +55,22 @@ class format_weeks extends format_base {
         if ((string)$section->name !== '') {
             // Return the name the user set.
             return format_string($section->name, true, array('context' => context_course::instance($this->courseid)));
-        } else if ($section->section == 0) {
+        } else {
+            return $this->get_default_section_name($section);
+        }
+    }
+
+    /**
+     * Returns the default section name for the weekly course format.
+     *
+     * If the section number is 0, it will use the string with key = section0name from the course format's lang file.
+     * Otherwise, the default format of "[start date] - [end date]" will be returned.
+     *
+     * @param stdClass $section Section object from database or just field course_sections section
+     * @return string The default value for the section name.
+     */
+    public function get_default_section_name($section) {
+        if ($section->section == 0) {
             // Return the general section.
             return get_string('section0name', 'format_weeks');
         } else {
index 255fa83..0e296c7 100644 (file)
@@ -24,6 +24,22 @@ Feature: Sections can be edited and deleted in weeks format
     And I follow "Course 1"
     And I turn editing mode on
 
+  Scenario: View the default name of the general section in weeks format
+    When I click on "Edit section" "link" in the "li#section-0" "css_element"
+    Then I should see "Use default section name [General]"
+
+  Scenario: Edit the default name of the general section in weeks format
+    When I click on "Edit section" "link" in the "li#section-0" "css_element"
+    And I set the following fields to these values:
+      | Use default section name | 0                           |
+      | name                     | This is the general section |
+    And I press "Save changes"
+    Then I should see "This is the general section" in the "li#section-0" "css_element"
+
+  Scenario: View the default name of the second section in weeks format
+    When I click on "Edit week" "link" in the "li#section-2" "css_element"
+    Then I should see "Use default section name [8 May - 14 May]"
+
   Scenario: Edit section summary in weeks format
     When I click on "Edit week" "link" in the "li#section-2" "css_element"
     And I set the following fields to these values:
index a388b59..ad014fc 100644 (file)
@@ -61,4 +61,95 @@ class format_weeks_testcase extends advanced_testcase {
         $this->assertEquals(8, count(get_fast_modinfo($course)->get_section_info_all()));
         $this->assertEquals(6, course_get_format($course)->get_course()->numsections);
     }
+
+    /**
+     * Tests for format_weeks::get_section_name method with default section names.
+     */
+    public function test_get_section_name() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Generate a course with 5 sections.
+        $generator = $this->getDataGenerator();
+        $numsections = 5;
+        $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'weeks'),
+            array('createsections' => true));
+
+        // Get section names for course.
+        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Test get_section_name with default section names.
+        $courseformat = course_get_format($course);
+        foreach ($coursesections as $section) {
+            // Assert that with unmodified section names, get_section_name returns the same result as get_default_section_name.
+            $this->assertEquals($courseformat->get_default_section_name($section), $courseformat->get_section_name($section));
+        }
+    }
+
+    /**
+     * Tests for format_weeks::get_section_name method with modified section names.
+     */
+    public function test_get_section_name_customised() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Generate a course with 5 sections.
+        $generator = $this->getDataGenerator();
+        $numsections = 5;
+        $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'weeks'),
+            array('createsections' => true));
+
+        // Get section names for course.
+        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Modify section names.
+        $customname = "Custom Section";
+        foreach ($coursesections as $section) {
+            $section->name = "$customname $section->section";
+            $DB->update_record('course_sections', $section);
+        }
+
+        // Requery updated section names then test get_section_name.
+        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+        $courseformat = course_get_format($course);
+        foreach ($coursesections as $section) {
+            // Assert that with modified section names, get_section_name returns the modified section name.
+            $this->assertEquals($section->name, $courseformat->get_section_name($section));
+        }
+    }
+
+    /**
+     * Tests for format_weeks::get_default_section_name.
+     */
+    public function test_get_default_section_name() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Generate a course with 5 sections.
+        $generator = $this->getDataGenerator();
+        $numsections = 5;
+        $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'weeks'),
+            array('createsections' => true));
+
+        // Get section names for course.
+        $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+
+        // Test get_default_section_name with default section names.
+        $courseformat = course_get_format($course);
+        foreach ($coursesections as $section) {
+            if ($section->section == 0) {
+                $sectionname = get_string('section0name', 'format_weeks');
+                $this->assertEquals($sectionname, $courseformat->get_default_section_name($section));
+            } else {
+                $dates = $courseformat->get_section_dates($section);
+                $dates->end = ($dates->end - 86400);
+                $dateformat = get_string('strftimedateshort');
+                $weekday = userdate($dates->start, $dateformat);
+                $endweekday = userdate($dates->end, $dateformat);
+                $sectionname = $weekday.' - '.$endweekday;
+
+                $this->assertEquals($sectionname, $courseformat->get_default_section_name($section));
+            }
+        }
+    }
 }
index ab7cd1e..4ab8d5b 100644 (file)
@@ -23,6 +23,8 @@
  */
 
 $string['canntenrol'] = 'Enrolment is disabled or inactive';
+$string['canntenrolearly'] = 'You cannot enrol yet; enrolment starts on {$a}.';
+$string['canntenrollate'] = 'You cannot enrol any more, since enrolment ended on {$a}.';
 $string['cohortnonmemberinfo'] = 'Only members of cohort \'{$a}\' can self-enrol.';
 $string['cohortonly'] = 'Only cohort members';
 $string['cohortonly_help'] = 'Self enrolment may be restricted to members of a specified cohort only. Note that changing this setting has no effect on existing enrolments.';
index 6c1364b..7ad82ef 100644 (file)
@@ -289,11 +289,11 @@ class enrol_self_plugin extends enrol_plugin {
         }
 
         if ($instance->enrolstartdate != 0 and $instance->enrolstartdate > time()) {
-            return get_string('canntenrol', 'enrol_self');
+            return get_string('canntenrolearly', 'enrol_self', userdate($instance->enrolstartdate));
         }
 
         if ($instance->enrolenddate != 0 and $instance->enrolenddate < time()) {
-            return get_string('canntenrol', 'enrol_self');
+            return get_string('canntenrollate', 'enrol_self', userdate($instance->enrolenddate));
         }
 
         if (!$instance->customint6) {
index 8e218cd..ad5dbb3 100644 (file)
--- a/index.php
+++ b/index.php
@@ -205,9 +205,9 @@ foreach (explode(',', $frontpagelayout) as $v) {
                 $newsforumcontext = context_module::instance($newsforumcm->id, MUST_EXIST);
 
                 $forumname = format_string($newsforum->name, true, array('context' => $newsforumcontext));
-                echo html_writer::tag('a',
+                echo html_writer::link('#',
                     get_string('skipa', 'access', core_text::strtolower(strip_tags($forumname))),
-                    array('href' => '#skipsitenews', 'class' => 'skip-block'));
+                    array('data-target' => '#skipsitenews', 'class' => 'skip-block skip'));
 
                 // Wraps site news forum in div container.
                 echo html_writer::start_tag('div', array('id' => 'site-news-forum'));
@@ -234,16 +234,16 @@ foreach (explode(',', $frontpagelayout) as $v) {
                 // End site news forum div container.
                 echo html_writer::end_tag('div');
 
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipsitenews'));
+                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipsitenews', 'tabindex' => '-1'));
             }
         break;
 
         case FRONTPAGEENROLLEDCOURSELIST:
             $mycourseshtml = $courserenderer->frontpage_my_courses();
             if (!empty($mycourseshtml)) {
-                echo html_writer::tag('a',
+                echo html_writer::link('#',
                     get_string('skipa', 'access', core_text::strtolower(get_string('mycourses'))),
-                    array('href' => '#skipmycourses', 'class' => 'skip-block'));
+                    array('data-target' => '#skipmycourses', 'class' => 'skip skip-block'));
 
                 // Wrap frontpage course list in div container.
                 echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
@@ -254,7 +254,7 @@ foreach (explode(',', $frontpagelayout) as $v) {
                 // End frontpage course list div container.
                 echo html_writer::end_tag('div');
 
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipmycourses'));
+                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipmycourses', 'tabindex' => '-1'));
                 break;
             }
             // No "break" here. If there are no enrolled courses - continue to 'Available courses'.
@@ -262,9 +262,9 @@ foreach (explode(',', $frontpagelayout) as $v) {
         case FRONTPAGEALLCOURSELIST:
             $availablecourseshtml = $courserenderer->frontpage_available_courses();
             if (!empty($availablecourseshtml)) {
-                echo html_writer::tag('a',
+                echo html_writer::link('#',
                     get_string('skipa', 'access', core_text::strtolower(get_string('availablecourses'))),
-                    array('href' => '#skipavailablecourses', 'class' => 'skip-block'));
+                    array('data-target' => '#skipavailablecourses', 'class' => 'skip skip-block'));
 
                 // Wrap frontpage course list in div container.
                 echo html_writer::start_tag('div', array('id' => 'frontpage-course-list'));
@@ -275,14 +275,14 @@ foreach (explode(',', $frontpagelayout) as $v) {
                 // End frontpage course list div container.
                 echo html_writer::end_tag('div');
 
-                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipavailablecourses'));
+                echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipavailablecourses', 'tabindex' => '-1'));
             }
         break;
 
         case FRONTPAGECATEGORYNAMES:
-            echo html_writer::tag('a',
+            echo html_writer::link('#',
                 get_string('skipa', 'access', core_text::strtolower(get_string('categories'))),
-                array('href' => '#skipcategories', 'class' => 'skip-block'));
+                array('data-target' => '#skipcategories', 'class' => 'skip skip-block'));
 
             // Wrap frontpage category names in div container.
             echo html_writer::start_tag('div', array('id' => 'frontpage-category-names'));
@@ -293,13 +293,13 @@ foreach (explode(',', $frontpagelayout) as $v) {
             // End frontpage category names div container.
             echo html_writer::end_tag('div');
 
-            echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcategories'));
+            echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcategories', 'tabindex' => '-1'));
         break;
 
         case FRONTPAGECATEGORYCOMBO:
-            echo html_writer::tag('a',
+            echo html_writer::link('#',
                 get_string('skipa', 'access', core_text::strtolower(get_string('courses'))),
-                array('href' => '#skipcourses', 'class' => 'skip-block'));
+                array('data-target' => '#skipcourses', 'class' => 'skip skip-block'));
 
             // Wrap frontpage category combo in div container.
             echo html_writer::start_tag('div', array('id' => 'frontpage-category-combo'));
@@ -310,7 +310,7 @@ foreach (explode(',', $frontpagelayout) as $v) {
             // End frontpage category combo div container.
             echo html_writer::end_tag('div');
 
-            echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcourses'));
+            echo html_writer::tag('span', '', array('class' => 'skip-block-to', 'id' => 'skipcourses', 'tabindex' => '-1'));
         break;
 
         case FRONTPAGECOURSESEARCH:
index debcc5f..ce151b9 100644 (file)
@@ -7691,6 +7691,12 @@ class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
     public function get_setting() {
         global $CFG;
 
+        // First check if is not set.
+        $result = $this->config_read($this->name);
+        if (is_null($result)) {
+            return null;
+        }
+
         // For install cli script, $CFG->defaultuserroleid is not set so return 0
         // Or if web services aren't enabled this can't be,
         if (empty($CFG->defaultuserroleid) || empty($CFG->enablewebservices)) {
@@ -7701,7 +7707,7 @@ class admin_setting_enablemobileservice extends admin_setting_configcheckbox {
         $webservicemanager = new webservice();
         $mobileservice = $webservicemanager->get_external_service_by_shortname(MOODLE_OFFICIAL_MOBILE_SERVICE);
         if ($mobileservice->enabled and $this->is_protocol_cap_allowed()) {
-            return $this->config_read($this->name); //same as returning 1
+            return $result;
         } else {
             return 0;
         }
index c757939..372e525 100644 (file)
Binary files a/lib/amd/build/localstorage.min.js and b/lib/amd/build/localstorage.min.js differ
index 545841a..5d86f16 100644 (file)
Binary files a/lib/amd/build/loglevel.min.js and b/lib/amd/build/loglevel.min.js differ
index 7cd824e..3ca51ec 100644 (file)
@@ -46,13 +46,22 @@ define(['core/config'], function(config) {
             // Disable cache if debugging.
             return false;
         }
-        if (typeof(window.localStorage) !== "undefined") {
-            try {
-                localStorage = window.localStorage;
-                return localStorage !== null;
-            } catch (ex) {
+        if (typeof(window.localStorage) === "undefined") {
+            return false;
+        }
+        var testKey = 'test';
+        try {
+            localStorage = window.localStorage;
+            if (localStorage === null) {
                 return false;
             }
+            // MDL-51461 - Some browsers misreport availability of local storage
+            // so check it is actually usable.
+            localStorage.setItem(testKey, '1');
+            localStorage.removeItem(testKey);
+            return true;
+        } catch (ex) {
+            return false;
         }
     };
 
index 08be34d..4d96ca1 100644 (file)
 // Copy loglevel.js into lib/amd/src/ in Moodle folder.\r
 // Add the license as a comment to the file and these instructions.\r
 // Add the jshint ignore:start and ignore:end comments.\r
+// Delete the jshint validthis:true comments.\r
 \r
 /* jshint ignore:start */\r
-/*! loglevel - v1.2.0 - https://github.com/pimterry/loglevel - (c) 2014 Tim Perry - licensed MIT */\r
+/*! loglevel - v1.4.0 - https://github.com/pimterry/loglevel - (c) 2015 Tim Perry - licensed MIT */\r
 (function (root, definition) {\r
+    "use strict";\r
     if (typeof module === 'object' && module.exports && typeof require === 'function') {\r
         module.exports = definition();\r
     } else if (typeof define === 'function' && typeof define.amd === 'object') {\r
@@ -39,7 +41,7 @@
         root.log = definition();\r
     }\r
 }(this, function () {\r
-    var self = {};\r
+    "use strict";\r
     var noop = function() {};\r
     var undefinedType = "undefined";\r
 \r
         }\r
     }\r
 \r
-    function enableLoggingWhenConsoleArrives(methodName, level) {\r
+    // these private functions always need `this` to be set properly\r
+\r
+    function enableLoggingWhenConsoleArrives(methodName, level, loggerName) {\r
         return function () {\r
             if (typeof console !== undefinedType) {\r
-                replaceLoggingMethods(level);\r
-                self[methodName].apply(self, arguments);\r
+                replaceLoggingMethods.call(this, level, loggerName);\r
+                this[methodName].apply(this, arguments);\r
             }\r
         };\r
     }\r
 \r
-    var logMethods = [\r
-        "trace",\r
-        "debug",\r
-        "info",\r
-        "warn",\r
-        "error"\r
-    ];\r
-\r
-    function replaceLoggingMethods(level) {\r
+    function replaceLoggingMethods(level, loggerName) {\r
         for (var i = 0; i < logMethods.length; i++) {\r
             var methodName = logMethods[i];\r
-            self[methodName] = (i < level) ? noop : self.methodFactory(methodName, level);\r
+            this[methodName] = (i < level) ?\r
+                noop :\r
+                this.methodFactory(methodName, level, loggerName);\r
         }\r
     }\r
 \r
-    function persistLevelIfPossible(levelNum) {\r
-        var levelName = (logMethods[levelNum] || 'silent').toUpperCase();\r
-\r
-        // Use localStorage if available\r
-        try {\r
-            window.localStorage['loglevel'] = levelName;\r
-            return;\r
-        } catch (ignore) {}\r
-\r
-        // Use session cookie as fallback\r
-        try {\r
-            window.document.cookie = "loglevel=" + levelName + ";";\r
-        } catch (ignore) {}\r
+    function defaultMethodFactory(methodName, level, loggerName) {\r
+        return realMethod(methodName) ||\r
+               enableLoggingWhenConsoleArrives.apply(this, arguments);\r
     }\r
 \r
-    function loadPersistedLevel() {\r
-        var storedLevel;\r
-\r
-        try {\r
-            storedLevel = window.localStorage['loglevel'];\r
-        } catch (ignore) {}\r
-\r
-        if (typeof storedLevel === undefinedType) {\r
-            try {\r
-                storedLevel = /loglevel=([^;]+)/.exec(window.document.cookie)[1];\r
-            } catch (ignore) {}\r
-        }\r
-        \r
-        if (self.levels[storedLevel] === undefined) {\r
-            storedLevel = "WARN";\r
-        }\r
+    var logMethods = [\r
+        "trace",\r
+        "debug",\r
+        "info",\r
+        "warn",\r
+        "error"\r
+    ];\r
 \r
-        self.setLevel(self.levels[storedLevel]);\r
+    function Logger(name, defaultLevel, factory) {\r
+      var self = this;\r
+      var currentLevel;\r
+      var storageKey = "loglevel";\r
+      if (name) {\r
+        storageKey += ":" + name;\r
+      }\r
+\r
+      function persistLevelIfPossible(levelNum) {\r
+          var levelName = (logMethods[levelNum] || 'silent').toUpperCase();\r
+\r
+          // Use localStorage if available\r
+          try {\r
+              window.localStorage[storageKey] = levelName;\r
+              return;\r
+          } catch (ignore) {}\r
+\r
+          // Use session cookie as fallback\r
+          try {\r
+              window.document.cookie =\r
+                encodeURIComponent(storageKey) + "=" + levelName + ";";\r
+          } catch (ignore) {}\r
+      }\r
+\r
+      function getPersistedLevel() {\r
+          var storedLevel;\r
+\r
+          try {\r
+              storedLevel = window.localStorage[storageKey];\r
+          } catch (ignore) {}\r
+\r
+          if (typeof storedLevel === undefinedType) {\r
+              try {\r
+                  var cookie = window.document.cookie;\r
+                  var location = cookie.indexOf(\r
+                      encodeURIComponent(storageKey) + "=");\r
+                  if (location) {\r
+                      storedLevel = /^([^;]+)/.exec(cookie.slice(location))[1];\r
+                  }\r
+              } catch (ignore) {}\r
+          }\r
+\r
+          // If the stored level is not valid, treat it as if nothing was stored.\r
+          if (self.levels[storedLevel] === undefined) {\r
+              storedLevel = undefined;\r
+          }\r
+\r
+          return storedLevel;\r
+      }\r
+\r
+      /*\r
+       *\r
+       * Public API\r
+       *\r
+       */\r
+\r
+      self.levels = { "TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3,\r
+          "ERROR": 4, "SILENT": 5};\r
+\r
+      self.methodFactory = factory || defaultMethodFactory;\r
+\r
+      self.getLevel = function () {\r
+          return currentLevel;\r
+      };\r
+\r
+      self.setLevel = function (level, persist) {\r
+          if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) {\r
+              level = self.levels[level.toUpperCase()];\r
+          }\r
+          if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) {\r
+              currentLevel = level;\r
+              if (persist !== false) {  // defaults to true\r
+                  persistLevelIfPossible(level);\r
+              }\r
+              replaceLoggingMethods.call(self, level, name);\r
+              if (typeof console === undefinedType && level < self.levels.SILENT) {\r
+                  return "No console available for logging";\r
+              }\r
+          } else {\r
+              throw "log.setLevel() called with invalid level: " + level;\r
+          }\r
+      };\r
+\r
+      self.setDefaultLevel = function (level) {\r
+          if (!getPersistedLevel()) {\r
+              self.setLevel(level, false);\r
+          }\r
+      };\r
+\r
+      self.enableAll = function(persist) {\r
+          self.setLevel(self.levels.TRACE, persist);\r
+      };\r
+\r
+      self.disableAll = function(persist) {\r
+          self.setLevel(self.levels.SILENT, persist);\r
+      };\r
+\r
+      // Initialize with the right level\r
+      var initialLevel = getPersistedLevel();\r
+      if (initialLevel == null) {\r
+          initialLevel = defaultLevel == null ? "WARN" : defaultLevel;\r
+      }\r
+      self.setLevel(initialLevel, false);\r
     }\r
 \r
     /*\r
      *\r
-     * Public API\r
+     * Package-level API\r
      *\r
      */\r
 \r
-    self.levels = { "TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3,\r
-        "ERROR": 4, "SILENT": 5};\r
+    var defaultLogger = new Logger();\r
 \r
-    self.methodFactory = function (methodName, level) {\r
-        return realMethod(methodName) ||\r
-               enableLoggingWhenConsoleArrives(methodName, level);\r
-    };\r
-\r
-    self.setLevel = function (level) {\r
-        if (typeof level === "string" && self.levels[level.toUpperCase()] !== undefined) {\r
-            level = self.levels[level.toUpperCase()];\r
-        }\r
-        if (typeof level === "number" && level >= 0 && level <= self.levels.SILENT) {\r
-            persistLevelIfPossible(level);\r
-            replaceLoggingMethods(level);\r
-            if (typeof console === undefinedType && level < self.levels.SILENT) {\r
-                return "No console available for logging";\r
-            }\r
-        } else {\r
-            throw "log.setLevel() called with invalid level: " + level;\r
+    var _loggersByName = {};\r
+    defaultLogger.getLogger = function getLogger(name) {\r
+        if (typeof name !== "string" || name === "") {\r
+          throw new TypeError("You must supply a name when creating a logger.");\r
         }\r
-    };\r
-\r
-    self.enableAll = function() {\r
-        self.setLevel(self.levels.TRACE);\r
-    };\r
 \r
-    self.disableAll = function() {\r
-        self.setLevel(self.levels.SILENT);\r
+        var logger = _loggersByName[name];\r
+        if (!logger) {\r
+          logger = _loggersByName[name] = new Logger(\r
+            name, defaultLogger.getLevel(), defaultLogger.methodFactory);\r
+        }\r
+        return logger;\r
     };\r
 \r
     // Grab the current global log variable in case of overwrite\r
     var _log = (typeof window !== undefinedType) ? window.log : undefined;\r
-    self.noConflict = function() {\r
+    defaultLogger.noConflict = function() {\r
         if (typeof window !== undefinedType &&\r
-               window.log === self) {\r
+               window.log === defaultLogger) {\r
             window.log = _log;\r
         }\r
 \r
-        return self;\r
+        return defaultLogger;\r
     };\r
 \r
-    loadPersistedLevel();\r
-    return self;\r
+    return defaultLogger;\r
 }));\r
 /* jshint ignore:end */\r
index 9a0f927..de8750a 100644 (file)
       <INDEXES>
         <INDEX NAME="useridto" UNIQUE="false" FIELDS="useridto"/>
         <INDEX NAME="useridfromtodeleted" UNIQUE="false" FIELDS="useridfrom, useridto, timeuserfromdeleted, timeusertodeleted"/>
+        <INDEX NAME="notificationtimeread" UNIQUE="false" FIELDS="notification, timeread"/>
       </INDEXES>
     </TABLE>
     <TABLE NAME="message_contacts" COMMENT="Maintains lists of relationships between users">
index 84b9be1..4de6b78 100644 (file)
@@ -1237,6 +1237,7 @@ $services = array(
             'mod_choice_submit_choice_response',
             'mod_choice_view_choice',
             'mod_choice_get_choices_by_courses',
+            'mod_lti_get_tool_launch_data',
             'mod_imscp_view_imscp',
             'mod_imscp_get_imscps_by_courses',
             ),
index 56e153f..4fd7aea 100644 (file)
@@ -4584,5 +4584,20 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2015092900.00);
     }
 
+    if ($oldversion < 2015100600.00) {
+
+        // Define index notification (not unique) to be added to message_read.
+        $table = new xmldb_table('message_read');
+        $index = new xmldb_index('notificationtimeread', XMLDB_INDEX_NOTUNIQUE, array('notification', 'timeread'));
+
+        // Conditionally launch add index notification.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2015100600.00);
+    }
+
     return true;
 }
index 971d480..760c8a1 100644 (file)
Binary files a/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-debug.js and b/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-debug.js differ
index 16a998f..9b3b4fd 100644 (file)
Binary files a/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-min.js and b/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-min.js differ
index 971d480..760c8a1 100644 (file)
Binary files a/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button.js and b/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button.js differ
index fdf92b8..653cb1c 100644 (file)
@@ -69,6 +69,7 @@ Y.namespace('M.atto_backcolor').Button = Y.Base.create('button', Y.M.editor_atto
             icon: 'e/text_highlight',
             overlayWidth: '4',
             globalItemConfig: {
+                inlineFormat: true,
                 callback: this._changeStyle
             },
             items: items
index 9c7b078..52449ed 100644 (file)
Binary files a/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-debug.js and b/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-debug.js differ
index 9eb70b3..b63e60a 100644 (file)
Binary files a/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-min.js and b/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-min.js differ
index 9c7b078..52449ed 100644 (file)
Binary files a/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button.js and b/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button.js differ
index f6aed22..7899332 100644 (file)
@@ -71,6 +71,7 @@ Y.namespace('M.atto_fontcolor').Button = Y.Base.create('button', Y.M.editor_atto
             overlayWidth: '4',
             menuColor: '#333333',
             globalItemConfig: {
+                inlineFormat: true,
                 callback: this._changeStyle
             },
             items: items
index 43a1171..d1fbd24 100644 (file)
Binary files a/lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button-debug.js and b/lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button-debug.js differ
index bce6ccd..b8d0e23 100644 (file)
Binary files a/lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button-min.js and b/lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button-min.js differ
index 43a1171..d1fbd24 100644 (file)
Binary files a/lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button.js and b/lib/editor/atto/plugins/noautolink/yui/build/moodle-atto_noautolink-button/moodle-atto_noautolink-button.js differ
index 6fd9c7b..1f6db9d 100644 (file)
@@ -36,6 +36,7 @@ Y.namespace('M.atto_noautolink').Button = Y.Base.create('button', Y.M.editor_att
         this.addButton({
             icon: 'e/prevent_autolink',
             callback: this._preventAutoLink,
+            inlineFormat: true,
             tags: '.nolink'
         });
     },
index e567786..da400f2 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js differ
index 5c75d06..6cedbfa 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js differ
index 240efbf..b7f5eff 100644 (file)
@@ -177,6 +177,7 @@ EditorPluginButtons.prototype = {
      * specified, in the class for the button.
      * @param {function} config.callback A callback function to call when the button is clicked.
      * @param {object} [config.callbackArgs] Any arguments to pass to the callback.
+     * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed.
      * @return {Node} The Node representing the newly created button.
      */
     addButton: function(config) {
@@ -340,6 +341,7 @@ EditorPluginButtons.prototype = {
      * specified, in the class for the button.
      * @param {function} config.callback A callback function to call when the button is clicked.
      * @param {object} [config.callbackArgs] Any arguments to pass to the callback.
+     * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed.
      * @param {array} config.entries List of menu entries with the string (entry.text) and the handlers (entry.handler).
      * @param {number} [config.overlayWidth=14] The width of the menu. This will be suffixed with the 'em' unit.
      * @param {string} [config.menuColor] menu icon background color
@@ -525,6 +527,7 @@ EditorPluginButtons.prototype = {
      * @param {object} config
      * @param {function} config.callback A callback function to call when the button is clicked.
      * @param {object} [config.callbackArgs] Any arguments to pass to the callback.
+     * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed.
      * @param {object} [inheritFrom] A parent configuration that this configuration may inherit from.
      * @return {object} The normalized configuration
      * @private
index 8c77cdc..77b3c94 100644 (file)
@@ -793,15 +793,28 @@ class grade_category extends grade_object {
 
         // Reset aggregation to unknown and 0 for all grade items for this user and category.
         $params = array('categoryid' => $this->id, 'userid' => $userid);
-        $itemssql = "SELECT id
-                       FROM {grade_items}
-                      WHERE categoryid = :categoryid";
-
-        $sql = "UPDATE {grade_grades}
-                   SET aggregationstatus = 'unknown',
-                       aggregationweight = 0
-                 WHERE userid = :userid
-                   AND itemid IN ($itemssql)";
+
+        switch ($DB->get_dbfamily()) {
+            case 'mysql':
+                // Optimize the query for MySQL by using a join rather than a sub-query.
+                $sql = "UPDATE {grade_grades} g
+                          JOIN {grade_items} gi ON (g.itemid = gi.id)
+                           SET g.aggregationstatus = 'unknown',
+                               g.aggregationweight = 0
+                         WHERE g.userid = :userid
+                           AND gi.categoryid = :categoryid";
+                break;
+            default:
+                $itemssql = "SELECT id
+                               FROM {grade_items}
+                              WHERE categoryid = :categoryid";
+
+                $sql = "UPDATE {grade_grades}
+                           SET aggregationstatus = 'unknown',
+                               aggregationweight = 0
+                         WHERE userid = :userid
+                           AND itemid IN ($itemssql)";
+        }
 
         $DB->execute($sql, $params);
 
index 349d7eb..1c75ddd 100644 (file)
@@ -1828,3 +1828,17 @@ M.util.load_flowplayer = function() {
         document.getElementsByTagName('head')[0].appendChild(fileref);
     }
 };
+
+/**
+ * Initiates the listeners for skiplink interaction
+ *
+ * @param {YUI} Y
+ */
+M.util.init_skiplink = function(Y) {
+    Y.one(Y.config.doc.body).delegate('click', function(e) {
+        e.preventDefault();
+        e.stopPropagation();
+        Y.one(this.getAttribute('data-target')).focus();
+        return true;
+    }, 'a.skip');
+};
index 3169d1a..7cc9c0e 100644 (file)
@@ -3809,8 +3809,10 @@ function update_user_record_by_id($id) {
         $customfields = $userauth->get_custom_user_profile_fields();
 
         foreach ($newinfo as $key => $value) {
-            $key = strtolower($key);
             $iscustom = in_array($key, $customfields);
+            if (!$iscustom) {
+                $key = strtolower($key);
+            }
             if ((!property_exists($oldinfo, $key) && !$iscustom) or $key === 'username' or $key === 'id'
                     or $key === 'auth' or $key === 'mnethostid' or $key === 'deleted') {
                 // Unknown or must not be changed.
index 4efe670..7e85830 100644 (file)
@@ -533,13 +533,23 @@ class pix_icon implements renderable, templatable {
         $this->component  = $component;
         $this->attributes = (array)$attributes;
 
-        $this->attributes['alt'] = $alt;
         if (empty($this->attributes['class'])) {
             $this->attributes['class'] = 'smallicon';
         }
-        if (!isset($this->attributes['title'])) {
-            $this->attributes['title'] = $this->attributes['alt'];
-        } else if (empty($this->attributes['title'])) {
+
+        // If the alt is empty, don't place it in the attributes, otherwise it will override parent alt text.
+        if (!is_null($alt)) {
+            $this->attributes['alt'] = $alt;
+
+            // If there is no title, set it to the attribute.
+            if (!isset($this->attributes['title'])) {
+                $this->attributes['title'] = $this->attributes['alt'];
+            }
+        } else {
+            unset($this->attributes['alt']);
+        }
+
+        if (empty($this->attributes['title'])) {
             // Remove the title attribute if empty, we probably want to use the parent node's title
             // and some browsers might overwrite it with an empty title.
             unset($this->attributes['title']);
index 97abc70..a42d377 100644 (file)
@@ -1394,8 +1394,11 @@ class core_renderer extends renderer_base {
             $output = '';
             $skipdest = '';
         } else {
-            $output = html_writer::tag('a', get_string('skipa', 'access', $skiptitle), array('href' => '#sb-' . $bc->skipid, 'class' => 'skip-block'));
-            $skipdest = html_writer::tag('span', '', array('id' => 'sb-' . $bc->skipid, 'class' => 'skip-block-to'));
+            $output = html_writer::link('#', get_string('skipa', 'access', $skiptitle),
+                      array('class' => 'skip skip-block', 'id'=>'fsb-' . $bc->skipid,
+                      'data-target' => '#sb-'.$bc->skipid));
+            $skipdest = html_writer::span('', 'skip-block-to',
+                      array('id' => 'sb-' . $bc->skipid, 'tabindex' => '-1'));
         }
 
         $output .= html_writer::start_tag('div', $bc->attributes);
@@ -2960,7 +2963,7 @@ EOD;
      * @return string the HTML to output.
      */
     public function skip_link_target($id = null) {
-        return html_writer::tag('span', '', array('id' => $id));
+        return html_writer::span('', '', array('id' => $id, 'tabindex' => '-1'));
     }
 
     /**
index 85fb414..85f86a7 100644 (file)
@@ -1537,12 +1537,13 @@ class page_requirements_manager {
     public function get_top_of_body_code() {
         // First the skip links.
         $links = '';
-        $attributes = array('class'=>'skip');
+        $attributes = array('class' => 'skip');
         foreach ($this->skiplinks as $url => $text) {
-            $attributes['href'] = '#' . $url;
-            $links .= html_writer::tag('a', $text, $attributes);
+            $attributes['data-target'] = '#'.$url;
+            $links .= html_writer::link('#', $text, $attributes);
         }
         $output = html_writer::tag('div', $links, array('class'=>'skiplinks')) . "\n";
+        $this->js_init_call('M.util.init_skiplink');
 
         // YUI3 JS needs to be loaded early in the body. It should be cached well by the browser.
         $output .= $this->get_yui3lib_headcode();
diff --git a/lib/requirejs/readme_moodle.txt b/lib/requirejs/readme_moodle.txt
new file mode 100644 (file)
index 0000000..b0df977
--- /dev/null
@@ -0,0 +1,3 @@
+Description of import into Moodle:
+// Download from https://requirejs.org/docs/download.html
+// Put the require.js and require.min.js and LICENSE file in this folder.
index 77a5bb1..5237640 100644 (file)
@@ -1,5 +1,5 @@
 /** vim: et:ts=4:sw=4:sts=4
- * @license RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ * @license RequireJS 2.1.20 Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved.
  * Available via the MIT or new BSD license.
  * see: http://github.com/jrburke/requirejs for details
  */
@@ -12,7 +12,7 @@ var requirejs, require, define;
 (function (global) {
     var req, s, head, baseElement, dataMain, src,
         interactiveScript, currentlyAddingScript, mainScript, subPath,
-        version = '2.1.15',
+        version = '2.1.20',
         commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
         cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
         jsSuffixRegExp = /\.js$/,
@@ -21,7 +21,6 @@ var requirejs, require, define;
         ostring = op.toString,
         hasOwn = op.hasOwnProperty,
         ap = Array.prototype,
-        apsp = ap.splice,
         isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document),
         isWebWorker = !isBrowser && typeof importScripts !== 'undefined',
         //PS3 indicates loaded and complete, but need to wait for complete
@@ -244,7 +243,7 @@ var requirejs, require, define;
                     // still work when converted to a path, even though
                     // as an ID it is less than ideal. In larger point
                     // releases, may be better to just kick out an error.
-                    if (i === 0 || (i == 1 && ary[2] === '..') || ary[i - 1] === '..') {
+                    if (i === 0 || (i === 1 && ary[2] === '..') || ary[i - 1] === '..') {
                         continue;
                     } else if (i > 0) {
                         ary.splice(i - 1, 2);
@@ -554,11 +553,13 @@ var requirejs, require, define;
         function takeGlobalQueue() {
             //Push all the globalDefQueue items into the context's defQueue
             if (globalDefQueue.length) {
-                //Array splice in the values since the context code has a
-                //local var ref to defQueue, so cannot just reassign the one
-                //on context.
-                apsp.apply(defQueue,
-                           [defQueue.length, 0].concat(globalDefQueue));
+                each(globalDefQueue, function(queueItem) {
+                    var id = queueItem[0];
+                    if (typeof id === 'string') {
+                        context.defQueueMap[id] = true;
+                    }
+                    defQueue.push(queueItem);
+                });
                 globalDefQueue = [];
             }
         }
@@ -589,7 +590,7 @@ var requirejs, require, define;
                         id: mod.map.id,
                         uri: mod.map.url,
                         config: function () {
-                            return  getOwn(config.config, mod.map.id) || {};
+                            return getOwn(config.config, mod.map.id) || {};
                         },
                         exports: mod.exports || (mod.exports = {})
                     });
@@ -845,7 +846,10 @@ var requirejs, require, define;
                     factory = this.factory;
 
                 if (!this.inited) {
-                    this.fetch();
+                    // Only fetch if not already in the defQueue.
+                    if (!hasProp(context.defQueueMap, id)) {
+                        this.fetch();
+                    }
                 } else if (this.error) {
                     this.emit('error', this.error);
                 } else if (!this.defining) {
@@ -1117,12 +1121,22 @@ var requirejs, require, define;
                         this.depCount += 1;
 
                         on(depMap, 'defined', bind(this, function (depExports) {
+                            if (this.undefed) {
+                                return;
+                            }
                             this.defineDep(i, depExports);
                             this.check();
                         }));
 
                         if (this.errback) {
                             on(depMap, 'error', bind(this, this.errback));
+                        } else if (this.events.error) {
+                            // No direct errback on this module, but something
+                            // else is listening for errors, so be sure to
+                            // propagate the error correctly.
+                            on(depMap, 'error', bind(this, function(err) {
+                                this.emit('error', err);
+                            }));
                         }
                     }
 
@@ -1226,13 +1240,15 @@ var requirejs, require, define;
             while (defQueue.length) {
                 args = defQueue.shift();
                 if (args[0] === null) {
-                    return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
+                    return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' +
+                        args[args.length - 1]));
                 } else {
                     //args are id, deps, factory. Should be normalized by the
                     //define() function.
                     callGetModule(args);
                 }
             }
+            context.defQueueMap = {};
         }
 
         context = {
@@ -1242,6 +1258,7 @@ var requirejs, require, define;
             defined: defined,
             urlFetched: urlFetched,
             defQueue: defQueue,
+            defQueueMap: {},
             Module: Module,
             makeModuleMap: makeModuleMap,
             nextTick: req.nextTick,
@@ -1313,7 +1330,7 @@ var requirejs, require, define;
                     each(cfg.packages, function (pkgObj) {
                         var location, name;
 
-                        pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
+                        pkgObj = typeof pkgObj === 'string' ? {name: pkgObj} : pkgObj;
 
                         name = pkgObj.name;
                         location = pkgObj.location;
@@ -1340,7 +1357,7 @@ var requirejs, require, define;
                     //late to modify them, and ignore unnormalized ones
                     //since they are transient.
                     if (!mod.inited && !mod.map.unnormalized) {
-                        mod.map = makeModuleMap(id);
+                        mod.map = makeModuleMap(id, null, true);
                     }
                 });
 
@@ -1476,6 +1493,7 @@ var requirejs, require, define;
                         var map = makeModuleMap(id, relMap, true),
                             mod = getOwn(registry, id);
 
+                        mod.undefed = true;
                         removeScript(id);
 
                         delete defined[id];
@@ -1486,10 +1504,11 @@ var requirejs, require, define;
                         //in array so that the splices do not
                         //mess up the iteration.
                         eachReverse(defQueue, function(args, i) {
-                            if(args[0] === id) {
+                            if (args[0] === id) {
                                 defQueue.splice(i, 1);
                             }
                         });
+                        delete context.defQueueMap[id];
 
                         if (mod) {
                             //Hold on to listeners in case the
@@ -1551,6 +1570,7 @@ var requirejs, require, define;
 
                     callGetModule(args);
                 }
+                context.defQueueMap = {};
 
                 //Do this after the cycle of callGetModule in case the result
                 //of those calls/init calls changes the registry.
@@ -1845,6 +1865,9 @@ var requirejs, require, define;
         if (isBrowser) {
             //In the browser so use a script tag
             node = req.createNode(config, moduleName, url);
+            if (config.onNodeCreated) {
+                config.onNodeCreated(node, config, moduleName, url);
+            }
 
             node.setAttribute('data-requirecontext', context.contextName);
             node.setAttribute('data-requiremodule', moduleName);
@@ -1973,7 +1996,7 @@ var requirejs, require, define;
                 //like a module name.
                 mainScript = mainScript.replace(jsSuffixRegExp, '');
 
-                 //If mainScript is still a path, fall back to dataMain
+                //If mainScript is still a path, fall back to dataMain
                 if (req.jsExtRegExp.test(mainScript)) {
                     mainScript = dataMain;
                 }
@@ -2052,14 +2075,18 @@ var requirejs, require, define;
         //where the module name is not known until the script onload event
         //occurs. If no context, use the global queue, and get it processed
         //in the onscript load callback.
-        (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
+        if (context) {
+            context.defQueue.push([name, deps, callback]);
+            context.defQueueMap[name] = true;
+        } else {
+            globalDefQueue.push([name, deps, callback]);
+        }
     };
 
     define.amd = {
         jQuery: true
     };
 
-
     /**
      * Executes the text. Normally just uses eval, but can be modified
      * to use a better, environment-specific call. Only used for transpiling
index be103a7..693164a 100644 (file)
@@ -1,36 +1,36 @@
 /*
- RequireJS 2.1.15 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ RequireJS 2.1.20 Copyright (c) 2010-2015, The Dojo Foundation All Rights Reserved.
  Available via the MIT or new BSD license.
  see: http://github.com/jrburke/requirejs for details
 */
 var requirejs,require,define;
-(function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function T(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function t(b,c){return fa.call(b,c)}function m(b,c){return t(b,c)&&b[c]}function B(b,c){for(var d in b)if(t(b,d)&&c(b[d],d))break}function U(b,c,d,e){c&&B(c,function(c,g){if(d||!t(b,g))e&&"object"===typeof c&&c&&!H(c)&&!G(c)&&!(c instanceof
-RegExp)?(b[g]||(b[g]={}),U(b[g],c,d,e)):b[g]=c});return b}function u(b,c){return function(){return c.apply(b,arguments)}}function ca(b){throw b;}function da(b){if(!b)return b;var c=ba;v(b.split("."),function(b){c=c[b]});return c}function C(b,c,d,e){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=e;d&&(c.originalError=d);return c}function ga(b){function c(a,k,b){var f,l,c,d,e,g,i,p,k=k&&k.split("/"),h=j.map,n=h&&h["*"];if(a){a=a.split("/");l=a.length-1;j.nodeIdCompat&&
-Q.test(a[l])&&(a[l]=a[l].replace(Q,""));"."===a[0].charAt(0)&&k&&(l=k.slice(0,k.length-1),a=l.concat(a));l=a;for(c=0;c<l.length;c++)if(d=l[c],"."===d)l.splice(c,1),c-=1;else if(".."===d&&!(0===c||1==c&&".."===l[2]||".."===l[c-1])&&0<c)l.splice(c-1,2),c-=2;a=a.join("/")}if(b&&h&&(k||n)){l=a.split("/");c=l.length;a:for(;0<c;c-=1){e=l.slice(0,c).join("/");if(k)for(d=k.length;0<d;d-=1)if(b=m(h,k.slice(0,d).join("/")))if(b=m(b,e)){f=b;g=c;break a}!i&&(n&&m(n,e))&&(i=m(n,e),p=c)}!f&&i&&(f=i,g=p);f&&(l.splice(0,
-g,f),a=l.join("/"))}return(f=m(j.pkgs,a))?f:a}function d(a){z&&v(document.getElementsByTagName("script"),function(k){if(k.getAttribute("data-requiremodule")===a&&k.getAttribute("data-requirecontext")===i.contextName)return k.parentNode.removeChild(k),!0})}function e(a){var k=m(j.paths,a);if(k&&H(k)&&1<k.length)return k.shift(),i.require.undef(a),i.makeRequire(null,{skipMap:!0})([a]),!0}function n(a){var k,c=a?a.indexOf("!"):-1;-1<c&&(k=a.substring(0,c),a=a.substring(c+1,a.length));return[k,a]}function p(a,
-k,b,f){var l,d,e=null,g=k?k.name:null,j=a,p=!0,h="";a||(p=!1,a="_@r"+(K+=1));a=n(a);e=a[0];a=a[1];e&&(e=c(e,g,f),d=m(r,e));a&&(e?h=d&&d.normalize?d.normalize(a,function(a){return c(a,g,f)}):-1===a.indexOf("!")?c(a,g,f):a:(h=c(a,g,f),a=n(h),e=a[0],h=a[1],b=!0,l=i.nameToUrl(h)));b=e&&!d&&!b?"_unnormalized"+(O+=1):"";return{prefix:e,name:h,parentMap:k,unnormalized:!!b,url:l,originalName:j,isDefine:p,id:(e?e+"!"+h:h)+b}}function s(a){var k=a.id,b=m(h,k);b||(b=h[k]=new i.Module(a));return b}function q(a,
-k,b){var f=a.id,c=m(h,f);if(t(r,f)&&(!c||c.defineEmitComplete))"defined"===k&&b(r[f]);else if(c=s(a),c.error&&"error"===k)b(c.error);else c.on(k,b)}function w(a,b){var c=a.requireModules,f=!1;if(b)b(a);else if(v(c,function(b){if(b=m(h,b))b.error=a,b.events.error&&(f=!0,b.emit("error",a))}),!f)g.onError(a)}function x(){R.length&&(ha.apply(A,[A.length,0].concat(R)),R=[])}function y(a){delete h[a];delete V[a]}function F(a,b,c){var f=a.map.id;a.error?a.emit("error",a.error):(b[f]=!0,v(a.depMaps,function(f,
-d){var e=f.id,g=m(h,e);g&&(!a.depMatched[d]&&!c[e])&&(m(b,e)?(a.defineDep(d,r[e]),a.check()):F(g,b,c))}),c[f]=!0)}function D(){var a,b,c=(a=1E3*j.waitSeconds)&&i.startTime+a<(new Date).getTime(),f=[],l=[],g=!1,h=!0;if(!W){W=!0;B(V,function(a){var i=a.map,j=i.id;if(a.enabled&&(i.isDefine||l.push(a),!a.error))if(!a.inited&&c)e(j)?g=b=!0:(f.push(j),d(j));else if(!a.inited&&(a.fetched&&i.isDefine)&&(g=!0,!i.prefix))return h=!1});if(c&&f.length)return a=C("timeout","Load timeout for modules: "+f,null,
-f),a.contextName=i.contextName,w(a);h&&v(l,function(a){F(a,{},{})});if((!c||b)&&g)if((z||ea)&&!X)X=setTimeout(function(){X=0;D()},50);W=!1}}function E(a){t(r,a[0])||s(p(a[0],null,!0)).init(a[1],a[2])}function I(a){var a=a.currentTarget||a.srcElement,b=i.onScriptLoad;a.detachEvent&&!Y?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=i.onScriptError;(!a.detachEvent||Y)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}function J(){var a;
-for(x();A.length;){a=A.shift();if(null===a[0])return w(C("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));E(a)}}var W,Z,i,L,X,j={waitSeconds:7,baseUrl:"./",paths:{},bundles:{},pkgs:{},shim:{},config:{}},h={},V={},$={},A=[],r={},S={},aa={},K=1,O=1;L={require:function(a){return a.require?a.require:a.require=i.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?r[a.map.id]=a.exports:a.exports=r[a.map.id]={}},module:function(a){return a.module?
-a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return m(j.config,a.map.id)||{}},exports:a.exports||(a.exports={})}}};Z=function(a){this.events=m($,a.id)||{};this.map=a;this.shim=m(j.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};Z.prototype={init:function(a,b,c,f){f=f||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=u(this,function(a){this.emit("error",a)}));this.depMaps=a&&a.slice(0);this.errback=
-c;this.inited=!0;this.ignore=f.ignore;f.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=b)},fetch:function(){if(!this.fetched){this.fetched=!0;i.startTime=(new Date).getTime();var a=this.map;if(this.shim)i.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],u(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?this.callPlugin():this.load()}},load:function(){var a=
-this.map.url;S[a]||(S[a]=!0,i.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var f=this.exports,l=this.factory;if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||g.onError!==ca)try{f=i.execCb(c,l,b,f)}catch(d){a=d}else f=i.execCb(c,l,b,f);this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports:this.usingExports&&
-(f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,g.onResourceLoad))g.onResourceLoad(i,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=
-this.map,b=a.id,d=p(a.prefix);this.depMaps.push(d);q(d,"defined",u(this,function(f){var l,d;d=m(aa,this.map.id);var e=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,n=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(e=f.normalize(e,function(a){return c(a,P,!0)})||""),f=p(a.prefix+"!"+e,this.map.parentMap),q(f,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(h,f.id)){this.depMaps.push(f);
-if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=i.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(h,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,e=p(d),P=M;c&&(f=c);P&&(M=!1);s(e);t(j.config,b)&&(j.config[d]=j.config[b]);try{g.exec(f)}catch(h){return w(C("fromtexteval",
-"fromText eval for "+b+" failed: "+h,h,[b]))}P&&(M=!0);this.depMaps.push(e);i.completeLoad(d);n([d],l)}),f.load(a.name,n,l,j))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=p(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",u(this,function(a){this.defineDep(b,
-a);this.check()}));this.errback&&q(a,"error",u(this,this.errback))}c=a.id;f=h[c];!t(L,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,u(this,function(a){var b=m(h,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:j,contextName:b,registry:h,defined:r,urlFetched:S,defQueue:A,Module:Z,makeModuleMap:p,
-nextTick:g.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(j[b]||(j[b]={}),U(j[b],a,!0,!0)):j[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(aa[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),j.shim=b);a.packages&&v(a.packages,function(a){var b,
-a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(j.paths[b]=a.location);j.pkgs[b]=a.name+"/"+(a.main||"main").replace(ia,"").replace(Q,"")});B(h,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=p(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,e){function j(c,d,m){var n,q;e.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild=
-!0);if("string"===typeof c){if(G(d))return w(C("requireargs","Invalid require call"),m);if(a&&t(L,c))return L[c](h[a.id]);if(g.get)return g.get(i,c,a,j);n=p(c,a,!1,!0);n=n.id;return!t(r,n)?w(C("notloaded",'Module name "'+n+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[n]}J();i.nextTick(function(){J();q=s(p(null,a));q.skipMap=e.skipMap;q.init(c,d,m,{enabled:!0});D()});return j}e=e||{};U(j,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),k=b.split("/")[0];if(-1!==
-e&&(!("."===k||".."===k)||1<e))d=b.substring(e,b.length),b=b.substring(0,e);return i.nameToUrl(c(b,a&&a.id,!0),d,!0)},defined:function(b){return t(r,p(b,a,!1,!0).id)},specified:function(b){b=p(b,a,!1,!0).id;return t(r,b)||t(h,b)}});a||(j.undef=function(b){x();var c=p(b,a,!0),e=m(h,b);d(b);delete r[b];delete S[c.url];delete $[b];T(A,function(a,c){a[0]===b&&A.splice(c,1)});e&&(e.events.defined&&($[b]=e.events),y(b))});return j},enable:function(a){m(h,a.id)&&s(a).enable()},completeLoad:function(a){var b,
-c,d=m(j.shim,a)||{},g=d.exports;for(x();A.length;){c=A.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);E(c)}c=m(h,a);if(!b&&!t(r,a)&&c&&!c.inited){if(j.enforceDefine&&(!g||!da(g)))return e(a)?void 0:w(C("nodefine","No define call for "+a,null,[a]));E([a,d.deps||[],d.exportsFn])}D()},nameToUrl:function(a,b,c){var d,e,h;(d=m(j.pkgs,a))&&(a=d);if(d=m(aa,a))return i.nameToUrl(d,b,c);if(g.jsExtRegExp.test(a))d=a+(b||"");else{d=j.paths;a=a.split("/");for(e=a.length;0<e;e-=1)if(h=a.slice(0,
-e).join("/"),h=m(d,h)){H(h)&&(h=h[0]);a.splice(0,e,h);break}d=a.join("/");d+=b||(/^data\:|\?/.test(d)||c?"":".js");d=("/"===d.charAt(0)||d.match(/^[\w\+\.\-]+:/)?"":j.baseUrl)+d}return j.urlArgs?d+((-1===d.indexOf("?")?"?":"&")+j.urlArgs):d},load:function(a,b){g.load(i,a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||ja.test((a.currentTarget||a.srcElement).readyState))N=null,a=I(a),i.completeLoad(a.id)},onScriptError:function(a){var b=I(a);if(!e(b.id))return w(C("scripterror",
-"Script error for: "+b.id,a,[b.id]))}};i.require=i.makeRequire();return i}var g,x,y,D,I,E,N,J,s,O,ka=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,la=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,Q=/\.js$/,ia=/^\.\//;x=Object.prototype;var K=x.toString,fa=x.hasOwnProperty,ha=Array.prototype.splice,z=!!("undefined"!==typeof window&&"undefined"!==typeof navigator&&window.document),ea=!z&&"undefined"!==typeof importScripts,ja=z&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,
-Y="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),F={},q={},R=[],M=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(G(requirejs))return;q=requirejs;requirejs=void 0}"undefined"!==typeof require&&!G(require)&&(q=require,require=void 0);g=requirejs=function(b,c,d,e){var n,p="_";!H(b)&&"string"!==typeof b&&(n=b,H(c)?(b=c,c=d,d=e):b=[]);n&&n.context&&(p=n.context);(e=m(F,p))||(e=F[p]=g.s.newContext(p));n&&e.configure(n);return e.require(b,c,d)};g.config=function(b){return g(b)};
-g.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=g);g.version="2.1.15";g.jsExtRegExp=/^\/|:|\?|\.js$/;g.isBrowser=z;x=g.s={contexts:F,newContext:ga};g({});v(["toUrl","undef","defined","specified"],function(b){g[b]=function(){var c=F._;return c.require[b].apply(c,arguments)}});if(z&&(y=x.head=document.getElementsByTagName("head")[0],D=document.getElementsByTagName("base")[0]))y=x.head=D.parentNode;g.onError=ca;g.createNode=function(b){var c=
-b.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script");c.type=b.scriptType||"text/javascript";c.charset="utf-8";c.async=!0;return c};g.load=function(b,c,d){var e=b&&b.config||{};if(z)return e=g.createNode(e,c,d),e.setAttribute("data-requirecontext",b.contextName),e.setAttribute("data-requiremodule",c),e.attachEvent&&!(e.attachEvent.toString&&0>e.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,e.attachEvent("onreadystatechange",b.onScriptLoad)):
-(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)),e.src=d,J=e,D?y.insertBefore(e,D):y.appendChild(e),J=null,e;if(ea)try{importScripts(d),b.completeLoad(c)}catch(m){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,m,[c]))}};z&&!q.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return s=I,q.baseUrl||(E=s.split("/"),s=E.pop(),O=E.length?E.join("/")+"/":"./",q.baseUrl=
-O),s=s.replace(Q,""),g.jsExtRegExp.test(s)&&(s=I),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ka,"").replace(la,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b||
-(b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}(g?g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(q)}})(this);
+(function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;d<b.length&&(!b[d]||!c(b[d],d,b));d+=1);}}function T(b,c){if(b){var d;for(d=b.length-1;-1<d&&(!b[d]||!c(b[d],d,b));d-=1);}}function t(b,c){return fa.call(b,c)}function n(b,c){return t(b,c)&&b[c]}function A(b,c){for(var d in b)if(t(b,d)&&c(b[d],d))break}function U(b,c,d,e){c&&A(c,function(c,i){if(d||!t(b,i))e&&"object"===typeof c&&c&&!H(c)&&!G(c)&&!(c instanceof
+RegExp)?(b[i]||(b[i]={}),U(b[i],c,d,e)):b[i]=c});return b}function u(b,c){return function(){return c.apply(b,arguments)}}function ca(b){throw b;}function da(b){if(!b)return b;var c=ba;v(b.split("."),function(b){c=c[b]});return c}function B(b,c,d,e){c=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);c.requireType=b;c.requireModules=e;d&&(c.originalError=d);return c}function ga(b){function c(a,j,b){var f,l,c,d,h,e,g,i,j=j&&j.split("/"),p=k.map,m=p&&p["*"];if(a){a=a.split("/");l=a.length-1;k.nodeIdCompat&&
+Q.test(a[l])&&(a[l]=a[l].replace(Q,""));"."===a[0].charAt(0)&&j&&(l=j.slice(0,j.length-1),a=l.concat(a));l=a;for(c=0;c<l.length;c++)if(d=l[c],"."===d)l.splice(c,1),c-=1;else if(".."===d&&!(0===c||1===c&&".."===l[2]||".."===l[c-1])&&0<c)l.splice(c-1,2),c-=2;a=a.join("/")}if(b&&p&&(j||m)){l=a.split("/");c=l.length;a:for(;0<c;c-=1){h=l.slice(0,c).join("/");if(j)for(d=j.length;0<d;d-=1)if(b=n(p,j.slice(0,d).join("/")))if(b=n(b,h)){f=b;e=c;break a}!g&&(m&&n(m,h))&&(g=n(m,h),i=c)}!f&&g&&(f=g,e=i);f&&(l.splice(0,
+e,f),a=l.join("/"))}return(f=n(k.pkgs,a))?f:a}function d(a){z&&v(document.getElementsByTagName("script"),function(j){if(j.getAttribute("data-requiremodule")===a&&j.getAttribute("data-requirecontext")===h.contextName)return j.parentNode.removeChild(j),!0})}function p(a){var j=n(k.paths,a);if(j&&H(j)&&1<j.length)return j.shift(),h.require.undef(a),h.makeRequire(null,{skipMap:!0})([a]),!0}function g(a){var j,c=a?a.indexOf("!"):-1;-1<c&&(j=a.substring(0,c),a=a.substring(c+1,a.length));return[j,a]}function i(a,
+j,b,f){var l,d,e=null,i=j?j.name:null,k=a,p=!0,m="";a||(p=!1,a="_@r"+(K+=1));a=g(a);e=a[0];a=a[1];e&&(e=c(e,i,f),d=n(q,e));a&&(e?m=d&&d.normalize?d.normalize(a,function(a){return c(a,i,f)}):-1===a.indexOf("!")?c(a,i,f):a:(m=c(a,i,f),a=g(m),e=a[0],m=a[1],b=!0,l=h.nameToUrl(m)));b=e&&!d&&!b?"_unnormalized"+(O+=1):"";return{prefix:e,name:m,parentMap:j,unnormalized:!!b,url:l,originalName:k,isDefine:p,id:(e?e+"!"+m:m)+b}}function r(a){var j=a.id,b=n(m,j);b||(b=m[j]=new h.Module(a));return b}function s(a,
+j,b){var f=a.id,c=n(m,f);if(t(q,f)&&(!c||c.defineEmitComplete))"defined"===j&&b(q[f]);else if(c=r(a),c.error&&"error"===j)b(c.error);else c.on(j,b)}function w(a,b){var c=a.requireModules,f=!1;if(b)b(a);else if(v(c,function(b){if(b=n(m,b))b.error=a,b.events.error&&(f=!0,b.emit("error",a))}),!f)e.onError(a)}function x(){R.length&&(v(R,function(a){var b=a[0];"string"===typeof b&&(h.defQueueMap[b]=!0);C.push(a)}),R=[])}function y(a){delete m[a];delete V[a]}function F(a,b,c){var f=a.map.id;a.error?a.emit("error",
+a.error):(b[f]=!0,v(a.depMaps,function(f,d){var e=f.id,h=n(m,e);h&&(!a.depMatched[d]&&!c[e])&&(n(b,e)?(a.defineDep(d,q[e]),a.check()):F(h,b,c))}),c[f]=!0)}function D(){var a,b,c=(a=1E3*k.waitSeconds)&&h.startTime+a<(new Date).getTime(),f=[],l=[],e=!1,i=!0;if(!W){W=!0;A(V,function(a){var h=a.map,g=h.id;if(a.enabled&&(h.isDefine||l.push(a),!a.error))if(!a.inited&&c)p(g)?e=b=!0:(f.push(g),d(g));else if(!a.inited&&(a.fetched&&h.isDefine)&&(e=!0,!h.prefix))return i=!1});if(c&&f.length)return a=B("timeout",
+"Load timeout for modules: "+f,null,f),a.contextName=h.contextName,w(a);i&&v(l,function(a){F(a,{},{})});if((!c||b)&&e)if((z||ea)&&!X)X=setTimeout(function(){X=0;D()},50);W=!1}}function E(a){t(q,a[0])||r(i(a[0],null,!0)).init(a[1],a[2])}function I(a){var a=a.currentTarget||a.srcElement,b=h.onScriptLoad;a.detachEvent&&!Y?a.detachEvent("onreadystatechange",b):a.removeEventListener("load",b,!1);b=h.onScriptError;(!a.detachEvent||Y)&&a.removeEventListener("error",b,!1);return{node:a,id:a&&a.getAttribute("data-requiremodule")}}
+function J(){var a;for(x();C.length;){a=C.shift();if(null===a[0])return w(B("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));E(a)}h.defQueueMap={}}var W,Z,h,L,X,k={waitSeconds:7,baseUrl:"./",paths:{},bundles:{},pkgs:{},shim:{},config:{}},m={},V={},$={},C=[],q={},S={},aa={},K=1,O=1;L={require:function(a){return a.require?a.require:a.require=h.makeRequire(a.map)},exports:function(a){a.usingExports=!0;if(a.map.isDefine)return a.exports?q[a.map.id]=a.exports:a.exports=q[a.map.id]={}},
+module:function(a){return a.module?a.module:a.module={id:a.map.id,uri:a.map.url,config:function(){return n(k.config,a.map.id)||{}},exports:a.exports||(a.exports={})}}};Z=function(a){this.events=n($,a.id)||{};this.map=a;this.shim=n(k.shim,a.id);this.depExports=[];this.depMaps=[];this.depMatched=[];this.pluginMaps={};this.depCount=0};Z.prototype={init:function(a,b,c,f){f=f||{};if(!this.inited){this.factory=b;if(c)this.on("error",c);else this.events.error&&(c=u(this,function(a){this.emit("error",a)}));
+this.depMaps=a&&a.slice(0);this.errback=c;this.inited=!0;this.ignore=f.ignore;f.enabled||this.enabled?this.enable():this.check()}},defineDep:function(a,b){this.depMatched[a]||(this.depMatched[a]=!0,this.depCount-=1,this.depExports[a]=b)},fetch:function(){if(!this.fetched){this.fetched=!0;h.startTime=(new Date).getTime();var a=this.map;if(this.shim)h.makeRequire(this.map,{enableBuildCallback:!0})(this.shim.deps||[],u(this,function(){return a.prefix?this.callPlugin():this.load()}));else return a.prefix?
+this.callPlugin():this.load()}},load:function(){var a=this.map.url;S[a]||(S[a]=!0,h.load(this.map.id,a))},check:function(){if(this.enabled&&!this.enabling){var a,b,c=this.map.id;b=this.depExports;var f=this.exports,l=this.factory;if(this.inited)if(this.error)this.emit("error",this.error);else{if(!this.defining){this.defining=!0;if(1>this.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||e.onError!==ca)try{f=h.execCb(c,l,b,f)}catch(d){a=d}else f=h.execCb(c,l,b,f);this.map.isDefine&&
+void 0===f&&((b=this.module)?f=b.exports:this.usingExports&&(f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(q[c]=f,e.onResourceLoad))e.onResourceLoad(h,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=
+!0)}}else t(h.defQueueMap,c)||this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=i(a.prefix);this.depMaps.push(d);s(d,"defined",u(this,function(f){var l,d;d=n(aa,this.map.id);var g=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,p=h.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(g=f.normalize(g,function(a){return c(a,P,!0)})||""),f=i(a.prefix+"!"+g,this.map.parentMap),s(f,"defined",u(this,function(a){this.init([],function(){return a},
+null,{enabled:!0,ignore:!0})})),d=n(m,f.id)){this.depMaps.push(f);if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=h.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];A(m,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,g=i(d),P=M;c&&(f=c);P&&
+(M=!1);r(g);t(k.config,b)&&(k.config[d]=k.config[b]);try{e.exec(f)}catch(m){return w(B("fromtexteval","fromText eval for "+b+" failed: "+m,m,[b]))}P&&(M=!0);this.depMaps.push(g);h.completeLoad(d);p([d],l)}),f.load(a.name,p,l,k))}));h.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=i(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=
+n(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",u(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?s(a,"error",u(this,this.errback)):this.events.error&&s(a,"error",u(this,function(a){this.emit("error",a)}))}c=a.id;f=m[c];!t(L,c)&&(f&&!f.enabled)&&h.enable(a,this)}));A(this.pluginMaps,u(this,function(a){var b=n(m,a.id);b&&!b.enabled&&h.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=
+[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};h={config:k,contextName:b,registry:m,defined:q,urlFetched:S,defQueue:C,defQueueMap:{},Module:Z,makeModuleMap:i,nextTick:e.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=k.shim,c={paths:!0,bundles:!0,config:!0,map:!0};A(a,function(a,b){c[b]?(k[b]||(k[b]={}),U(k[b],a,!0,!0)):k[b]=a});a.bundles&&A(a.bundles,function(a,b){v(a,
+function(a){a!==b&&(aa[a]=b)})});a.shim&&(A(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=h.makeShimExports(a);b[c]=a}),k.shim=b);a.packages&&v(a.packages,function(a){var b,a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(k.paths[b]=a.location);k.pkgs[b]=a.name+"/"+(a.main||"main").replace(ha,"").replace(Q,"")});A(m,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=i(b,null,!0))});if(a.deps||a.callback)h.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;
+a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,j){function g(c,d,p){var k,n;j.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild=!0);if("string"===typeof c){if(G(d))return w(B("requireargs","Invalid require call"),p);if(a&&t(L,c))return L[c](m[a.id]);if(e.get)return e.get(h,c,a,g);k=i(c,a,!1,!0);k=k.id;return!t(q,k)?w(B("notloaded",'Module name "'+k+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):q[k]}J();h.nextTick(function(){J();
+n=r(i(null,a));n.skipMap=j.skipMap;n.init(c,d,p,{enabled:!0});D()});return g}j=j||{};U(g,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),j=b.split("/")[0];if(-1!==e&&(!("."===j||".."===j)||1<e))d=b.substring(e,b.length),b=b.substring(0,e);return h.nameToUrl(c(b,a&&a.id,!0),d,!0)},defined:function(b){return t(q,i(b,a,!1,!0).id)},specified:function(b){b=i(b,a,!1,!0).id;return t(q,b)||t(m,b)}});a||(g.undef=function(b){x();var c=i(b,a,!0),e=n(m,b);e.undefed=!0;d(b);delete q[b];delete S[c.url];
+delete $[b];T(C,function(a,c){a[0]===b&&C.splice(c,1)});delete h.defQueueMap[b];e&&(e.events.defined&&($[b]=e.events),y(b))});return g},enable:function(a){n(m,a.id)&&r(a).enable()},completeLoad:function(a){var b,c,d=n(k.shim,a)||{},e=d.exports;for(x();C.length;){c=C.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);E(c)}h.defQueueMap={};c=n(m,a);if(!b&&!t(q,a)&&c&&!c.inited){if(k.enforceDefine&&(!e||!da(e)))return p(a)?void 0:w(B("nodefine","No define call for "+a,null,[a]));E([a,
+d.deps||[],d.exportsFn])}D()},nameToUrl:function(a,b,c){var d,g,i;(d=n(k.pkgs,a))&&(a=d);if(d=n(aa,a))return h.nameToUrl(d,b,c);if(e.jsExtRegExp.test(a))d=a+(b||"");else{d=k.paths;a=a.split("/");for(g=a.length;0<g;g-=1)if(i=a.slice(0,g).join("/"),i=n(d,i)){H(i)&&(i=i[0]);a.splice(0,g,i);break}d=a.join("/");d+=b||(/^data\:|\?/.test(d)||c?"":".js");d=("/"===d.charAt(0)||d.match(/^[\w\+\.\-]+:/)?"":k.baseUrl)+d}return k.urlArgs?d+((-1===d.indexOf("?")?"?":"&")+k.urlArgs):d},load:function(a,b){e.load(h,
+a,b)},execCb:function(a,b,c,d){return b.apply(d,c)},onScriptLoad:function(a){if("load"===a.type||ia.test((a.currentTarget||a.srcElement).readyState))N=null,a=I(a),h.completeLoad(a.id)},onScriptError:function(a){var b=I(a);if(!p(b.id))return w(B("scripterror","Script error for: "+b.id,a,[b.id]))}};h.require=h.makeRequire();return h}var e,x,y,D,I,E,N,J,r,O,ja=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ka=/[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,Q=/\.js$/,ha=/^\.\//;x=Object.prototype;var K=
+x.toString,fa=x.hasOwnProperty,z=!!("undefined"!==typeof window&&"undefined"!==typeof navigator&&window.document),ea=!z&&"undefined"!==typeof importScripts,ia=z&&"PLAYSTATION 3"===navigator.platform?/^complete$/:/^(complete|loaded)$/,Y="undefined"!==typeof opera&&"[object Opera]"===opera.toString(),F={},s={},R=[],M=!1;if("undefined"===typeof define){if("undefined"!==typeof requirejs){if(G(requirejs))return;s=requirejs;requirejs=void 0}"undefined"!==typeof require&&!G(require)&&(s=require,require=
+void 0);e=requirejs=function(b,c,d,p){var g,i="_";!H(b)&&"string"!==typeof b&&(g=b,H(c)?(b=c,c=d,d=p):b=[]);g&&g.context&&(i=g.context);(p=n(F,i))||(p=F[i]=e.s.newContext(i));g&&p.configure(g);return p.require(b,c,d)};e.config=function(b){return e(b)};e.nextTick="undefined"!==typeof setTimeout?function(b){setTimeout(b,4)}:function(b){b()};require||(require=e);e.version="2.1.20";e.jsExtRegExp=/^\/|:|\?|\.js$/;e.isBrowser=z;x=e.s={contexts:F,newContext:ga};e({});v(["toUrl","undef","defined","specified"],
+function(b){e[b]=function(){var c=F._;return c.require[b].apply(c,arguments)}});if(z&&(y=x.head=document.getElementsByTagName("head")[0],D=document.getElementsByTagName("base")[0]))y=x.head=D.parentNode;e.onError=ca;e.createNode=function(b){var c=b.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script");c.type=b.scriptType||"text/javascript";c.charset="utf-8";c.async=!0;return c};e.load=function(b,c,d){var p=b&&b.config||{},g;if(z){g=e.createNode(p,
+c,d);if(p.onNodeCreated)p.onNodeCreated(g,p,c,d);g.setAttribute("data-requirecontext",b.contextName);g.setAttribute("data-requiremodule",c);g.attachEvent&&!(g.attachEvent.toString&&0>g.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)):(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1));g.src=d;J=g;D?y.insertBefore(g,D):y.appendChild(g);J=null;return g}if(ea)try{importScripts(d),b.completeLoad(c)}catch(i){b.onError(B("importscripts",
+"importScripts failed for "+c+" at "+d,i,[c]))}};z&&!s.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return r=I,s.baseUrl||(E=r.split("/"),r=E.pop(),O=E.length?E.join("/")+"/":"./",s.baseUrl=O),r=r.replace(Q,""),e.jsExtRegExp.test(r)&&(r=I),s.deps=s.deps?s.deps.concat(r):[r],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ja,"").replace(ka,
+function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b||(b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):R.push([b,c,d])};define.amd={jQuery:!0};e.exec=function(b){return eval(b)};e(s)}})(this);
index 44ccaba..c15bfa9 100644 (file)
@@ -272,7 +272,17 @@ EOD;
             context_user::instance($userid);
         }
 
-        return $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
+        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
+
+        if (!$record['deleted'] && isset($record['interests'])) {
+            require_once($CFG->dirroot . '/user/editlib.php');
+            if (!is_array($record['interests'])) {
+                $record['interests'] = preg_split('/\s*,\s*/', trim($record['interests']), -1, PREG_SPLIT_NO_EMPTY);
+            }
+            useredit_update_interests($user, $record['interests']);
+        }
+
+        return $user;
     }
 
     /**
@@ -405,6 +415,10 @@ EOD;
             $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
         }
 
+        if (isset($record['tags']) && !is_array($record['tags'])) {
+            $record['tags'] = preg_split('/\s*,\s*/', trim($record['tags']), -1, PREG_SPLIT_NO_EMPTY);
+        }
+
         $course = create_course((object)$record);
         context_course::instance($course->id);
         if (!empty($options['createsections'])) {
index 0104d82..e90ab38 100644 (file)
@@ -55,6 +55,7 @@ class core_test_generator_testcase extends advanced_testcase {
 
     public function test_create_user() {
         global $DB, $CFG;
+        require_once($CFG->dirroot.'/user/lib.php');
 
         $this->resetAfterTest(true);
         $generator = $this->getDataGenerator();
@@ -124,6 +125,11 @@ class core_test_generator_testcase extends advanced_testcase {
         $this->assertSame('', $user->idnumber);
         $this->assertSame(md5($record['username']), $user->email);
         $this->assertFalse(context_user::instance($user->id, IGNORE_MISSING));
+
+        // Test generating user with interests.
+        $user = $generator->create_user(array('interests' => 'Cats, Dogs'));
+        $userdetails = user_get_user_details($user);
+        $this->assertSame('Cats, Dogs', $userdetails['interests']);
     }
 
     public function test_create() {
@@ -167,6 +173,9 @@ class core_test_generator_testcase extends advanced_testcase {
         $section = $generator->create_course_section(array('course'=>$course->id, 'section'=>3));
         $this->assertEquals($course->id, $section->course);
 
+        $course = $generator->create_course(array('tags' => 'Cat, Dog'));
+        $this->assertEquals('Cat, Dog', tag_get_tags_csv('course', $course->id, TAG_RETURN_TEXT));
+
         $scale = $generator->create_scale();
         $this->assertNotEmpty($scale);
     }
index 5c43e9f..23c61f2 100644 (file)
     <location>requirejs</location>
     <name>RequireJS</name>
     <license>MIT</license>
-    <version>2.1.15</version>
+    <version>2.1.20</version>
     <licenseversion></licenseversion>
   </library>
   <library>
     <location>amd/src/loglevel.js</location>
     <name>loglevel.js</name>
     <license>MIT</license>
-    <version>1.2.0</version>
+    <version>1.4.0</version>
   </library>
   <library>
     <location>mustache</location>
diff --git a/mod/choice/lang/en/deprecated.txt b/mod/choice/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..4177da3
--- /dev/null
@@ -0,0 +1 @@
+skipresultgraph,mod_choice
index f07b6a8..12aedca 100644 (file)
@@ -378,7 +378,6 @@ class mod_choice_renderer extends plugin_renderer_base {
 
         $header = html_writer::tag('h3',format_string(get_string("responses", "choice")));
         $html .= html_writer::tag('div', $header, array('class'=>'responseheader'));
-        $html .= html_writer::tag('a', get_string('skipresultgraph', 'choice'), array('href'=>'#skipresultgraph', 'class'=>'skip-block'));
         $html .= html_writer::tag('div', html_writer::table($table), array('class'=>'response'));
 
         return $html;
index 22c275d..e29a7d6 100644 (file)
@@ -176,36 +176,22 @@ class data_field_file extends data_field_base {
             $content = $DB->get_record('data_content', array('id'=>$id));
         }
 
-        // delete existing files
-        $fs->delete_area_files($this->context->id, 'mod_data', 'content', $content->id);
+        file_save_draft_area_files($value, $this->context->id, 'mod_data', 'content', $content->id);
 
         $usercontext = context_user::instance($USER->id);
-        $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $value, 'timecreated DESC');
+        $files = $fs->get_area_files($this->context->id, 'mod_data', 'content', $content->id, 'itemid, filepath, filename', false);
 
-        if (count($files)<2) {
-            // no file
+        // We expect no or just one file (maxfiles = 1 option is set for the form_filemanager).
+        if (count($files) == 0) {
+            $content->content = null;
         } else {
-            foreach ($files as $draftfile) {
-                if (!$draftfile->is_directory()) {
-                    $file_record = array(
-                        'contextid' => $this->context->id,
-                        'component' => 'mod_data',
-                        'filearea' => 'content',
-                        'itemid' => $content->id,
-                        'filepath' => '/',
-                        'filename' => $draftfile->get_filename(),
-                    );
-
-                    $content->content = $file_record['filename'];
-
-                    $fs->create_file_from_storedfile($file_record, $draftfile);
-                    $DB->update_record('data_content', $content);
-
-                    // Break from the loop now to avoid overwriting the uploaded file record
-                    break;
-                }
+            $content->content = array_values($files)[0]->get_filename();
+            if (count($files) > 1) {
+                // This should not happen with a consistent database. Inform admins/developers about the inconsistency.
+                debugging('more then one file found in mod_data instance {$this->data->id} file field (field id: {$this->field->id}) area during update data record {$recordid} (content id: {$content->id})', DEBUG_NORMAL);
             }
         }
+        $DB->update_record('data_content', $content);
     }
 
     function text_export_supported() {
index 8db4720..36bcd55 100644 (file)
@@ -235,39 +235,34 @@ class data_field_picture extends data_field_base {
         switch ($names[2]) {
             case 'file':
                 $fs = get_file_storage();
-                $fs->delete_area_files($this->context->id, 'mod_data', 'content', $content->id);
+                file_save_draft_area_files($value, $this->context->id, 'mod_data', 'content', $content->id);
                 $usercontext = context_user::instance($USER->id);
-                $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $value);
-                if (count($files)<2) {
-                    // no file
+                $files = $fs->get_area_files(
+                    $this->context->id,
+                    'mod_data', 'content',
+                    $content->id,
+                    'itemid, filepath, filename',
+                    false);
+
+                // We expect no or just one file (maxfiles = 1 option is set for the form_filemanager).
+                if (count($files) == 0) {
+                    $content->content = null;
                 } else {
-                    $count = 0;
-                    foreach ($files as $draftfile) {
-                        $file_record = array('contextid'=>$this->context->id, 'component'=>'mod_data', 'filearea'=>'content', 'itemid'=>$content->id, 'filepath'=>'/');
-                        if (!$draftfile->is_directory()) {
-                            $file_record['filename'] = $draftfile->get_filename();
-
-                            $content->content = $draftfile->get_filename();
-
-                            $file = $fs->create_file_from_storedfile($file_record, $draftfile);
-
-                            // If the file is not a valid image, redirect back to the upload form.
-                            if ($file->get_imageinfo() === false) {
-                                $url = new moodle_url('/mod/data/edit.php', array('d' => $this->field->dataid));
-                                redirect($url, get_string('invalidfiletype', 'error', $file->get_filename()));
-                            }
-
-                            $DB->update_record('data_content', $content);
-                            $this->update_thumbnail($content, $file);
-
-                            if ($count > 0) {
-                                break;
-                            } else {
-                                $count++;
-                            }
-                        }
+                    $file = array_values($files)[0];
+
+                    if (count($files) > 1) {
+                        // This should not happen with a consistent database. Inform admins/developers about the inconsistency.
+                        debugging('more then one file found in mod_data instance {$this->data->id} picture field (field id: {$this->field->id}) area during update data record {$recordid} (content id: {$content->id})', DEBUG_NORMAL);
+                    }
+
+                    if ($file->get_imageinfo() === false) {
+                        $url = new moodle_url('/mod/data/edit.php', array('d' => $this->field->dataid));
+                        redirect($url, get_string('invalidfiletype', 'error', $file->get_filename()));
                     }
+                    $content->content = $file->get_filename();
+                    $this->update_thumbnail($content, $file);
                 }
+                $DB->update_record('data_content', $content);
 
                 break;
 
diff --git a/mod/lti/classes/external.php b/mod/lti/classes/external.php
new file mode 100644 (file)
index 0000000..52ad37f
--- /dev/null
@@ -0,0 +1,125 @@
+<?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 tool module external API
+ *
+ * @package    mod_lti
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once($CFG->libdir . '/externallib.php');
+require_once($CFG->dirroot . '/mod/lti/lib.php');
+require_once($CFG->dirroot . '/mod/lti/locallib.php');
+
+/**
+ * External tool module external functions
+ *
+ * @package    mod_lti
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+class mod_lti_external extends external_api {
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.0
+     */
+    public static function get_tool_launch_data_parameters() {
+        return new external_function_parameters(
+            array(
+                'toolid' => new external_value(PARAM_INT, 'external tool instance id')
+            )
+        );
+    }
+
+    /**
+     * Return the launch data for a given external tool.
+     *
+     * @param int $toolid the external tool instance id
+     * @return array of warnings and launch data
+     * @since Moodle 3.0
+     * @throws moodle_exception
+     */
+    public static function get_tool_launch_data($toolid) {
+        global $DB, $CFG;
+        require_once($CFG->dirroot . '/mod/lti/lib.php');
+
+        $params = self::validate_parameters(self::get_tool_launch_data_parameters(),
+                                            array(
+                                                'toolid' => $toolid
+                                            ));
+        $warnings = array();
+
+        // Request and permission validation.
+        $lti = $DB->get_record('lti', array('id' => $params['toolid']), '*', MUST_EXIST);
+        list($course, $cm) = get_course_and_cm_from_instance($lti, 'lti');
+
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+
+        require_capability('mod/lti:view', $context);
+
+        $lti->cmid = $cm->id;
+        list($endpoint, $parms) = lti_get_launch_data($lti);
+
+        $parameters = array();
+        foreach ($parms as $name => $value) {
+            $parameters[] = array(
+                'name' => $name,
+                'value' => $value
+            );
+        }
+
+        $result = array();
+        $result['endpoint'] = $endpoint;
+        $result['parameters'] = $parameters;
+        $result['warnings'] = $warnings;
+        return $result;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 3.0
+     */
+    public static function get_tool_launch_data_returns() {
+        return new external_single_structure(
+            array(
+                'endpoint' => new external_value(PARAM_RAW, 'Endpoint URL'), // Using PARAM_RAW as is defined in the module.
+                'parameters' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'name' => new external_value(PARAM_NOTAGS, 'Parameter name'),
+                            'value' => new external_value(PARAM_RAW, 'Parameter value')
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+}
diff --git a/mod/lti/db/services.php b/mod/lti/db/services.php
new file mode 100644 (file)
index 0000000..49f42f5
--- /dev/null
@@ -0,0 +1,37 @@
+<?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 tool external functions and service definitions.
+ *
+ * @package    mod_lti
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+$functions = array(
+
+    'mod_lti_get_tool_launch_data' => array(
+        'classname'     => 'mod_lti_external',
+        'methodname'    => 'get_tool_launch_data',
+        'description'   => 'Return the launch data for a given external tool.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/lti:view'
+    ),
+
+);
index aa718d9..79a4607 100644 (file)
@@ -79,11 +79,13 @@ define('LTI_SETTING_ALWAYS', 1);
 define('LTI_SETTING_DELEGATE', 2);
 
 /**
- * Prints a Basic LTI activity
+ * Return the launch data required for opening the external tool.
  *
- * $param int $basicltiid       Basic LTI activity id
+ * @param  stdClass $instance the external tool activity settings
+ * @return array the endpoint URL and parameters (including the signature)
+ * @since  Moodle 3.0
  */
-function lti_view($instance) {
+function lti_get_launch_data($instance) {
     global $PAGE, $CFG;
 
     if (empty($instance->typeid)) {
@@ -245,6 +247,18 @@ function lti_view($instance) {
         $parms = $requestparams;
     }
 
+    return array($endpoint, $parms);
+}
+
+/**
+ * Prints an external tool activity.
+ *
+ * @param  stdClass $instance the external tool activity settings
+ * @return string           The HTML post form content
+ */
+function lti_view($instance) {
+
+    list($endpoint, $parms) = lti_get_launch_data($instance);
     $debuglaunch = ( $instance->debuglaunch == 1 );
 
     $content = lti_post_launch_html($parms, $endpoint, $debuglaunch);
diff --git a/mod/lti/tests/externallib_test.php b/mod/lti/tests/externallib_test.php
new file mode 100644 (file)
index 0000000..9291528
--- /dev/null
@@ -0,0 +1,99 @@
+<?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 tool module external functions tests
+ *
+ * @package    mod_lti
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once($CFG->dirroot . '/mod/lti/lib.php');
+
+/**
+ * External tool module external functions tests
+ *
+ * @package    mod_lti
+ * @category   external
+ * @copyright  2015 Juan Leyva <juan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.0
+ */
+class mod_lti_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Set up for every test
+     */
+    public function setUp() {
+        global $DB;
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Setup test data.
+        $this->course = $this->getDataGenerator()->create_course();
+        $this->lti = $this->getDataGenerator()->create_module('lti', array('course' => $this->course->id));
+        $this->context = context_module::instance($this->lti->cmid);
+        $this->cm = get_coursemodule_from_instance('lti', $this->lti->id);
+
+        // Create users.
+        $this->student = self::getDataGenerator()->create_user();
+        $this->teacher = self::getDataGenerator()->create_user();
+
+        // Users enrolments.
+        $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
+    }
+
+    /**
+     * Test view_lti
+     */
+    public function test_get_tool_launch_data() {
+        global $USER;
+
+        $result = mod_lti_external::get_tool_launch_data($this->lti->id);
+        $result = external_api::clean_returnvalue(mod_lti_external::get_tool_launch_data_returns(), $result);
+
+        // Basic test, the function returns what it's expected.
+        self::assertEquals($this->lti->toolurl, $result['endpoint']);
+        self::assertCount(35, $result['parameters']);
+
+        // Check some parameters.
+        $parameters = array();
+        foreach ($result['parameters'] as $param) {
+            $parameters[$param['name']] = $param['value'];
+        }
+        self::assertEquals($this->lti->resourcekey, $parameters['oauth_consumer_key']);
+        self::assertEquals($this->course->fullname, $parameters['context_title']);
+        self::assertEquals($this->course->shortname, $parameters['context_label']);
+        self::assertEquals($USER->id, $parameters['user_id']);
+        self::assertEquals($USER->firstname, $parameters['lis_person_name_given']);
+        self::assertEquals($USER->lastname, $parameters['lis_person_name_family']);
+        self::assertEquals(fullname($USER), $parameters['lis_person_name_full']);
+        self::assertEquals($USER->username, $parameters['ext_user_username']);
+
+    }
+
+}
index 139b7e9..bfb386b 100644 (file)
@@ -48,7 +48,7 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2015051100;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2015051101;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015050500;    // Requires this Moodle version.
 $plugin->component = 'mod_lti';     // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 0;
index 2457ac3..cae7a1c 100644 (file)
@@ -679,6 +679,8 @@ class mod_scorm_external extends external_api {
                         }
                     }
 
+                    $module['protectpackagedownloads'] = get_config('scorm', 'protectpackagedownloads');
+
                     $viewablefields = array('version', 'maxgrade', 'grademethod', 'whatgrade', 'maxattempt', 'forcecompleted',
                                             'forcenewattempt', 'lastattemptlock', 'displayattemptstatus', 'displaycoursestructure',
                                             'sha1hash', 'md5hash', 'revision', 'launch', 'skipview', 'hidebrowse', 'hidetoc', 'nav',
@@ -766,6 +768,8 @@ class mod_scorm_external extends external_api {
                                                                         VALUE_OPTIONAL),
                             'scormtype' => new external_value(PARAM_ALPHA, 'SCORM type', VALUE_OPTIONAL),
                             'reference' => new external_value(PARAM_NOTAGS, 'Reference to the package', VALUE_OPTIONAL),
+                            'protectpackagedownloads' => new external_value(PARAM_BOOL, 'Protect package downloads?',
+                                                                            VALUE_OPTIONAL),
                             'updatefreq' => new external_value(PARAM_INT, 'Auto-update frequency for remote packages',
                                                                 VALUE_OPTIONAL),
                             'options' => new external_value(PARAM_RAW, 'Additional options', VALUE_OPTIONAL),
index bd7897b..49b6ea3 100644 (file)
@@ -311,6 +311,8 @@ $string['position_error'] = 'The {$a->tag} tag can\'t be child of {$a->parent} t
 $string['preferencesuser'] = 'Preferences for this report';
 $string['preferencespage'] = 'Preferences just for this page';
 $string['prev'] = 'Previous';
+$string['protectpackagedownloads'] = 'Protect package downloads';
+$string['protectpackagedownloads_desc'] = 'If enabled, SCORM packages can be downloaded only if the user has the course:manageactivities capability. If disabled, SCORM packages can always be downloaded (by mobile or other means).';
 $string['raw'] = 'Raw score';
 $string['regular'] = 'Regular manifest';
 $string['report'] = 'Report';
index 1e2164f..bafc6d3 100644 (file)
@@ -947,7 +947,9 @@ function scorm_pluginfile($course, $cm, $context, $filearea, $args, $forcedownlo
         // TODO: add any other access restrictions here if needed!
 
     } else if ($filearea === 'package') {
-        if (!has_capability('moodle/course:manageactivities', $context)) {
+        // Check if the global setting for disabling package downloads is enabled.
+        $protectpackagedownloads = get_config('scorm', 'protectpackagedownloads');
+        if ($protectpackagedownloads and !has_capability('moodle/course:manageactivities', $context)) {
             return false;
         }
         $revision = (int)array_shift($args); // Prevents caching problems - ignored here.
index 5287a37..9dbc57e 100644 (file)
@@ -157,4 +157,7 @@ if ($ADMIN->fulltree) {
 
     $settings->add(new admin_setting_configtext('scorm/apidebugmask', get_string('apidebugmask', 'scorm'), '', '.*'));
 
+    $settings->add(new admin_setting_configcheckbox('scorm/protectpackagedownloads', get_string('protectpackagedownloads', 'scorm'),
+                                                    get_string('protectpackagedownloads_desc', 'scorm'), 0));
+
 }
index 6f59c8b..17baf35 100644 (file)
@@ -640,8 +640,10 @@ class mod_scorm_external_testcase extends externallib_advanced_testcase {
         $scorm2->packagesize = $packagesize;
         $scorm2->packageurl = $packageurl2;
 
-        $expected1 = array();
-        $expected2 = array();
+        // Forced to boolean as it is returned as PARAM_BOOL.
+        $protectpackages = (bool)get_config('scorm', 'protectpackagedownloads');
+        $expected1 = array('protectpackagedownloads' => $protectpackages);
+        $expected2 = array('protectpackagedownloads' => $protectpackages);
         foreach ($expectedfields as $field) {
 
             // Since we return the fields used as boolean as PARAM_BOOL instead PARAM_INT we need to force casting here.
index 09fc251..ff2b6ca 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015092200;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2015092201;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2015050500;    // Requires this Moodle version.
 $plugin->component = 'mod_scorm';   // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 300;
index ea2ebcc..6e07e93 100644 (file)
@@ -43,6 +43,7 @@ $string['defaultformat_help'] = 'This setting determines the default format used
 * HTML - The HTML editor is available
 * Creole - A common wiki markup language for which a small edit toolbar is available
 * Nwiki - Mediawiki-like markup language used in the contributed Nwiki module';
+$string['deleteallpages'] = 'Delete all wiki pages';
 $string['deletecomment'] = 'Deleting comment';
 $string['deletecommentcheck'] = 'Delete comment';
 $string['deletecommentcheckfull'] = 'Are you sure you want to delete the comment?';
index 31298ab..c48ad75 100644 (file)
@@ -145,6 +145,7 @@ function wiki_reset_userdata($data) {
     global $CFG,$DB;
     require_once($CFG->dirroot . '/mod/wiki/pagelib.php');
     require_once($CFG->dirroot . '/tag/lib.php');
+    require_once($CFG->dirroot . "/mod/wiki/locallib.php");
 
     $componentstr = get_string('modulenameplural', 'wiki');
     $status = array();
@@ -156,33 +157,56 @@ function wiki_reset_userdata($data) {
     $errors = false;
     foreach ($wikis as $wiki) {
 
-        // remove all comments
-        if (!empty($data->reset_wiki_comments)) {
-            if (!$cm = get_coursemodule_from_instance('wiki', $wiki->id)) {
-                continue;
-            }
-            $context = context_module::instance($cm->id);
-            $DB->delete_records_select('comments', "contextid = ? AND commentarea='wiki_page'", array($context->id));
-            $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallcomments'), 'error'=>false);
+        if (!$cm = get_coursemodule_from_instance('wiki', $wiki->id)) {
+            continue;
         }
+        $context = context_module::instance($cm->id);
 
-        if (!empty($data->reset_wiki_tags)) {
-            # Get subwiki information #
-            $subwikis = $DB->get_records('wiki_subwikis', array('wikiid' => $wiki->id));
+        // Remove tags or all pages.
+        if (!empty($data->reset_wiki_pages) || !empty($data->reset_wiki_tags)) {
+
+            // Get subwiki information.
+            $subwikis = wiki_get_subwikis($wiki->id);
 
             foreach ($subwikis as $subwiki) {
-                if ($pages = $DB->get_records('wiki_pages', array('subwikiid' => $subwiki->id))) {
-                    foreach ($pages as $page) {
-                        $tags = tag_get_tags_array('wiki_pages', $page->id);
-                        foreach ($tags as $tagid => $tagname) {
-                            // Delete the related tag_instances related to the wiki page.
-                            $errors = tag_delete_instance('wiki_pages', $page->id, $tagid);
-                            $status[] = array('component' => $componentstr, 'item' => get_string('tagsdeleted', 'wiki'), 'error' => $errors);
+                // Get existing pages.
+                if ($pages = wiki_get_page_list($subwiki->id)) {
+                    if (!empty($data->reset_wiki_tags)) {
+                        // Go through each page and delete the tags.
+                        foreach ($pages as $page) {
+
+                            $tags = tag_get_tags_array('wiki_pages', $page->id);
+                            foreach ($tags as $tagid => $tagname) {
+                                // Delete the related tag_instances related to the wiki page.
+                                $errors = tag_delete_instance('wiki_pages', $page->id, $tagid);
+                                $status[] = array('component' => $componentstr, 'item' => get_string('tagsdeleted', 'wiki'),
+                                        'error' => $errors);
+                            }
                         }
+                    } else {
+                        // Delete pages.
+                        wiki_delete_pages($context, $pages, $subwiki->id);
                     }
                 }
+                if (!empty($data->reset_wiki_pages)) {
+                    // Delete any subwikis.
+                    $DB->delete_records('wiki_subwikis', array('id' => $subwiki->id), IGNORE_MISSING);
+
+                    // Delete any attached files.
+                    $fs = get_file_storage();
+                    $fs->delete_area_files($context->id, 'mod_wiki', 'attachments');
+
+                    $status[] = array('component' => $componentstr, 'item' => get_string('deleteallpages', 'wiki'),
+                            'error' => $errors);
+                }
             }
         }
+
+        // Remove all comments.
+        if (!empty($data->reset_wiki_comments) || !empty($data->reset_wiki_pages)) {
+            $DB->delete_records_select('comments', "contextid = ? AND commentarea='wiki_page'", array($context->id));
+            $status[] = array('component' => $componentstr, 'item' => get_string('deleteallcomments'), 'error' => false);
+        }
     }
     return $status;
 }
@@ -190,6 +214,7 @@ function wiki_reset_userdata($data) {
 
 function wiki_reset_course_form_definition(&$mform) {
     $mform->addElement('header', 'wikiheader', get_string('modulenameplural', 'wiki'));
+    $mform->addElement('advcheckbox', 'reset_wiki_pages', get_string('deleteallpages', 'wiki'));
     $mform->addElement('advcheckbox', 'reset_wiki_tags', get_string('removeallwikitags', 'wiki'));
     $mform->addElement('advcheckbox', 'reset_wiki_comments', get_string('deleteallcomments'));
 }
index e3f4d32..6292da2 100644 (file)
@@ -6,24 +6,15 @@ Feature: Manager is able to delete tags
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email                |
-      | manager1 | Manager   | 1        | manager1@example.com |
-      | user1    | User      | 1        | user1@example.com    |
+      | username | firstname | lastname | email                | interests      |
+      | manager1 | Manager   | 1        | manager1@example.com |                |
+      | user1    | User      | 1        | user1@example.com    | Cat,Dog,Turtle |
     And the following "system role assigns" exist:
       | user     | course               | role      |
       | manager1 | Acceptance test site | manager   |
     And the following "tags" exist:
       | name         | tagtype  |
       | Neverusedtag | official |
-    And I log in as "user1"
-    And I navigate to "Site blogs" node in "Site pages"
-    And I follow "Add a new entry"
-    And I set the following fields to these values:
-      | Entry title                                 | Blog post header  |
-      | Blog entry body                             | Blog post content |
-      | Other tags (enter tags separated by commas) | Cat,Dog,Turtle    |
-    And I press "Save changes"
-    And I log out
 
   Scenario: Deleting a tag with javascript disabled
     When I log in as "manager1"
@@ -31,7 +22,8 @@ Feature: Manager is able to delete tags
     And I click on "Delete" "link" in the "Dog" "table_row"
     And I should see "Tag(s) deleted"
     Then I should not see "Dog"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I should see "Cat"
     And I should not see "Dog"
     And I log out
@@ -46,7 +38,8 @@ Feature: Manager is able to delete tags
     Then I should see "Tag(s) deleted"
     And I should not see "Dog"
     And I should not see "Neverusedtag"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I should see "Cat"
     And I should not see "Dog"
     And I log out
@@ -67,7 +60,8 @@ Feature: Manager is able to delete tags
     And I should not see "Dog"
     And I follow "Manage tags"
     And I should not see "Dog"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I should see "Cat"
     And I should not see "Dog"
     And I log out
@@ -99,7 +93,8 @@ Feature: Manager is able to delete tags
     And I follow "Manage tags"
     And I should not see "Dog"
     And I should not see "Neverusedtag"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I should see "Cat"
     And I should not see "Dog"
     And I log out
index 1a717ba..a4df735 100644 (file)
@@ -6,10 +6,10 @@ Feature: Users can edit tags to add description or rename
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email                |
-      | manager1 | Manager   | 1        | manager1@example.com |
-      | user1    | User      | 1        | user1@example.com    |
-      | editor1  | Editor    | 1        | editor1@example.com  |
+      | username | firstname | lastname | email                | interests         |
+      | manager1 | Manager   | 1        | manager1@example.com |                   |
+      | user1    | User      | 1        | user1@example.com    | Cat,Dog,Turtle    |
+      | editor1  | Editor    | 1        | editor1@example.com  |                   |
     Given the following "roles" exist:
       | name       | shortname |
       | Tag editor | tageditor |
@@ -20,24 +20,18 @@ Feature: Users can edit tags to add description or rename
     And the following "tags" exist:
       | name         | tagtype  |
       | Neverusedtag | official |
-    And I log in as "user1"
-    And I navigate to "Site blogs" node in "Site pages"
-    And I follow "Add a new entry"
-    And I set the following fields to these values:
-      | Entry title                                 | Blog post header  |
-      | Blog entry body                             | Blog post content |
-      | Other tags (enter tags separated by commas) | Cat,Dog,Turtle    |
-    And I press "Save changes"
-    And I log out
 
   Scenario: User with tag editing capability can change tag description
     Given I log in as "admin"
     And I set the following system permissions of "Tag editor" role:
-      | capability      | permission |
-      | moodle/tag:edit | Allow      |
+      | capability                   | permission |
+      | moodle/tag:edit              | Allow      |
+      | moodle/site:viewparticipants | Allow      |
+      | moodle/user:viewdetails      | Allow      |
     And I log out
     When I log in as "editor1"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I follow "Cat"
     And I follow "Edit this tag"
     And I should not see "Tag name"
@@ -53,7 +47,8 @@ Feature: Users can edit tags to add description or rename
 
   Scenario: Manager can change tag description, related tags and rename the tag from tag view page
     When I log in as "manager1"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I follow "Cat"
     And I follow "Edit this tag"
     And I set the following fields to these values:
@@ -77,7 +72,8 @@ Feature: Users can edit tags to add description or rename
 
   Scenario: Renaming the tag from tag view page
     When I log in as "manager1"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I follow "Cat"
     And I follow "Edit this tag"
     And I set the following fields to these values:
index 494be54..7001498 100644 (file)
@@ -6,40 +6,40 @@ Feature: Users can flag tags and manager can reset flags
 
   Background:
     Given the following "users" exist:
-      | username | firstname | lastname | email                |
-      | manager1 | Manager   | 1        | manager1@example.com |
-      | user1    | User      | 1        | user1@example.com    |
-      | user2    | User      | 2        | user2@example.com    |
-      | user3    | User      | 3        | user3@example.com    |
+      | username | firstname | lastname | email                | interests                 |
+      | manager1 | Manager   | 1        | manager1@example.com |                           |
+      | user1    | User      | 1        | user1@example.com    | Nicetag, Badtag, Sweartag |
+      | user2    | User      | 2        | user2@example.com    |                           |
+      | user3    | User      | 3        | user3@example.com    |                           |
     And the following "system role assigns" exist:
       | user     | course               | role    |
       | manager1 | Acceptance test site | manager |
     And the following "tags" exist:
       | name         | tagtype  |
       | Neverusedtag | official |
-    And I log in as "user1"
-    And I navigate to "Site blogs" node in "Site pages"
-    And I follow "Add a new entry"
-    And I set the following fields to these values:
-      | Entry title                                 | Blog post from teacher    |
-      | Blog entry body                             | Teacher blog post content |
-      | Other tags (enter tags separated by commas) | Nicetag, Badtag, Sweartag |
-    And I press "Save changes"
+    And I log in as "admin"
+    And I set the following system permissions of "Authenticated user" role:
+      | capability                   | permission |
+      | moodle/site:viewparticipants | Allow      |
+      | moodle/user:viewdetails      | Allow      |
     And I log out
     And I log in as "user2"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I follow "Badtag"
     And I follow "Flag as inappropriate"
     And I should see "The person responsible will be notified"
     And I follow "Continue"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I follow "Sweartag"
     And I follow "Flag as inappropriate"
     And I should see "The person responsible will be notified"
     And I follow "Continue"
     And I log out
     And I log in as "user3"
-    And I navigate to "Site blogs" node in "Site pages"
+    And I navigate to "Participants" node in "Site pages"
+    And I follow "User 1"
     And I follow "Sweartag"
     And I follow "Flag as inappropriate"
     And I should see "The person responsible will be notified"
index a03c925..e3ec921 100644 (file)
@@ -264,6 +264,8 @@ function useredit_update_trackforums($user, $usernew) {
  * @param array $interests
  */
 function useredit_update_interests($user, $interests) {
+    global $CFG;
+    require_once($CFG->dirroot . '/tag/lib.php');
     tag_set('user', $user->id, $interests, 'core', context_user::instance($user->id)->id);
 }
 
index 4732f16..9e73323 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2015100200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2015100601.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
index a984f57..0ba462f 100644 (file)
@@ -27,7 +27,6 @@
 $capabilities = array(
 
     'webservice/amf:use' => array(
-        'riskbitmask' => RISK_CONFIG | RISK_DATALOSS | RISK_SPAM | RISK_PERSONAL | RISK_XSS,
         'captype' => 'read', // in fact this may be considered read and write at the same time
         'contextlevel' => CONTEXT_COURSE, // the context level should be probably CONTEXT_MODULE
         'archetypes' => array(
index 03b89a1..4432f58 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015051101;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'webservice_amf';  // Full name of the plugin (used for diagnostics)
index f49480f..2d3cdad 100644 (file)
@@ -38,6 +38,9 @@ require_once(dirname(dirname(__FILE__)) . '/config.php');
 require_once($CFG->libdir . '/filelib.php');
 require_once($CFG->dirroot . '/webservice/lib.php');
 
+// Allow CORS requests.
+header('Access-Control-Allow-Origin: *');
+
 //authenticate the user
 $token = required_param('token', PARAM_ALPHANUM);
 $webservicelib = new webservice();
index 551e8df..62949b4 100644 (file)
@@ -27,7 +27,6 @@
 $capabilities = array(
 
     'webservice/rest:use' => array(
-        'riskbitmask' => RISK_CONFIG | RISK_DATALOSS | RISK_SPAM | RISK_PERSONAL | RISK_XSS,
         'captype' => 'read', // in fact this may be considered read and write at the same time
         'contextlevel' => CONTEXT_COURSE, // the context level should be probably CONTEXT_MODULE
         'archetypes' => array(
index f748ef3..bf238c5 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015051101;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'webservice_rest'; // Full name of the plugin (used for diagnostics)
index a3a90a3..de96adf 100644 (file)
@@ -27,7 +27,6 @@
 $capabilities = array(
 
     'webservice/soap:use' => array(
-        'riskbitmask' => RISK_CONFIG | RISK_DATALOSS | RISK_SPAM | RISK_PERSONAL | RISK_XSS,
         'captype' => 'read', // in fact this may be considered read and write at the same time
         'contextlevel' => CONTEXT_COURSE, // the context level should be probably CONTEXT_MODULE
         'archetypes' => array(
index 86ee3b2..0f5e559 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015051101;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'webservice_soap'; // Full name of the plugin (used for diagnostics)
index 6e457ce..54122e6 100644 (file)
@@ -3,6 +3,13 @@ information provided here is intended especially for developers.
 
 This information is intended for authors of webservices, not people writing webservice clients.
 
+=== 3.0 ===
+
+* WS protocols webservice/myprotocol:use capabilities were defined with a high riskbitmask value
+  when the fact that a user has that capability does not imply any risk, but other capabilities
+  that the user may have do. If your ws protocol does not imply and risk by itself, you can remove the
+  riskbitmask from your $capabilities array in webservice/myprotocol/db/access.php
+
 === 2.9 ===
 
 * The deprecated functions can not be added to services anymore and
index 1c71a8c..3f4f90e 100644 (file)
@@ -27,7 +27,6 @@
 $capabilities = array(
 
     'webservice/xmlrpc:use' => array(
-        'riskbitmask' => RISK_CONFIG | RISK_DATALOSS | RISK_SPAM | RISK_PERSONAL | RISK_XSS,
         'captype' => 'read', // in fact this may be considered read and write at the same time
         'contextlevel' => CONTEXT_COURSE, // the context level should be probably CONTEXT_MODULE
         'archetypes' => array(
index aa7f1bc..ae5c39c 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2015051100;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2015051101;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2015050500;        // Requires this Moodle version
 $plugin->component = 'webservice_xmlrpc'; // Full name of the plugin (used for diagnostics)