Merge branch 'MDL-3782_multichoice_multiple' of git://github.com/davosmith/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 8 Aug 2016 06:04:53 +0000 (14:04 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 8 Aug 2016 06:04:53 +0000 (14:04 +0800)
151 files changed:
.stylelintrc [new file with mode: 0644]
Gruntfile.js
admin/blocks.php
badges/criteria/award_criteria.php
badges/newbadge.php
badges/tests/events_test.php
blocks/search_forums/tests/behat/block_search_forums_course.feature [new file with mode: 0644]
blocks/search_forums/tests/behat/block_search_forums_frontpage.feature [new file with mode: 0644]
calendar/lib.php
calendar/tests/events_test.php
calendar/tests/ical_test.php
completion/tests/behat/behat_completion.php
course/externallib.php
course/tests/search_test.php
course/upgrade.txt
enrol/lti/tool.php
install/lang/ckb/moodle.php
install/lang/he/admin.php
lang/en/badges.php
lang/en/calendar.php
lang/en/moodle.php
lib/amd/build/chart_output_chartjs.min.js
lib/amd/build/chart_output_htmltable.min.js
lib/amd/src/chart_output_chartjs.js
lib/amd/src/chart_output_htmltable.js
lib/badgeslib.php
lib/classes/event/badge_archived.php [new file with mode: 0644]
lib/classes/event/badge_created.php [new file with mode: 0644]
lib/classes/event/badge_criteria_created.php [new file with mode: 0644]
lib/classes/event/badge_criteria_deleted.php [new file with mode: 0644]
lib/classes/event/badge_criteria_updated.php [new file with mode: 0644]
lib/classes/event/badge_deleted.php [new file with mode: 0644]
lib/classes/event/badge_disabled.php [new file with mode: 0644]
lib/classes/event/badge_duplicated.php [new file with mode: 0644]
lib/classes/event/badge_enabled.php [new file with mode: 0644]
lib/classes/event/badge_updated.php [new file with mode: 0644]
lib/classes/event/calendar_subscription_created.php [new file with mode: 0644]
lib/classes/event/calendar_subscription_deleted.php [new file with mode: 0644]
lib/classes/event/calendar_subscription_updated.php [new file with mode: 0644]
lib/componentlib.class.php
lib/db/services.php
lib/deprecatedlib.php
lib/filebrowser/file_info_context_coursecat.php
lib/filebrowser/file_info_context_system.php
lib/filelib.php
lib/filestorage/file_packer.php
lib/filestorage/mbz_packer.php
lib/filestorage/tests/mbz_packer_test.php
lib/filestorage/tests/tgz_packer_test.php
lib/filestorage/tests/zip_packer_test.php
lib/filestorage/tgz_packer.php
lib/filestorage/zip_packer.php
lib/odslib.class.php
lib/outputlib.php
lib/outputrenderers.php
lib/templates/chart.mustache
lib/tests/behat/behat_general.php
lib/tests/htmlpurifier_test.php
lib/upgrade.txt
lib/weblib.php
mod/assign/externallib.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/comment.js
mod/assign/lang/en/assign.php
mod/assign/lang/en/deprecated.txt [new file with mode: 0644]
mod/assign/module.js
mod/assign/submission_form.php
mod/assign/tests/behat/edit_student_submission.feature [new file with mode: 0644]
mod/assign/tests/externallib_test.php
mod/assign/upgrade.txt
mod/choice/classes/external.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/locallib.php
mod/choice/mod_form.php
mod/choice/renderer.php
mod/choice/tests/behat/allow_preview.feature
mod/choice/tests/behat/choice_availability.feature [new file with mode: 0644]
mod/choice/tests/behat/my_home.feature
mod/choice/tests/behat/publish_results.feature
mod/choice/view.php
mod/data/tests/generator/lib.php
mod/data/tests/generator_test.php
mod/feedback/item/multichoice/lib.php
mod/feedback/item/multichoicerated/lib.php
mod/feedback/tests/behat/behat_mod_feedback.php
mod/forum/externallib.php
mod/forum/tests/behat/advanced_search.feature [new file with mode: 0644]
mod/forum/upgrade.txt
mod/glossary/classes/external.php
mod/glossary/upgrade.txt
mod/lti/externalregistrationreturn.php
mod/lti/locallib.php
mod/lti/tests/behat/addtool.feature
mod/lti/tests/behat/addtype.feature
mod/lti/tests/behat/toolconfigure.feature
mod/lti/tests/externallib_test.php
mod/lti/tests/fixtures/tool_provider.html [deleted file]
mod/lti/tests/fixtures/tool_provider.php [new file with mode: 0644]
mod/lti/tests/generator/lib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/wiki/classes/external.php
mod/wiki/tests/externallib_test.php
mod/wiki/upgrade.txt
mod/workshop/exsubmission.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/mod_form.php
mod/workshop/submission.php
mod/workshop/tests/behat/example_submission.feature [new file with mode: 0644]
mod/workshop/tests/behat/grade_to_pass.feature [new file with mode: 0644]
npm-shrinkwrap.json
package.json
question/format/webct/format.php
report/log/classes/renderable.php
report/log/classes/renderer.php
report/log/classes/table_log.php
report/log/index.php
report/log/lang/en/report_log.php
report/log/locallib.php
search/classes/document.php
theme/bootstrapbase/less/bootstrap/variables.less
theme/bootstrapbase/less/editor.less
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/backup-restore.less
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/bootstrapoverride.less
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/chat.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/dock.less
theme/bootstrapbase/less/moodle/expendable.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/less/moodle/templates.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/readme_moodle.txt
theme/bootstrapbase/style/moodle.css
user/externallib.php
user/tests/externallib_test.php
version.php

diff --git a/.stylelintrc b/.stylelintrc
new file mode 100644 (file)
index 0000000..e5e61a2
--- /dev/null
@@ -0,0 +1,92 @@
+{
+    "rules": {
+        "at-rule-empty-line-before": [ "always",
+          {"except": [ "blockless-group", "first-nested" ], ignore: ["after-comment"]}
+        ],
+        "at-rule-name-case": "lower",
+        "at-rule-name-space-after": "always-single-line",
+        "at-rule-no-unknown": true,
+        "at-rule-semicolon-newline-after": "always",
+        "block-closing-brace-newline-after": "always",
+        "block-closing-brace-newline-before": "always-multi-line",
+        "block-closing-brace-space-before": "always-single-line",
+        "block-no-empty": true,
+        "block-no-single-line": true,
+        "block-opening-brace-newline-after": "always-multi-line",
+        "block-opening-brace-space-after": "always-single-line",
+        "block-opening-brace-space-before": "always",
+        "color-hex-case": ["lower", { "severity": "warning" }],
+        "color-hex-length": ["short", { "severity": "warning" }],
+        "color-no-invalid-hex": true,
+        "declaration-bang-space-after": "never",
+        "declaration-bang-space-before": "always",
+        "declaration-block-no-duplicate-properties": true,
+        "declaration-block-no-ignored-properties": true,
+        "declaration-block-no-shorthand-property-overrides": true,
+        "declaration-block-semicolon-newline-after": "always-multi-line",
+        "declaration-block-semicolon-space-after": "always-single-line",
+        "declaration-block-semicolon-space-before": "never",
+        "declaration-block-single-line-max-declarations": 1,
+        "declaration-block-trailing-semicolon": "always",
+        "declaration-colon-newline-after": "always-multi-line",
+        "declaration-colon-space-after": "always-single-line",
+        "declaration-colon-space-before": "never",
+        "function-calc-no-unspaced-operator": true,
+        "function-comma-newline-after": "always-multi-line",
+        "function-comma-space-after": "always-single-line",
+        "function-comma-space-before": "never",
+        "function-linear-gradient-no-nonstandard-direction": true,
+        "function-max-empty-lines": 0,
+        "function-name-case": "lower",
+        "function-parentheses-newline-inside": "always-multi-line",
+        "function-parentheses-space-inside": "never-single-line",
+        "function-url-data-uris": never,
+        "function-whitespace-after": "always",
+        "indentation": 4,
+        "keyframe-declaration-no-important": true,
+        "length-zero-no-unit": [true, { "severity": "warning" }],
+        "max-empty-lines": 2,
+        "max-line-length": [132, { "severity": "warning" }],
+        "media-feature-colon-space-after": "always",
+        "media-feature-colon-space-before": "never",
+        "media-feature-no-missing-punctuation": true,
+        "media-feature-range-operator-space-after": "always",
+        "media-feature-range-operator-space-before": "always",
+        "media-query-list-comma-newline-after": "always-multi-line",
+        "media-query-list-comma-space-after": "always-single-line",
+        "media-query-list-comma-space-before": "never",
+        "no-browser-hacks": [true, { "severity": "warning" }],
+        "no-empty-source": true,
+        "no-eol-whitespace": true,
+        "no-extra-semicolons": [true, { "severity": "warning" }],
+        "no-invalid-double-slash-comments": true,
+        "no-unknown-animations": true,
+        "property-case": "lower",
+        "selector-attribute-brackets-space-inside": "never",
+        "selector-attribute-operator-space-after": "never",
+        "selector-attribute-operator-space-before": "never",
+        "selector-combinator-space-after": "always",
+        "selector-combinator-space-before": "always",
+        "selector-list-comma-newline-after": "always",
+        "selector-list-comma-space-before": "never",
+        "selector-max-empty-lines": 0,
+        "selector-pseudo-class-case": "lower",
+        "selector-pseudo-class-no-unknown": true,
+        "selector-pseudo-class-parentheses-space-inside": "never",
+        "selector-pseudo-element-case": "lower",
+        "selector-pseudo-element-no-unknown": true,
+        "selector-root-no-composition": true,
+        "selector-type-case": "lower",
+        "selector-type-no-unknown": true,
+        "shorthand-property-no-redundant-values": [null, { "severity": "warning" }],
+        "string-no-newline": true,
+        "time-no-imperceptible": true,
+        "unit-blacklist": ["pt", "rem"],
+        "unit-case": "lower",
+        "unit-no-unknown": true,
+        "value-keyword-case": ["lower", {"ignoreKeywords": ["/@/"]}],
+        "value-list-comma-newline-after": "always-multi-line",
+        "value-list-comma-space-after": "always-single-line",
+        "value-list-comma-space-before": "never",
+  }
+}
index 467f13b..4d9a81d 100644 (file)
@@ -157,7 +157,7 @@ module.exports = function(grunt) {
             },
             bootstrapbase: {
                 files: ["theme/bootstrapbase/less/**/*.less"],
-                tasks: ["less:bootstrapbase"]
+                tasks: ["css"]
             },
             yui: {
                 files: ['**/yui/src/**/*.js'],
@@ -169,6 +169,27 @@ module.exports = function(grunt) {
                 recursive: true,
                 paths: [cwd]
             }
+        },
+        stylelint: {
+            less: {
+                options: {
+                    syntax: 'less',
+                    configOverrides: {
+                        rules: {
+                            // TODO: MDL-55165 -Enable these rules once we make output-changing changes to less.
+                            "declaration-block-no-ignored-properties": null,
+                            "value-keyword-case": null,
+                            "declaration-block-no-duplicate-properties": null,
+                            "declaration-block-no-shorthand-property-overrides": null,
+                            "selector-type-no-unknown": null,
+                            "length-zero-no-unit": null,
+                            "color-hex-case": null,
+                            "color-hex-length": null
+                        }
+                    }
+                },
+                src: ['theme/**/*.less', '!theme/bootstrapbase/less/bootstrap/*'],
+            }
         }
     });
 
@@ -295,6 +316,7 @@ module.exports = function(grunt) {
           grunt.config('eslint.yui.src', files);
           grunt.config('uglify.amd.files', [{ expand: true, src: files, rename: uglifyRename }]);
           grunt.config('shifter.options.paths', files);
+          grunt.config('stylelint.less.src', files);
           changedFiles = Object.create(null);
     }, 200);
 
@@ -308,6 +330,7 @@ module.exports = function(grunt) {
     grunt.loadNpmTasks('grunt-contrib-less');
     grunt.loadNpmTasks('grunt-contrib-watch');
     grunt.loadNpmTasks('grunt-eslint');
+    grunt.loadNpmTasks('grunt-stylelint');
 
     // Register JS tasks.
     grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
@@ -317,7 +340,7 @@ module.exports = function(grunt) {
     grunt.registerTask('js', ['amd', 'yui']);
 
     // Register CSS taks.
-    grunt.registerTask('css', ['less:bootstrapbase']);
+    grunt.registerTask('css', ['stylelint:less', 'less:bootstrapbase']);
 
     // Register the startup task.
     grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup);
index 77febe6..dcf00f6 100644 (file)
                 $settings = '<a href="' . $blocksettings->url .  '">' . get_string('settings') . '</a>';
             } else if ($blocksettings instanceof admin_settingpage) {
                 $settings = '<a href="'.$CFG->wwwroot.'/'.$CFG->admin.'/settings.php?section=blocksetting'.$block->name.'">'.$strsettings.'</a>';
-            } else {
+            } else if (!file_exists($CFG->dirroot.'/blocks/'.$block->name.'/settings.php')) {
+                // If the block's settings node was not found, we check that the block really provides the settings.php file.
+                // Note that blocks can inject their settings to other nodes in the admin tree without using the default locations.
+                // This can be done by assigning null to $setting in settings.php and it is a valid case.
                 debugging('Warning: block_'.$block->name.' returns true in has_config() but does not provide a settings.php file',
                     DEBUG_DEVELOPER);
             }
index 58ccf86..fdc9ee5 100644 (file)
@@ -332,7 +332,7 @@ abstract class award_criteria {
      *
      */
     public function delete() {
-        global $DB;
+        global $DB, $PAGE;
 
         // Remove any records if it has already been met.
         $DB->delete_records('badge_criteria_met', array('critid' => $this->id));
@@ -342,6 +342,13 @@ abstract class award_criteria {
 
         // Finally remove criterion itself.
         $DB->delete_records('badge_criteria', array('id' => $this->id));
+
+        // Trigger event, badge criteria deleted.
+        $eventparams = array('objectid' => $this->id,
+            'context' => $PAGE->context,
+            'other' => array('badgeid' => $this->badgeid));
+        $event = \core\event\badge_criteria_deleted::create($eventparams);
+        $event->trigger();
     }
 
     /**
@@ -350,7 +357,7 @@ abstract class award_criteria {
      * @param array $params Values from the form or any other array.
      */
     public function save($params = array()) {
-        global $DB;
+        global $DB, $PAGE;
 
         // Figure out criteria description.
         // If it is coming from the form editor, it is an array(text, format).
@@ -386,6 +393,13 @@ abstract class award_criteria {
             $fordb->id = $cid;
             $DB->update_record('badge_criteria', $fordb, true);
 
+            // Trigger event: badge_criteria_updated.
+            $eventparams = array('objectid' => $this->id,
+                'context' => $PAGE->context,
+                'other' => array('badgeid' => $this->badgeid));
+            $event = \core\event\badge_criteria_updated::create($eventparams);
+            $event->trigger();
+
             $existing = $DB->get_fieldset_select('badge_criteria_param', 'name', 'critid = ?', array($cid));
             $todelete = array_diff($existing, $requiredkeys);
 
@@ -429,6 +443,12 @@ abstract class award_criteria {
                     $DB->insert_record('badge_criteria_param', $newp, false, true);
                 }
             }
+            // Trigger event: badge_criteria_created.
+            $eventparams = array('objectid' => $this->id,
+                'context' => $PAGE->context,
+                'other' => array('badgeid' => $this->badgeid));
+            $event = \core\event\badge_criteria_created::create($eventparams);
+            $event->trigger();
         }
         $t->allow_commit();
     }
index 6c3b74c..60f3951 100644 (file)
@@ -98,6 +98,11 @@ if ($form->is_cancelled()) {
 
     $newid = $DB->insert_record('badge', $fordb, true);
 
+    // Trigger event, badge created.
+    $eventparams = array('objectid' => $newid, 'context' => $PAGE->context);
+    $event = \core\event\badge_created::create($eventparams);
+    $event->trigger();
+
     $newbadge = new badge($newid);
     badges_process_badge_image($newbadge, $form->save_temp_file('image'));
     // If a user can configure badge criteria, they will be redirected to the criteria page.
index 833cf09..cfe5215 100644 (file)
@@ -55,4 +55,259 @@ class core_badges_events_testcase extends core_badges_badgeslib_testcase {
 
         $sink->close();
     }
-}
\ No newline at end of file
+
+    /**
+     * Test the badge created event.
+     *
+     * There is no external API for creating a badge, so the unit test will simply
+     * create and trigger the event and ensure data is returned as expected.
+     */
+    public function test_badge_created() {
+
+        $badge = new badge($this->badgeid);
+        // Trigger an event: badge created.
+        $eventparams = array(
+            'userid' => $badge->usercreated,
+            'objectid' => $badge->id,
+            'context' => $badge->get_context(),
+        );
+
+        $event = \core\event\badge_created::create($eventparams);
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_created', $event);
+        $this->assertEquals($badge->usercreated, $event->userid);
+        $this->assertEquals($badge->id, $event->objectid);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Test the badge archived event.
+     *
+     */
+    public function test_badge_archived() {
+        $badge = new badge($this->badgeid);
+        $sink = $this->redirectEvents();
+
+        // Trigger and capture the event.
+        $badge->delete(true);
+        $events = $sink->get_events();
+        $this->assertCount(2, $events);
+        $event = $events[1];
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_archived', $event);
+        $this->assertEquals($badge->id, $event->objectid);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+
+    /**
+     * Test the badge updated event.
+     *
+     */
+    public function test_badge_updated() {
+        $badge = new badge($this->badgeid);
+        $sink = $this->redirectEvents();
+
+        // Trigger and capture the event.
+        $badge->save();
+        $events = $sink->get_events();
+        $event = reset($events);
+        $this->assertCount(1, $events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_updated', $event);
+        $this->assertEquals($badge->id, $event->objectid);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+    /**
+     * Test the badge deleted event.
+     */
+    public function test_badge_deleted() {
+        $badge = new badge($this->badgeid);
+        $sink = $this->redirectEvents();
+
+        // Trigger and capture the event.
+        $badge->delete(false);
+        $events = $sink->get_events();
+        $event = reset($events);
+        $this->assertCount(1, $events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_deleted', $event);
+        $this->assertEquals($badge->id, $event->objectid);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Test the badge duplicated event.
+     *
+     */
+    public function test_badge_duplicated() {
+        $badge = new badge($this->badgeid);
+        $sink = $this->redirectEvents();
+
+        // Trigger and capture the event.
+        $newid = $badge->make_clone();
+        $events = $sink->get_events();
+        $event = reset($events);
+        $this->assertCount(1, $events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_duplicated', $event);
+        $this->assertEquals($newid, $event->objectid);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Test the badge disabled event.
+     *
+     */
+    public function test_badge_disabled() {
+        $badge = new badge($this->badgeid);
+        $sink = $this->redirectEvents();
+
+        // Trigger and capture the event.
+        $badge->set_status(BADGE_STATUS_INACTIVE);
+        $events = $sink->get_events();
+        $event = reset($events);
+        $this->assertCount(2, $events);
+        $event = $events[1];
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_disabled', $event);
+        $this->assertEquals($badge->id, $event->objectid);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Test the badge enabled event.
+     *
+     */
+    public function test_badge_enabled() {
+        $badge = new badge($this->badgeid);
+        $sink = $this->redirectEvents();
+
+        // Trigger and capture the event.
+        $badge->set_status(BADGE_STATUS_ACTIVE);
+        $events = $sink->get_events();
+        $event = reset($events);
+        $this->assertCount(2, $events);
+        $event = $events[1];
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\badge_enabled', $event);
+        $this->assertEquals($badge->id, $event->objectid);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Test the badge criteria created event.
+     *
+     * There is no external API for this, so the unit test will simply
+     * create and trigger the event and ensure data is returned as expected.
+     */
+    public function test_badge_criteria_created() {
+
+        $badge = new badge($this->badgeid);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
+        $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL));
+        $criteriaprofile = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
+        $params = array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address');
+        $criteriaprofile->save($params);
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertCount(1, $events);
+        $this->assertInstanceOf('\core\event\badge_criteria_created', $event);
+        $this->assertEquals($criteriaprofile->id, $event->objectid);
+        $this->assertEquals($criteriaprofile->badgeid, $event->other['badgeid']);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Test the badge criteria updated event.
+     *
+     * There is no external API for this, so the unit test will simply
+     * create and trigger the event and ensure data is returned as expected.
+     */
+    public function test_badge_criteria_updated() {
+
+        $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $this->badgeid));
+        $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL));
+        $criteriaprofile = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $this->badgeid));
+        $params = array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address');
+        $criteriaprofile->save($params);
+        $badge = new badge($this->badgeid);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $criteria = $badge->criteria[BADGE_CRITERIA_TYPE_PROFILE];
+        $params2 = array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address', 'id' => $criteria->id);
+        $criteria->save((array)$params2);
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertCount(1, $events);
+        $this->assertInstanceOf('\core\event\badge_criteria_updated', $event);
+        $this->assertEquals($criteria->id, $event->objectid);
+        $this->assertEquals($this->badgeid, $event->other['badgeid']);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Test the badge criteria deleted event.
+     *
+     * There is no external API for this, so the unit test will simply
+     * create and trigger the event and ensure data is returned as expected.
+     */
+    public function test_badge_criteria_deleted() {
+
+        $criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $this->badgeid));
+        $criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL));
+        $badge = new badge($this->badgeid);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->delete();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertCount(1, $events);
+        $this->assertInstanceOf('\core\event\badge_criteria_deleted', $event);
+        $this->assertEquals($criteriaoverall->badgeid, $event->other['badgeid']);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+}
diff --git a/blocks/search_forums/tests/behat/block_search_forums_course.feature b/blocks/search_forums/tests/behat/block_search_forums_course.feature
new file mode 100644 (file)
index 0000000..48d164d
--- /dev/null
@@ -0,0 +1,69 @@
+@block @block_search_forums @mod_forum
+Feature: The search forums block allows users to search for forum posts
+  In order to search for a forum post
+  As a user
+  I can use the search forums block
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the field "id_newsitems" to "1"
+    And I press "Save and display"
+    And I log out
+
+  Scenario: Use the search forum block in a course without any forum posts
+    Given I log in as "student1"
+    And I follow "Course 1"
+    When I set the following fields to these values:
+      | searchform_search | Moodle |
+    And I press "Go"
+    Then I should see "No posts"
+
+  Scenario: Use the search forum block in a course with a hidden forum and search for posts
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I add a new topic to "Announcements" forum with:
+      | Subject | My subject |
+      | Message | My message |
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I follow "Announcements"
+    And I navigate to "Edit settings" node in "Forum administration"
+    And I expand all fieldsets
+    And I set the field "id_visible" to "0"
+    And I press "Save and return to course"
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And "Search forums" "block" should exist
+    And I set the following fields to these values:
+      | searchform_search | message |
+    And I press "Go"
+    Then I should see "No posts"
+
+  Scenario: Use the search forum block in a course and search for posts
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I add a new topic to "Announcements" forum with:
+      | Subject | My subject |
+      | Message | My message |
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And "Search forums" "block" should exist
+    And I set the following fields to these values:
+      | searchform_search | message |
+    And I press "Go"
+    Then I should see "My subject"
diff --git a/blocks/search_forums/tests/behat/block_search_forums_frontpage.feature b/blocks/search_forums/tests/behat/block_search_forums_frontpage.feature
new file mode 100644 (file)
index 0000000..4d2aa52
--- /dev/null
@@ -0,0 +1,31 @@
+@block @block_search_forums @mod_forum
+Feature: The search forums block allows users to search for forum posts
+  In order to search for a forum post
+  As an administrator
+  I can add the search forums block
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And I log in as "admin"
+    And I am on site homepage
+    And I navigate to "Turn editing on" node in "Front page settings"
+    And I add the "Search forums" block
+    And I log out
+
+  Scenario: Use the search forum block on the frontpage and search for posts as a user
+    Given I log in as "student1"
+    And I am on site homepage
+    When I set the following fields to these values:
+      | searchform_search | Moodle |
+    And I press "Go"
+    Then I should see "No posts"
+
+  Scenario: Use the search forum block on the frontpage and search for posts as a guest
+    Given I log in as "guest"
+    And I am on site homepage
+    When I set the following fields to these values:
+      | searchform_search | Moodle |
+    And I press "Go"
+    Then I should see "No posts"
index 5376d8f..c675c65 100644 (file)
@@ -2956,6 +2956,14 @@ function calendar_add_subscription($sub) {
         if (empty($sub->id)) {
             $id = $DB->insert_record('event_subscriptions', $sub);
             // we cannot cache the data here because $sub is not complete.
+            $sub->id = $id;
+            // Trigger event, calendar subscription added.
+            $eventparams = array('objectid' => $sub->id,
+                'context' => calendar_get_calendar_context($sub),
+                'other' => array('eventtype' => $sub->eventtype, 'courseid' => $sub->courseid)
+            );
+            $event = \core\event\calendar_subscription_created::create($eventparams);
+            $event->trigger();
             return $id;
         } else {
             // Why are we doing an update here?
@@ -3112,13 +3120,21 @@ function calendar_process_subscription_row($subscriptionid, $pollinterval, $acti
 function calendar_delete_subscription($subscription) {
     global $DB;
 
-    if (is_object($subscription)) {
-        $subscription = $subscription->id;
+    if (!is_object($subscription)) {
+        $subscription = $DB->get_record('event_subscriptions', array('id' => $subscription), '*', MUST_EXIST);
     }
     // Delete subscription and related events.
-    $DB->delete_records('event', array('subscriptionid' => $subscription));
-    $DB->delete_records('event_subscriptions', array('id' => $subscription));
-    cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription));
+    $DB->delete_records('event', array('subscriptionid' => $subscription->id));
+    $DB->delete_records('event_subscriptions', array('id' => $subscription->id));
+    cache_helper::invalidate_by_definition('core', 'calendar_subscriptions', array(), array($subscription->id));
+
+    // Trigger event, calendar subscription deleted.
+    $eventparams = array('objectid' => $subscription->id,
+        'context' => calendar_get_calendar_context($subscription),
+        'other' => array('courseid' => $subscription->courseid)
+    );
+    $event = \core\event\calendar_subscription_deleted::create($eventparams);
+    $event->trigger();
 }
 /**
  * From a URL, fetch the calendar and return an iCalendar object.
@@ -3246,6 +3262,14 @@ function calendar_update_subscription($subscription) {
     // Update cache.
     $cache = cache::make('core', 'calendar_subscriptions');
     $cache->set($subscription->id, $subscription);
+    // Trigger event, calendar subscription updated.
+    $eventparams = array('userid' => $subscription->userid,
+        'objectid' => $subscription->id,
+        'context' => calendar_get_calendar_context($subscription),
+        'other' => array('eventtype' => $subscription->eventtype, 'courseid' => $subscription->courseid)
+        );
+    $event = \core\event\calendar_subscription_updated::create($eventparams);
+    $event->trigger();
 }
 
 /**
@@ -3320,3 +3344,23 @@ function calendar_cron() {
 
     return true;
 }
+
+/**
+ * Helper function to determine the context of a calendar subscription.
+ * Subscriptions can be created in two contexts COURSE, or USER.
+ *
+ * @param stdClass $subscription
+ * @return context instance
+ */
+function calendar_get_calendar_context($subscription) {
+
+    // Determine context based on calendar type.
+    if ($subscription->eventtype === 'site') {
+        $context = context_course::instance(SITEID);
+    } else if ($subscription->eventtype === 'group' || $subscription->eventtype === 'course') {
+        $context = context_course::instance($subscription->courseid);
+    } else {
+        $context = context_user::instance($subscription->userid);
+    }
+    return $context;
+}
index a0b3968..207d66a 100644 (file)
@@ -386,4 +386,95 @@ class core_calendar_events_testcase extends advanced_testcase {
             $this->assertContains('The \'timestart\' value must be set in other.', $e->getMessage());
         }
     }
+
+    /**
+     * Tests for calendar_subscription_added event.
+     */
+    public function test_calendar_subscription_created() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'site';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $id = calendar_add_subscription($subscription);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_created', $event);
+        $this->assertEquals($id, $event->objectid);
+        $this->assertEquals($subscription->courseid, $event->other['courseid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Tests for calendar_subscription_updated event.
+     */
+    public function test_calendar_subscription_updated() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'site';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+        $subscription->id = calendar_add_subscription($subscription);
+        // Now edit it.
+        $subscription->name = 'awesome';
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_update_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_updated', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($subscription->courseid, $event->other['courseid']);
+        $this->assertEquals($subscription->eventtype, $event->other['eventtype']);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
+
+    /**
+     * Tests for calendar_subscription_deleted event.
+     */
+    public function test_calendar_subscription_deleted() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/lib.php');
+        $this->resetAfterTest(true);
+
+        // Create a mock subscription.
+        $subscription = new stdClass();
+        $subscription->eventtype = 'site';
+        $subscription->name = 'test';
+        $subscription->courseid = $this->course->id;
+        $subscription->id = calendar_add_subscription($subscription);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        calendar_delete_subscription($subscription);
+        $events = $sink->get_events();
+        $event = reset($events);
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\calendar_subscription_deleted', $event);
+        $this->assertEquals($subscription->id, $event->objectid);
+        $this->assertEquals($subscription->courseid, $event->other['courseid']);
+        $this->assertDebuggingNotCalled();
+        $sink->close();
+
+    }
 }
index 2fc3c02..d2223b5 100644 (file)
@@ -57,14 +57,14 @@ class core_calendar_ical_testcase extends advanced_testcase {
         $id = calendar_add_subscription($subscription);
 
         $subscription = new stdClass();
-        $subscription->id = $id;
+        $subscription = calendar_get_subscription($id);
         $subscription->name = 'awesome';
         calendar_update_subscription($subscription);
         $sub = calendar_get_subscription($id);
         $this->assertEquals($subscription->name, $sub->name);
 
         $subscription = new stdClass();
-        $subscription->id = $id;
+        $subscription = calendar_get_subscription($id);
         $subscription->name = 'awesome2';
         $subscription->pollinterval = 604800;
         calendar_update_subscription($subscription);
index 817a582..8a0925e 100644 (file)
@@ -102,6 +102,9 @@ class behat_completion extends behat_base {
         // Go to course editing.
         $this->execute("behat_general::click_link", get_string('editsettings'));
 
+        // Expand all the form fields.
+        $this->execute("behat_forms::i_expand_all_fieldsets");
+
         // Enable completion.
         $this->execute("behat_forms::i_set_the_field_to",
             array(get_string('enablecompletion', 'completion'), $toggle));
index d9c90fe..7a2367d 100644 (file)
@@ -2267,7 +2267,10 @@ class core_course_external extends external_api {
                 $files[] = array(
                     'filename' => $file->get_filename(),
                     'fileurl' => $fileurl,
-                    'filesize' => $file->get_filesize()
+                    'filesize' => $file->get_filesize(),
+                    'filepath' => $file->get_filepath(),
+                    'mimetype' => $file->get_mimetype(),
+                    'timemodified' => $file->get_timemodified(),
                 );
             }
 
@@ -2337,16 +2340,7 @@ class core_course_external extends external_api {
                             'categoryname' => new external_value(PARAM_TEXT, 'category name'),
                             'summary' => new external_value(PARAM_RAW, 'summary'),
                             'summaryformat' => new external_format_value('summary'),
-                            'overviewfiles' => new external_multiple_structure(
-                                new external_single_structure(
-                                    array(
-                                        'filename' => new external_value(PARAM_FILE, 'overview file name'),
-                                        'fileurl'  => new external_value(PARAM_URL, 'overview file url'),
-                                        'filesize'  => new external_value(PARAM_INT, 'overview file size'),
-                                    )
-                                ),
-                                'additional overview files attached to this course'
-                            ),
+                            'overviewfiles' => new external_files('additional overview files attached to this course'),
                             'contacts' => new external_multiple_structure(
                                 new external_single_structure(
                                     array(
index 5512e07..e8ed2fa 100644 (file)
@@ -123,7 +123,7 @@ class course_search_testcase extends advanced_testcase {
         $this->assertEquals($course->fullname, $doc->get('title'));
 
         // Not nice. Applying \core_search\document::set line breaks clean up.
-        $summary = preg_replace("/\s+/", " ", content_to_text($course->summary, $course->summaryformat));
+        $summary = preg_replace("/\s+/u", " ", content_to_text($course->summary, $course->summaryformat));
         $this->assertEquals($summary, $doc->get('content'));
         $this->assertEquals($course->shortname, $doc->get('description1'));
     }
index 8e73dd9..39fa5d3 100644 (file)
@@ -4,4 +4,7 @@ information provided here is intended especially for developers.
 === 3.2 ===
 
  * External function core_course_external::get_course_contents now returns the section's number in the course (new section field).
+ * External functions that were returning file information now return the following file fields:
+   filename, filepath, mimetype, filesize, timemodified and fileurl.
+   Those fields are now marked as VALUE_OPTIONAL for backwards compatibility.
 
index 9dcc3bc..556d0f1 100644 (file)
@@ -27,7 +27,6 @@ require_once($CFG->dirroot . '/user/lib.php');
 require_once($CFG->dirroot . '/enrol/lti/ims-blti/blti.php');
 
 $toolid = required_param('id', PARAM_INT);
-$lticontextid = required_param('context_id', PARAM_RAW);
 
 // Get the tool.
 $tool = \enrol_lti\helper::get_lti_tool($toolid);
index ccef7c5..cbf9d26 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'زمان';
+$string['moodlelogo'] = 'لۆگۆی موودڵ';
 $string['next'] = 'دواتر';
 $string['previous'] = 'پێشوو';
 $string['reload'] = 'بارکردنەوە';
index 5374ba9..bda628c 100644 (file)
@@ -41,5 +41,5 @@ $string['cliunknowoption'] = 'אפשרויות לא מוכרות :
 {$a}
 אנא השתמש באפשרות העזרה.';
 $string['cliyesnoprompt'] = 'רשום y (שפרושו כן) או n (שפרושו לא)';
-$string['environmentrequireinstall'] = 'נדרש להתקין/לאפשר זאת';
-$string['environmentrequireversion'] = '×\92×\99רסת {$a->needed} נדרשת אך הגירסה הנוכחית היא {$a->current}';
+$string['environmentrequireinstall'] = 'נדרש להתקין ולאפשר הרחבה זו';
+$string['environmentrequireversion'] = '×\92×\99רס×\94 {$a->needed} נדרשת אך הגירסה הנוכחית היא {$a->current}';
index 1077607..aaf102b 100644 (file)
@@ -248,7 +248,17 @@ $string['error:requesttimeout'] = 'The connection request timed out before it co
 $string['error:requesterror'] = 'The connection request failed (error code {$a}).';
 $string['error:save'] = 'Cannot save the badge.';
 $string['error:userdeleted'] = '{$a->user} (This user no longer exists in {$a->site})';
+$string['eventbadgearchived'] = 'Badge archived';
 $string['eventbadgeawarded'] = 'Badge awarded';
+$string['eventbadgecreated'] = 'Badge created';
+$string['eventbadgecriteriacreated'] = 'Badge criteria created';
+$string['eventbadgecriteriadeleted'] = 'Badge criteria deleted';
+$string['eventbadgecriteriaupdated'] = 'Badge criteria updated';
+$string['eventbadgedeleted'] = 'Badge deleted';
+$string['eventbadgedisabled'] = 'Badge disabled';
+$string['eventbadgeduplicated'] = 'Badge duplicated';
+$string['eventbadgeenabled'] = 'Badge enabled';
+$string['eventbadgeupdated'] = 'Badge updated';
 $string['evidence'] = 'Evidence';
 $string['existingrecipients'] = 'Existing badge recipients';
 $string['expired'] = 'Expired';
index 9858392..f85bb34 100644 (file)
@@ -95,6 +95,9 @@ $string['eventview'] = 'Event details';
 $string['eventcalendareventcreated'] = 'Calendar event created';
 $string['eventcalendareventupdated'] = 'Calendar event updated';
 $string['eventcalendareventdeleted'] = 'Calendar event deleted';
+$string['eventsubscriptioncreated'] = 'Calendar subscription created';
+$string['eventsubscriptionupdated'] = 'Calendar subscription updated';
+$string['eventsubscriptiondeleted'] = 'Calendar subscription deleted';
 $string['expired'] = 'Expired';
 $string['explain_site_timeformat'] = 'You can choose to see times in either 12 or 24 hour format for the whole site. If you choose "default", then the format will be automatically chosen according to the language you use in the site. This setting can be overridden by user preferences.';
 $string['export'] = 'Export';
index 9e1ee63..b6e7255 100644 (file)
@@ -923,6 +923,7 @@ $string['hiddensectionscollapsed'] = 'Hidden sections are shown in collapsed for
 $string['hiddensectionsinvisible'] = 'Hidden sections are completely invisible';
 $string['hide'] = 'Hide';
 $string['hideadvancedsettings'] = 'Hide advanced settings';
+$string['hidechartdata'] = 'Hide chart data';
 $string['hidepicture'] = 'Hide picture';
 $string['hidesection'] = 'Hide section {$a}';
 $string['hidesettings'] = 'Hide settings';
index fdc8e84..c035888 100644 (file)
Binary files a/lib/amd/build/chart_output_chartjs.min.js and b/lib/amd/build/chart_output_chartjs.min.js differ
index cea5152..a2e9030 100644 (file)
Binary files a/lib/amd/build/chart_output_htmltable.min.js and b/lib/amd/build/chart_output_htmltable.min.js differ
index b0075f2..d738479 100644 (file)
@@ -274,7 +274,7 @@ define([
 
         // Add serie labels to the tooltip if any.
         if (serieLabels !== null) {
-            tooltip += ' ' + serieLabels[tooltipItem.index];
+            tooltip = serieLabels[tooltipItem.index];
         }
 
         return tooltip;
index ab55bbd..2ab02a1 100644 (file)
@@ -102,7 +102,7 @@ define([
                 value = series[serieId].getValues()[rowId];
                 seriesLabels = series[serieId].getLabels();
                 if (seriesLabels !== null) {
-                    value += ' ' + series[serieId].getLabels()[rowId];
+                    value = series[serieId].getLabels()[rowId];
                 }
                 node.append($('<td>').text(value));
             }
index 9103fcd..d77d739 100644 (file)
@@ -222,6 +222,10 @@ class badge {
 
         $fordb->timemodified = time();
         if ($DB->update_record_raw('badge', $fordb)) {
+            // Trigger event, badge updated.
+            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
+            $event = \core\event\badge_updated::create($eventparams);
+            $event->trigger();
             return true;
         } else {
             throw new moodle_exception('error:save', 'badges');
@@ -236,7 +240,7 @@ class badge {
      * @return int ID of new badge.
      */
     public function make_clone() {
-        global $DB, $USER;
+        global $DB, $USER, $PAGE;
 
         $fordb = new stdClass();
         foreach (get_object_vars($this) as $k => $v) {
@@ -274,6 +278,11 @@ class badge {
                 $crit->make_clone($new);
             }
 
+            // Trigger event, badge duplicated.
+            $eventparams = array('objectid' => $new, 'context' => $PAGE->context);
+            $event = \core\event\badge_duplicated::create($eventparams);
+            $event->trigger();
+
             return $new;
         } else {
             throw new moodle_exception('error:clone', 'badges');
@@ -312,6 +321,17 @@ class badge {
     public function set_status($status = 0) {
         $this->status = $status;
         $this->save();
+        if ($status == BADGE_STATUS_ACTIVE) {
+            // Trigger event, badge enabled.
+            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
+            $event = \core\event\badge_enabled::create($eventparams);
+            $event->trigger();
+        } else if ($status == BADGE_STATUS_INACTIVE) {
+            // Trigger event, badge disabled.
+            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
+            $event = \core\event\badge_disabled::create($eventparams);
+            $event->trigger();
+        }
     }
 
     /**
@@ -628,6 +648,11 @@ class badge {
         if ($archive) {
             $this->status = BADGE_STATUS_ARCHIVED;
             $this->save();
+
+            // Trigger event, badge archived.
+            $eventparams = array('objectid' => $this->id, 'context' => $this->get_context());
+            $event = \core\event\badge_archived::create($eventparams);
+            $event->trigger();
             return;
         }
 
@@ -654,6 +679,14 @@ class badge {
 
         // Finally, remove badge itself.
         $DB->delete_records('badge', array('id' => $this->id));
+
+        // Trigger event, badge deleted.
+        $eventparams = array('objectid' => $this->id,
+            'context' => $this->get_context(),
+            'other' => array('badgetype' => $this->type, 'courseid' => $this->courseid)
+            );
+        $event = \core\event\badge_deleted::create($eventparams);
+        $event->trigger();
     }
 }
 
diff --git a/lib/classes/event/badge_archived.php b/lib/classes/event/badge_archived.php
new file mode 100644 (file)
index 0000000..4482e42
--- /dev/null
@@ -0,0 +1,98 @@
+<?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/>.
+
+/**
+ * Badge archived event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a badge is archived.
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_archived extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgearchived', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has archived the badge with id '$this->objectid'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/overview.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge', 'restore' => 'badge');
+    }
+
+}
+
+
+
diff --git a/lib/classes/event/badge_created.php b/lib/classes/event/badge_created.php
new file mode 100644 (file)
index 0000000..3cfc9db
--- /dev/null
@@ -0,0 +1,96 @@
+<?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/>.
+
+/**
+ * Badge created event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a badge is created.
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_created extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgecreated', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has created the badge with id '$this->objectid'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/overview.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge', 'restore' => 'badge');
+    }
+
+}
+
diff --git a/lib/classes/event/badge_criteria_created.php b/lib/classes/event/badge_criteria_created.php
new file mode 100644 (file)
index 0000000..fd69497
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+/**
+ * Badge criteria created event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after criteria is created for a badge.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int badgeid: The ID of the badge affected
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_criteria_created extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge_criteria';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgecriteriacreated', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has created criteria to the badge with id '".$this->other['badgeid']."'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/criteria.php', array('id' => $this->other['badgeid']));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+        if (!isset($this->other['badgeid'])) {
+            throw new \coding_exception('The \'badgeid\' value must be set in other.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge_criteria', 'restore' => 'badge_criteria');
+    }
+
+    /**
+     * Used for maping events on restore
+     *
+     * @return bool
+     */
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['badgeid'] = array('db' => 'badge', 'restore' => 'badge');
+        return $othermapped;
+    }
+}
+
+
diff --git a/lib/classes/event/badge_criteria_deleted.php b/lib/classes/event/badge_criteria_deleted.php
new file mode 100644 (file)
index 0000000..71c5e01
--- /dev/null
@@ -0,0 +1,117 @@
+<?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/>.
+
+/**
+ * Badge criteria deleted event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after criteria is deleted from a badge.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int badgeid: The ID of the badge affected
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_criteria_deleted extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge_criteria';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgecriteriadeleted', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has deleted criteria from the badge with id '".$this->other['badgeid']."'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/criteria.php', array('id' => $this->other['badgeid']));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+        if (!isset($this->other['badgeid'])) {
+            throw new \coding_exception('The \'badgeid\' value must be set in other.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge_criteria', 'restore' => 'badge_criteria');
+    }
+
+    /**
+     * Used for maping events on restore
+     *
+     * @return bool
+     */
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['badgeid'] = array('db' => 'badge', 'restore' => 'badge');
+        return $othermapped;
+    }
+}
+
+
+
diff --git a/lib/classes/event/badge_criteria_updated.php b/lib/classes/event/badge_criteria_updated.php
new file mode 100644 (file)
index 0000000..0406e6e
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Badge criteria updated event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after criteria is updated to a badge.
+ *
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int badgeid: The ID of the badge affected
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_criteria_updated extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge_criteria';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgecriteriaupdated', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has updated criteria to the badge with id '".$this->other['badgeid']."'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/criteria.php', array('id' => $this->other['badgeid']));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+        if (!isset($this->other['badgeid'])) {
+            throw new \coding_exception('The \'badgeid\' value must be set in other.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge_criteria', 'restore' => 'badge_criteria');
+    }
+
+    /**
+     * Used for maping events on restore
+     *
+     * @return bool
+     */
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['badgeid'] = array('db' => 'badge', 'restore' => 'badge');
+        return $othermapped;
+    }
+}
diff --git a/lib/classes/event/badge_deleted.php b/lib/classes/event/badge_deleted.php
new file mode 100644 (file)
index 0000000..0dd4fba
--- /dev/null
@@ -0,0 +1,119 @@
+<?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/>.
+
+/**
+ * Badge deleted event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/badgeslib.php');
+
+
+/**
+ * Event triggered after a badge is deleted.
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_deleted extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgedeleted', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has deleted the badge with id '$this->objectid'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if ($this->other['badgetype'] == BADGE_TYPE_COURSE) {
+            // Course badge.
+            $return = new \moodle_url('/badges/index.php',
+                    array('type' => BADGE_TYPE_COURSE, 'id' => $this->other['courseid']));
+        } else {
+            // Site badge.
+            $return = new \moodle_url('/badges/index.php', array('type' => BADGE_TYPE_SITE));
+        }
+        return $return;
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+        if (!isset($this->other['badgetype'])) {
+            throw new \coding_exception('The \'badgetype\' value must be set in other.');
+        } else {
+            if (($this->other['badgetype'] != BADGE_TYPE_COURSE) && ($this->other['badgetype'] != BADGE_TYPE_SITE)) {
+                throw new \coding_exception('Invalid \'badgetype\' value.');
+            }
+        }
+        if ($this->other['badgetype'] == BADGE_TYPE_COURSE) {
+            if (!isset($this->other['courseid'])) {
+                throw new \coding_exception('The \'courseid\' value must be set in other.');
+            }
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge', 'restore' => 'badge');
+    }
+
+}
+
+
diff --git a/lib/classes/event/badge_disabled.php b/lib/classes/event/badge_disabled.php
new file mode 100644 (file)
index 0000000..bcd9f55
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Badge disabled event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a badge is disabled.
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_disabled extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgedisabled', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has disabled access to the badge with id '$this->objectid'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/overview.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge', 'restore' => 'badge');
+    }
+
+}
+
+
diff --git a/lib/classes/event/badge_duplicated.php b/lib/classes/event/badge_duplicated.php
new file mode 100644 (file)
index 0000000..e159e9f
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Badge duplicated event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a badge is duplicated.
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_duplicated extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgeduplicated', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has duplicated the badge with id '$this->objectid'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/overview.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge', 'restore' => 'badge');
+    }
+
+}
+
+
diff --git a/lib/classes/event/badge_enabled.php b/lib/classes/event/badge_enabled.php
new file mode 100644 (file)
index 0000000..774e4ad
--- /dev/null
@@ -0,0 +1,97 @@
+<?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/>.
+
+/**
+ * Badge enabled event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a badge is enabled.
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_enabled extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgeenabled', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has enabled access to the badge with id '$this->objectid'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/overview.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge', 'restore' => 'badge');
+    }
+
+}
+
+
diff --git a/lib/classes/event/badge_updated.php b/lib/classes/event/badge_updated.php
new file mode 100644 (file)
index 0000000..9237762
--- /dev/null
@@ -0,0 +1,96 @@
+<?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/>.
+
+/**
+ * Badge updated event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a badge is updated.
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class badge_updated extends base {
+
+    /**
+     * Set basic properties for the event.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'badge';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventbadgeupdated', 'badges');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' has updated the badge with id '$this->objectid'.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/badges/overview.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+    }
+
+    /**
+     * Used for maping events on restore
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'badge', 'restore' => 'badge');
+    }
+
+}
+
diff --git a/lib/classes/event/calendar_subscription_created.php b/lib/classes/event/calendar_subscription_created.php
new file mode 100644 (file)
index 0000000..4b2c650
--- /dev/null
@@ -0,0 +1,119 @@
+<?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/>.
+
+/**
+ * calendar subscription added event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a calendar subscription is added.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - string eventtype: the type of events (site, course, group, user).
+ *      - int courseid: The ID of the course (SITEID, User(0) or actual course)
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class calendar_subscription_created extends base
+{
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'event_subscriptions';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubscriptioncreated', 'calendar');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "User {$this->userid} has added a calendar
+         subscription with id {$this->objectid} of event type {$this->other['eventtype']}.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
+            return new \moodle_url('calendar/managesubscriptions.php');
+        } else {
+            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+        }
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->context)) {
+            throw new \coding_exception('The \'context\' must be set.');
+        }
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+        if (!isset($this->other['eventtype'])) {
+            throw new \coding_exception('The \'eventtype\' value must be set in other.');
+        }
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
+    }
+
+    /**
+     * Returns mappings for restore
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'event_subscriptions', 'restore' => 'event_subscriptions');
+    }
+}
diff --git a/lib/classes/event/calendar_subscription_deleted.php b/lib/classes/event/calendar_subscription_deleted.php
new file mode 100644 (file)
index 0000000..0f7ab2a
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * calendar subscription deleted event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a calendar subscription is deleted.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - int courseid: The ID of the course (SITEID, User(0) or actual course)
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class calendar_subscription_deleted extends base
+{
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'd';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'event_subscriptions';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubscriptiondeleted', 'calendar');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "User {$this->userid} has deleted a calendar
+         subscription with id {$this->objectid}.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
+            return new \moodle_url('calendar/managesubscriptions.php');
+        } else {
+            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+        }
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->context)) {
+            throw new \coding_exception('The \'context\' must be set.');
+        }
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
+    }
+
+    /**
+     * Returns mappings for restore
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'event_subscriptions', 'restore' => 'event_subscriptions');
+    }
+}
diff --git a/lib/classes/event/calendar_subscription_updated.php b/lib/classes/event/calendar_subscription_updated.php
new file mode 100644 (file)
index 0000000..eb61033
--- /dev/null
@@ -0,0 +1,119 @@
+<?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/>.
+
+/**
+ * calendar subscription updated event.
+ *
+ * @package    core
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event triggered after a calendar subscription is updated.
+ *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - string eventtype: the type of events (site, course, group, user).
+ *      - int courseid: The ID of the course (SITEID, User(0) or actual course)
+ * }
+ *
+ * @package    core
+ * @since      Moodle 3.2
+ * @copyright  2016 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class calendar_subscription_updated extends base
+{
+
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+        $this->data['objecttable'] = 'event_subscriptions';
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventsubscriptionupdated', 'calendar');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "User {$this->userid} has updated a calendar
+        subscription with id {$this->objectid} of event type {$this->other['eventtype']}.";
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if (($this->other['courseid'] == SITEID) || ($this->other['courseid'] == 0)) {
+            return new \moodle_url('calendar/managesubscriptions.php');
+        } else {
+            return new \moodle_url('calendar/managesubscriptions.php', array('course' => $this->other['courseid']));
+        }
+    }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->context)) {
+            throw new \coding_exception('The \'context\' must be set.');
+        }
+        if (!isset($this->objectid)) {
+            throw new \coding_exception('The \'objectid\' must be set.');
+        }
+        if (!isset($this->other['eventtype'])) {
+            throw new \coding_exception('The \'eventtype\' value must be set in other.');
+        }
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
+    }
+
+    /**
+     * Returns mappings for restore
+     *
+     * @return array
+     */
+    public static function get_objectid_mapping() {
+        return array('db' => 'event_subscriptions', 'restore' => 'event_subscriptions');
+    }
+}
index 0d07797..858038d 100644 (file)
@@ -268,15 +268,13 @@ class component_installer {
      * compare md5 values, download, unzip, install and regenerate
      * local md5 file
      *
-     * @global object
      * @uses COMPONENT_ERROR
      * @uses COMPONENT_UPTODATE
      * @uses COMPONENT_ERROR
      * @uses COMPONENT_INSTALLED
      * @return int COMPONENT_(ERROR | UPTODATE | INSTALLED)
      */
-    function install() {
-
+    public function install() {
         global $CFG;
 
     /// Check requisites are passed
@@ -330,25 +328,30 @@ class component_installer {
             $this->errorstring='downloadedfilecheckfailed';
             return COMPONENT_ERROR;
         }
-    /// Move current revision to a safe place
-        $destinationdir = $CFG->dataroot.'/'.$this->destpath;
-        $destinationcomponent = $destinationdir.'/'.$this->componentname;
-        @remove_dir($destinationcomponent.'_old');     // Deleting a possible old version.
+
+        // Move current revision to a safe place.
+        $destinationdir = $CFG->dataroot . '/' . $this->destpath;
+        $destinationcomponent = $destinationdir . '/' . $this->componentname;
+        $destinationcomponentold = $destinationcomponent . '_old';
+        @remove_dir($destinationcomponentold);     // Deleting a possible old version.
 
         // Moving to a safe place.
-        @rename($destinationcomponent, $destinationcomponent.'_old');
+        @rename($destinationcomponent, $destinationcomponentold);
 
-    /// Unzip new version
-        if (!unzip_file($zipfile, $destinationdir, false)) {
-        /// Error so, go back to the older
+        // Unzip new version.
+        $packer = get_file_packer('application/zip');
+        $unzipsuccess = $packer->extract_to_pathname($zipfile, $destinationdir, null, null, true);
+        if (!$unzipsuccess) {
             @remove_dir($destinationcomponent);
-            @rename ($destinationcomponent.'_old', $destinationcomponent);
-            $this->errorstring='cannotunzipfile';
+            @rename($destinationcomponentold, $destinationcomponent);
+            $this->errorstring = 'cannotunzipfile';
             return COMPONENT_ERROR;
         }
-    /// Delete old component version
-        @remove_dir($destinationcomponent.'_old');
-    /// Create local md5
+
+        // Delete old component version.
+        @remove_dir($destinationcomponentold);
+
+        // Create local md5.
         if ($file = fopen($destinationcomponent.'/'.$this->componentname.'.md5', 'w')) {
             if (!fwrite($file, $new_md5)) {
                 fclose($file);
index 34744dc..8694520 100644 (file)
@@ -871,6 +871,14 @@ $functions = array(
         'capabilities' => 'moodle/user:viewdetails',
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_user_get_user_preferences' => array(
+        'classname' => 'core_user_external',
+        'methodname' => 'get_user_preferences',
+        'classpath' => 'user/externallib.php',
+        'description' => 'Return user preferences.',
+        'type' => 'read',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 
     // Competencies functions.
     'core_competency_create_competency_framework' => array(
index bda248c..fcc6cfb 100644 (file)
@@ -608,11 +608,13 @@ function detect_munged_arguments($string, $allowdots=1) {
  * @param string $zipfile The zip file to unzip
  * @param string $destination The location to unzip to
  * @param bool $showstatus_ignored Unused
+ * @deprecated since 2.0 MDL-15919
  */
 function unzip_file($zipfile, $destination = '', $showstatus_ignored = true) {
-    global $CFG;
+    debugging(__FUNCTION__ . '() is deprecated. '
+            . 'Please use the application/zip file_packer implementation instead.', DEBUG_DEVELOPER);
 
-    //Extract everything from zipfile
+    // Extract everything from zipfile.
     $path_parts = pathinfo(cleardoubleslashes($zipfile));
     $zippath = $path_parts["dirname"];       //The path of the zip file
     $zipfilename = $path_parts["basename"];  //The name of the zip file
@@ -674,11 +676,14 @@ function unzip_file($zipfile, $destination = '', $showstatus_ignored = true) {
  * @param array $originalfiles Files to zip
  * @param string $destination The destination path
  * @return bool Outcome
+ *
+ * @deprecated since 2.0 MDL-15919
  */
-function zip_files ($originalfiles, $destination) {
-    global $CFG;
+function zip_files($originalfiles, $destination) {
+    debugging(__FUNCTION__ . '() is deprecated. '
+            . 'Please use the application/zip file_packer implementation instead.', DEBUG_DEVELOPER);
 
-    //Extract everything from destination
+    // Extract everything from destination.
     $path_parts = pathinfo(cleardoubleslashes($destination));
     $destpath = $path_parts["dirname"];       //The path of the zip file
     $destfilename = $path_parts["basename"];  //The name of the zip file
index 5b49526..7c354bd 100644 (file)
@@ -97,7 +97,10 @@ class file_info_context_coursecat extends file_info {
     protected function get_area_coursecat_description($itemid, $filepath, $filename) {
         global $CFG;
 
-        if (!has_capability('moodle/course:update', $this->context)) {
+        if (!$this->category->visible and !has_capability('moodle/category:viewhiddencategories', $this->context)) {
+            return null;
+        }
+        if (!has_capability('moodle/category:manage', $this->context)) {
             return null;
         }
 
index 18331a9..27144c0 100644 (file)
@@ -145,10 +145,11 @@ class file_info_context_system extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
-        global $DB, $USER;
+        global $DB;
 
         $children = array();
 
+        // Add course categories on the top level that are either visible or user is able to view hidden categories.
         $course_cats = $DB->get_records('course_categories', array('parent'=>0), 'sortorder', 'id,visible');
         foreach ($course_cats as $category) {
             $context = context_coursecat::instance($category->id);
@@ -160,20 +161,49 @@ class file_info_context_system extends file_info {
             }
         }
 
-        $courses = $DB->get_records('course', array('category'=>0), 'sortorder', 'id,visible');
-        foreach ($courses as $course) {
-            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
-                continue;
-            }
-            $context = context_course::instance($course->id);
-            if ($child = $this->browser->get_file_info($context)) {
-                $children[] = $child;
+        // Add courses where user is enrolled that are located in hidden course categories because they would not
+        // be present in the above tree but user may still be able to access files in them.
+        if ($hiddencontexts = $this->get_inaccessible_coursecat_contexts()) {
+            $courses = enrol_get_my_courses();
+            foreach ($courses as $course) {
+                $context = context_course::instance($course->id);
+                $parents = $context->get_parent_context_ids();
+                if (array_intersect($hiddencontexts, $parents)) {
+                    // This course has hidden parent category.
+                    if ($child = $this->browser->get_file_info($context)) {
+                        $children[] = $child;
+                    }
+                }
             }
         }
 
         return $children;
     }
 
+    /**
+     * Returns list of course categories contexts that current user can not see
+     *
+     * @return array array of course categories contexts ids
+     */
+    protected function get_inaccessible_coursecat_contexts() {
+        global $DB;
+
+        $sql = context_helper::get_preload_record_columns_sql('ctx');
+        $records = $DB->get_records_sql("SELECT ctx.id, $sql
+            FROM {course_categories} c
+            JOIN {context} ctx ON c.id = ctx.instanceid AND ctx.contextlevel = ?
+            WHERE c.visible = ?", [CONTEXT_COURSECAT, 0]);
+        $hiddencontexts = [];
+        foreach ($records as $record) {
+            context_helper::preload_from_record($record);
+            $context = context::instance_by_id($record->id);
+            if (!has_capability('moodle/category:viewhiddencategories', $context)) {
+                $hiddencontexts[] = $record->id;
+            }
+        }
+        return $hiddencontexts;
+    }
+
     /**
      * Returns parent file_info instance
      *
index 6bd5a85..e23467a 100644 (file)
@@ -4182,6 +4182,14 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 require_login();
             }
 
+            // Check if user can view this category.
+            if (!has_capability('moodle/category:viewhiddencategories', $context)) {
+                $coursecatvisible = $DB->get_field('course_categories', 'visible', array('id' => $context->instanceid));
+                if (!$coursecatvisible) {
+                    send_file_not_found();
+                }
+            }
+
             $filename = array_pop($args);
             $filepath = $args ? '/'.implode('/', $args).'/' : '/';
             if (!$file = $fs->get_file($context->id, 'coursecat', 'description', 0, $filepath, $filename) or $file->is_directory()) {
index 5e6ec4f..468fed4 100644 (file)
@@ -94,10 +94,12 @@ abstract class file_packer {
      * @param string $pathname target directory
      * @param array $onlyfiles only extract files present in the array
      * @param file_progress $progress Progress indicator callback or null if not required
+     * @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error
+     * details.
      * @return array|bool list of processed files; false if error
      */
     public abstract function extract_to_pathname($archivefile, $pathname,
-            array $onlyfiles = NULL, file_progress $progress = null);
+            array $onlyfiles = NULL, file_progress $progress = null, $returnbool = false);
 
     /**
      * Extract file to given file path (real OS filesystem), existing files are overwritten.
index 749c622..3fde394 100644 (file)
@@ -95,13 +95,15 @@ class mbz_packer extends file_packer {
      * @param string $pathname target directory
      * @param array $onlyfiles only extract files present in the array
      * @param file_progress $progress Progress indicator callback or null if not required
+     * @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error
+     * details.
      * @return array list of processed files (name=>true)
      * @throws moodle_exception If error
      */
     public function extract_to_pathname($archivefile, $pathname,
-            array $onlyfiles = null, file_progress $progress = null) {
+            array $onlyfiles = null, file_progress $progress = null, $returnbool = false) {
         return $this->get_packer_for_read_operation($archivefile)->extract_to_pathname(
-                $archivefile, $pathname, $onlyfiles, $progress);
+                $archivefile, $pathname, $onlyfiles, $progress, $returnbool);
     }
 
     /**
index 5882218..b64c693 100644 (file)
@@ -87,4 +87,55 @@ class core_files_mbz_packer_testcase extends advanced_testcase {
         $this->assertNotEmpty($out);
         $this->assertEquals('frog', $out->get_content());
     }
+
+    public function usezipbackups_provider() {
+        return [
+            'Use zips'  => [true],
+            'Use tgz'   => [false],
+        ];
+    }
+
+    /**
+     * @dataProvider usezipbackups_provider
+     */
+    public function test_extract_to_pathname_returnvalue_successful($usezipbackups) {
+        global $CFG;
+        $this->resetAfterTest();
+
+        $packer = get_file_packer('application/vnd.moodle.backup');
+
+        // Set up basic archive contents.
+        $files = array('1.txt' => array('frog'));
+
+        // Create 2 archives (each with one file in) in zip mode.
+        $CFG->usezipbackups = $usezipbackups;
+
+        $mbzfile = make_request_directory() . '/file.mbz';
+        $packer->archive_to_pathname($files, $mbzfile);
+
+        $target = make_request_directory();
+        $result = $packer->extract_to_pathname($mbzfile, $target, null, null, true);
+        $this->assertTrue($result);
+    }
+
+    /**
+     * @dataProvider usezipbackups_provider
+     */
+    public function test_extract_to_pathname_returnvalue_failure($usezipbackups) {
+        global $CFG;
+        $this->resetAfterTest();
+
+        $packer = get_file_packer('application/vnd.moodle.backup');
+
+        // Create 2 archives (each with one file in) in zip mode.
+        $CFG->usezipbackups = $usezipbackups;
+
+        $mbzfile = make_request_directory() . '/file.mbz';
+        file_put_contents($mbzfile, 'Content');
+
+        $target = make_request_directory();
+        $result = $packer->extract_to_pathname($mbzfile, $target, null, null, true);
+        $this->assertDebuggingCalledCount(1);
+        $this->assertFalse($result);
+    }
 }
index d61d264..74dba64 100644 (file)
@@ -247,6 +247,42 @@ class core_files_tgz_packer_testcase extends advanced_testcase implements file_p
         $this->assertTrue(is_dir($outdir . '/out6'));
     }
 
+    /**
+     * Tests extracting files returning only a boolean state with success.
+     */
+    public function test_extract_to_pathname_returnvalue_successful() {
+        $packer = get_file_packer('application/x-gzip');
+
+        // Prepare files.
+        $files = $this->prepare_file_list();
+        $archivefile = make_request_directory() . DIRECTORY_SEPARATOR . 'test.tgz';
+        $packer->archive_to_pathname($files, $archivefile);
+
+        // Extract same files.
+        $outdir = make_request_directory();
+        $result = $packer->extract_to_pathname($archivefile, $outdir, null, null, true);
+
+        $this->assertTrue($result);
+    }
+
+    /**
+     * Tests extracting files returning only a boolean state with failure.
+     */
+    public function test_extract_to_pathname_returnvalue_failure() {
+        $packer = get_file_packer('application/x-gzip');
+
+        // Create sample files.
+        $archivefile = make_request_directory() . DIRECTORY_SEPARATOR . 'test.tgz';
+        file_put_contents($archivefile, '');
+
+        // Extract same files.
+        $outdir = make_request_directory();
+
+        $result = $packer->extract_to_pathname($archivefile, $outdir, null, null, true);
+
+        $this->assertFalse($result);
+    }
+
     /**
      * Tests the progress reporting.
      */
index 8da89dd..d7f2500 100644 (file)
@@ -294,6 +294,41 @@ class core_files_zip_packer_testcase extends advanced_testcase implements file_p
 
     }
 
+    /**
+     * @depends test_archive_to_storage
+     */
+    public function test_extract_to_pathname_returnvalue_successful() {
+        global $CFG;
+
+        $this->resetAfterTest(false);
+
+        $packer = get_file_packer('application/zip');
+
+        $target = make_request_directory();
+
+        $archive = "$CFG->tempdir/archive.zip";
+        $this->assertFileExists($archive);
+        $result = $packer->extract_to_pathname($archive, $target, null, null, true);
+        $this->assertTrue($result);
+    }
+
+    /**
+     * @depends test_archive_to_storage
+     */
+    public function test_extract_to_pathname_returnvalue_failure() {
+        global $CFG;
+
+        $this->resetAfterTest(false);
+
+        $packer = get_file_packer('application/zip');
+
+        $target = make_request_directory();
+
+        $archive = "$CFG->tempdir/noarchive.zip";
+        $result = $packer->extract_to_pathname($archive, $target, null, null, true);
+        $this->assertFalse($result);
+    }
+
     /**
      * @depends test_archive_to_storage
      */
index 5aea584..23a8cd3 100644 (file)
@@ -635,14 +635,37 @@ class tgz_packer extends file_packer {
      * @param string $pathname target directory
      * @param array $onlyfiles only extract files present in the array
      * @param file_progress $progress Progress indicator callback or null if not required
+     * @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error
+     * details.
      * @return array list of processed files (name=>true)
      * @throws moodle_exception If error
      */
     public function extract_to_pathname($archivefile, $pathname,
-            array $onlyfiles = null, file_progress $progress = null) {
+            array $onlyfiles = null, file_progress $progress = null, $returnbool = false) {
         $extractor = new tgz_extractor($archivefile);
-        return $extractor->extract(
-                new tgz_packer_extract_to_pathname($pathname, $onlyfiles), $progress);
+        try {
+            $result = $extractor->extract(
+                    new tgz_packer_extract_to_pathname($pathname, $onlyfiles), $progress);
+            if ($returnbool) {
+                if (!is_array($result)) {
+                    return false;
+                }
+                foreach ($result as $status) {
+                    if ($status !== true) {
+                        return false;
+                    }
+                }
+                return true;
+            } else {
+                return $result;
+            }
+        } catch (moodle_exception $e) {
+            if ($returnbool) {
+                return false;
+            } else {
+                throw $e;
+            }
+        }
     }
 
     /**
index 175a552..47395f7 100644 (file)
@@ -258,10 +258,12 @@ class zip_packer extends file_packer {
      * @param array $onlyfiles only extract files present in the array. The path to files MUST NOT
      *              start with a /. Example: array('myfile.txt', 'directory/anotherfile.txt')
      * @param file_progress $progress Progress indicator callback or null if not required
+     * @param bool $returnbool Whether to return a basic true/false indicating error state, or full per-file error
+     * details.
      * @return bool|array list of processed files; false if error
      */
     public function extract_to_pathname($archivefile, $pathname,
-            array $onlyfiles = null, file_progress $progress = null) {
+            array $onlyfiles = null, file_progress $progress = null, $returnbool = false) {
         global $CFG;
 
         if (!is_string($archivefile)) {
@@ -269,6 +271,7 @@ class zip_packer extends file_packer {
         }
 
         $processed = array();
+        $success = true;
 
         $pathname = rtrim($pathname, '/');
         if (!is_readable($archivefile)) {
@@ -308,6 +311,7 @@ class zip_packer extends file_packer {
                 // directory
                 if (is_file($newdir) and !unlink($newdir)) {
                     $processed[$name] = 'Can not create directory, file already exists'; // TODO: localise
+                    $success = false;
                     continue;
                 }
                 if (is_dir($newdir)) {
@@ -318,6 +322,7 @@ class zip_packer extends file_packer {
                         $processed[$name] = true;
                     } else {
                         $processed[$name] = 'Can not create directory'; // TODO: localise
+                        $success = false;
                     }
                 }
                 continue;
@@ -330,6 +335,7 @@ class zip_packer extends file_packer {
             if (!is_dir($newdir)) {
                 if (!mkdir($newdir, $CFG->directorypermissions, true)) {
                     $processed[$name] = 'Can not create directory'; // TODO: localise
+                    $success = false;
                     continue;
                 }
             }
@@ -337,10 +343,12 @@ class zip_packer extends file_packer {
             $newfile = "$newdir/$filename";
             if (!$fp = fopen($newfile, 'wb')) {
                 $processed[$name] = 'Can not write target file'; // TODO: localise
+                $success = false;
                 continue;
             }
             if (!$fz = $ziparch->get_stream($info->index)) {
                 $processed[$name] = 'Can not read file from zip archive'; // TODO: localise
+                $success = false;
                 fclose($fp);
                 continue;
             }
@@ -353,6 +361,7 @@ class zip_packer extends file_packer {
             fclose($fp);
             if (filesize($newfile) !== $size) {
                 $processed[$name] = 'Unknown error during zip extraction'; // TODO: localise
+                $success = false;
                 // something went wrong :-(
                 @unlink($newfile);
                 continue;
@@ -360,7 +369,12 @@ class zip_packer extends file_packer {
             $processed[$name] = true;
         }
         $ziparch->close();
-        return $processed;
+
+        if ($returnbool) {
+            return $success;
+        } else {
+            return $processed;
+        }
     }
 
     /**
index c5d5afd..7ac88b3 100644 (file)
@@ -72,6 +72,9 @@ class MoodleODSWorkbook {
      * Close the Moodle Workbook.
      */
     public function close() {
+        global $CFG;
+        require_once($CFG->libdir . '/filelib.php');
+
         $writer = new MoodleODSWriter($this->worksheets);
         $contents = $writer->get_file_content();
 
@@ -825,53 +828,30 @@ class MoodleODSWriter {
         $this->worksheets = $worksheets;
     }
 
+    /**
+     * Fetch the file ocntnet for the ODS.
+     *
+     * @return string
+     */
     public function get_file_content() {
-        global $CFG;
-
-        require_once($CFG->libdir.'/filelib.php');
-
-        do {
-            $dir = 'ods/'.time().'_'.rand(0, 10000);
-        } while (file_exists($CFG->tempdir.'/'.$dir));
-
-        make_temp_directory($dir);
-        make_temp_directory($dir.'/META-INF');
-        $dir = "$CFG->tempdir/$dir";
-        $files = array();
-
-        $handle = fopen("$dir/mimetype", 'w');
-        fwrite($handle, $this->get_ods_mimetype());
-        $files[] = "$dir/mimetype";
-
-        $handle = fopen("$dir/content.xml", 'w');
-        fwrite($handle, $this->get_ods_content($this->worksheets));
-        $files[] = "$dir/content.xml";
-
-        $handle = fopen("$dir/meta.xml", 'w');
-        fwrite($handle, $this->get_ods_meta());
-        $files[] = "$dir/meta.xml";
-
-        $handle = fopen("$dir/styles.xml", 'w');
-        fwrite($handle, $this->get_ods_styles());
-        $files[] = "$dir/styles.xml";
-
-        $handle = fopen("$dir/settings.xml", 'w');
-        fwrite($handle, $this->get_ods_settings());
-        $files[] = "$dir/settings.xml";
-
-        $handle = fopen("$dir/META-INF/manifest.xml", 'w');
-        fwrite($handle, $this->get_ods_manifest());
-        $files[] = "$dir/META-INF";
+        $dir = make_request_directory();
+        $filename = $dir . '/result.ods';
 
-        $filename = "$dir/result.ods";
-        zip_files($files, $filename);
+        $files = [
+                'mimetype'              => [$this->get_ods_mimetype()],
+                'content.xml'           => [$this->get_ods_content($this->worksheets)],
+                'meta.xml'              => [$this->get_ods_meta()],
+                'styles.xml'            => [$this->get_ods_styles()],
+                'settings.xml'          => [$this->get_ods_settings()],
+                'META-INF/manifest.xml' => [$this->get_ods_manifest()],
+            ];
 
-        $handle = fopen($filename, 'rb');
-        $contents = fread($handle, filesize($filename));
-        fclose($handle);
+        $packer = get_file_packer('application/zip');
+        $packer->archive_to_pathname($files, $filename);
 
-        remove_dir($dir); // Cleanup the temp directory.
+        $contents = file_get_contents($filename);
 
+        remove_dir($dir);
         return $contents;
     }
 
@@ -1286,7 +1266,7 @@ class MoodleODSWriter {
                       office:version="1.2">
     <office:meta>
         <meta:generator>Moodle '.$CFG->release.'</meta:generator>
-        <meta:initial-creator>'.fullname($USER, true).'</meta:initial-creator>
+        <meta:initial-creator>' . htmlspecialchars(fullname($USER, true), ENT_QUOTES, 'utf-8') . '</meta:initial-creator>
         <meta:creation-date>'.strftime('%Y-%m-%dT%H:%M:%S').'</meta:creation-date>
         <meta:document-statistic meta:table-count="1" meta:cell-count="0" meta:object-count="0"/>
     </office:meta>
index 64e0299..11e42fa 100644 (file)
@@ -313,6 +313,13 @@ class theme_config {
      */
     public $uarrow = null;
 
+    /**
+     * @var string Accessibility: Down arrow-like character.
+     * If the theme does not set characters, appropriate defaults
+     * are set automatically.
+     */
+    public $darrow = null;
+
     /**
      * @var bool Some themes may want to disable ajax course editing.
      */
@@ -491,7 +498,7 @@ class theme_config {
             'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets',
             'javascripts', 'javascripts_footer', 'parents_exclude_javascripts',
             'layouts', 'enable_dock', 'enablecourseajax', 'supportscssoptimisation',
-            'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow',
+            'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
             'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
             'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod');
 
@@ -570,7 +577,7 @@ class theme_config {
     }
 
     /**
-     * Checks if arrows $THEME->rarrow, $THEME->larrow, $THEME->uarrow have been set (theme/-/config.php).
+     * Checks if arrows $THEME->rarrow, $THEME->larrow, $THEME->uarrow, $THEME->darrow have been set (theme/-/config.php).
      * If not it applies sensible defaults.
      *
      * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,
@@ -584,6 +591,7 @@ class theme_config {
             $this->rarrow = '&#x25BA;';
             $this->larrow = '&#x25C4;';
             $this->uarrow = '&#x25B2;';
+            $this->darrow = '&#x25BC;';
             if (empty($_SERVER['HTTP_USER_AGENT'])) {
                 $uagent = '';
             } else {
@@ -603,6 +611,7 @@ class theme_config {
                 $this->rarrow = '&rarr;';
                 $this->larrow = '&larr;';
                 $this->uarrow = '&uarr;';
+                $this->darrow = '&darr;';
             }
             elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])
                 && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {
@@ -611,6 +620,7 @@ class theme_config {
                 $this->rarrow = '&gt;';
                 $this->larrow = '&lt;';
                 $this->uarrow = '^';
+                $this->darrow = 'v';
             }
 
             // RTL support - in RTL languages, swap r and l arrows
index ee04867..4f89e27 100644 (file)
@@ -3616,6 +3616,17 @@ EOD;
         return $this->page->theme->uarrow;
     }
 
+    /**
+     * Accessibility: Down arrow-like character.
+     * If the theme does not set characters, appropriate defaults
+     * are set automatically.
+     *
+     * @return string
+     */
+    public function darrow() {
+        return $this->page->theme->darrow;
+    }
+
     /**
      * Returns the custom menu if one has been set
      *
index b04977c..3486735 100644 (file)
@@ -40,20 +40,22 @@ require([
         uniqid = "{{uniqid}}",
         chartArea = $('#chart-area-' + uniqid),
         chartImage = chartArea.find('.chart-image'),
-        chartTable = chartArea.find('.chart-table-data');
-
+        chartTable = chartArea.find('.chart-table-data'),
+        chartLink = chartArea.find('.chart-table-expand a');
     Builder.make(data).then(function(ChartInst) {
         new Output(chartImage, ChartInst);
         new OutputTable(chartTable, ChartInst);
     });
 
-    chartArea.find('.chart-table-expand a').first().click(function(e) {
+    chartLink.on('click', function(e) {
         e.preventDefault();
         if (chartTable.is(':visible')) {
             chartTable.hide();
+            chartLink.text({{#quote}}{{#str}}showchartdata, moodle{{/str}}{{/quote}});
             chartTable.attr('aria-expanded', false);
         } else {
             chartTable.show();
+            chartLink.text({{#quote}}{{#str}}hidechartdata, moodle{{/str}}{{/quote}});
             chartTable.attr('aria-expanded', true);
         }
     });
index f8c6cb6..1455857 100644 (file)
@@ -1559,4 +1559,27 @@ class behat_general extends behat_base {
         $node = $this->get_selected_node($selectortype, $element);
         $this->getSession()->getDriver()->post_key("\xEE\x80\x84", $node->getXpath());
     }
+
+    /**
+     * Checks if database family used is using one of the specified, else skip. (mysql, postgres, mssql, oracle, etc.)
+     *
+     * @Given /^database family used is one of the following:$/
+     * @param TableNode $databasefamilies list of database.
+     * @return void.
+     * @throws \Moodle\BehatExtension\Exception\SkippedException
+     */
+    public function database_family_used_is_one_of_the_following(TableNode $databasefamilies) {
+        global $DB;
+
+        $dbfamily = $DB->get_dbfamily();
+
+        // Check if used db family is one of the specified ones. If yes then return.
+        foreach ($databasefamilies->getRows() as $dbfamilytocheck) {
+            if ($dbfamilytocheck[0] == $dbfamily) {
+                return;
+            }
+        }
+
+        throw new \Moodle\BehatExtension\Exception\SkippedException();
+    }
 }
index ae22dae..31e3e53 100644 (file)
@@ -318,4 +318,117 @@ class core_htmlpurifier_testcase extends basic_testcase {
         $text = '<a href="hmmm://www.example.com">link</a>';
         $this->assertSame('<a>link</a>', purify_html($text));
     }
+
+    /**
+     * Tests media tags.
+     *
+     * @dataProvider media_tags_provider
+     * @param string $mediatag HTML media tag
+     * @param string $expected expected result
+     */
+    public function test_media_tags($mediatag, $expected) {
+        $actual = format_text($mediatag, FORMAT_MOODLE, ['filter' => false, 'noclean' => true]);
+        $this->assertEquals($expected, $actual);
+    }
+
+    /**
+     * Test cases for the test_media_tags test.
+     */
+    public function media_tags_provider() {
+        // Takes an array of attributes, then generates a test for each of them.
+        $generatetestcases = function($prefix, array $attrs, array $templates) {
+            return array_reduce($attrs, function($carry, $attr) use ($prefix, $templates) {
+                $testcase = [$prefix . '/' . $attr => [
+                    sprintf($templates[0], $attr),
+                    sprintf($templates[1], $attr)
+                ]];
+                return empty(array_values($carry)[0]) ? $testcase : $carry + $testcase;
+            }, [[]]);
+        };
+
+        $audioattrs = [
+            'preload="auto"', 'autoplay=""', 'loop=""', 'muted=""', 'controls=""',
+            'crossorigin="anonymous"', 'crossorigin="use-credentials"'
+        ];
+        $videoattrs = [
+            'crossorigin="anonymous"', 'crossorigin="use-credentials"',
+            'poster="https://upload.wikimedia.org/wikipedia/en/1/14/Space_jam.jpg"',
+            'preload=""', 'autoplay=""', 'playsinline=""', 'loop=""', 'muted=""',
+            'controls=""', 'width="420px"', 'height="69px"'
+        ];
+        return $generatetestcases('Plain audio', $audioattrs + ['src="http://example.com/jam.wav"'], [
+                '<audio %1$s>Looks like you can\'t slam the jams.</audio>',
+                '<div class="text_to_html"><audio %1$s>Looks like you can\'t slam the jams.</audio></div>'
+            ]) + $generatetestcases('Audio with one source', $audioattrs, [
+                '<audio %1$s><source src="http://example.com/getup.wav">No tasty jams for you.</audio>',
+                '<div class="text_to_html">' .
+                    '<audio %1$s>' .
+                        '<source src="http://example.com/getup.wav">' .
+                        'No tasty jams for you.' .
+                    '</audio>' .
+                '</div>'
+            ]) + $generatetestcases('Audio with multiple sources', $audioattrs, [
+                '<audio %1$s>' .
+                    '<source src="http://example.com/getup.wav" type="audio/wav">' .
+                    '<source src="http://example.com/getup.mp3" type="audio/mpeg">' .
+                    '<source src="http://example.com/getup.ogg" type="audio/ogg">' .
+                    'No tasty jams for you.' .
+                '</audio>',
+                '<div class="text_to_html">' .
+                     '<audio %1$s>' .
+                        '<source src="http://example.com/getup.wav" type="audio/wav">' .
+                        '<source src="http://example.com/getup.mp3" type="audio/mpeg">' .
+                        '<source src="http://example.com/getup.ogg" type="audio/ogg">' .
+                        'No tasty jams for you.' .
+                    '</audio>' .
+                '</div>'
+            ]) + $generatetestcases('Plain video', $videoattrs + ['src="http://example.com/prettygood.mp4'], [
+                '<video %1$s>Oh, that\'s pretty bad 😦</video>',
+                '<div class="text_to_html"><video %1$s>Oh, that\'s pretty bad 😦</video></div>'
+            ]) + $generatetestcases('Video with one source', $videoattrs, [
+                '<video %1$s><source src="http://example.com/prettygood.mp4">Oh, that\'s pretty bad 😦</video>',
+                '<div class="text_to_html">' .
+                    '<video %1$s>' .
+                        '<source src="http://example.com/prettygood.mp4">' .
+                        'Oh, that\'s pretty bad 😦' .
+                    '</video>' .
+                '</div>'
+            ]) + $generatetestcases('Video with multiple sources', $videoattrs, [
+                '<video %1$s>' .
+                    '<source src="http://example.com/prettygood.mp4" type="video/mp4">' .
+                    '<source src="http://example.com/eljefe.mp4" type="video/mp4">' .
+                    '<source src="http://example.com/turnitup.mov type="video/mov"' .
+                    'Oh, that\'s pretty bad 😦' .
+                '</video>',
+                '<div class="text_to_html">' .
+                    '<video %1$s>' .
+                        '<source src="http://example.com/prettygood.mp4" type="video/mp4">' .
+                        '<source src="http://example.com/eljefe.mp4" type="video/mp4">' .
+                        '<source src="http://example.com/turnitup.mov type="video/mov"' .
+                        'Oh, that\'s pretty bad 😦' .
+                    '</video>' .
+                '</div>'
+            ] + [
+                'Video with invalid crossorigin' => [
+                    '<video src="http://example.com/turnitup.mov type="video/mov crossorigin="can i pls hab?">' .
+                        'Oh, that\'s pretty bad 😦' .
+                    '</video>',
+                    '<div class="text_to_html">' .
+                       '<video src="http://example.com/turnitup.mov type="video/mov">' .
+                           'Oh, that\'s pretty bad 😦' .
+                        '</video>',
+                    '</div>'
+                ],
+                'Audio with invalid crossorigin' => [
+                    '<audio src="http://example.com/getup.wav" type="audio/wav" crossorigin="give me. the jams.">' .
+                        'nyemnyemnyem' .
+                    '</audio>',
+                    '<div class="text_to_html">' .
+                        '<audio src="http://example.com/getup.wav" type="audio/wav" crossorigin="give me. the jams.">' .
+                            'nyemnyemnyem' .
+                        '</audio>' .
+                    '</div>'
+                ]
+            ]);
+    }
 }
index 7d58d3f..926ad3d 100644 (file)
@@ -25,6 +25,8 @@ information provided here is intended especially for developers.
 * The following functions have been deprecated and are not used any more:
   - get_records_csv() Please use csv_import_reader::load_csv_content() instead.
   - put_records_csv() Please use download_as_dataformat (lib/dataformatlib.php) instead.
+  - zip_files()   - See MDL-24343 for more information.
+  - unzip_file()  - See MDL-24343 for more information.
 * The password_compat library was removed as it is no longer required.
 * Phpunit has been upgraded to 5.4.x and following has been deprecated and is not used any more:
   - setExpectedException(), use @expectedException or $this->expectException() and $this->expectExceptionMessage()
@@ -46,6 +48,9 @@ information provided here is intended especially for developers.
   Calling them through the magic method __call() will throw a coding exception.
 * The alfresco library has been removed from core. It was an old version of
   the library which was not compatible with newer versions of Alfresco.
+* Added down arrow: $OUTPUT->darrow.
+* All file_packer implementations now accept an additional parameter to allow a simple boolean return value instead of
+  an array of individual file statuses.
 
 === 3.1 ===
 
index 9bc283d..530a1f0 100644 (file)
@@ -1780,7 +1780,7 @@ function purify_html($text, $options = array()) {
         $config = HTMLPurifier_Config::createDefault();
 
         $config->set('HTML.DefinitionID', 'moodlehtml');
-        $config->set('HTML.DefinitionRev', 4);
+        $config->set('HTML.DefinitionRev', 5);
         $config->set('Cache.SerializerPath', $cachedir);
         $config->set('Cache.SerializerPermissions', $CFG->directorypermissions);
         $config->set('Core.NormalizeNewlines', false);
@@ -1820,6 +1820,37 @@ function purify_html($text, $options = array()) {
             $def->addElement('lang', 'Block', 'Flow', array(), array('lang'=>'CDATA')); // Original multilang style - only our hacked lang attribute.
             $def->addAttribute('span', 'xxxlang', 'CDATA');                             // Current very problematic multilang.
 
+            // Media elements.
+            // https://html.spec.whatwg.org/#the-video-element
+            $def->addElement('video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
+                'src' => 'URI',
+                'crossorigin' => 'Enum#anonymous,use-credentials',
+                'poster' => 'URI',
+                'preload' => 'Enum#auto,metadata,none',
+                'autoplay' => 'Bool',
+                'playsinline' => 'Bool',
+                'loop' => 'Bool',
+                'muted' => 'Bool',
+                'controls' => 'Bool',
+                'width' => 'Length',
+                'height' => 'Length',
+            ]);
+            // https://html.spec.whatwg.org/#the-audio-element
+            $def->addElement('audio', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
+                'src' => 'URI',
+                'crossorigin' => 'Enum#anonymous,use-credentials',
+                'preload' => 'Enum#auto,metadata,none',
+                'autoplay' => 'Bool',
+                'loop' => 'Bool',
+                'muted' => 'Bool',
+                'controls' => 'Bool'
+            ]);
+            // https://html.spec.whatwg.org/#the-source-element
+            $def->addElement('source', 'Block', 'Flow', 'Common', [
+                'src' => 'URI',
+                'type' => 'Text'
+            ]);
+
             // Use the built-in Ruby module to add annotation support.
             $def->manager->addModule(new HTMLPurifier_HTMLModule_Ruby());
 
index 225ecc4..846f467 100644 (file)
@@ -455,22 +455,8 @@ class mod_assign_external extends external_api {
                         $assignment['introfiles'] = external_util::get_area_files($context->id, 'mod_assign', 'intro', false,
                                                                                     false);
 
-                        $fs = get_file_storage();
-                        if ($files = $fs->get_area_files($context->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA,
-                                                            0, 'timemodified', false)) {
-
-                            $assignment['introattachments'] = array();
-                            foreach ($files as $file) {
-                                $filename = $file->get_filename();
-
-                                $assignment['introattachments'][] = array(
-                                    'filename' => $filename,
-                                    'mimetype' => $file->get_mimetype(),
-                                    'fileurl'  => moodle_url::make_webservice_pluginfile_url(
-                                        $context->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0, '/', $filename)->out(false)
-                                );
-                            }
-                        }
+                        $assignment['introattachments'] = external_util::get_area_files($context->id, 'mod_assign',
+                                                            ASSIGN_INTROATTACHMENT_FILEAREA, 0);
                     }
 
                     if ($module->requiresubmissionstatement) {
@@ -541,15 +527,7 @@ class mod_assign_external extends external_api {
                     'assignment intro, not allways returned because it deppends on the activity configuration', VALUE_OPTIONAL),
                 'introformat' => new external_format_value('intro', VALUE_OPTIONAL),
                 'introfiles' => new external_files('Files in the introduction text', VALUE_OPTIONAL),
-                'introattachments' => new external_multiple_structure(
-                    new external_single_structure(
-                        array (
-                            'filename' => new external_value(PARAM_FILE, 'file name'),
-                            'mimetype' => new external_value(PARAM_RAW, 'mime type'),
-                            'fileurl'  => new external_value(PARAM_URL, 'file download url')
-                        )
-                    ), 'intro attachments files', VALUE_OPTIONAL
-                )
+                'introattachments' => new external_files('intro attachments files', VALUE_OPTIONAL),
             ), 'assignment information object');
     }
 
@@ -637,24 +615,14 @@ class mod_assign_external extends external_api {
             $fileareas = $assignplugin->get_file_areas();
             foreach ($fileareas as $filearea => $name) {
                 $fileareainfo = array('area' => $filearea);
-                $files = $fs->get_area_files(
+
+                $fileareainfo['files'] = external_util::get_area_files(
                     $assign->get_context()->id,
                     $component,
                     $filearea,
-                    $item->id,
-                    "timemodified",
-                    false
+                    $item->id
                 );
-                foreach ($files as $file) {
-                    $filepath = $file->get_filepath().$file->get_filename();
-                    $fileurl = file_encode_url($CFG->wwwroot . '/webservice/pluginfile.php', '/' . $assign->get_context()->id .
-                        '/' . $component. '/'. $filearea . '/' . $item->id . $filepath);
-                    $fileinfo = array(
-                        'filepath' => $filepath,
-                        'fileurl' => $fileurl
-                        );
-                    $fileareainfo['files'][] = $fileinfo;
-                }
+
                 $plugin['fileareas'][] = $fileareainfo;
             }
 
@@ -822,15 +790,7 @@ class mod_assign_external extends external_api {
                     new external_single_structure(
                         array (
                             'area' => new external_value (PARAM_TEXT, 'file area'),
-                            'files' => new external_multiple_structure(
-                                new external_single_structure(
-                                    array (
-                                        'filepath' => new external_value (PARAM_TEXT, 'file path'),
-                                        'fileurl' => new external_value (PARAM_URL, 'file download url',
-                                            VALUE_OPTIONAL)
-                                    )
-                                ), 'files', VALUE_OPTIONAL
-                            )
+                            'files' => new external_files('files', VALUE_OPTIONAL),
                         )
                     ), 'fileareas', VALUE_OPTIONAL
                 ),
index 4ada0a7..1b57ae1 100644 (file)
@@ -119,7 +119,7 @@ EOD;
      */
     protected static function strip_images($html) {
         $dom = new DOMDocument();
-        $dom->loadHTML($html);
+        $dom->loadHTML("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" . $html);
         $images = $dom->getElementsByTagName('img');
         $i = 0;
 
@@ -135,7 +135,8 @@ EOD;
             $text = $dom->createTextNode($replacement);
             $node->parentNode->replaceChild($text, $node);
         }
-        return $dom->saveHTML();
+        $count = 1;
+        return str_replace("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>", "", $dom->saveHTML(), $count);
     }
 
     /**
index 9fca563..1c32501 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index c4965e1..c0373e1 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 9fca563..1c32501 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 0a9985d..e94f5a2 100644 (file)
@@ -267,6 +267,7 @@ var COMMENT = function(editor, gradeid, pageno, x, y, width, colour, rawtext) {
 
         node.on('gesturemovestart', function(e) {
             if (editor.currentedit.tool === 'select') {
+                e.preventDefault();
                 node.setData('dragging', true);
                 node.setData('offsetx', e.clientX - node.getX());
                 node.setData('offsety', e.clientY - node.getY());
index 2f51ed3..097a09f 100644 (file)
@@ -104,7 +104,6 @@ $string['blindmarkingenabledwarning'] = 'Blind marking is enabled for this activ
 $string['blindmarking_help'] = 'Blind marking hides the identity of students from markers. Blind marking settings will be locked once a submission or grade has been made in relation to this assignment.';
 $string['changeuser'] = 'Change user';
 $string['changefilters'] = 'Change filters';
-$string['changegradewarning'] = 'This assignment has graded submissions and changing the grade will not automatically re-calculate existing submission grades. You must re-grade all existing submissions, if you wish to change the grade.';
 $string['choosegradingaction'] = 'Grading action';
 $string['choosemarker'] = 'Choose...';
 $string['chooseoperation'] = 'Choose operation';
@@ -503,3 +502,6 @@ $string['viewsubmissiongradingtable'] = 'View submission grading table.';
 $string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.';
 $string['workflowfilter'] = 'Workflow filter';
 $string['xofy'] = '{$a->x} of {$a->y}';
+
+// Deprecated since Moodle 3.2.
+$string['changegradewarning'] = 'This assignment has graded submissions and changing the grade will not automatically re-calculate existing submission grades. You must re-grade all existing submissions, if you wish to change the grade.';
diff --git a/mod/assign/lang/en/deprecated.txt b/mod/assign/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..76d8f54
--- /dev/null
@@ -0,0 +1 @@
+changegradewarning,mod_assign
\ No newline at end of file
index 1e0655e..3504927 100644 (file)
@@ -150,18 +150,6 @@ M.mod_assign.init_grading_options = function(Y) {
     });
 };
 
-M.mod_assign.init_grade_change = function(Y) {
-    var gradenode = Y.one('#id_grade');
-    if (gradenode) {
-        var originalvalue = gradenode.get('value');
-        gradenode.on('change', function() {
-            if (gradenode.get('value') != originalvalue) {
-                alert(M.util.get_string('changegradewarning', 'mod_assign'));
-            }
-        });
-    }
-};
-
 M.mod_assign.init_plugin_summary = function(Y, subtype, type, submissionid) {
     suffix = subtype + '_' + type + '_' + submissionid;
     classname = 'contract_' + suffix;
index 99f4416..42d34a2 100644 (file)
@@ -44,12 +44,11 @@ class mod_assign_submission_form extends moodleform {
         global $USER;
         $mform = $this->_form;
         list($assign, $data) = $this->_customdata;
-
         $instance = $assign->get_instance();
         if ($instance->teamsubmission) {
-            $submission = $assign->get_group_submission($USER->id, 0, true);
+            $submission = $assign->get_group_submission($data->userid, 0, true);
         } else {
-            $submission = $assign->get_user_submission($USER->id, true);
+            $submission = $assign->get_user_submission($data->userid, true);
         }
         if ($submission) {
             $mform->addElement('hidden', 'lastmodified', $submission->timemodified);
diff --git a/mod/assign/tests/behat/edit_student_submission.feature b/mod/assign/tests/behat/edit_student_submission.feature
new file mode 100644 (file)
index 0000000..f8527bd
--- /dev/null
@@ -0,0 +1,53 @@
+@mod @mod_assign @javascript
+Feature: In an assignment, the administrator can edit students' submissions
+  In order to edit a student's submissions
+  As an administrator
+  I need to grade multiple students on one page
+
+  Scenario: Editing a student's submission
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+    When I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment name |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | groupmode | No groups |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I press "Add submission"
+    And I set the following fields to these values:
+      | Online text | I'm the student1 submission |
+    And I press "Save changes"
+    And I log out
+    And I log in as "admin"
+    And I am on site homepage
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I follow "View all submissions"
+    And I click on "Edit" "link" in the "Student 1" "table_row"
+    And I follow "Edit submission"
+    And I set the following fields to these values:
+      | Online text | Have you seen the movie Chef? |
+    And I press "Save changes"
+    And I follow "View all submissions"
+    Then I should see "Have you seen the movie Chef?"
+    And I click on "Edit" "link" in the "Student 1" "table_row"
+    And I follow "Edit submission"
+    And I set the following fields to these values:
+      | Online text | I have seen the movie chef. |
+    And I press "Save changes"
+    And I follow "View all submissions"
+    Then I should see "I have seen the movie chef."
index 5283603..d24714a 100644 (file)
@@ -1945,7 +1945,8 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
             $submissionplugins[$plugin['type']] = $plugin;
         }
         $this->assertEquals('Submission text', $submissionplugins['onlinetext']['editorfields'][0]['text']);
-        $this->assertEquals('/t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']);
+        $this->assertEquals('/', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']);
+        $this->assertEquals('t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filename']);
     }
 
     /**
@@ -2087,7 +2088,8 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
             $submissionplugins[$plugin['type']] = $plugin;
         }
         $this->assertEquals('Submission text', $submissionplugins['onlinetext']['editorfields'][0]['text']);
-        $this->assertEquals('/t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']);
+        $this->assertEquals('/', $submissionplugins['file']['fileareas'][0]['files'][0]['filepath']);
+        $this->assertEquals('t.txt', $submissionplugins['file']['fileareas'][0]['files'][0]['filename']);
     }
 
     /**
index 02f988c..9e224d1 100644 (file)
@@ -7,6 +7,10 @@ This files describes API changes in the assign code.
 * Proper checking for empty submissions
 * Submission modification time checking - this will help students working in groups not clobber each others'
   submissions
+* External functions that were returning file information now return the following file fields:
+  filename, filepath, mimetype, filesize, timemodified and fileurl.
+  Those fields are now marked as VALUE_OPTIONAL for backwards compatibility.
+  Please, note that previously the filename was part of the filepath field, now they are separated.
 
 === 3.1 ===
 * The feedback plugins now need to implement the is_feedback_modified() method. The default is to return true
index 6732b94..48053d3 100644 (file)
@@ -204,20 +204,19 @@ class mod_choice_external extends external_api {
         $choiceopen = true;
         $showpreview = false;
 
-        if ($choice->timeclose != 0) {
-            if ($choice->timeopen > $timenow) {
-                $choiceopen = false;
-                $warnings[1] = get_string("notopenyet", "choice", userdate($choice->timeopen));
-                if ($choice->showpreview) {
-                    $warnings[2] = get_string('previewonly', 'choice', userdate($choice->timeopen));
-                    $showpreview = true;
-                }
-            }
-            if ($timenow > $choice->timeclose) {
-                $choiceopen = false;
-                $warnings[3] = get_string("expired", "choice", userdate($choice->timeclose));
+        if (!empty($choice->timeopen) && ($choice->timeopen > $timenow)) {
+            $choiceopen = false;
+            $warnings[1] = get_string("notopenyet", "choice", userdate($choice->timeopen));
+            if ($choice->showpreview) {
+                $warnings[2] = get_string('previewonly', 'choice', userdate($choice->timeopen));
+                $showpreview = true;
             }
         }
+        if (!empty($choice->timeclose) && ($timenow > $choice->timeclose)) {
+            $choiceopen = false;
+            $warnings[3] = get_string("expired", "choice", userdate($choice->timeclose));
+        }
+
         $optionsarray = array();
 
         if ($choiceopen or $showpreview) {
@@ -333,13 +332,12 @@ class mod_choice_external extends external_api {
         require_capability('mod/choice:choose', $context);
 
         $timenow = time();
-        if ($choice->timeclose != 0) {
-            if ($choice->timeopen > $timenow) {
-                throw new moodle_exception("notopenyet", "choice", '', userdate($choice->timeopen));
-            } else if ($timenow > $choice->timeclose) {
-                throw new moodle_exception("expired", "choice", '', userdate($choice->timeclose));
-            }
+        if (!empty($choice->timeopen) && ($choice->timeopen > $timenow)) {
+            throw new moodle_exception("notopenyet", "choice", '', userdate($choice->timeopen));
+        } else if (!empty($choice->timeclose) && ($timenow > $choice->timeclose)) {
+            throw new moodle_exception("expired", "choice", '', userdate($choice->timeclose));
         }
+
         if (!choice_get_my_response($choice) or $choice->allowupdate) {
             // When a single response is given, we convert the array to a simple variable
             // in order to avoid choice_user_submit_response to check with allowmultiple even
@@ -645,10 +643,8 @@ class mod_choice_external extends external_api {
         } else if ($choice->allowupdate) {
             // Check if we can delate our own responses.
             $timenow = time();
-            if ($choice->timeclose != 0) {
-                if ($timenow > $choice->timeclose) {
-                    throw new moodle_exception("expired", "choice", '', userdate($choice->timeclose));
-                }
+            if (!empty($choice->timeclose) && ($timenow > $choice->timeclose)) {
+                throw new moodle_exception("expired", "choice", '', userdate($choice->timeclose));
             }
             // Delete only our responses.
             $myresponses = array_keys(choice_get_my_response($choice));
index 8e259b0..dcd01e5 100644 (file)
@@ -47,14 +47,14 @@ $string['havetologin'] = 'You have to log in before you can submit your choice';
 $string['choice'] = 'Choice';
 $string['choiceactivityname'] = 'Choice: {$a}';
 $string['choice:addinstance'] = 'Add a new choice';
-$string['choiceclose'] = 'Until';
+$string['choiceclose'] = 'Allow responses until';
 $string['choice:deleteresponses'] = 'Delete responses';
 $string['choice:downloadresponses'] = 'Download responses';
 $string['choicefull'] = 'This choice is full and there are no available places.';
 $string['choice:choose'] = 'Record a choice';
 $string['choicecloseson'] = 'Choice closes on {$a}';
 $string['choicename'] = 'Choice name';
-$string['choiceopen'] = 'Open';
+$string['choiceopen'] = 'Allow responses from';
 $string['choiceoptions'] = 'Choice options';
 $string['choiceoptions_help'] = 'Here is where you specify the options that participants have to choose from.
 
@@ -123,7 +123,6 @@ $string['showunanswered'] = 'Show column for unanswered';
 $string['spaceleft'] = 'space available';
 $string['spacesleft'] = 'spaces available';
 $string['taken'] = 'Taken';
-$string['timerestrict'] = 'Restrict answering to this time period';
 $string['viewallresponses'] = 'View {$a} responses';
 $string['withselected'] = 'With selected';
 $string['userchoosethisoption'] = 'Users who chose this option';
index 65cd252..d7a00b0 100644 (file)
@@ -119,11 +119,6 @@ function choice_add_instance($choice) {
 
     $choice->timemodified = time();
 
-    if (empty($choice->timerestrict)) {
-        $choice->timeopen = 0;
-        $choice->timeclose = 0;
-    }
-
     //insert answers
     $choice->id = $DB->insert_record("choice", $choice);
     foreach ($choice->option as $key => $value) {
@@ -162,12 +157,6 @@ function choice_update_instance($choice) {
     $choice->id = $choice->instance;
     $choice->timemodified = time();
 
-
-    if (empty($choice->timerestrict)) {
-        $choice->timeopen = 0;
-        $choice->timeclose = 0;
-    }
-
     //update, delete or insert answers
     foreach ($choice->option as $key => $value) {
         $value = trim($value);
@@ -1067,16 +1056,14 @@ function choice_get_availability_status($choice) {
     $available = true;
     $warnings = array();
 
-    if ($choice->timeclose != 0) {
-        $timenow = time();
+    $timenow = time();
 
-        if ($choice->timeopen > $timenow) {
-            $available = false;
-            $warnings['notopenyet'] = userdate($choice->timeopen);
-        } else if ($timenow > $choice->timeclose) {
-            $available = false;
-            $warnings['expired'] = userdate($choice->timeclose);
-        }
+    if (!empty($choice->timeopen) && ($choice->timeopen > $timenow)) {
+        $available = false;
+        $warnings['notopenyet'] = userdate($choice->timeopen);
+    } else if (!empty($choice->timeclose) && ($timenow > $choice->timeclose)) {
+        $available = false;
+        $warnings['expired'] = userdate($choice->timeclose);
     }
     if (!$choice->allowupdate && choice_get_my_response($choice)) {
         $available = false;
index b3291ba..4dd6418 100644 (file)
@@ -45,7 +45,7 @@ function choice_set_events($choice) {
     $event = new stdClass();
     if ($event->id = $DB->get_field('event', 'id',
             array('modulename' => 'choice', 'instance' => $choice->id, 'eventtype' => 'open'))) {
-        if ($choice->timeopen > 0) {
+        if ((!empty($choice->timeopen)) && ($choice->timeopen > 0)) {
             // Calendar event exists so update it.
             $event->name         = get_string('calendarstart', 'choice', $choice->name);
             $event->description  = format_module_intro('choice', $choice, $choice->coursemodule);
@@ -61,7 +61,7 @@ function choice_set_events($choice) {
         }
     } else {
         // Event doesn't exist so create one.
-        if ($choice->timeopen > 0) {
+        if ((!empty($choice->timeopen)) && ($choice->timeopen > 0)) {
             $event->name         = get_string('calendarstart', 'choice', $choice->name);
             $event->description  = format_module_intro('choice', $choice, $choice->coursemodule);
             $event->courseid     = $choice->course;
@@ -81,7 +81,7 @@ function choice_set_events($choice) {
     $event = new stdClass();
     if ($event->id = $DB->get_field('event', 'id',
             array('modulename' => 'choice', 'instance' => $choice->id, 'eventtype' => 'close'))) {
-        if ($choice->timeclose > 0) {
+        if ((!empty($choice->timeclose)) && ($choice->timeclose > 0)) {
             // Calendar event exists so update it.
             $event->name         = get_string('calendarend', 'choice', $choice->name);
             $event->description  = format_module_intro('choice', $choice, $choice->coursemodule);
@@ -97,7 +97,7 @@ function choice_set_events($choice) {
         }
     } else {
         // Event doesn't exist so create one.
-        if ($choice->timeclose > 0) {
+        if ((!empty($choice->timeclose)) && ($choice->timeclose > 0)) {
             $event = new stdClass();
             $event->name         = get_string('calendarend', 'choice', $choice->name);
             $event->description  = format_module_intro('choice', $choice, $choice->coursemodule);
index e15ad2d..8256390 100644 (file)
@@ -70,18 +70,16 @@ class mod_choice_mod_form extends moodleform_mod {
         }
 
 //-------------------------------------------------------------------------------
-        $mform->addElement('header', 'timerestricthdr', get_string('availability'));
-        $mform->addElement('checkbox', 'timerestrict', get_string('timerestrict', 'choice'));
+        $mform->addElement('header', 'availabilityhdr', get_string('availability'));
+        $mform->addElement('date_time_selector', 'timeopen', get_string("choiceopen", "choice"),
+            array('optional' => true));
 
-        $mform->addElement('date_time_selector', 'timeopen', get_string("choiceopen", "choice"));
-        $mform->disabledIf('timeopen', 'timerestrict');
-
-        $mform->addElement('date_time_selector', 'timeclose', get_string("choiceclose", "choice"));
-        $mform->disabledIf('timeclose', 'timerestrict');
+        $mform->addElement('date_time_selector', 'timeclose', get_string("choiceclose", "choice"),
+            array('optional' => true));
 
         $mform->addElement('advcheckbox', 'showpreview', get_string('showpreview', 'choice'));
         $mform->addHelpButton('showpreview', 'showpreview', 'choice');
-        $mform->disabledIf('showpreview', 'timerestrict');
+        $mform->disabledIf('showpreview', 'timeopen[enabled]');
 
 //-------------------------------------------------------------------------------
         $mform->addElement('header', 'resultshdr', get_string('results', 'choice'));
@@ -117,11 +115,6 @@ class mod_choice_mod_form extends moodleform_mod {
             }
 
         }
-        if (empty($default_values['timeopen'])) {
-            $default_values['timerestrict'] = 0;
-        } else {
-            $default_values['timerestrict'] = 1;
-        }
 
     }
 
index 7bd4a9c..f40f331 100644 (file)
@@ -327,7 +327,7 @@ class mod_choice_renderer extends plugin_renderer_base {
             }
             $data['labels'][$count] = $option->text;
             $data['series'][$count] = $numberofuser;
-            $data['series_labels'][$count] = '(' . format_float($percentageamount, 1) . '%)';
+            $data['series_labels'][$count] = $numberofuser . ' (' . format_float($percentageamount, 1) . '%)';
             $count++;
             $numberofuser = 0;
         }
index 9d2c1d2..32023fb 100644 (file)
@@ -26,7 +26,8 @@ Feature: Allow choice preview
       | Description | Choice Description |
       | option[0] | Option 1 |
       | option[1] | Option 2 |
-      | Restrict answering to this time period | 1 |
+      | timeopen[enabled] | 1 |
+      | timeclose[enabled] | 1 |
       | timeopen[day] | 30 |
       | timeopen[month] | December |
       | timeopen[year] | 2037 |
diff --git a/mod/choice/tests/behat/choice_availability.feature b/mod/choice/tests/behat/choice_availability.feature
new file mode 100644 (file)
index 0000000..1894ac0
--- /dev/null
@@ -0,0 +1,89 @@
+@mod @mod_choice
+Feature: Restrict availability of the choice module to a deadline
+  In order to limit the time a student can mace a selection
+  As a teacher
+  I need to restrict answering to within a time period
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+
+  Scenario: Enable the choice activity with a start deadline in the future
+    Given I add a "Choice" to section "1" and I fill the form with:
+      | Choice name | Choice name |
+      | Description | Choice Description |
+      | option[0] | Option 1 |
+      | option[1] | Option 2 |
+      | timeopen[enabled] | 1 |
+      | timeopen[day] | 30 |
+      | timeopen[month] | December |
+      | timeopen[year] | 2037 |
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Choice name"
+    Then I should see "Sorry, this activity is not available until"
+
+  Scenario: Enable the choice activity with a start deadline in the past
+    Given I add a "Choice" to section "1" and I fill the form with:
+      | Choice name | Choice name |
+      | Description | Choice Description |
+      | option[0] | Option 1 |
+      | option[1] | Option 2 |
+      | timeopen[enabled] | 1 |
+      | timeopen[day] | 30 |
+      | timeopen[month] | December |
+      | timeopen[year] | 2007 |
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Choice name"
+    And "choice_1" "radio" should exist
+    And "choice_2" "radio" should exist
+    And "Save my choice" "button" should exist
+
+  Scenario: Enable the choice activity with a end deadline in the future
+    Given I add a "Choice" to section "1" and I fill the form with:
+      | Choice name | Choice name |
+      | Description | Choice Description |
+      | option[0] | Option 1 |
+      | option[1] | Option 2 |
+      | timeclose[enabled] | 1 |
+      | timeclose[day] | 30 |
+      | timeclose[month] | December |
+      | timeclose[year] | 2037 |
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Choice name"
+    And "choice_1" "radio" should exist
+    And "choice_2" "radio" should exist
+    And "Save my choice" "button" should exist
+
+  Scenario: Enable the choice activity with a end deadline in the past
+    Given I add a "Choice" to section "1" and I fill the form with:
+      | Choice name | Choice name |
+      | Description | Choice Description |
+      | option[0] | Option 1 |
+      | option[1] | Option 2 |
+      | timeclose[enabled] | 1 |
+      | timeclose[day] | 30 |
+      | timeclose[month] | December |
+      | timeclose[year] | 2007 |
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Choice name"
+    Then I should see "Sorry, this activity closed on"
index 17f571b..2ff3cc9 100644 (file)
@@ -24,7 +24,8 @@ Feature: Test the display of the choice module on my home
     And I set the following fields to these values:
       | Choice name | Test choice name |
       | Description | Test choice description |
-      | id_timerestrict| 1 |
+      | timeopen[enabled] | 1 |
+      | timeclose[enabled] | 1 |
       | timeclose[day] | 1 |
       | timeclose[month] | January |
       | timeclose[year] | 2030 |
index d0f9bc2..ac80abc 100644 (file)
@@ -72,10 +72,11 @@ Feature: A teacher can choose one of 4 options for publishing choice results
     And I follow "Edit settings"
     And I expand all fieldsets
     And I set the following fields to these values:
-      | Restrict answering to this time period | 1 |
+      | timeopen[enabled] | 1 |
       | timeopen[day] | 1 |
       | timeopen[month] | January |
       | timeopen[year] | 2010 |
+      | timeclose[enabled] | 1 |
       | timeclose[day] | 2 |
       | timeclose[month] | January |
       | timeclose[year] | 2010 |
index 7ced17e..59cc9df 100644 (file)
@@ -141,19 +141,17 @@ if (isloggedin() && (!empty($current)) &&
 
 /// Print the form
 $choiceopen = true;
-if ($choice->timeclose !=0) {
-    if ($choice->timeopen > $timenow ) {
-        if ($choice->showpreview) {
-            echo $OUTPUT->box(get_string('previewonly', 'choice', userdate($choice->timeopen)), 'generalbox alert');
-        } else {
-            echo $OUTPUT->box(get_string("notopenyet", "choice", userdate($choice->timeopen)), "generalbox notopenyet");
-            echo $OUTPUT->footer();
-            exit;
-        }
-    } else if ($timenow > $choice->timeclose) {
-        echo $OUTPUT->box(get_string("expired", "choice", userdate($choice->timeclose)), "generalbox expired");
-        $choiceopen = false;
+if ((!empty($choice->timeopen)) && ($choice->timeopen > $timenow)) {
+    if ($choice->showpreview) {
+        echo $OUTPUT->box(get_string('previewonly', 'choice', userdate($choice->timeopen)), 'generalbox alert');
+    } else {
+        echo $OUTPUT->box(get_string("notopenyet", "choice", userdate($choice->timeopen)), "generalbox notopenyet");
+        echo $OUTPUT->footer();
+        exit;
     }
+} else if ((!empty($choice->timeclose)) && ($timenow > $choice->timeclose)) {
+    echo $OUTPUT->box(get_string("expired", "choice", userdate($choice->timeclose)), "generalbox expired");
+    $choiceopen = false;
 }
 
 if ( (!$current or $choice->allowupdate) and $choiceopen and is_enrolled($context, NULL, 'mod/choice:choose')) {
index 9098904..b02a97c 100644 (file)
@@ -15,7 +15,8 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * mod_data data generator
+ * mod_data data generator class
+ * Currently, the field types in the ignoredfieldtypes array aren't supported.
  *
  * @package    mod_data
  * @category   test
@@ -28,6 +29,7 @@ defined('MOODLE_INTERNAL') || die();
 
 /**
  * Database module data generator class
+ * Currently, the field types in the ignoredfieldtypes array aren't supported.
  *
  * @package    mod_data
  * @category   test
@@ -36,6 +38,41 @@ defined('MOODLE_INTERNAL') || die();
  */
 class mod_data_generator extends testing_module_generator {
 
+    /**
+     * @var int keep track of how many database fields have been created.
+     */
+    protected $databasefieldcount = 0;
+
+    /**
+     * @var int keep track of how many database records have been created.
+     */
+    protected $databaserecordcount = 0;
+
+    /**
+     * @var The field types which not handled by the generator as of now.
+     */
+    protected $ignoredfieldtypes = array('latlong', 'file', 'picture');
+
+
+    /**
+     * To be called from data reset code only,
+     * do not use in tests.
+     * @return void
+     */
+    public function reset() {
+        $this->databasefieldcount = 0;
+        $this->databaserecordcount = 0;
+
+        parent::reset();
+    }
+
+    /**
+     * Creates a mod_data instance
+     *
+     * @param array $record
+     * @param array $options
+     * @return StdClass
+     */
     public function create_instance($record = null, array $options = null) {
         $record = (object)(array)$record;
 
@@ -48,4 +85,259 @@ class mod_data_generator extends testing_module_generator {
 
         return parent::create_instance($record, (array)$options);
     }
+
+
+    /**
+     * Creates a field for a mod_data instance.
+     * Currently, the field types in the ignoredfieldtypes array aren't supported.
+     *
+     * @param StdClass $record
+     * @param mod_data $data
+     * @return data_field_{type}
+     */
+    public function create_field($record = null, $data = null) {
+        global $DB;
+
+        $record = (array) $record;
+
+        if (in_array($record['type'], $this->ignoredfieldtypes)) {
+            throw new coding_exception('$record\'s type value must not be same as values in ignoredfieldtypes
+                    in phpunit_util::create_field()');
+            return false;
+        }
+
+        $this->databasefieldcount++;
+
+        if (!isset($data->course)) {
+            throw new coding_exception('course must be present in phpunit_util::create_field() $data');
+        }
+
+        if (!isset($data->id)) {
+            throw new coding_exception('dataid must be present in phpunit_util::create_field() $data');
+        } else {
+            $record['dataid'] = $data->id;
+        }
+
+        if (!isset($record['type'])) {
+            throw new coding_exception('type must be present in phpunit_util::create_field() $record');
+        }
+
+        if (!isset($record['required'])) {
+            $record['required'] = 0;
+        }
+
+        if (!isset($record['name'])) {
+            $record['name'] = "testField - " . $this->databasefieldcount;
+        }
+
+        if (!isset($record['description'])) {
+            $record['description'] = " This is testField - " . $this->databasefieldcount;
+        }
+
+        if (!isset($record['param1'])) {
+            if (in_array($record['type'], array('checkbox', 'menu', 'multimenu', 'radiobutton'))) {
+                $record['param1'] = implode("\n", array('one', 'two', 'three', 'four'));
+            } else if (($record['type'] === 'text') || ($record['type'] === 'url')) {
+                $record['param1'] = 1;
+            } else {
+                $record['param1'] = '';
+            }
+        }
+
+        if (!isset($record['param2'])) {
+
+            if ($record['type'] === 'textarea') {
+                $record['param2'] = 60;
+            } else {
+                $record['param2'] = '';
+            }
+        }
+
+        if (!isset($record['param3'])) {
+
+            if (($record['type'] === 'textarea')) {
+                $record['param3'] = 35;
+            } else {
+                $record['param3'] = '';
+            }
+        }
+
+        if (!isset($record['param4'])) {
+
+            if (($record['type'] === 'textarea')) {
+                $record['param4'] = 1;
+            }
+        }
+
+        if (!isset($record['param5'])) {
+            if (($record['type'] === 'textarea')) {
+                $record['param5'] = 0;
+            }
+        }
+
+        $record = (object) $record;
+
+        $field = data_get_field($record, $data);
+        $field->insert_field();
+
+        data_generate_default_template($data, 'addtemplate', 0, false, true);
+
+        return $field;
+    }
+
+    /**
+     * Creates a field for a mod_data instance.
+     * Currently, the field types in the ignoredfieldtypes array aren't supported.
+     * The developers using the generator must adhere to the following format :
+     *
+     *   Syntax : $contents[ fieldid ] = fieldvalue
+     *   $contents['checkbox'] = array('val1', 'val2', 'val3' .....)
+     *   $contents['data'] = 'dd-mm-yyyy'
+     *   $contents['menu'] = 'value';
+     *   $contents['multimenu'] =  array('val1', 'val2', 'val3' .....)
+     *   $contents['number'] = 'numeric value'
+     *   $contents['radiobuton'] = 'value'
+     *   $contents['text'] = 'text'
+     *   $contents['textarea'] = 'text'
+     *   $contents['url'] = 'example.url' or array('example.url', 'urlname')
+     *
+     * @param mod_data $data
+     * @param array $contents
+     * @return data_field_{type}
+     */
+    public function create_entry($data, $contents) {
+        global $DB;
+
+        $this->databaserecordcount++;
+
+        $recordid = data_add_record($data);
+
+        $fields = $DB->get_records('data_fields', array( 'dataid' => $data->id));
+
+        // Validating whether required field are filled.
+        foreach ($fields as $field) {
+            $fieldhascontent = false;
+
+            if (in_array($field->type, $this->ignoredfieldtypes)) {
+                continue;
+            }
+
+            $field = data_get_field($field, $data);
+
+            $fieldid = $field->field->id;
+
+            if ($field->type === 'date') {
+                $values = array();
+
+                $temp = explode('-', $contents[$fieldid], 3);
+
+                $values['field_'.$fieldid.'_day'] = $temp[0];
+                $values['field_'.$fieldid.'_month'] = $temp[1];
+                $values['field_'.$fieldid.'_year'] = $temp[2];
+
+                foreach ($values as $fieldname => $value) {
+                    if ($field->notemptyfield($value, $fieldname)) {
+                        continue 2;
+                    }
+                }
+            } else if ($field->type === 'textarea') {
+                $values = array();
+
+                $values['field_'.$fieldid] = $contents[$fieldid];
+                $values['field_'.$fieldid.'_content1'] = 1;
+
+                foreach ($values as $fieldname => $value) {
+                    if ($field->notemptyfield ($value, $fieldname)) {
+                        continue 2;
+                    }
+                }
+            } else if ($field->type === 'url') {
+                $values = array();
+
+                if (is_array($contents[$fieldid])) {
+                    foreach ($contents[$fieldid] as $key => $value) {
+                        $values['field_'.$fieldid.'_'.$key] = $value;
+                    }
+                } else {
+                    $values['field_'.$fieldid.'_0'] = $contents[$fieldid];
+                }
+
+                foreach ($values as $fieldname => $value) {
+                    if ($field->notemptyfield ($value, $fieldname)) {
+                        continue 2;
+                    }
+                }
+
+            } else {
+                if ($field->notemptyfield ($contents[$fieldid], 'field_'.$fieldid.'_0')) {
+                    continue;
+                }
+            }
+
+            if ($field->field->required && !$fieldhascontent) {
+                return false;
+            }
+        }
+
+        foreach ($contents as $fieldid => $content) {
+
+            $field = $DB->get_record('data_fields', array( 'id' => $fieldid));
+            $field = data_get_field($field, $data);
+
+            if (in_array($field->field->type, $this->ignoredfieldtypes)) {
+                continue;
+            }
+
+            if ($field->type === 'date') {
+                $values = array();
+
+                $temp = explode('-', $content, 3);
+
+                $values['field_'.$fieldid.'_day'] = $temp[0];
+                $values['field_'.$fieldid.'_month'] = $temp[1];
+                $values['field_'.$fieldid.'_year'] = $temp[2];
+
+                foreach ($values as $fieldname => $value) {
+                    $field->update_content($recordid, (string)(int)trim($value), $fieldname);
+                }
+
+                continue;
+            }
+
+            if ($field->type === 'textarea') {
+                $values = array();
+
+                $values['field_'.$fieldid] = $content;
+                $values['field_'.$fieldid.'_content1'] = 1;
+
+                foreach ($values as $fieldname => $value) {
+                    $field->update_content($recordid, $value, $fieldname);
+                }
+
+                continue;
+            }
+
+            if ($field->type === 'url') {
+                $values = array();
+
+                if (is_array($content)) {
+                    foreach ($content as $key => $value) {
+                        $values['field_'.$fieldid.'_'.$key] = $value;
+                    }
+                } else {
+                    $values['field_'.$fieldid.'_0'] = $content;
+                }
+
+                foreach ($values as $fieldname => $value) {
+                    $field->update_content($recordid, $value, $fieldname);
+                }
+
+                continue;
+            }
+
+            $field->update_content($recordid, $contents[$fieldid]);
+        }
+
+        return $recordid;
+    }
 }
index a10d11a..113a114 100644 (file)
@@ -49,9 +49,9 @@ class mod_data_generator_testcase extends advanced_testcase {
         $this->assertInstanceOf('mod_data_generator', $generator);
         $this->assertEquals('data', $generator->get_modulename());
 
-        $generator->create_instance(array('course'=>$course->id));
-        $generator->create_instance(array('course'=>$course->id));
-        $data = $generator->create_instance(array('course'=>$course->id));
+        $generator->create_instance(array('course' => $course->id));
+        $generator->create_instance(array('course' => $course->id));
+        $data = $generator->create_instance(array('course' => $course->id));
         $this->assertEquals(3, $DB->count_records('data'));
 
         $cm = get_coursemodule_from_instance('data', $data->id);
@@ -63,12 +63,144 @@ class mod_data_generator_testcase extends advanced_testcase {
         $this->assertEquals($data->cmid, $context->instanceid);
 
         // test gradebook integration using low level DB access - DO NOT USE IN PLUGIN CODE!
-        $data = $generator->create_instance(array('course'=>$course->id, 'assessed'=>1, 'scale'=>100));
-        $gitem = $DB->get_record('grade_items', array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule'=>'data', 'iteminstance'=>$data->id));
+        $data = $generator->create_instance(array('course' => $course->id, 'assessed' => 1, 'scale' => 100));
+        $gitem = $DB->get_record('grade_items', array('courseid' => $course->id, 'itemtype' => 'mod',
+                'itemmodule' => 'data', 'iteminstance' => $data->id));
         $this->assertNotEmpty($gitem);
         $this->assertEquals(100, $gitem->grademax);
         $this->assertEquals(0, $gitem->grademin);
         $this->assertEquals(GRADE_TYPE_VALUE, $gitem->gradetype);
 
     }
-}
+
+
+    public function test_create_field() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $this->setAdminUser();
+        $this->assertEquals(0, $DB->count_records('data'));
+
+        $course = $this->getDataGenerator()->create_course();
+
+        /** @var mod_data_generator $generator */
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+        $this->assertInstanceOf('mod_data_generator', $generator);
+        $this->assertEquals('data', $generator->get_modulename());
+
+        $data = $generator->create_instance(array('course' => $course->id));
+        $this->assertEquals(1, $DB->count_records('data'));
+
+        $cm = get_coursemodule_from_instance('data', $data->id);
+        $this->assertEquals($data->id, $cm->instance);
+        $this->assertEquals('data', $cm->modname);
+        $this->assertEquals($course->id, $cm->course);
+
+        $context = context_module::instance($cm->id);
+        $this->assertEquals($data->cmid, $context->instanceid);
+
+        $fieldtypes = array( 'checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url' );
+
+        $count = 1;
+
+        // Creating test Fields with default parameter values.
+        foreach ($fieldtypes as $fieldtype) {
+
+            // Creating variables dynamically.
+            $fieldname = 'field-'.$count;
+            $record = new StdClass();
+            $record->name = $fieldname;
+            $record->type = $fieldtype;
+
+            ${$fieldname} = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data);
+
+            $this->assertInstanceOf('data_field_'.$fieldtype , ${$fieldname});
+            $count++;
+        }
+
+        $this->assertEquals(count($fieldtypes), $DB->count_records('data_fields', array( 'dataid' => $data->id )));
+
+        $addtemplate = $DB->get_record('data', array( 'id' => $data->id ), 'addtemplate');
+        $addtemplate = $addtemplate->addtemplate;
+
+        for ($i = 1; $i < $count; $i++) {
+            $fieldname = 'field-'.$i;
+            $this->assertTrue(strpos($addtemplate, '[['.$fieldname.']]') >= 0);
+        }
+    }
+
+
+    public function test_create_entry() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $this->setAdminUser();
+        $this->assertEquals(0, $DB->count_records('data'));
+
+        $course = $this->getDataGenerator()->create_course();
+
+        /** @var mod_data_generator $generator */
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_data');
+        $this->assertInstanceOf('mod_data_generator', $generator);
+        $this->assertEquals('data', $generator->get_modulename());
+
+        $data = $generator->create_instance(array('course' => $course->id));
+        $this->assertEquals(1, $DB->count_records('data'));
+
+        $cm = get_coursemodule_from_instance('data', $data->id);
+        $this->assertEquals($data->id, $cm->instance);
+        $this->assertEquals('data', $cm->modname);
+        $this->assertEquals($course->id, $cm->course);
+
+        $context = context_module::instance($cm->id);
+        $this->assertEquals($data->cmid, $context->instanceid);
+
+        $fieldtypes = array( 'checkbox', 'date', 'menu', 'multimenu', 'number', 'radiobutton', 'text', 'textarea', 'url' );
+
+        $count = 1;
+
+        // Creating test Fields with default parameter values.
+        foreach ($fieldtypes as $fieldtype) {
+
+            // Creating variables dynamically.
+            $fieldname = 'field-'.$count;
+            $record = new StdClass();
+            $record->name = $fieldname;
+            $record->type = $fieldtype;
+
+            ${$fieldname} = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_field($record, $data);
+            $this->assertInstanceOf('data_field_'.$fieldtype , ${$fieldname});
+            $count++;
+        }
+
+        $this->assertEquals(count($fieldtypes), $DB->count_records('data_fields', array( 'dataid' => $data->id )));
+
+        $fields = $DB->get_records('data_fields', array('dataid' => $data->id), 'id');
+
+        $contents = array();
+        $contents[] = array('one', 'two', 'three', 'four');
+        $contents[] = '01-01-2100';
+        $contents[] = 'one';
+        $contents[] = array('one', 'two', 'three', 'four');
+        $contents[] = '12345';
+        $contents[] = 'one';
+        $contents[] = 'text for testing';
+        $contents[] = '<p>text area testing<br /></p>';
+        $contents[] = array('example.url', 'sampleurl');
+        $count = 0;
+        $fieldcontents = array();
+        foreach ($fields as $fieldrecord) {
+            $fieldcontents[$fieldrecord->id] = $contents[$count++];
+        }
+
+        $datarecordid = $this->getDataGenerator()->get_plugin_generator('mod_data')->create_entry($data, $fieldcontents);
+
+        $this->assertEquals(1, $DB->count_records('data_records', array( 'dataid' => $data->id )));
+
+        $this->assertEquals(count($contents), $DB->count_records('data_content', array('recordid' => $datarecordid)));
+
+    }
+
+}
\ No newline at end of file
index 63c515b..98b9a35 100644 (file)
@@ -223,14 +223,14 @@ class feedback_item_multichoice extends feedback_item_base {
                 $quotient = format_float($val->quotient * 100, 2);
                 $strquotient = '';
                 if ($val->quotient > 0) {
-                    $strquotient = '('. $quotient . ' %)';
+                    $strquotient = ' ('. $quotient . ' %)';
                 }
                 $answertext = format_text(trim($val->answertext), FORMAT_HTML,
                         array('noclean' => true, 'para' => false));
 
                 $data['labels'][$count] = $answertext;
                 $data['series'][$count] = $val->answercount;
-                $data['series_labels'][$count] = $strquotient;
+                $data['series_labels'][$count] = $val->answercount . $strquotient;
                 $count++;
             }
             $chart = new \core\chart_bar();
index a28a6d2..7c51cf9 100644 (file)
@@ -198,14 +198,14 @@ class feedback_item_multichoicerated extends feedback_item_base {
                         array('noclean' => true, 'para' => false));
 
                 if ($val->quotient > 0) {
-                    $strquotient = '('.$quotient.' %)';
+                    $strquotient = ' ('.$quotient.' %)';
                 } else {
                     $strquotient = '';
                 }
 
                 $data['labels'][$count] = $answertext;
                 $data['series'][$count] = $val->answercount;
-                $data['series_labels'][$count] = $strquotient;
+                $data['series_labels'][$count] = $val->answercount . $strquotient;
                 $count++;
             }
             $chart = new \core\chart_bar();
index 5c46245..1f0a898 100644 (file)
@@ -138,14 +138,15 @@ class behat_mod_feedback extends behat_base {
 
         $feedbackxpath = "//th[contains(normalize-space(string(.)), \"" . $feedbackname . "\")]/ancestor::table/" .
             "following-sibling::div[contains(concat(' ', normalize-space(@class), ' '), ' chart-area ')][1]" .
-            "//p[contains(concat(' ', normalize-space(@class), ' '), ' chart-table-expand ')]";
+            "//p[contains(concat(' ', normalize-space(@class), ' '), ' chart-table-expand ') and ".
+            "//a[contains(normalize-space(string(.)), '".get_string('showchartdata')."')]]";
 
         $charttabledataxpath = $feedbackxpath .
             "/following-sibling::div[contains(concat(' ', normalize-space(@class), ' '), ' chart-table-data ')][1]";
 
         // If chart data is not visible then expand.
         $node = $this->get_selected_node("xpath_element", $charttabledataxpath);
-        if (!$node->isVisible()) {
+        if ($node && !$node->isVisible()) {
             $this->execute('behat_general::i_click_on_in_the', array(
                 get_string('showchartdata'),
                 'link',
index 0025c4f..c0db8fe 100644 (file)
@@ -283,22 +283,7 @@ class mod_forum_external extends external_api {
 
             // List attachments.
             if (!empty($post->attachment)) {
-                $post->attachments = array();
-
-                $fs = get_file_storage();
-                if ($files = $fs->get_area_files($modcontext->id, 'mod_forum', 'attachment', $post->id, "filename", false)) {
-                    foreach ($files as $file) {
-                        $filename = $file->get_filename();
-                        $fileurl = moodle_url::make_webservice_pluginfile_url(
-                                        $modcontext->id, 'mod_forum', 'attachment', $post->id, '/', $filename);
-
-                        $post->attachments[] = array(
-                            'filename' => $filename,
-                            'mimetype' => $file->get_mimetype(),
-                            'fileurl'  => $fileurl->out(false)
-                        );
-                    }
-                }
+                $post->attachments = external_util::get_area_files($modcontext->id, 'mod_forum', 'attachment', $post->id);
             }
 
             $posts[] = $post;
@@ -334,15 +319,7 @@ class mod_forum_external extends external_api {
                                 'messageformat' => new external_format_value('message'),
                                 'messagetrust' => new external_value(PARAM_INT, 'Can we trust?'),
                                 'attachment' => new external_value(PARAM_RAW, 'Has attachments?'),
-                                'attachments' => new external_multiple_structure(
-                                    new external_single_structure(
-                                        array (
-                                            'filename' => new external_value(PARAM_FILE, 'file name'),
-                                            'mimetype' => new external_value(PARAM_RAW, 'mime type'),
-                                            'fileurl'  => new external_value(PARAM_URL, 'file download url')
-                                        )
-                                    ), 'attachments', VALUE_OPTIONAL
-                                ),
+                                'attachments' => new external_files('attachments', VALUE_OPTIONAL),
                                 'totalscore' => new external_value(PARAM_INT, 'The post message total score'),
                                 'mailnow' => new external_value(PARAM_INT, 'Mail now?'),
                                 'children' => new external_multiple_structure(new external_value(PARAM_INT, 'children post id')),
@@ -518,22 +495,8 @@ class mod_forum_external extends external_api {
 
                 // List attachments.
                 if (!empty($discussion->attachment)) {
-                    $discussion->attachments = array();
-
-                    $fs = get_file_storage();
-                    if ($files = $fs->get_area_files($modcontext->id, 'mod_forum', 'attachment',
-                                                        $discussion->id, "filename", false)) {
-                        foreach ($files as $file) {
-                            $filename = $file->get_filename();
-
-                            $discussion->attachments[] = array(
-                                'filename' => $filename,
-                                'mimetype' => $file->get_mimetype(),
-                                'fileurl'  => file_encode_url($CFG->wwwroot.'/webservice/pluginfile.php',
-                                                '/'.$modcontext->id.'/mod_forum/attachment/'.$discussion->id.'/'.$filename)
-                            );
-                        }
-                    }
+                    $discussion->attachments = external_util::get_area_files($modcontext->id, 'mod_forum', 'attachment',
+                                                                                $discussion->id);
                 }
 
                 $discussions[] = $discussion;
@@ -577,15 +540,7 @@ class mod_forum_external extends external_api {
                                 'messageformat' => new external_format_value('message'),
                                 'messagetrust' => new external_value(PARAM_INT, 'Can we trust?'),
                                 'attachment' => new external_value(PARAM_RAW, 'Has attachments?'),
-                                'attachments' => new external_multiple_structure(
-                                    new external_single_structure(
-                                        array (
-                                            'filename' => new external_value(PARAM_FILE, 'file name'),
-                                            'mimetype' => new external_value(PARAM_RAW, 'mime type'),
-                                            'fileurl'  => new external_value(PARAM_URL, 'file download url')
-                                        )
-                                    ), 'attachments', VALUE_OPTIONAL
-                                ),
+                                'attachments' => new external_files('attachments', VALUE_OPTIONAL),
                                 'totalscore' => new external_value(PARAM_INT, 'The post message total score'),
                                 'mailnow' => new external_value(PARAM_INT, 'Mail now?'),
                                 'userfullname' => new external_value(PARAM_TEXT, 'Post author full name'),
diff --git a/mod/forum/tests/behat/advanced_search.feature b/mod/forum/tests/behat/advanced_search.feature
new file mode 100644 (file)
index 0000000..e38968e
--- /dev/null
@@ -0,0 +1,109 @@
+@mod @mod_forum
+Feature: The forum search allows users to perform advanced searches for forum posts
+  In order to perform an advanced search for a forum post
+  As a teacher
+  I can use the search feature
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | ONE | teacher1@example.com | T1 |
+      | teacher2 | Teacher | TWO | teacher2@example.com | T1 |
+      | student1 | Student | 1 | student1@example.com | S1 |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | teacher2 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Edit settings" node in "Course administration"
+    And I set the field "id_newsitems" to "1"
+    And I press "Save and display"
+    And I add a new topic to "Announcements" forum with:
+      | Subject | My subject |
+      | Message | My message |
+    And I follow "Course 1"
+    And I add a new topic to "Announcements" forum with:
+      | Subject | My subjective|
+      | Message | My long message |
+    And I log out
+
+  Scenario: Perform an advanced search using any term
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Announcements"
+    And I press "Search forums"
+    And I should see "Advanced search"
+    And I set the field "words" to "subject"
+    When I press "Search forums"
+    Then I should see "My subject"
+    And I should see "My subjective"
+
+  Scenario: Perform an advanced search avoiding words
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Announcements"
+    And I press "Search forums"
+    And I should see "Advanced search"
+    And I set the field "words" to "My"
+    And I set the field "notwords" to "subjective"
+    When I press "Search forums"
+    Then I should see "My subject"
+    And I should not see "My subjective"
+
+  Scenario: Perform an advanced search using whole words
+    Given database family used is one of the following:
+      | mysql |
+      | postgres |
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Announcements"
+    And I press "Search forums"
+    And I should see "Advanced search"
+    And I set the field "fullwords" to "subject"
+    When I press "Search forums"
+    Then I should see "My subject"
+    And I should not see "My subjective"
+
+  Scenario: Perform an advanced search matching the subject
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Announcements"
+    And I press "Search forums"
+    And I should see "Advanced search"
+    And I set the field "subject" to "subjective"
+    When I press "Search forums"
+    Then I should not see "My message"
+    And I should see "My subjective"
+
+  Scenario: Perform an advanced search matching the author
+    Given I log in as "teacher2"
+    And I follow "Course 1"
+    And I add a new topic to "Announcements" forum with:
+      | Subject | My Subjects |
+      | Message | My message |
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Announcements"
+    And I press "Search forums"
+    And I should see "Advanced search"
+    And I set the field "user" to "TWO"
+    And I press "Search forums"
+    Then I should see "Teacher TWO"
+    And I should not see "Teacher ONE"
+
+  Scenario: Perform an advanced search with multiple words
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Announcements"
+    And I press "Search forums"
+    And I should see "Advanced search"
+    And I set the field "subject" to "my subjective"
+    When I press "Search forums"
+    Then I should not see "My message"
+    And I should see "My subjective"
index bbbe97a..6705414 100644 (file)
@@ -30,6 +30,9 @@ information provided here is intended especially for developers.
   - forum_get_subscribed_forums
   - forum_get_optional_subscribed_forums
   - forum_get_potential_subscribers
+ * External functions that were returning file information now return the following file fields:
+   filename, filepath, mimetype, filesize, timemodified and fileurl.
+   Those fields are now marked as VALUE_OPTIONAL for backwards compatibility.
 
 === 3.1 ===
  * The inteface to forum_get_email_message_id() has changed and no longer needs the $host argument.
index a8b7fc6..a0e254f 100644 (file)
@@ -91,13 +91,7 @@ class mod_glossary_external extends external_api {
             'definitionformat' => new external_format_value('definition'),
             'definitiontrust' => new external_value(PARAM_BOOL, 'The definition trust flag'),
             'attachment' => new external_value(PARAM_BOOL, 'Whether or not the entry has attachments'),
-            'attachments' => new external_multiple_structure(
-                new external_single_structure(array(
-                    'filename' => new external_value(PARAM_FILE, 'File name'),
-                    'mimetype' => new external_value(PARAM_RAW, 'Mime type'),
-                    'fileurl'  => new external_value(PARAM_URL, 'File download URL')
-                )), 'attachments', VALUE_OPTIONAL
-            ),
+            'attachments' => new external_files('attachments', VALUE_OPTIONAL),
             'timecreated' => new external_value(PARAM_INT, 'Time created'),
             'timemodified' => new external_value(PARAM_INT, 'Time modified'),
             'teacherentry' => new external_value(PARAM_BOOL, 'The entry was created by a teacher, or equivalent.'),
@@ -148,19 +142,7 @@ class mod_glossary_external extends external_api {
         $entry->attachment = !empty($entry->attachment) ? 1 : 0;
         $entry->attachments = array();
         if ($entry->attachment) {
-            $fs = get_file_storage();
-            if ($files = $fs->get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id, 'filename', false)) {
-                foreach ($files as $file) {
-                    $filename = $file->get_filename();
-                    $fileurl = moodle_url::make_webservice_pluginfile_url($context->id, 'mod_glossary', 'attachment',
-                        $entry->id, '/', $filename);
-                    $entry->attachments[] = array(
-                        'filename' => $filename,
-                        'mimetype' => $file->get_mimetype(),
-                        'fileurl'  => $fileurl->out(false)
-                    );
-                }
-            }
+            $entry->attachments = external_util::get_area_files($context->id, 'mod_glossary', 'attachment', $entry->id);
         }
     }
 
index 9f066ec..a537f65 100644 (file)
@@ -1,6 +1,11 @@
 This files describes API changes in /mod/glossary/*,
 information provided here is intended especially for developers.
 
+=== 3.2 ===
+* External functions that were returning file information now return the following file fields:
+  filename, filepath, mimetype, filesize, timemodified and fileurl.
+  Those fields are now marked as VALUE_OPTIONAL for backwards compatibility.
+
 === 2.8 ===
 * The glossary_print_entry_attachment function no longer takes an `align`
   or `insidetable` property. Instead the attachments are printed within a
index b6c8fc1..57fdcaa 100644 (file)
@@ -45,6 +45,17 @@ $PAGE->set_pagelayout('maintenance');
 $output = $PAGE->get_renderer('mod_lti');
 echo $output->header();
 
+// Check status and lti_errormsg.
+if ($status !== 'success' && empty($err)) {
+    // We have a failed status and an empty lti_errormsg. Check if we can use lti_msg.
+    if (!empty($msg)) {
+        // The lti_msg attribute is set, use this as the error message.
+        $err = $msg;
+    } else {
+        // Otherwise, use our generic error message.
+        $err = get_string('failedtocreatetooltype', 'mod_lti');
+    }
+}
 $params = array('message' => s($msg), 'error' => s($err), 'id' => $id, 'status' => s($status));
 
 $page = new \mod_lti\output\external_registration_return_page();
index 092ac4c..ddec864 100644 (file)
@@ -2700,24 +2700,32 @@ function lti_load_tool_from_cartridge($url, $lti) {
 function lti_load_cartridge($url, $map, $propertiesmap = array()) {
     global $CFG;
     require_once($CFG->libdir. "/filelib.php");
-    // TODO MDL-46023 Replace this code with a call to the new library.
-    $origentity = libxml_disable_entity_loader(true);
 
     $curl = new curl();
     $response = $curl->get($url);
 
+    // TODO MDL-46023 Replace this code with a call to the new library.
+    $origerrors = libxml_use_internal_errors(true);
+    $origentity = libxml_disable_entity_loader(true);
+    libxml_clear_errors();
+
     $document = new DOMDocument();
     @$document->loadXML($response, LIBXML_DTDLOAD | LIBXML_DTDATTR);
 
     $cartridge = new DomXpath($document);
 
     $errors = libxml_get_errors();
+
+    libxml_clear_errors();
+    libxml_use_internal_errors($origerrors);
+    libxml_disable_entity_loader($origentity);
+
     if (count($errors) > 0) {
         $message = 'Failed to load cartridge.';
         foreach ($errors as $error) {
             $message .= "\n" . trim($error->message, "\n\r\t .") . " at line " . $error->line;
         }
-        throw new moodle_exception($message);
+        throw new moodle_exception('errorreadingfile', '', '', $url, $message);
     }
 
     $toolinfo = array();
@@ -2735,7 +2743,7 @@ function lti_load_cartridge($url, $map, $propertiesmap = array()) {
             }
         }
     }
-    libxml_disable_entity_loader($origentity);
+
     return $toolinfo;
 }
 
index 6da74b4..27336fc 100644 (file)
@@ -21,7 +21,7 @@ Feature: Add tools
     And I set the following fields to these values:
       | Tool name | Teaching Tool 1 |
       | Tool configuration usage | Show in activity chooser and as a preconfigured tool |
-    And I set the field "Tool base URL/cartridge URL" to local url "/mod/lti/tests/fixtures/tool_provider.html"
+    And I set the field "Tool base URL/cartridge URL" to local url "/mod/lti/tests/fixtures/tool_provider.php"
     And I press "Save changes"
     And I log out
 
index ecf14a2..3d10586 100644 (file)
@@ -67,7 +67,7 @@ Feature: Add preconfigured tools via teacher interface
       | Activity name | Test tool activity 1 |
     And I open "Test tool activity 1" actions menu
     And I follow "Edit settings" in the open menu
-    And I set the field "Launch/cartridge URL" to local url "/mod/lti/tests/fixtures/tool_provider.html"
+    And I set the field "Launch/cartridge URL" to local url "/mod/lti/tests/fixtures/tool_provider.php"
     And I press "Save and return to course"
     And I follow "Test tool activity 1"
     And I switch to "contentframe" iframe
index e1afbda..b30f5b1 100644 (file)
@@ -37,7 +37,7 @@ Feature: Configure tool types
 
   @javascript
   Scenario: Attempt to add a tool type from a configuration URL, then cancel
-    When I set the field "url" to local url "/mod/lti/tests/fixtures/tool_provider.html"
+    When I set the field "url" to local url "/mod/lti/tests/fixtures/tool_provider.php"
     And I press "Add"
     Then I should see "Cancel"
     And I press "cancel-external-registration"
index ee5ae39..526ba11 100644 (file)
@@ -52,7 +52,8 @@ class mod_lti_external_testcase extends externallib_advanced_testcase {
 
         // Setup test data.
         $this->course = $this->getDataGenerator()->create_course();
-        $this->lti = $this->getDataGenerator()->create_module('lti', array('course' => $this->course->id));
+        $this->lti = $this->getDataGenerator()->create_module('lti',
+            array('course' => $this->course->id, 'toolurl' => 'http://localhost/not/real/tool.php'));
         $this->context = context_module::instance($this->lti->cmid);
         $this->cm = get_coursemodule_from_instance('lti', $this->lti->id);
 
diff --git a/mod/lti/tests/fixtures/tool_provider.html b/mod/lti/tests/fixtures/tool_provider.html
deleted file mode 100644 (file)
index 543a796..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<html>
-  <head>
-    <title>Tool provider</title>
-  </head>
-  <body>
-    <p>This represents a tool provider</p>
-  </body>
-</html>
diff --git a/mod/lti/tests/fixtures/tool_provider.php b/mod/lti/tests/fixtures/tool_provider.php
new file mode 100644 (file)
index 0000000..1f7f242
--- /dev/null
@@ -0,0 +1,33 @@
+<?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/>.
+
+/**
+ * Testing fixture.
+ *
+ * @package   mod_lti
+ * @copyright 2016 John Okely
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+?>
+<html>
+  <head>
+    <title>Tool provider</title>
+  </head>
+  <body>
+    <p>This represents a tool provider</p>
+  </body>
+</html>
index 4703f91..a896328 100644 (file)
@@ -41,7 +41,7 @@ class mod_lti_generator extends testing_module_generator {
         $record  = (object) (array) $record;
 
         if (!isset($record->toolurl)) {
-            $record->toolurl = 'http://www.imsglobal.org/developers/LTI/test/v1p1/tool.php';
+            $record->toolurl = '';
         }
         if (!isset($record->resourcekey)) {
             $record->resourcekey = '12345';
index ed4f87e..c8367e3 100644 (file)
@@ -239,6 +239,10 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
             $data->overduehandling = get_config('quiz', 'overduehandling');
         }
 
+        // Old shufflequestions setting is now stored in quiz sections,
+        // so save it here if necessary so it is available when we need it.
+        $this->legacyshufflequestionsoption = !empty($data->shufflequestions);
+
         // Insert the quiz record.
         $newitemid = $DB->insert_record('quiz', $data);
         // Immediately after inserting "activity" record, call this.
index c79f544..ebd0836 100644 (file)
@@ -728,23 +728,7 @@ class mod_wiki_external extends external_api {
             throw new moodle_exception('cannotviewfiles', 'wiki');
         } else if ($subwiki->id != -1) {
             // The subwiki exists, let's get the files.
-            $fs = get_file_storage();
-            if ($files = $fs->get_area_files($context->id, 'mod_wiki', 'attachments', $subwiki->id, 'filename', false)) {
-                foreach ($files as $file) {
-                    $filename = $file->get_filename();
-                    $fileurl = moodle_url::make_webservice_pluginfile_url(
-                                    $context->id, 'mod_wiki', 'attachments', $subwiki->id, '/', $filename);
-
-                    $returnedfiles[] = array(
-                        'filename' => $filename,
-                        'mimetype' => $file->get_mimetype(),
-                        'fileurl'  => $fileurl->out(false),
-                        'filepath' => $file->get_filepath(),
-                        'filesize' => $file->get_filesize(),
-                        'timemodified' => $file->get_timemodified()
-                    );
-                }
-            }
+            $returnedfiles = external_util::get_area_files($context->id, 'mod_wiki', 'attachments', $subwiki->id);
         }
 
         $result = array();
@@ -763,18 +747,7 @@ class mod_wiki_external extends external_api {
 
         return new external_single_structure(
             array(
-                'files' => new external_multiple_structure(
-                    new external_single_structure(
-                        array(
-                            'filename' => new external_value(PARAM_FILE, 'File name.'),
-                            'filepath' => new external_value(PARAM_PATH, 'File path.'),
-                            'filesize' => new external_value(PARAM_INT, 'File size.'),
-                            'fileurl' => new external_value(PARAM_URL, 'Downloadable file url.'),
-                            'timemodified' => new external_value(PARAM_INT, 'Time modified.'),
-                            'mimetype' => new external_value(PARAM_RAW, 'File mime type.'),
-                        ), 'Files'
-                    )
-                ),
+                'files' => new external_files('Files'),
                 'warnings' => new external_warnings(),
             )
         );
@@ -824,7 +797,7 @@ class mod_wiki_external extends external_api {
         return new external_function_parameters (
             array(
                 'pageid' => new external_value(PARAM_INT, 'Page ID to edit.'),
-                'section' => new external_value(PARAM_TEXT, 'Section page title.', VALUE_DEFAULT, null)
+                'section' => new external_value(PARAM_RAW, 'Section page title.', VALUE_DEFAULT, null)
             )
         );
     }
@@ -1088,7 +1061,7 @@ class mod_wiki_external extends external_api {
             array(
                 'pageid' => new external_value(PARAM_INT, 'Page ID.'),
                 'content' => new external_value(PARAM_RAW, 'Page contents.'),
-                'section' => new external_value(PARAM_TEXT, 'Section page title.', VALUE_DEFAULT, null)
+                'section' => new external_value(PARAM_RAW, 'Section page title.', VALUE_DEFAULT, null)
             )
         );
     }
index 2e1660b..37b6435 100644 (file)
@@ -1155,7 +1155,8 @@ class mod_wiki_external_testcase extends externallib_advanced_testcase {
 
         $this->create_individual_wikis_with_groups();
 
-        $sectioncontent = '<h1>Title1</h1>Text inside section';
+        // We add a <span> in the first title to verify the WS works sending HTML in section.
+        $sectioncontent = '<h1><span>Title1</span></h1>Text inside section';
         $pagecontent = $sectioncontent.'<h1>Title2</h1>Text inside section';
         $newpage = $this->getDataGenerator()->get_plugin_generator('mod_wiki')->create_page(
                                 $this->wiki, array('content' => $pagecontent));
@@ -1181,7 +1182,7 @@ class mod_wiki_external_testcase extends externallib_advanced_testcase {
             'version' => '1'
         );
 
-        $result = mod_wiki_external::get_page_for_editing($newpage->id, 'Title1');
+        $result = mod_wiki_external::get_page_for_editing($newpage->id, '<span>Title1</span>');
         $result = external_api::clean_returnvalue(mod_wiki_external::get_page_for_editing_returns(), $result);
         $this->assertEquals($expected, $result['pagesection']);
     }
@@ -1273,8 +1274,9 @@ class mod_wiki_external_testcase extends externallib_advanced_testcase {
             array('group' => $this->group1->id, 'content' => 'Test'));
 
         // Test edit whole page.
-        $sectioncontent = '<h1>Title1</h1>Text inside section';
-        $newpagecontent = $sectioncontent.'<h1>Title2</h1>Text inside section';
+        // We add <span> in the titles to verify the WS works sending HTML in section.
+        $sectioncontent = '<h1><span>Title1</span></h1>Text inside section';
+        $newpagecontent = $sectioncontent.'<h1><span>Title2</span></h1>Text inside section';
 
         $result = mod_wiki_external::edit_page($newpage->id, $newpagecontent);
         $result = external_api::clean_returnvalue(mod_wiki_external::edit_page_returns(), $result);
@@ -1284,8 +1286,8 @@ class mod_wiki_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals($newpagecontent, $version->content);
 
         // Test edit section.
-        $newsectioncontent = '<h1>Title2</h1>New test2';
-        $section = 'Title2';
+        $newsectioncontent = '<h1><span>Title2</span></h1>New test2';
+        $section = '<span>Title2</span>';
 
         $result = mod_wiki_external::edit_page($newpage->id, $newsectioncontent, $section);
         $result = external_api::clean_returnvalue(mod_wiki_external::edit_page_returns(), $result);
@@ -1297,8 +1299,8 @@ class mod_wiki_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals($expected, $version->content);
 
         // Test locked section.
-        $newsectioncontent = '<h1>Title2</h1>New test2';
-        $section = 'Title2';
+        $newsectioncontent = '<h1><span>Title2</span></h1>New test2';
+        $section = '<span>Title2</span>';
 
         try {
             // Using user 1 to avoid other users to edit.
index 611e8a6..d6371a3 100644 (file)
@@ -1,5 +1,10 @@
 This files describes API changes in /mod/wiki/*,
 information provided here is intended especially for developers.
 
+=== 3.2 ===
+* External functions that were returning file information now return the following file fields:
+  filename, filepath, mimetype, filesize, timemodified and fileurl.
+  Those fields are now marked as VALUE_OPTIONAL for backwards compatibility.
+
 === 3.1 ===
  * Added a new param $sort to wiki_get_page_list function. Default value behaves exactly like before (sort by title ASC).
index f95843b..1f7ebd4 100644 (file)
@@ -154,11 +154,13 @@ if ($edit and $canmanage) {
                 throw new moodle_exception('err_examplesubmissionid', 'workshop');
             }
         }
-        // save and relink embedded images and save attachments
-        $formdata = file_postupdate_standard_editor($formdata, 'content', $contentopts, $workshop->context,
-                                                      'mod_workshop', 'submission_content', $example->id);
-        $formdata = file_postupdate_standard_filemanager($formdata, 'attachment', $attachmentopts, $workshop->context,
-                                                           'mod_workshop', 'submission_attachment', $example->id);
+
+        // Save and relink embedded images and save attachments.
+        $formdata = file_postupdate_standard_editor($formdata, 'content', $workshop->submission_content_options(),
+            $workshop->context, 'mod_workshop', 'submission_content', $example->id);
+        $formdata = file_postupdate_standard_filemanager($formdata, 'attachment', $workshop->submission_attachment_options(),
+            $workshop->context, 'mod_workshop', 'submission_attachment', $example->id);
+
         if (empty($formdata->attachment)) {
             // explicit cast to zero integer
             $formdata->attachment = 0;
index c9f4048..07664a8 100644 (file)
@@ -82,11 +82,11 @@ function workshop_add_instance(stdclass $workshop) {
     $workshop->evaluation            = 'best';
 
     if (isset($workshop->gradinggradepass)) {
-        $workshop->gradinggradepass = unformat_float($workshop->gradinggradepass);
+        $workshop->gradinggradepass = (float)unformat_float($workshop->gradinggradepass);
     }
 
     if (isset($workshop->submissiongradepass)) {
-        $workshop->submissiongradepass = unformat_float($workshop->submissiongradepass);
+        $workshop->submissiongradepass = (float)unformat_float($workshop->submissiongradepass);
     }
 
     if (isset($workshop->submissionfiletypes)) {
@@ -158,11 +158,11 @@ function workshop_update_instance(stdclass $workshop) {
     $workshop->phaseswitchassessment = (int)!empty($workshop->phaseswitchassessment);
 
     if (isset($workshop->gradinggradepass)) {
-        $workshop->gradinggradepass = unformat_float($workshop->gradinggradepass);
+        $workshop->gradinggradepass = (float)unformat_float($workshop->gradinggradepass);
     }
 
     if (isset($workshop->submissiongradepass)) {
-        $workshop->submissiongradepass = unformat_float($workshop->submissiongradepass);
+        $workshop->submissiongradepass = (float)unformat_float($workshop->submissiongradepass);
     }
 
     if (isset($workshop->submissionfiletypes)) {
index b26d56a..d074add 100644 (file)
@@ -2486,6 +2486,9 @@ class workshop {
      * @return array
      */
     public function submission_content_options() {
+        global $CFG;
+        require_once($CFG->dirroot.'/repository/lib.php');
+
         return array(
             'trusttext' => true,
             'subdirs' => false,
@@ -2502,6 +2505,8 @@ class workshop {
      * @return array
      */
     public function submission_attachment_options() {
+        global $CFG;
+        require_once($CFG->dirroot.'/repository/lib.php');
 
         $options = array(
             'subdirs' => true,
@@ -2523,12 +2528,16 @@ class workshop {
      * @return array
      */
     public function overall_feedback_content_options() {
+        global $CFG;
+        require_once($CFG->dirroot.'/repository/lib.php');
+
         return array(
             'subdirs' => 0,
             'maxbytes' => $this->overallfeedbackmaxbytes,
             'maxfiles' => $this->overallfeedbackfiles,
             'changeformat' => 1,
             'context' => $this->context,
+            'return_types' => FILE_INTERNAL,
         );
     }
 
@@ -2538,6 +2547,8 @@ class workshop {
      * @return array
      */
     public function overall_feedback_attachment_options() {
+        global $CFG;
+        require_once($CFG->dirroot.'/repository/lib.php');
 
         $options = array(
             'subdirs' => 1,
index 0a87b5b..db864e0 100644 (file)
@@ -396,9 +396,9 @@ class mod_workshop_mod_form extends moodleform_mod {
         }
 
         // Check that the submission grade pass is a valid number.
-        if (isset($data['submissiongradepass'])) {
+        if (!empty($data['submissiongradepass'])) {
             $submissiongradefloat = unformat_float($data['submissiongradepass'], true);
-            if ($submissiongradefloat === false || $submissiongradefloat === null) {
+            if ($submissiongradefloat === false) {
                 $errors['submissiongradepass'] = get_string('err_numeric', 'form');
             } else {
                 if ($submissiongradefloat > $data['grade']) {
@@ -408,9 +408,9 @@ class mod_workshop_mod_form extends moodleform_mod {
         }
 
         // Check that the grade pass is a valid number.
-        if (isset($data['gradinggradepass'])) {
+        if (!empty($data['gradinggradepass'])) {
             $gradepassfloat = unformat_float($data['gradinggradepass'], true);
-            if ($gradepassfloat === false || $gradepassfloat === null) {
+            if ($gradepassfloat === false) {
                 $errors['gradinggradepass'] = get_string('err_numeric', 'form');
             } else {
                 if ($gradepassfloat > $data['gradinggrade']) {
index 3d7e1fb..6fc9724 100644 (file)
@@ -25,7 +25,6 @@
 
 require(__DIR__.'/../../config.php');
 require_once(__DIR__.'/locallib.php');
-require_once($CFG->dirroot . '/repository/lib.php');
 
 $cmid = required_param('cmid', PARAM_INT); // Course module id.
 $id = optional_param('id', 0, PARAM_INT); // Submission id.
diff --git a/mod/workshop/tests/behat/example_submission.feature b/mod/workshop/tests/behat/example_submission.feature
new file mode 100644 (file)
index 0000000..52bc641
--- /dev/null
@@ -0,0 +1,39 @@
+@mod @mod_workshop @_file_upload
+Feature: Provide example submission
+  In order to let students practise the assessment process in the workshop
+  As a teacher
+  I need to be able to define example submission and its referential assessment
+
+  @javascript
+  Scenario: Add example submission with attachments to a workshop
+    # Prepare the users, course, enrolments and the workshop instance.
+    Given the following "users" exist:
+      | username | firstname | lastname | email            |
+      | teacher1 | Terry1    | Teacher1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname  | shortname |
+      | Course1   | c1        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | c1     | editingteacher |
+    And the following "activities" exist:
+      | activity | name         | intro                     | course | idnumber  | useexamples |
+      | workshop | TestWorkshop | Test workshop description | c1     | workshop1 | 1           |
+    # As a teacher, define the assessment form to be used in the workshop.
+    When I log in as "teacher1"
+    And I follow "Course1"
+    And I edit assessment form in workshop "TestWorkshop" as:"
+      | id_description__idx_0_editor | Aspect1 |
+      | id_description__idx_1_editor | Aspect2 |
+      | id_description__idx_2_editor |         |
+    # Add an example submission with an attachment.
+    And I press "Add example submission"
+    And I set the following fields to these values:
+      | Title | First example submission |
+      | Submission content | Just an example but hey, it works! |
+      | Attachment | lib/tests/fixtures/empty.txt |
+    And I press "Save changes"
+    # Make sure that the submission was saved.
+    Then I should see "First example submission"
+    And I should see "Just an example but hey, it works!"
+    And "empty.txt" "link" should exist
diff --git a/mod/workshop/tests/behat/grade_to_pass.feature b/mod/workshop/tests/behat/grade_to_pass.feature
new file mode 100644 (file)
index 0000000..da72a7a
--- /dev/null
@@ -0,0 +1,91 @@
+@mod @mod_workshop
+Feature: Setting grades to pass via workshop editing form
+  In order to define grades to pass
+  As a teacher
+  I can set them in the workshop settings form, without the need to go to the gradebook
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Terry1    | Teacher1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname  | shortname |
+      | Course1   | c1        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | c1     | editingteacher |
+
+  Scenario: Adding a new workshop with grade to pass field set
+    Given I log in as "teacher1"
+    And I follow "Course1"
+    And I turn editing mode on
+    When I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Awesome workshop |
+      | Description | Grades to pass are set here |
+      | Submission grade to pass | 45   |
+      | Assessment grade to pass | 10.5 |
+    Then I should not see "Adding a new Workshop"
+    And I follow "Awesome workshop"
+    And I navigate to "Edit settings" node in "Workshop administration"
+    And the field "Submission grade to pass" matches value "45.00"
+    And the field "Assessment grade to pass" matches value "10.50"
+
+  Scenario: Adding a new workshop with grade to pass fields left empty
+    Given I log in as "teacher1"
+    And I follow "Course1"
+    And I turn editing mode on
+    When I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Another awesome workshop |
+      | Description | No grades to pass are set here |
+      | Submission grade to pass |    |
+      | Assessment grade to pass |    |
+    Then I should not see "Adding a new Workshop"
+    And I follow "Another awesome workshop"
+    And I navigate to "Edit settings" node in "Workshop administration"
+    And the field "Submission grade to pass" matches value "0.00"
+    And the field "Assessment grade to pass" matches value "0.00"
+
+  Scenario: Adding a new workshop with non-numeric value of a grade to pass
+    Given I log in as "teacher1"
+    And I follow "Course1"
+    And I turn editing mode on
+    When I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Almost awesome workshop |
+      | Description | Invalid grade to pass is set here |
+      | Assessment grade to pass | You shall not pass! |
+    Then I should see "Adding a new Workshop"
+    And I should see "You must enter a number here"
+
+  Scenario: Adding a new workshop with invalid value of a grade to pass
+    Given I log in as "teacher1"
+    And I follow "Course1"
+    And I turn editing mode on
+    When I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Almost awesome workshop |
+      | Description | Invalid grade to pass is set here |
+      | Assessment grade to pass | 10000000 |
+    Then I should see "Adding a new Workshop"
+    And I should see "The grade to pass can not be greater than the maximum possible grade"
+
+  Scenario: Emptying grades to pass fields sets them to zero
+    Given I log in as "teacher1"
+    And I follow "Course1"
+    And I turn editing mode on
+    And I add a "Workshop" to section "1" and I fill the form with:
+      | Workshop name | Super awesome workshop |
+      | Description | Grade to pass are set and then unset here |
+      | Submission grade to pass | 59.99 |
+      | Assessment grade to pass | 0.000 |
+    And I should not see "Adding a new Workshop"
+    And I follow "Super awesome workshop"
+    And I navigate to "Edit settings" node in "Workshop administration"
+    And the field "Submission grade to pass" matches value "59.99"
+    And the field "Assessment grade to pass" matches value "0.00"
+    When I set the field "Submission grade to pass" to ""
+    And I set the field "Assessment grade to pass" to ""
+    And I press "Save and display"
+    Then I should not see "Adding a new Workshop"
+    And I follow "Super awesome workshop"
+    And I navigate to "Edit settings" node in "Workshop administration"
+    And the field "Submission grade to pass" matches value "0.00"
+    And the field "Assessment grade to pass" matches value "0.00"
index 8da4417..e57da66 100644 (file)
@@ -2,9 +2,9 @@
   "name": "Moodle",
   "dependencies": {
     "abbrev": {
-      "version": "1.0.7",
+      "version": "1.0.9",
       "from": "abbrev@>=1.0.0 <2.0.0",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz"
     },
     "acorn": {
       "version": "3.2.0",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
     },
     "ansi-styles": {
-      "version": "2.1.0",
-      "from": "ansi-styles@>=2.1.0 <3.0.0",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.1.0.tgz"
+      "version": "2.2.1",
+      "from": "ansi-styles@>=2.2.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
     },
     "argparse": {
       "version": "1.0.7",
-      "from": "argparse@>=1.0.2 <2.0.0",
+      "from": "argparse@>=1.0.7 <2.0.0",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.7.tgz"
     },
+    "array-differ": {
+      "version": "1.0.0",
+      "from": "array-differ@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz"
+    },
+    "array-find-index": {
+      "version": "1.0.1",
+      "from": "array-find-index@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.1.tgz"
+    },
     "array-union": {
       "version": "1.0.2",
       "from": "array-union@>=1.0.1 <2.0.0",
     },
     "async": {
       "version": "1.5.2",
-      "from": "async@*",
+      "from": "async@1.5.2",
       "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz"
     },
+    "autoprefixer": {
+      "version": "6.3.7",
+      "from": "autoprefixer@>=6.0.0 <7.0.0",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.3.7.tgz"
+    },
     "aws-sign2": {
       "version": "0.6.0",
       "from": "aws-sign2@>=0.6.0 <0.7.0",
       "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz"
     },
     "balanced-match": {
-      "version": "0.3.0",
-      "from": "balanced-match@>=0.3.0 <0.4.0",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz"
+      "version": "0.4.1",
+      "from": "balanced-match@>=0.4.1 <0.5.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
     },
     "bl": {
       "version": "1.1.2",
       "from": "bl@>=1.1.2 <1.2.0",
-      "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz",
-      "dependencies": {
-        "isarray": {
-          "version": "1.0.0",
-          "from": "isarray@>=1.0.0 <1.1.0",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
-        },
-        "readable-stream": {
-          "version": "2.0.6",
-          "from": "readable-stream@>=2.0.5 <2.1.0",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz"
-        }
-      }
+      "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz"
     },
     "bluebird": {
       "version": "3.4.1",
       "from": "body-parser@>=1.14.0 <1.15.0",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz",
       "dependencies": {
-        "debug": {
-          "version": "2.2.0",
-          "from": "debug@~2.2.0",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
+        "qs": {
+          "version": "5.2.0",
+          "from": "qs@5.2.0",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz"
         }
       }
     },
       "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
     },
     "brace-expansion": {
-      "version": "1.1.2",
+      "version": "1.1.5",
       "from": "brace-expansion@>=1.0.0 <2.0.0",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.2.tgz"
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.5.tgz"
     },
     "browserify-zlib": {
       "version": "0.1.4",
       "from": "browserify-zlib@>=0.1.4 <0.2.0",
       "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz"
     },
+    "browserslist": {
+      "version": "1.3.5",
+      "from": "browserslist@>=1.3.4 <1.4.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.3.5.tgz"
+    },
     "builtin-modules": {
       "version": "1.1.1",
       "from": "builtin-modules@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz"
     },
     "camelcase": {
-      "version": "2.0.1",
+      "version": "2.1.1",
       "from": "camelcase@>=2.0.0 <3.0.0",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.0.1.tgz"
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz"
     },
     "camelcase-keys": {
-      "version": "2.0.0",
+      "version": "2.1.0",
       "from": "camelcase-keys@>=2.0.0 <3.0.0",
-      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.0.0.tgz"
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz"
+    },
+    "caniuse-db": {
+      "version": "1.0.30000512",
+      "from": "caniuse-db@>=1.0.30000488 <2.0.0",
+      "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000512.tgz"
     },
     "caseless": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz"
     },
     "chalk": {
-      "version": "1.1.1",
-      "from": "chalk@>=1.0.0 <2.0.0",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.1.tgz"
+      "version": "1.1.3",
+      "from": "chalk@>=1.1.3 <2.0.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz"
     },
     "cli": {
       "version": "0.6.6",
     "cliui": {
       "version": "2.1.0",
       "from": "cliui@>=2.1.0 <3.0.0",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz"
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+      "dependencies": {
+        "wordwrap": {
+          "version": "0.0.2",
+          "from": "wordwrap@0.0.2",
+          "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz"
+        }
+      }
+    },
+    "clone-regexp": {
+      "version": "1.0.0",
+      "from": "clone-regexp@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.0.tgz"
     },
     "code-point-at": {
       "version": "1.0.0",
       "from": "coffee-script@>=1.10.0 <1.11.0",
       "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.10.0.tgz"
     },
+    "color-diff": {
+      "version": "0.1.7",
+      "from": "color-diff@>=0.1.3 <0.2.0",
+      "resolved": "https://registry.npmjs.org/color-diff/-/color-diff-0.1.7.tgz"
+    },
+    "colorguard": {
+      "version": "1.2.0",
+      "from": "colorguard@>=1.2.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/colorguard/-/colorguard-1.2.0.tgz",
+      "dependencies": {
+        "yargs": {
+          "version": "1.3.3",
+          "from": "yargs@>=1.2.6 <2.0.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.3.tgz"
+        }
+      }
+    },
     "colors": {
       "version": "1.1.2",
       "from": "colors@>=1.1.2 <1.2.0",
     "concat-stream": {
       "version": "1.5.1",
       "from": "concat-stream@>=1.4.6 <2.0.0",
-      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz",
-      "dependencies": {
-        "readable-stream": {
-          "version": "2.0.5",
-          "from": "readable-stream@>=2.0.0 <2.1.0",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.5.tgz"
-        }
-      }
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz"
     },
     "console-browserify": {
       "version": "1.1.0",
       "from": "core-util-is@>=1.0.0 <1.1.0",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
     },
+    "cosmiconfig": {
+      "version": "1.1.0",
+      "from": "cosmiconfig@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-1.1.0.tgz",
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "from": "minimist@>=1.2.0 <2.0.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+        }
+      }
+    },
     "cpr": {
       "version": "0.0.6",
       "from": "cpr@>=0.0.6 <0.1.0",
       "from": "cryptiles@>=2.0.0 <3.0.0",
       "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
     },
+    "css-color-names": {
+      "version": "0.0.3",
+      "from": "css-color-names@0.0.3",
+      "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.3.tgz"
+    },
+    "css-rule-stream": {
+      "version": "1.1.0",
+      "from": "css-rule-stream@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/css-rule-stream/-/css-rule-stream-1.1.0.tgz"
+    },
+    "css-tokenize": {
+      "version": "1.0.1",
+      "from": "css-tokenize@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/css-tokenize/-/css-tokenize-1.0.1.tgz",
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "from": "isarray@0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+        },
+        "readable-stream": {
+          "version": "1.1.14",
+          "from": "readable-stream@>=1.0.33 <2.0.0",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz"
+        }
+      }
+    },
     "csslint": {
       "version": "0.10.0",
       "from": "csslint@>=0.10.0 <0.11.0",
       "from": "cssproc@>=0.0.1 <0.1.0",
       "resolved": "https://registry.npmjs.org/cssproc/-/cssproc-0.0.7.tgz"
     },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "from": "currently-unhandled@>=0.4.1 <0.5.0",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz"
+    },
     "d": {
       "version": "0.1.1",
       "from": "d@>=0.1.1 <0.2.0",
       "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz"
     },
     "debug": {
-      "version": "0.7.4",
-      "from": "debug@>=0.7.0 <0.8.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz"
+      "version": "2.2.0",
+      "from": "debug@>=2.1.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
     },
     "decamelize": {
-      "version": "1.1.2",
+      "version": "1.2.0",
       "from": "decamelize@>=1.1.2 <2.0.0",
-      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.1.2.tgz"
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
     },
     "deep-is": {
       "version": "0.1.3",
           "version": "1.1.6",
           "from": "esutils@>=1.1.6 <2.0.0",
           "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz"
-        },
-        "isarray": {
-          "version": "1.0.0",
-          "from": "isarray@>=1.0.0 <2.0.0",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+        }
+      }
+    },
+    "doiuse": {
+      "version": "2.4.1",
+      "from": "doiuse@>=2.3.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/doiuse/-/doiuse-2.4.1.tgz",
+      "dependencies": {
+        "source-map": {
+          "version": "0.4.4",
+          "from": "source-map@>=0.4.2 <0.5.0",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz"
         }
       }
     },
           "version": "1.1.3",
           "from": "domelementtype@>=1.1.1 <1.2.0",
           "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz"
-        },
-        "entities": {
-          "version": "1.1.1",
-          "from": "entities@>=1.1.1 <1.2.0",
-          "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz"
         }
       }
     },
     "domelementtype": {
       "version": "1.3.0",
-      "from": "domelementtype@>=1.0.0 <2.0.0",
+      "from": "domelementtype@>=1.3.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz"
     },
     "domhandler": {
       "version": "2.3.0",
-      "from": "domhandler@>=2.3.0 <2.4.0",
+      "from": "domhandler@>=2.3.0 <3.0.0",
       "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz"
     },
     "domutils": {
       "version": "1.5.1",