Merge branch 'MDL-59831_master' of git://github.com/markn86/moodle
authorDavid Monllao <davidm@moodle.com>
Tue, 12 Sep 2017 06:07:52 +0000 (08:07 +0200)
committerDavid Monllao <davidm@moodle.com>
Tue, 12 Sep 2017 06:07:52 +0000 (08:07 +0200)
125 files changed:
.eslintrc
Gruntfile.js
admin/roles/classes/capability_table_with_risks.php
admin/tool/lp/amd/src/planactions.js
blocks/activity_results/block_activity_results.php
blocks/calendar_month/block_calendar_month.php
blocks/calendar_month/tests/behat/block_calendar_month.feature
blocks/myoverview/amd/src/event_list.js
blocks/participants/block_participants.php
calendar/amd/build/calendar.min.js
calendar/amd/build/calendar_filter.min.js [new file with mode: 0644]
calendar/amd/build/calendar_mini.min.js [new file with mode: 0644]
calendar/amd/build/event_form.min.js
calendar/amd/build/events.min.js
calendar/amd/build/modal_event_form.min.js
calendar/amd/build/selectors.min.js [new file with mode: 0644]
calendar/amd/build/view_manager.min.js
calendar/amd/src/calendar.js
calendar/amd/src/calendar_filter.js [new file with mode: 0644]
calendar/amd/src/calendar_mini.js [new file with mode: 0644]
calendar/amd/src/event_form.js
calendar/amd/src/events.js
calendar/amd/src/modal_event_form.js
calendar/amd/src/selectors.js [new file with mode: 0644]
calendar/amd/src/view_manager.js
calendar/classes/external/calendar_event_exporter.php
calendar/classes/external/day_exporter.php
calendar/classes/external/month_exporter.php
calendar/classes/external/week_day_exporter.php [new file with mode: 0644]
calendar/classes/external/week_exporter.php
calendar/classes/local/event/forms/create.php
calendar/lib.php
calendar/renderer.php
calendar/set.php [deleted file]
calendar/templates/calendar_mini.mustache [new file with mode: 0644]
calendar/templates/calendar_month.mustache [new file with mode: 0644]
calendar/templates/event_filter.mustache [new file with mode: 0644]
calendar/templates/event_filter_key.mustache [new file with mode: 0644]
calendar/templates/minicalendar_day_link.mustache
calendar/templates/modal_event_form.mustache
calendar/templates/month_detailed.mustache
calendar/templates/month_mini.mustache [new file with mode: 0644]
calendar/tests/behat/behat_calendar.php
calendar/view.php
course/amd/src/actions.js
course/classes/search/mycourse.php
course/lib.php
course/recent_form.php
course/tests/courselib_test.php
enrol/externallib.php
lang/en/access.php
lang/en/calendar.php
lang/en/deprecated.txt
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/amd/src/templates.js
lib/classes/analytics/target/no_teaching.php
lib/filebrowser/file_browser.php
lib/filebrowser/file_info_context_course.php
lib/filebrowser/file_info_context_coursecat.php
lib/filebrowser/file_info_context_module.php
lib/filebrowser/file_info_context_system.php
lib/filebrowser/tests/file_browser_test.php [new file with mode: 0644]
lib/navigationlib.php
lib/templates/loginform.mustache
lib/upgrade.txt
message/classes/api.php
message/output/popup/amd/src/notification_area_control_area.js
mod/assign/classes/search/activity.php
mod/assign/locallib.php
mod/assign/tests/behat/allow_another_attempt.feature
mod/assign/upgrade.txt
mod/book/classes/search/activity.php
mod/book/classes/search/chapter.php
mod/book/lib.php
mod/chat/classes/search/activity.php
mod/choice/classes/search/activity.php
mod/data/classes/search/activity.php
mod/feedback/classes/search/activity.php
mod/folder/classes/search/activity.php
mod/forum/classes/search/activity.php
mod/forum/classes/search/post.php
mod/forum/lib.php
mod/glossary/classes/search/activity.php
mod/glossary/classes/search/entry.php
mod/imscp/classes/search/activity.php
mod/label/classes/search/activity.php
mod/lesson/classes/search/activity.php
mod/lti/classes/search/activity.php
mod/page/classes/search/activity.php
mod/quiz/classes/search/activity.php
mod/resource/classes/search/activity.php
mod/resource/tests/search_test.php
mod/scorm/classes/search/activity.php
mod/scorm/mod_form.php
mod/survey/classes/search/activity.php
mod/url/classes/search/activity.php
mod/wiki/classes/search/activity.php
mod/wiki/classes/search/collaborative_page.php
mod/workshop/classes/search/activity.php
notes/delete.php
notes/edit.php
notes/index.php
npm-shrinkwrap.json
package.json
phpunit.xml.dist
repository/local/lib.php
search/classes/base.php
search/classes/base_activity.php
search/classes/base_mod.php
search/classes/manager.php
search/tests/base_activity_test.php [new file with mode: 0644]
search/tests/base_test.php [new file with mode: 0644]
search/tests/behat/behat_search.php [new file with mode: 0644]
search/tests/behat/search_query.feature [new file with mode: 0644]
theme/boost/scss/moodle/calendar.scss
theme/boost/templates/core_calendar/minicalendar_day_link.mustache
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/core_calendar/minicalendar_day_link.mustache
user/classes/search/user.php
user/externallib.php
user/index.php
user/messageselect.php
webservice/rest/simpleserver.php

index c50d2bb..961467d 100644 (file)
--- a/.eslintrc
+++ b/.eslintrc
@@ -16,6 +16,7 @@
 
     // === Possible Errors ===
     'comma-dangle': 'off',
+    'no-compare-neg-zero': 'error',
     'no-cond-assign': 'error',
     'no-console': 'error',
     'no-constant-condition': 'error',
@@ -67,7 +68,6 @@
     'no-fallthrough': 'error',
     'no-floating-decimal': 'warn',
     'no-global-assign': 'warn',
-    // Enabled by grunt for AMD modules: 'no-implicit-globals': 'error',
     'no-implied-eval': 'error',
     'no-invalid-this': 'error',
     'no-iterator': 'error',
     'block-spacing': 'warn',
     'brace-style': ['warn', '1tbs'],
     'camelcase': 'warn',
+    'capitalized-comments': ['warn', 'always', { 'ignoreConsecutiveComments': true }],
     'comma-spacing': ['warn', { 'before': false, 'after': true }],
     'comma-style': ['warn', 'last'],
     'computed-property-spacing': 'error',
         'message': 'Use AMD module "core/str" or M.util.get_string()'
     }],
 
-  }
+  },
+  overrides: [
+    {
+      files: ["**/yui/src/**/*.js"],
+      // Disable some rules which we can't safely define for YUI rollups.
+      rules: {
+        'no-undef': 'off',
+        'no-unused-vars': 'off',
+        'no-unused-expressions': 'off'
+      }
+    },
+    {
+      files: ["**/amd/src/*.js"],
+      // Check AMD with some slightly stricter rules.
+      rules: {
+        'no-unused-vars': 'error',
+        'no-implicit-globals': 'error'
+      }
+    }
+  ]
 }
index 2026c23..e0afa2d 100644 (file)
@@ -112,26 +112,9 @@ module.exports = function(grunt) {
             // Even though warnings dont stop the build we don't display warnings by default because
             // at this moment we've got too many core warnings.
             options: {quiet: !grunt.option('show-lint-warnings')},
-            amd: {
-              src: amdSrc,
-              // Check AMD with some slightly stricter rules.
-              rules: {
-                'no-unused-vars': 'error',
-                'no-implicit-globals': 'error'
-              }
-            },
+            amd: {src: amdSrc},
             // Check YUI module source files.
-            yui: {
-               src: ['**/yui/src/**/*.js', '!*/**/yui/src/*/meta/*.js'],
-               options: {
-                   // Disable some rules which we can't safely define for YUI rollups.
-                   rules: {
-                     'no-undef': 'off',
-                     'no-unused-vars': 'off',
-                     'no-unused-expressions': 'off'
-                   }
-               }
-            }
+            yui: {src: ['**/yui/src/**/*.js', '!*/**/yui/src/*/meta/*.js']}
         },
         uglify: {
             amd: {
index b62ad19..db7e16b 100644 (file)
@@ -183,9 +183,14 @@ abstract class core_role_capability_table_with_risks extends core_role_capabilit
     public function get_risk_icon($type) {
         global $OUTPUT;
 
-        $text = $OUTPUT->pix_icon('i/' . str_replace('risk', 'risk_', $type), get_string($type . 'short', 'admin'));
+        $alt = get_string("{$type}short", "admin");
+        $title = get_string($type, "admin");
+
+        $text = $OUTPUT->pix_icon('i/' . str_replace('risk', 'risk_', $type), $alt, 'moodle', [
+                'title' => $title,
+            ]);
         $action = new popup_action('click', $this->risksurl, 'docspopup');
-        $riskicon = $OUTPUT->action_link($this->risksurl, $text, $action, array('title'=>get_string($type, 'admin')));
+        $riskicon = $OUTPUT->action_link($this->risksurl, $text, $action);
 
         return $riskicon;
     }
index be39396..44a335d 100644 (file)
@@ -229,7 +229,7 @@ define(['jquery',
                 notification.confirm(
                     strings[0], // Confirm.
                     strings[1], // Reopen plan X?
-                    strings[2], // reopen.
+                    strings[2], // Reopen.
                     strings[3], // Cancel.
                     function() {
                         self._doReopenPlan(planData);
index de9b9ed..52fb8e9 100644 (file)
@@ -26,6 +26,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->dirroot . '/lib/grade/constants.php');
+require_once($CFG->dirroot . '/course/lib.php');
 
 define('B_ACTIVITYRESULTS_NAME_FORMAT_FULL', 1);
 define('B_ACTIVITYRESULTS_NAME_FORMAT_ID',   2);
@@ -328,7 +329,7 @@ class block_activity_results extends block_base {
                 if ($nameformat == B_ACTIVITYRESULTS_NAME_FORMAT_FULL) {
                     if (has_capability('moodle/course:managegroups', $context)) {
                         $grouplink = $CFG->wwwroot.'/group/overview.php?id='.$courseid.'&amp;group=';
-                    } else if (has_capability('moodle/course:viewparticipants', $context)) {
+                    } else if (course_can_view_participants($context)) {
                         $grouplink = $CFG->wwwroot.'/user/index.php?id='.$courseid.'&amp;group=';
                     } else {
                         $grouplink = '';
index cc3d6f6..18a57d7 100644 (file)
@@ -70,29 +70,26 @@ class block_calendar_month extends block_base {
         if ($issite) {
             // Being displayed at site level. This will cause the filter to fall back to auto-detecting
             // the list of courses it will be grabbing events from.
-            $filtercourse = calendar_get_default_courses();
+            $course = get_site();
+            $courses = calendar_get_default_courses();
         } else {
             // Forcibly filter events to include only those from the particular course we are in.
-            $filtercourse = array($courseid => $this->page->course);
+            $course = $this->page->course;
+            $courses = [$course->id => $course];
         }
 
-        list($courses, $group, $user) = calendar_set_filters($filtercourse);
-        if ($issite) {
-            // For the front page.
-            $this->content->text .= calendar_get_mini($courses, $group, $user, false, false,
-                'frontpage', $courseid, $time);
-            // No filters for now.
-        } else {
-            // For any other course.
-            $this->content->text .= calendar_get_mini($courses, $group, $user, false, false,
-                'course', $courseid, $time);
-            $this->content->text .= '<h3 class="eventskey">'.get_string('eventskey', 'calendar').'</h3>';
-            $this->content->text .= '<div class="filters calendar_filters">' .
-                calendar_filter_controls($this->page->url) . '</div>';
+        $renderer = $this->page->get_renderer('core_calendar');
+
+        $calendar = new calendar_information(0, 0, 0, $time);
+        $calendar->prepare_for_view($course, $courses);
+
+        list($data, $template) = calendar_get_view($calendar, 'mini');
+        $this->content->text .= $renderer->render_from_template($template, $data);
+
+        if (!$issite) {
+            $this->content->text .= $renderer->event_filter();
         }
 
         return $this->content;
     }
 }
-
-
index 8753167..bc34346 100644 (file)
@@ -23,7 +23,7 @@ Feature: Enable the calendar block in a course and test it's functionality
     Given I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     When I add the "Calendar" block
-    Then I should see "Events key" in the "Calendar" "block"
+    Then "Calendar" "block" should exist
 
   @javascript
   Scenario: View a global event in the calendar block
@@ -136,9 +136,14 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I press "Save and display"
     And I turn editing mode on
     And I add the "Calendar" block
-    And I create a calendar event with form data:
+    And I click on "This month" "link"
+    And I click on "New event" "button"
+    And I set the following fields to these values:
       | id_eventtype | Group |
       | id_name | Group Event |
+    And I set the following fields to these values:
+      | Group | Group 1 |
+    And I press "Save"
     And I log out
     Then I log in as "student1"
     And I am on "Course 1" course homepage
@@ -173,9 +178,14 @@ Feature: Enable the calendar block in a course and test it's functionality
       | id_eventtype | Course |
       | id_name | Course Event 1 |
     And I am on "Course 1" course homepage
-    And I create a calendar event with form data:
+    And I click on "This month" "link"
+    And I click on "New event" "button"
+    And I set the following fields to these values:
       | id_eventtype | Group |
       | id_name | Group Event 1 |
+    And I set the following fields to these values:
+      | Group | Group 1 |
+    And I press "Save"
     And I log out
     Then I log in as "student1"
     And I am on "Course 1" course homepage
index 5d1d42d..9d96fc5 100644 (file)
@@ -369,7 +369,7 @@ define(['jquery', 'core/notification', 'core/templates',
             // Render the events.
             return render(root, calendarEvents).then(function(renderCount) {
                 if (renderCount < calendarEvents.length) {
-                    // if the number of events that was rendered is less than
+                    // If the number of events that was rendered is less than
                     // the number we sent for rendering we can assume that there
                     // are no groups to add them in. Since the ordering of the
                     // events is guaranteed it means that any future requests will
index f8214df..b1b433b 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * Participants block
+ *
+ * @package    block_participants
+ * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
 class block_participants extends block_list {
     function init() {
         $this->title = get_string('pluginname', 'block_participants');
@@ -48,12 +59,12 @@ class block_participants extends block_list {
             $this->content = '';
             return $this->content;
         } else if ($this->page->course->id == SITEID) {
-            if (!has_capability('moodle/site:viewparticipants', context_system::instance())) {
+            if (!course_can_view_participants(context_system::instance())) {
                 $this->content = '';
                 return $this->content;
             }
         } else {
-            if (!has_capability('moodle/course:viewparticipants', $currentcontext)) {
+            if (!course_can_view_participants($currentcontext)) {
                 $this->content = '';
                 return $this->content;
             }
index aac1c29..1999702 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
diff --git a/calendar/amd/build/calendar_filter.min.js b/calendar/amd/build/calendar_filter.min.js
new file mode 100644 (file)
index 0000000..ef94d15
Binary files /dev/null and b/calendar/amd/build/calendar_filter.min.js differ
diff --git a/calendar/amd/build/calendar_mini.min.js b/calendar/amd/build/calendar_mini.min.js
new file mode 100644 (file)
index 0000000..979e92e
Binary files /dev/null and b/calendar/amd/build/calendar_mini.min.js differ
index 3a3c976..1f6e3ff 100644 (file)
Binary files a/calendar/amd/build/event_form.min.js and b/calendar/amd/build/event_form.min.js differ
index 762a824..77b2153 100644 (file)
Binary files a/calendar/amd/build/events.min.js and b/calendar/amd/build/events.min.js differ
index b27c823..4d52804 100644 (file)
Binary files a/calendar/amd/build/modal_event_form.min.js and b/calendar/amd/build/modal_event_form.min.js differ
diff --git a/calendar/amd/build/selectors.min.js b/calendar/amd/build/selectors.min.js
new file mode 100644 (file)
index 0000000..8dbeb18
Binary files /dev/null and b/calendar/amd/build/selectors.min.js differ
index d502824..301ef42 100644 (file)
Binary files a/calendar/amd/build/view_manager.min.js and b/calendar/amd/build/view_manager.min.js differ
index 947bac0..2958c83 100644 (file)
@@ -296,9 +296,7 @@ define([
     /**
      * Register event listeners for the module.
      */
-    var registerEventListeners = function() {
-        var root = $(SELECTORS.ROOT);
-
+    var registerEventListeners = function(root) {
         // Bind click events to event links.
         root.on('click', SELECTORS.EVENT_ITEM, function(e) {
             e.preventDefault();
@@ -350,9 +348,11 @@ define([
     };
 
     return {
-        init: function() {
-            CalendarViewManager.init();
-            registerEventListeners();
+        init: function(root) {
+            root = $(root);
+
+            CalendarViewManager.init(root);
+            registerEventListeners(root);
         }
     };
 });
diff --git a/calendar/amd/src/calendar_filter.js b/calendar/amd/src/calendar_filter.js
new file mode 100644 (file)
index 0000000..5352a7c
--- /dev/null
@@ -0,0 +1,119 @@
+// 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/>.
+
+/**
+ * This module is responsible for the calendar filter.
+ *
+ * @module     core_calendar/calendar_filter
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+    'core/str',
+    'core/templates',
+],
+function(
+    $,
+    CalendarSelectors,
+    CalendarEvents,
+    Str,
+    Templates
+) {
+
+    var registerEventListeners = function(root) {
+        root.on('click', CalendarSelectors.eventFilterItem, function(e) {
+            var target = $(e.currentTarget);
+
+            toggleFilter(target);
+
+            e.preventDefault();
+        });
+
+        $('body').on(CalendarEvents.viewUpdated, function() {
+            var filters = root.find(CalendarSelectors.eventFilterItem);
+
+            filters.each(function(i, filter) {
+                filter = $(filter);
+                if (filter.data('eventtype-hidden')) {
+                    var data = getFilterData(filter);
+                    fireFilterChangedEvent(data);
+                }
+            });
+        });
+    };
+
+    var toggleFilter = function(target) {
+        var data = getFilterData(target);
+
+        // Toggle the hidden. We need to render the template before we change the value.
+        data.hidden = !data.hidden;
+
+        return Str.get_string('eventtype' + data.type, 'calendar')
+        .then(function(nameStr) {
+            data.name = nameStr;
+
+            return data;
+        })
+        .then(function(context) {
+            return Templates.render('core_calendar/event_filter_key', context);
+        })
+        .then(function(html, js) {
+            return Templates.replaceNode(target, html, js);
+        })
+        .then(function() {
+            fireFilterChangedEvent(data);
+            return;
+        });
+    };
+
+    /**
+     * Fire the filterChanged event for the specified data.
+     *
+     * @param   {object} data The data to include
+     */
+    var fireFilterChangedEvent = function(data) {
+        M.util.js_pending("month-mini-filterChanged");
+        $('body').trigger(CalendarEvents.filterChanged, {
+            type: data.type,
+            hidden: data.hidden,
+        });
+        M.util.js_complete("month-mini-filterChanged");
+    };
+
+    /**
+     * Get the filter data for the specified target.
+     *
+     * @param   {jQuery} target The target node
+     * @return  {Object}
+     */
+    var getFilterData = function(target) {
+        return {
+            type: target.data('eventtype'),
+            hidden: target.data('eventtype-hidden'),
+        };
+    };
+
+    return {
+        init: function(root) {
+            root = $(root);
+
+            registerEventListeners(root);
+        }
+    };
+});
diff --git a/calendar/amd/src/calendar_mini.js b/calendar/amd/src/calendar_mini.js
new file mode 100644 (file)
index 0000000..48a4f44
--- /dev/null
@@ -0,0 +1,57 @@
+// 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/>.
+
+/**
+ * This module is the highest level module for the calendar. It is
+ * responsible for initialising all of the components required for
+ * the calendar to run. It also coordinates the interaction between
+ * components by listening for and responding to different events
+ * triggered within the calendar UI.
+ *
+ * @module     core_calendar/calendar
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+    'core_calendar/view_manager',
+],
+function(
+    $,
+    CalendarSelectors,
+    CalendarEvents,
+    CalendarViewManager
+) {
+
+    var registerEventListeners = function(root) {
+        $('body').on(CalendarEvents.filterChanged, function(e, data) {
+            var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]);
+
+            daysWithEvent.toggleClass('calendar_event_' + data.type, !data.hidden);
+        });
+    };
+
+    return {
+        init: function(root) {
+            root = $(root);
+
+            CalendarViewManager.init(root);
+            registerEventListeners(root);
+        }
+    };
+});
index ea3e417..f537f6d 100644 (file)
  * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/templates'], function($, Templates) {
+define(['jquery'], function($) {
 
     var SELECTORS = {
-        EVENT_TYPE: '[name="eventtype"]',
-        EVENT_COURSE_ID: '[name="courseid"]',
         EVENT_GROUP_COURSE_ID: '[name="groupcourseid"]',
         EVENT_GROUP_ID: '[name="groupid"]',
-        FORM_GROUP: '.form-group',
         SELECT_OPTION: 'option',
-        ADVANCED_ELEMENT: '.fitem.advanced',
-        FIELDSET_ADVANCED_ELEMENTS: 'fieldset.containsadvancedelements',
-        MORELESS_TOGGLE: '.moreless-actions'
-    };
-
-    var EVENTS = {
-        SHOW_ADVANCED: 'event_form-show-advanced',
-        HIDE_ADVANCED: 'event_form-hide-advanced',
-        ADVANCED_SHOWN: 'event_form-advanced-shown',
-        ADVANCED_HIDDEN: 'event_form-advanced-hidden',
-    };
-
-    /**
-     * Find the old show more / show less toggle added by the mform and destroy it.
-     * We are handling the visibility of the advanced fields with the more/less button
-     * in the footer of the modal that this form is rendered within.
-     *
-     * @method destroyOldMoreLessToggle
-     * @param {object} formElement The root form element
-     */
-    var destroyOldMoreLessToggle = function(formElement) {
-        formElement.find(SELECTORS.FIELDSET_ADVANCED_ELEMENTS).removeClass('containsadvancedelements');
-        var element = formElement.find(SELECTORS.MORELESS_TOGGLE);
-        Templates.replaceNode(element, '', '');
-    };
-
-    /**
-     * Find each of the advanced form elements and make them visible.
-     *
-     * This function triggers the ADVANCED_SHOWN event for any other
-     * component to handle (e.g. the event form modal).
-     *
-     * @method destroyOldMoreLessToggle
-     * @param {object} formElement The root form element
-     */
-    var showAdvancedElements = function(formElement) {
-        formElement.find(SELECTORS.ADVANCED_ELEMENT).removeClass('hidden');
-        formElement.trigger(EVENTS.ADVANCED_SHOWN);
-    };
-
-    /**
-     * Find each of the advanced form elements and hide them.
-     *
-     * This function triggers the ADVANCED_HIDDEN event for any other
-     * component to handle (e.g. the event form modal).
-     *
-     * @method hideAdvancedElements
-     * @param {object} formElement The root form element
-     */
-    var hideAdvancedElements = function(formElement) {
-        formElement.find(SELECTORS.ADVANCED_ELEMENT).addClass('hidden');
-        formElement.trigger(EVENTS.ADVANCED_HIDDEN);
-    };
-
-    /**
-     * Listen for any events telling this module to show or hide it's
-     * advanced elements.
-     *
-     * This function listens for SHOW_ADVANCED and HIDE_ADVANCED.
-     *
-     * @method listenForShowHideEvents
-     * @param {object} formElement The root form element
-     */
-    var listenForShowHideEvents = function(formElement) {
-        formElement.on(EVENTS.SHOW_ADVANCED, function() {
-            showAdvancedElements(formElement);
-        });
-
-        formElement.on(EVENTS.HIDE_ADVANCED, function() {
-            hideAdvancedElements(formElement);
-        });
     };
 
     /**
@@ -177,28 +103,15 @@ define(['jquery', 'core/templates'], function($, Templates) {
      *
      * @method init
      * @param {string} formId The value of the form's id attribute
-     * @param {bool} hasError If the form has errors rendered form the server.
      */
-    var init = function(formId, hasError) {
+    var init = function(formId) {
         var formElement = $('#' + formId);
 
-        listenForShowHideEvents(formElement);
-        destroyOldMoreLessToggle(formElement);
         parseGroupSelect(formElement);
         addCourseGroupSelectListeners(formElement);
-
-        // If we know that the form has been rendered with server side
-        // errors then we need to display all of the elements in the form
-        // in case one of those elements has the error.
-        if (hasError) {
-            showAdvancedElements(formElement);
-        } else {
-            hideAdvancedElements(formElement);
-        }
     };
 
     return {
         init: init,
-        events: EVENTS,
     };
 });
index 95e0968..29b5fd2 100644 (file)
@@ -31,6 +31,8 @@ define([], function() {
         editActionEvent: 'calendar-events:edit_action_event',
         eventMoved: 'calendar-events:event_moved',
         monthChanged: 'calendar-events:month_changed',
-        moveEvent: 'calendar-events:move_event'
+        moveEvent: 'calendar-events:move_event',
+        filterChanged: 'calendar-events:filter_changed',
+        viewUpdated: 'calendar-events:view_updated',
     };
 });
index 034e98d..24f7b84 100644 (file)
@@ -33,8 +33,7 @@ define([
             'core/modal_registry',
             'core/fragment',
             'core_calendar/events',
-            'core_calendar/repository',
-            'core_calendar/event_form'
+            'core_calendar/repository'
         ],
         function(
             $,
@@ -47,13 +46,11 @@ define([
             ModalRegistry,
             Fragment,
             CalendarEvents,
-            Repository,
-            EventForm
+            Repository
         ) {
 
     var registered = false;
     var SELECTORS = {
-        MORELESS_BUTTON: '[data-action="more-less-toggle"]',
         SAVE_BUTTON: '[data-action="save"]',
         LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
     };
@@ -71,7 +68,6 @@ define([
         this.reloadingBody = false;
         this.reloadingTitle = false;
         this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
-        this.moreLessButton = this.getFooter().find(SELECTORS.MORELESS_BUTTON);
     };
 
     ModalEventForm.TYPE = 'core_calendar-modal_event_form';
@@ -185,7 +181,6 @@ define([
      */
     ModalEventForm.prototype.disableButtons = function() {
         this.saveButton.prop('disabled', true);
-        this.moreLessButton.prop('disabled', true);
     };
 
     /**
@@ -195,53 +190,6 @@ define([
      */
     ModalEventForm.prototype.enableButtons = function() {
         this.saveButton.prop('disabled', false);
-        this.moreLessButton.prop('disabled', false);
-    };
-
-    /**
-     * Set the more/less button in the footer to the "more"
-     * state.
-     *
-     * @method setMoreButton
-     */
-    ModalEventForm.prototype.setMoreButton = function() {
-        this.moreLessButton.attr('data-collapsed', 'true');
-        Str.get_string('more', 'calendar').then(function(string) {
-            this.moreLessButton.text(string);
-            return;
-        }.bind(this));
-    };
-
-    /**
-     * Set the more/less button in the footer to the "less"
-     * state.
-     *
-     * @method setLessButton
-     */
-    ModalEventForm.prototype.setLessButton = function() {
-        this.moreLessButton.attr('data-collapsed', 'false');
-        Str.get_string('less', 'calendar').then(function(string) {
-            this.moreLessButton.text(string);
-            return;
-        }.bind(this));
-    };
-
-    /**
-     * Toggle the more/less button in the footer from the current
-     * state to it's opposite state.
-     *
-     * @method toggleMoreLessButton
-     */
-    ModalEventForm.prototype.toggleMoreLessButton = function() {
-        var form = this.getForm();
-
-        if (this.moreLessButton.attr('data-collapsed') == 'true') {
-            form.trigger(EventForm.events.SHOW_ADVANCED);
-            this.setLessButton();
-        } else {
-            form.trigger(EventForm.events.HIDE_ADVANCED);
-            this.setMoreButton();
-        }
     };
 
     /**
@@ -288,10 +236,9 @@ define([
      *
      * @method reloadBodyContent
      * @param {string} formData The serialised form data
-     * @param {bool} hasError True if we know the form data is erroneous
      * @return {object} A promise resolved with the fragment html and js from
      */
-    ModalEventForm.prototype.reloadBodyContent = function(formData, hasError) {
+    ModalEventForm.prototype.reloadBodyContent = function(formData) {
         if (this.reloadingBody) {
             return this.bodyPromise;
         }
@@ -318,8 +265,6 @@ define([
             args.formdata = formData;
         }
 
-        args.haserror = (typeof hasError == 'undefined') ? false : hasError;
-
         this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
 
         this.setBody(this.bodyPromise);
@@ -416,7 +361,7 @@ define([
                     // If there was a server side validation error then
                     // we need to re-request the rendered form from the server
                     // in order to display the error for the user.
-                    return this.reloadBodyContent(formData, true);
+                    return this.reloadBodyContent(formData);
                 } else {
                     // No problemo! Our work here is done.
                     this.hide();
@@ -469,28 +414,6 @@ define([
             e.preventDefault();
             e.stopPropagation();
         }.bind(this));
-
-        // Toggle the state of the more/less button in the footer.
-        this.getModal().on(CustomEvents.events.activate, SELECTORS.MORELESS_BUTTON, function(e, data) {
-            this.toggleMoreLessButton();
-
-            data.originalEvent.preventDefault();
-            e.stopPropagation();
-        }.bind(this));
-
-        // When the event form tells us that the advanced fields are shown
-        // then the more/less button should be set to less to allow the user
-        // to hide the advanced fields.
-        this.getModal().on(EventForm.events.ADVANCED_SHOWN, function() {
-            this.setLessButton();
-        }.bind(this));
-
-        // When the event form tells us that the advanced fields are hidden
-        // then the more/less button should be set to more to allow the user
-        // to show the advanced fields.
-        this.getModal().on(EventForm.events.ADVANCED_HIDDEN, function() {
-            this.setMoreButton();
-        }.bind(this));
     };
 
     // Automatically register with the modal registry the first time this module is imported so that you can create modals
diff --git a/calendar/amd/src/selectors.js b/calendar/amd/src/selectors.js
new file mode 100644 (file)
index 0000000..89bb65a
--- /dev/null
@@ -0,0 +1,40 @@
+// 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/>.
+
+/**
+ * This module is responsible for the calendar filter.
+ *
+ * @module     core_calendar/calendar_selectors
+ * @package    core_calendar
+ * @copyright  2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([], function() {
+    return {
+        eventFilterItem: "[data-action='filter-event-type']",
+        eventType: {
+            site: "[data-eventtype-site]",
+            course: "[data-eventtype-course]",
+            group: "[data-eventtype-group]",
+            user: "[data-eventtype-user]",
+        },
+        popoverType: {
+            site: "[data-popover-eventtype-site]",
+            course: "[data-popover-eventtype-course]",
+            group: "[data-popover-eventtype-group]",
+            user: "[data-popover-eventtype-user]",
+        },
+    };
+});
index 5604d6e..f8636ef 100644 (file)
@@ -42,7 +42,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
             root.on('click', SELECTORS.CALENDAR_NAV_LINK, function(e) {
                 var courseId = $(root).find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
                 var link = $(e.currentTarget);
-                changeMonth(link.attr('href'), link.data('time'), courseId);
+                changeMonth(root, link.attr('href'), link.data('time'), courseId);
 
                 e.preventDefault();
             });
@@ -55,17 +55,19 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
          * @param {Number} courseid The id of the course whose events are shown
          * @return {promise}
          */
-        var refreshMonthContent = function(time, courseid) {
-            var root = $(SELECTORS.ROOT);
-
+        var refreshMonthContent = function(root, time, courseid) {
             startLoading(root);
 
             return CalendarRepository.getCalendarMonthData(time, courseid)
                 .then(function(context) {
-                    return Templates.render('core_calendar/month_detailed', context);
+                    return Templates.render(root.attr('data-template'), context);
                 })
                 .then(function(html, js) {
-                    return Templates.replaceNode(SELECTORS.CALENDAR_MONTH_WRAPPER, html, js);
+                    return Templates.replaceNode(root.find(SELECTORS.CALENDAR_MONTH_WRAPPER), html, js);
+                })
+                .then(function() {
+                    $('body').trigger(CalendarEvents.viewUpdated);
+                    return;
                 })
                 .always(function() {
                     return stopLoading(root);
@@ -81,10 +83,12 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
          * @param {Number} courseid The id of the course whose events are shown
          * @return {promise}
          */
-        var changeMonth = function(url, time, courseid) {
-            return refreshMonthContent(time, courseid)
+        var changeMonth = function(root, url, time, courseid) {
+            return refreshMonthContent(root, time, courseid)
                 .then(function() {
-                    window.history.pushState({}, '', url);
+                    if (url.length && url !== '#') {
+                        window.history.pushState({}, '', url);
+                    }
                     return arguments;
                 })
                 .then(function() {
@@ -106,7 +110,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
             if (!courseId) {
                 courseId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
             }
-            return refreshMonthContent(time, courseId);
+            return refreshMonthContent(root, time, courseId);
         };
 
         /**
@@ -134,8 +138,8 @@ define(['jquery', 'core/templates', 'core/notification', 'core_calendar/reposito
         };
 
         return {
-            init: function() {
-                registerEventListeners(SELECTORS.ROOT);
+            init: function(root) {
+                registerEventListeners(root);
             },
             reloadCurrentMonth: reloadCurrentMonth,
             changeMonth: changeMonth,
index 7ba5805..f83f273 100644 (file)
@@ -47,6 +47,16 @@ class calendar_event_exporter extends event_exporter_base {
 
         $values = parent::define_other_properties();
         $values['url'] = ['type' => PARAM_URL];
+        $values['islastday'] = [
+            'type' => PARAM_BOOL,
+            'default' => false,
+        ];
+        $values['calendareventtype'] = [
+            'type' => PARAM_TEXT,
+        ];
+        $values['popupname'] = [
+            'type' => PARAM_RAW,
+        ];
 
         return $values;
     }
@@ -58,6 +68,8 @@ class calendar_event_exporter extends event_exporter_base {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        global $CFG;
+
         $values = parent::get_other_values($output);
 
         $eventid = $this->event->get_id();
@@ -65,6 +77,55 @@ class calendar_event_exporter extends event_exporter_base {
         $url = new \moodle_url($this->related['daylink'], [], "event_{$eventid}");
         $values['url'] = $url->out(false);
 
+        $values['islastday'] = false;
+        $today = $this->related['type']->timestamp_to_date_array($this->related['today']);
+
+        $values['popupname'] = $this->event->get_name();
+
+        $times = $this->event->get_times();
+        if ($duration = $times->get_duration()) {
+            $enddate = $this->related['type']->timestamp_to_date_array($times->get_end_time()->getTimestamp());
+            $values['islastday'] = true;
+            $values['islastday'] = $values['islastday'] && $enddate['year'] == $today['year'];
+            $values['islastday'] = $values['islastday'] && $enddate['mon'] == $today['mon'];
+            $values['islastday'] = $values['islastday'] && $enddate['mday'] == $today['mday'];
+        }
+
+        $subscription = $this->event->get_subscription();
+        if ($subscription && !empty($subscription->get('id')) && $CFG->calendar_showicalsource) {
+            $a = (object) [
+                'name' => $values['popupname'],
+                'source' => $subscription->get('name'),
+            ];
+            $values['popupname'] = get_string('namewithsource', 'calendar', $a);
+        } else {
+            if ($values['islastday']) {
+                $startdate = $this->related['type']->timestamp_to_date_array($times->get_start_time()->getTimestamp());
+                $samedate = true;
+                $samedate = $samedate && $startdate['mon'] == $enddate['mon'];
+                $samedate = $samedate && $startdate['year'] == $enddate['year'];
+                $samedate = $samedate && $startdate['mday'] == $enddate['mday'];
+
+                if (!$samedate) {
+                    $values['popupname'] = get_string('eventendtimewrapped', 'calendar', $values['popupname']);
+                }
+            }
+        }
+
+        // Include course's shortname into the event name, if applicable.
+        $course = $this->event->get_course();
+        if ($course && $course->get('id') && $course->get('id') !== SITEID) {
+            $eventnameparams = (object) [
+                'name' => $values['popupname'],
+                'course' => format_string($course->get('shortname'), true, [
+                        'context' => $this->related['context'],
+                    ])
+            ];
+            $values['popupname'] = get_string('eventnameandcourse', 'calendar', $eventnameparams);
+        }
+
+        $values['calendareventtype'] = $this->get_calendar_event_type();
+
         return $values;
     }
 
@@ -76,7 +137,24 @@ class calendar_event_exporter extends event_exporter_base {
     protected static function define_related() {
         $related = parent::define_related();
         $related['daylink'] = \moodle_url::class;
+        $related['type'] = '\core_calendar\type_base';
+        $related['today'] = 'int';
 
         return $related;
     }
+
+    /**
+     * Return the normalised event type.
+     * Activity events are normalised to be course events.
+     *
+     * @return string
+     */
+    public function get_calendar_event_type() {
+        $type = $this->event->get_type();
+        if ($type == 'open' || $type == 'close') {
+            $type = 'course';
+        }
+
+        return $type;
+    }
 }
index 4a713bc..0f5b92b 100644 (file)
@@ -39,6 +39,24 @@ use moodle_url;
  */
 class day_exporter extends exporter {
 
+    /**
+     * @var \calendar_information $calendar The calendar being displayed.
+     */
+    protected $calendar;
+
+    /**
+     * Constructor.
+     *
+     * @param \calendar_information $calendar The calendar information for the period being displayed
+     * @param mixed $data Either an stdClass or an array of values.
+     * @param array $related Related objects.
+     */
+    public function __construct(\calendar_information $calendar, $data, $related) {
+        $this->calendar = $calendar;
+
+        parent::__construct($data, $related);
+    }
+
     /**
      * Return the list of properties.
      *
@@ -69,6 +87,15 @@ class day_exporter extends exporter {
             'yday' => [
                 'type' => PARAM_INT,
             ],
+            // These are additional params.
+            'istoday' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
+            'isweekend' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
         ];
     }
 
@@ -85,14 +112,6 @@ class day_exporter extends exporter {
             'neweventtimestamp' => [
                 'type' => PARAM_INT,
             ],
-            'istoday' => [
-                'type' => PARAM_BOOL,
-                'default' => false,
-            ],
-            'isweekend' => [
-                'type' => PARAM_BOOL,
-                'default' => false,
-            ],
             'viewdaylink' => [
                 'type' => PARAM_URL,
                 'optional' => true,
@@ -100,7 +119,19 @@ class day_exporter extends exporter {
             'events' => [
                 'type' => calendar_event_exporter::read_properties_definition(),
                 'multiple' => true,
-            ]
+            ],
+            'calendareventtypes' => [
+                'type' => PARAM_RAW,
+                'multiple' => true,
+            ],
+            'popovertitle' => [
+                'type' => PARAM_RAW,
+                'default' => '',
+            ],
+            'haslastdayofevent' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
         ];
     }
 
@@ -131,22 +162,46 @@ class day_exporter extends exporter {
         $url = new moodle_url('/calendar/view.php', [
                 'view' => 'day',
                 'time' => $timestamp,
+                'course' => $this->calendar->course->id,
             ]);
         $return['viewdaylink'] = $url->out(false);
 
         $cache = $this->related['cache'];
-        $return['events'] = array_map(function($event) use ($cache, $output, $url) {
+        $eventexporters = array_map(function($event) use ($cache, $output, $url) {
             $context = $cache->get_context($event);
             $course = $cache->get_course($event);
             $exporter = new calendar_event_exporter($event, [
                 'context' => $context,
                 'course' => $course,
                 'daylink' => $url,
+                'type' => $this->related['type'],
+                'today' => $this->data[0],
             ]);
 
-            return $exporter->export($output);
+            return $exporter;
         }, $this->related['events']);
 
+        $return['events'] = array_map(function($exporter) use ($output) {
+            return $exporter->export($output);
+        }, $eventexporters);
+
+        if ($popovertitle = $this->get_popover_title()) {
+            $return['popovertitle'] = $popovertitle;
+        }
+
+        $return['calendareventtypes'] = array_map(function($exporter) {
+            return $exporter->get_calendar_event_type();
+        }, $eventexporters);
+        $return['calendareventtypes'] = array_values(array_unique($return['calendareventtypes']));
+
+        $return['haslastdayofevent'] = false;
+        foreach ($return['events'] as $event) {
+            if ($event->islastday) {
+                $return['haslastdayofevent'] = true;
+                break;
+            }
+        }
+
         return $return;
     }
 
@@ -162,4 +217,26 @@ class day_exporter extends exporter {
             'type' => '\core_calendar\type_base',
         ];
     }
+
+    /**
+     * Get the title for this popover.
+     *
+     * @return string
+     */
+    protected function get_popover_title() {
+        $title = null;
+
+        $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+        if (count($this->related['events'])) {
+            $title = get_string('eventsfor', 'calendar', $userdate);
+        } else if ($this->data['istoday']) {
+            $title = $userdate;
+        }
+
+        if ($this->data['istoday']) {
+            $title = get_string('todayplustitle', 'calendar', $userdate);
+        }
+
+        return $title;
+    }
 }
index 9b99498..6d180aa 100644 (file)
@@ -76,7 +76,19 @@ class month_exporter extends exporter {
 
         $related['type'] = $type;
 
-        parent::__construct([], $related);
+        $data = [
+            'url' => $this->url->out(false),
+        ];
+
+        parent::__construct($data, $related);
+    }
+
+    protected static function define_properties() {
+        return [
+            'url' => [
+                'type' => PARAM_URL,
+            ],
+        ];
     }
 
     /**
@@ -106,15 +118,38 @@ class month_exporter extends exporter {
             'view' => [
                 'type' => PARAM_ALPHA,
             ],
+            'time' => [
+                'type' => PARAM_INT,
+            ],
+            'periodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
             'previousperiod' => [
                 'type' => PARAM_INT,
             ],
+            'previousperiodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
             'nextperiod' => [
                 'type' => PARAM_INT,
             ],
-            'time' => [
-                'type' => PARAM_INT,
-            ]
+            'nextperiodname' => [
+                // Note: We must use RAW here because the calendar type returns the formatted month name based on a
+                // calendar format.
+                'type' => PARAM_RAW,
+            ],
+            'larrow' => [
+                // The left arrow defined by the theme.
+                'type' => PARAM_RAW,
+            ],
+            'rarrow' => [
+                // The right arrow defined by the theme.
+                'type' => PARAM_RAW,
+            ],
         ];
     }
 
@@ -125,16 +160,24 @@ class month_exporter extends exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        $previousperiod = $this->get_previous_month_timestamp();
+        $nextperiod = $this->get_next_month_timestamp();
+
         return [
             'courseid' => $this->calendar->courseid,
-            'view' => 'month',
-            'previousperiod' => $this->get_previous_month_timestamp(),
-            'nextperiod' => $this->get_next_month_timestamp(),
             'filter_selector' => $this->get_course_filter_selector($output),
             'navigation' => $this->get_navigation($output),
             'weeks' => $this->get_weeks($output),
             'daynames' => $this->get_day_names($output),
-            'time' => $this->calendar->time
+            'view' => 'month',
+            'time' => $this->calendar->time,
+            'periodname' => userdate($this->calendar->time, get_string('strftimemonthyear')),
+            'previousperiod' => $previousperiod,
+            'previousperiodname' => userdate($previousperiod, get_string('strftimemonthyear')),
+            'nextperiod' => $nextperiod,
+            'nextperiodname' => userdate($nextperiod, get_string('strftimemonthyear')),
+            'larrow' => $output->larrow(),
+            'rarrow' => $output->rarrow(),
         ];
     }
 
@@ -213,13 +256,13 @@ class month_exporter extends exporter {
         $prepadding = ($firstdayno + $daysinweek - 1) % $daysinweek;
         $daysinfirstweek = $daysinweek - $prepadding;
         $days = array_slice($alldays, 0, $daysinfirstweek);
-        $week = new week_exporter($days, $prepadding, ($daysinweek - count($days) - $prepadding), $this->related);
+        $week = new week_exporter($this->calendar, $days, $prepadding, ($daysinweek - count($days) - $prepadding), $this->related);
         $weeks[] = $week->export($output);
 
         // Now chunk up the remaining day. and turn them into weeks.
         $daychunks = array_chunk(array_slice($alldays, $daysinfirstweek), $daysinweek);
         foreach ($daychunks as $days) {
-            $week = new week_exporter($days, 0, ($daysinweek - count($days)), $this->related);
+            $week = new week_exporter($this->calendar, $days, 0, ($daysinweek - count($days)), $this->related);
             $weeks[] = $week->export($output);
         }
 
@@ -279,7 +322,7 @@ class month_exporter extends exporter {
      */
     protected function get_next_month_timestamp() {
         $date = $this->related['type']->timestamp_to_date_array($this->calendar->time);
-        $month = calendar_sub_month($date['mon'], $date['year']);
+        $month = calendar_add_month($date['mon'], $date['year']);
         $monthtime = $this->related['type']->convert_to_gregorian($month[1], $month[0], 1);
 
         return make_timestamp($monthtime['year'], $monthtime['month'], $monthtime['day'], $monthtime['hour'], $monthtime['minute']);
diff --git a/calendar/classes/external/week_day_exporter.php b/calendar/classes/external/week_day_exporter.php
new file mode 100644 (file)
index 0000000..67442f5
--- /dev/null
@@ -0,0 +1,180 @@
+<?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/>.
+
+/**
+ * Contains event class for displaying the day on month view.
+ *
+ * @package   core_calendar
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use renderer_base;
+use moodle_url;
+
+/**
+ * Class for displaying the day on month view.
+ *
+ * @package   core_calendar
+ * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class week_day_exporter extends day_exporter {
+
+    /**
+     * Return the list of additional properties.
+     *
+     * @return array
+     */
+    protected static function define_other_properties() {
+        $return = parent::define_other_properties();
+        $return = array_merge($return, [
+            'timestamp' => [
+                'type' => PARAM_INT,
+            ],
+            'neweventtimestamp' => [
+                'type' => PARAM_INT,
+            ],
+            'viewdaylink' => [
+                'type' => PARAM_URL,
+                'optional' => true,
+            ],
+            'calendareventtypes' => [
+                'type' => PARAM_RAW,
+                'multiple' => true,
+            ],
+            'popovertitle' => [
+                'type' => PARAM_RAW,
+                'default' => '',
+            ],
+            'haslastdayofevent' => [
+                'type' => PARAM_BOOL,
+                'default' => false,
+            ],
+        ]);
+
+        return $return;
+    }
+
+    /**
+     * Get the additional values to inject while exporting.
+     *
+     * @param renderer_base $output The renderer.
+     * @return array Keys are the property names, values are their values.
+     */
+    protected function get_other_values(renderer_base $output) {
+        $timestamp = $this->data[0];
+        // Need to account for user's timezone.
+        $usernow = usergetdate(time());
+        $today = new \DateTimeImmutable();
+        // The start time should use the day's date but the current
+        // time of the day (adjusted for user's timezone).
+        $neweventstarttime = $today->setTimestamp($timestamp)->setTime(
+            $usernow['hours'],
+            $usernow['minutes'],
+            $usernow['seconds']
+        );
+
+        $return = [
+            'timestamp' => $timestamp,
+            'neweventtimestamp' => $neweventstarttime->getTimestamp()
+        ];
+
+        $url = new moodle_url('/calendar/view.php', [
+                'view' => 'day',
+                'time' => $timestamp,
+                'course' => $this->calendar->course->id,
+            ]);
+        $return['viewdaylink'] = $url->out(false);
+
+        $cache = $this->related['cache'];
+        $eventexporters = array_map(function($event) use ($cache, $output, $url) {
+            $context = $cache->get_context($event);
+            $course = $cache->get_course($event);
+            $exporter = new calendar_event_exporter($event, [
+                'context' => $context,
+                'course' => $course,
+                'daylink' => $url,
+                'type' => $this->related['type'],
+                'today' => $this->data[0],
+            ]);
+
+            return $exporter;
+        }, $this->related['events']);
+
+        $return['events'] = array_map(function($exporter) use ($output) {
+            return $exporter->export($output);
+        }, $eventexporters);
+
+        if ($popovertitle = $this->get_popover_title()) {
+            $return['popovertitle'] = $popovertitle;
+        }
+
+        $return['calendareventtypes'] = array_map(function($exporter) {
+            return $exporter->get_calendar_event_type();
+        }, $eventexporters);
+        $return['calendareventtypes'] = array_values(array_unique($return['calendareventtypes']));
+
+        $return['haslastdayofevent'] = false;
+        foreach ($return['events'] as $event) {
+            if ($event->islastday) {
+                $return['haslastdayofevent'] = true;
+                break;
+            }
+        }
+
+        return $return;
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return [
+            'events' => '\core_calendar\local\event\entities\event_interface[]',
+            'cache' => '\core_calendar\external\events_related_objects_cache',
+            'type' => '\core_calendar\type_base',
+        ];
+    }
+
+    /**
+     * Get the title for this popover.
+     *
+     * @return string
+     */
+    protected function get_popover_title() {
+        $title = null;
+
+        $userdate = userdate($this->data[0], get_string('strftimedayshort'));
+        if (count($this->related['events'])) {
+            $title = get_string('eventsfor', 'calendar', $userdate);
+        } else if ($this->data['istoday']) {
+            $title = $userdate;
+        }
+
+        if ($this->data['istoday']) {
+            $title = get_string('todayplustitle', 'calendar', $userdate);
+        }
+
+        return $title;
+    }
+}
index afc92b2..ff75a35 100644 (file)
@@ -53,18 +53,25 @@ class week_exporter extends exporter {
      */
     protected $postpadding = 0;
 
+    /**
+     * @var \calendar_information $calendar The calendar being displayed.
+     */
+    protected $calendar;
+
     /**
      * Constructor.
      *
+     * @param \calendar_information $calendar The calendar information for the period being displayed
      * @param mixed $days An array of day_exporter objects.
      * @param int $prepadding The number of pre-padding days at the start of the week.
      * @param int $postpadding The number of post-padding days at the start of the week.
      * @param array $related Related objects.
      */
-    public function __construct($days, $prepadding, $postpadding, $related) {
+    public function __construct(\calendar_information $calendar, $days, $prepadding, $postpadding, $related) {
         $this->days = $days;
         $this->prepadding = $prepadding;
         $this->postpadding = $postpadding;
+        $this->calendar = $calendar;
 
         parent::__construct([], $related);
     }
@@ -85,7 +92,7 @@ class week_exporter extends exporter {
                 'multiple' => true,
             ],
             'days' => [
-                'type' => day_exporter::read_properties_definition(),
+                'type' => week_day_exporter::read_properties_definition(),
                 'multiple' => true,
             ],
         ];
@@ -98,6 +105,7 @@ class week_exporter extends exporter {
      * @return array Keys are the property names, values are their values.
      */
     protected function get_other_values(renderer_base $output) {
+        global $CFG;
         $return = [
             'prepadding' => [],
             'postpadding' => [],
@@ -112,6 +120,14 @@ class week_exporter extends exporter {
         }
 
         $return['days'] = [];
+        $today = $this->related['type']->timestamp_to_date_array(time());
+
+        $weekend = CALENDAR_DEFAULT_WEEKEND;
+        if (isset($CFG->calendar_weekend)) {
+            $weekend = intval($CFG->calendar_weekend);
+        }
+        $numberofdaysinweek = $this->related['type']->get_num_weekdays();
+
         foreach ($this->days as $daydata) {
             $events = [];
             foreach ($this->related['events'] as $event) {
@@ -132,7 +148,14 @@ class week_exporter extends exporter {
                 $events[] = $event;
             }
 
-            $day = new day_exporter($daydata, [
+            $istoday = true;
+            $istoday = $istoday && $today['year'] == $daydata['year'];
+            $istoday = $istoday && $today['yday'] == $daydata['yday'];
+            $daydata['istoday'] = $istoday;
+
+            $daydata['isweekend'] = !!($weekend & (1 << ($daydata['wday'] % $numberofdaysinweek)));
+
+            $day = new week_day_exporter($this->calendar, $daydata, [
                 'events' => $events,
                 'cache' => $this->related['cache'],
                 'type' => $this->related['type'],
index 165b145..8459913 100644 (file)
@@ -41,7 +41,6 @@ class create extends \moodleform {
         global $PAGE;
 
         $mform = $this->_form;
-        $haserror = !empty($this->_customdata['haserror']);
         $starttime = isset($this->_customdata['starttime']) ? $this->_customdata['starttime'] : 0;
         $eventtypes = calendar_get_all_allowed_types();
 
@@ -82,8 +81,7 @@ class create extends \moodleform {
         $this->add_event_repeat_elements($mform);
 
         // Add the javascript required to enhance this mform.
-        // Including the show/hide of advanced elements and the display of the correct select elements for event types.
-        $PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id'), $haserror]);
+        $PAGE->requires->js_call_amd('core_calendar/event_form', 'init', [$mform->getAttribute('id')]);
     }
 
     /**
index f574472..713dc0c 100644 (file)
@@ -1049,7 +1049,7 @@ class calendar_information {
     public function add_sidecalendar_blocks(core_calendar_renderer $renderer, $showfilters=false, $view=null) {
         if ($showfilters) {
             $filters = new block_contents();
-            $filters->content = $renderer->fake_block_filters($this->courseid, 0, 0, 0, $view, $this->courses);
+            $filters->content = $renderer->event_filter();
             $filters->footer = '';
             $filters->title = get_string('eventskey', 'calendar');
             $renderer->add_pretend_calendar_block($filters, BLOCK_POS_RIGHT);
@@ -1478,7 +1478,7 @@ function calendar_get_mini($courses, $groups, $users, $calmonth = false, $calyea
 
             // Class and cell content.
             if (isset($typesbyday[$day]['startglobal'])) {
-                $class .= ' calendar_event_global';
+                $class .= ' calendar_event_site';
             } else if (isset($typesbyday[$day]['startcourse'])) {
                 $class .= ' calendar_event_course';
             } else if (isset($typesbyday[$day]['startgroup'])) {
@@ -1981,88 +1981,6 @@ function calendar_top_controls($type, $data) {
     return $content;
 }
 
-/**
- * Formats a filter control element.
- *
- * @param moodle_url $url of the filter
- * @param int $type constant defining the type filter
- * @return string html content of the element
- */
-function calendar_filter_controls_element(moodle_url $url, $type) {
-    global $OUTPUT;
-
-    switch ($type) {
-        case CALENDAR_EVENT_GLOBAL:
-            $typeforhumans = 'global';
-            $class = 'calendar_event_global';
-            break;
-        case CALENDAR_EVENT_COURSE:
-            $typeforhumans = 'course';
-            $class = 'calendar_event_course';
-            break;
-        case CALENDAR_EVENT_GROUP:
-            $typeforhumans = 'groups';
-            $class = 'calendar_event_group';
-            break;
-        case CALENDAR_EVENT_USER:
-            $typeforhumans = 'user';
-            $class = 'calendar_event_user';
-            break;
-    }
-
-    if (calendar_show_event_type($type)) {
-        $icon = $OUTPUT->pix_icon('t/hide', get_string('hide'));
-        $str = get_string('hide' . $typeforhumans . 'events', 'calendar');
-    } else {
-        $icon = $OUTPUT->pix_icon('t/show', get_string('show'));
-        $str = get_string('show' . $typeforhumans . 'events', 'calendar');
-    }
-    $content = \html_writer::start_tag('li', array('class' => 'calendar_event'));
-    $content .= \html_writer::start_tag('a', array('href' => $url, 'rel' => 'nofollow'));
-    $content .= \html_writer::tag('span', $icon, array('class' => $class));
-    $content .= \html_writer::tag('span', $str, array('class' => 'eventname'));
-    $content .= \html_writer::end_tag('a');
-    $content .= \html_writer::end_tag('li');
-
-    return $content;
-}
-
-/**
- * Get the controls filter for calendar.
- *
- * Filter is used to hide calendar info from the display page.
- *
-
- * @param moodle_url $returnurl return-url for filter controls
- * @return string $content return filter controls in html
- */
-function calendar_filter_controls(moodle_url $returnurl) {
-    $groupevents = true;
-
-    $seturl = new \moodle_url('/calendar/set.php', array('return' => base64_encode($returnurl->out_as_local_url(false)),
-        'sesskey' => sesskey()));
-    $content = \html_writer::start_tag('ul');
-
-    $seturl->param('var', 'showglobal');
-    $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GLOBAL);
-
-    $seturl->param('var', 'showcourses');
-    $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_COURSE);
-
-    if (isloggedin() && !isguestuser()) {
-        if ($groupevents) {
-            // This course MIGHT have group events defined, so show the filter.
-            $seturl->param('var', 'showgroups');
-            $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_GROUP);
-        }
-        $seturl->param('var', 'showuser');
-        $content .= calendar_filter_controls_element($seturl, CALENDAR_EVENT_USER);
-    }
-    $content .= \html_writer::end_tag('ul');
-
-    return $content;
-}
-
 /**
  * Return the representation day.
  *
@@ -3464,6 +3382,11 @@ function calendar_get_view(\calendar_information $calendar, $view) {
         $monthdays = $type->get_num_days_in_month($date['year'], $date['mon']);
         $tend = $tstart + ($monthdays * DAYSECS) - 1;
         $selectortitle = get_string('detailedmonthviewfor', 'calendar');
+        if ($view === 'mini') {
+            $template = 'core_calendar/calendar_mini';
+        } else {
+            $template = 'core_calendar/calendar_month';
+        }
     }
 
     list($userparam, $groupparam, $courseparam) = array_map(function($param) {
@@ -3518,7 +3441,6 @@ function calendar_get_view(\calendar_information $calendar, $view) {
 
     $month = new \core_calendar\external\month_exporter($calendar, $type, $related);
     $data = $month->export($renderer);
-    $template = 'core_calendar/month_detailed';
 
     return [$data, $template];
 }
@@ -3546,10 +3468,6 @@ function calendar_output_fragment_event_form($args) {
         parse_str(clean_param($args['formdata'], PARAM_TEXT), $data);
     }
 
-    if (isset($args['haserror'])) {
-        $formoptions['haserror'] = clean_param($args['haserror'], PARAM_BOOL);
-    }
-
     if ($starttime) {
         $formoptions['starttime'] = $starttime;
     }
@@ -3654,3 +3572,25 @@ function calendar_get_footer_options($calendar) {
 
     return [$data, $template];
 }
+
+/**
+ * Get the list of potential calendar filter types as a type => name
+ * combination.
+ *
+ * @return array
+ */
+function calendar_get_filter_types() {
+    $types = [
+        'site',
+        'course',
+        'group',
+        'user',
+    ];
+
+    return array_map(function($type) {
+        return [
+            'type' => $type,
+            'name' => get_string("eventtype{$type}", "calendar"),
+        ];
+    }, $types);
+}
index 1818234..dc597ab 100644 (file)
@@ -50,24 +50,6 @@ class core_calendar_renderer extends plugin_renderer_base {
         return html_writer::end_tag('div');
     }
 
-    /**
-     * Produces the content for the filters block (pretend block)
-     *
-     * @param int $courseid
-     * @param int $day
-     * @param int $month
-     * @param int $year
-     * @param int $view
-     * @param int $courses
-     * @return string
-     */
-    public function fake_block_filters($courseid, $day, $month, $year, $view, $courses) {
-        $returnurl = $this->page->url;
-        $returnurl->param('course', $courseid);
-        return html_writer::tag('div', calendar_filter_controls($returnurl),
-            array('class' => 'calendar_filters filters'));
-    }
-
     /**
      * Produces the content for the three months block (pretend block)
      *
@@ -502,4 +484,16 @@ class core_calendar_renderer extends plugin_renderer_base {
         $html .= html_writer::end_tag('form');
         return $html;
     }
+
+    /**
+     * Render the event filter region.
+     *
+     * @return  string
+     */
+    public function event_filter() {
+        $data = [
+            'eventtypes' => calendar_get_filter_types(),
+        ];
+        return $this->render_from_template('core_calendar/event_filter', $data);
+    }
 }
diff --git a/calendar/set.php b/calendar/set.php
deleted file mode 100644 (file)
index c69de93..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?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/>.
-
-/**
- * Sets the events filter for the calendar view.
- *
- * @package   core_calendar
- * @copyright 2003 Jon Papaioannou (pj@moodle.org)
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-require_once('../config.php');
-require_once($CFG->dirroot.'/calendar/lib.php');
-
-$var = required_param('var', PARAM_ALPHA);
-$return = clean_param(base64_decode(required_param('return', PARAM_RAW)), PARAM_LOCALURL);
-$courseid = optional_param('id', -1, PARAM_INT);
-if ($courseid != -1) {
-    $return = new moodle_url($return, array('course' => $courseid));
-} else {
-    $return = new moodle_url($return);
-}
-
-if (!confirm_sesskey()) {
-    // Do not call require_sesskey() since this page may be accessed without session (for example by bots).
-    redirect($return);
-}
-
-$url = new moodle_url('/calendar/set.php', array('return'=>base64_encode($return->out_as_local_url(false)), 'course' => $courseid, 'var'=>$var, 'sesskey'=>sesskey()));
-$PAGE->set_url($url);
-$PAGE->set_context(context_system::instance());
-
-switch($var) {
-    case 'showgroups':
-        calendar_set_event_type_display(CALENDAR_EVENT_GROUP);
-        break;
-    case 'showcourses':
-        calendar_set_event_type_display(CALENDAR_EVENT_COURSE);
-        break;
-    case 'showglobal':
-        calendar_set_event_type_display(CALENDAR_EVENT_GLOBAL);
-        break;
-    case 'showuser':
-        calendar_set_event_type_display(CALENDAR_EVENT_USER);
-        break;
-}
-
-redirect($return);
diff --git a/calendar/templates/calendar_mini.mustache b/calendar/templates/calendar_mini.mustache
new file mode 100644 (file)
index 0000000..6e28cbf
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/calendar_mini
+
+    Calendar month view for blocks.
+
+    The purpose of this template is to render the month view for blocks.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div{{!
+  }} id="calendar-month-{{uniqid}}-{{time}}" {{!
+    }} data-template="core_calendar/month_mini" {{!
+    }} data-includenavigation="{{#includenavigation}}true{{/includenavigation}}{{^includenavigation}}false{{/includenavigation}}"{{!
+    }}>
+    {{> core_calendar/month_mini}}
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_mini'], function($, CalendarMini) {
+    CalendarMini.init($("#calendar-month-{{uniqid}}-{{time}}"));
+});
+{{/js}}
diff --git a/calendar/templates/calendar_month.mustache b/calendar/templates/calendar_month.mustache
new file mode 100644 (file)
index 0000000..9e5866b
--- /dev/null
@@ -0,0 +1,41 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/calendar_month
+
+    Calendar month view for blcoks.
+
+    The purpose of this template is to render the main, detailed monthly view.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div id="calendar-month-{{uniqid}}" data-template="core_calendar/month_detailed">
+    {{> core_calendar/month_detailed}}
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar'], function($, Calendar) {
+    Calendar.init($("#calendar-month-{{uniqid}}"));
+});
+{{/js}}
diff --git a/calendar/templates/event_filter.mustache b/calendar/templates/event_filter.mustache
new file mode 100644 (file)
index 0000000..4e6e789
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/event_filter
+
+    Filter to select which event types displayed on this page.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div id="event-filter-{{uniqid}}" class="calendar_filters m-t-2">
+    <ul>
+        {{#eventtypes}}
+            <li>
+                {{> core_calendar/event_filter_key }}
+            </li>
+        {{/eventtypes}}
+    </ul>
+</div>
+{{#js}}
+require(['jquery', 'core_calendar/calendar_filter'], function($, CalendarFilter) {
+    CalendarFilter.init($("#event-filter-{{uniqid}}"));
+});
+{{/js}}
diff --git a/calendar/templates/event_filter_key.mustache b/calendar/templates/event_filter_key.mustache
new file mode 100644 (file)
index 0000000..be1154b
--- /dev/null
@@ -0,0 +1,45 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/event_filter_key
+
+    Key item for the event filter.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<a href="#" data-action="filter-event-type" data-eventtype="{{type}}" data-eventtype-hidden="{{hidden}}">
+    {{^hidden}}
+        <span class="calendar_event_{{type}}">
+            {{#pix}}i/hide, core, {{#str}}hideeventtype, calendar, {{name}}{{/str}}{{/pix}}
+        </span>
+        <span class="eventname">{{#str}}hideeventtype, calendar, {{name}}{{/str}}</span>
+    {{/hidden}}
+    {{#hidden}}
+        <span class="calendar_event_{{type}}">
+            {{#pix}}i/show, core, {{#str}}showeventtype, calendar, {{name}}{{/str}}{{/pix}}
+        </span>
+        <span class="eventname">{{#str}}showeventtype, calendar, {{name}}{{/str}}</span>
+    {{/hidden}}
+</a>
index bdb5e67..d6c91af 100644 (file)
 }}
 {{< core/hover_tooltip }}
     {{$anchor}}
-        <a href="{{url}}">{{day}}</a>
+        <a href="{{url}}">{{$day}}{{day}}{{/day}}</a>
     {{/anchor}}
     {{$tooltip}}
-        <b>{{title}}</b>
-        {{{content}}}
+        <b>{{$title}}{{title}}{{/title}}</b>
+        {{$content}}{{{content}}}{{/content}}
     {{/tooltip}}
 {{/ core/hover_tooltip }}
index e99c491..99a2f59 100644 (file)
 
 {{< core/modal }}
     {{$footer}}
-        <button type="button"
-                class="btn btn-secondary"
-                data-collapsed="true"
-                data-action="more-less-toggle">
-
-            {{#str}} more, calendar {{/str}}
-        </button>
         <button type="button"
                 class="btn btn-primary"
                 data-context-id="{{contextid}}"
index 80005dc..463b687 100644 (file)
@@ -77,8 +77,9 @@
                                             <li class="events-underway">[{{name}}]</li>
                                         {{/underway}}
                                         {{^underway}}
-                                            <li class="calendar_event_{{eventtype}}"
+                                            <li class="calendar_event_{{calendareventtype}}"
                                                 data-region="event-item"
+                                                data-eventtype-{{calendareventtype}}="1"
                                                 {{#canedit}}
                                                     draggable="true"
                                                     data-drag-type="move"
     </table>
 </div>
 {{#js}}
-require(['jquery', 'core_calendar/month_view_drag_drop'], function($, DragDrop) {
+require([
+    'jquery',
+    'core_calendar/month_view_drag_drop',
+    'core_calendar/selectors',
+    'core_calendar/events',
+], function(
+    $,
+    DragDrop,
+    CalendarSelectors,
+    CalendarEvents
+) {
     var root = $('#month-detailed-{{uniqid}}');
     DragDrop.init(root);
+
+    $('body').on(CalendarEvents.filterChanged, function(e, data) {
+        M.util.js_pending("month-detailed-{{uniqid}}-filterChanged");
+        // A filter value has been changed.
+        // Find all matching cells in the popover data, and hide them.
+        $("#month-detailed-{{uniqid}}")
+            .find(CalendarSelectors.eventType[data.type])
+            .toggleClass('hidden', !!data.hidden);
+        M.util.js_complete("month-detailed-{{uniqid}}-filterChanged");
+    });
 });
 {{/js}}
diff --git a/calendar/templates/month_mini.mustache b/calendar/templates/month_mini.mustache
new file mode 100644 (file)
index 0000000..b14afc9
--- /dev/null
@@ -0,0 +1,154 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template calendar/month_mini
+
+    Calendar month view for blocks.
+
+    The purpose of this template is to render the month view for blocks.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Example context (json):
+    {
+    }
+}}
+<div id="month-mini-{{uniqid}}-{{time}}" class="calendarwrapper" data-courseid="{{courseid}}" data-current-time="{{time}}">
+    {{> core/overlay_loading}}
+    <table class="minicalendar calendartable">
+        <caption class="calendar-controls">
+            <a href="#" class="arrow_link previous" title="{{#str}}monthprev, calendar{{/str}}" data-time="{{previousperiod}}"><span class="arrow">{{{larrow}}}</span></a>
+            <span class="hide"> | </span>
+            <span class="current">
+                <a href="{{{url}}}" title="{{#str}}monththis, calendar{{/str}}" data-time="">{{periodname}}</a>
+            </span>
+            <span class="hide"> | </span>
+            <a href="#" class="arrow_link next" title="{{#str}}monthnext, calendar{{/str}}" data-time="{{nextperiod}}"><span class="arrow">{{{rarrow}}}</span></a>
+        </caption>
+        <thead>
+          <tr>
+                {{# daynames }}
+                <th class="header text-xs-center" scope="col">
+                    <abbr title="{{fullname}}">{{shortname}}</abbr>
+                </th>
+                {{/ daynames }}
+            </tr>
+        </thead>
+        <tbody>
+{{#weeks}}
+            <tr data-region="month-view-week">
+                {{#prepadding}}
+                    <td class="dayblank">&nbsp;</td>
+                {{/prepadding}}
+                {{#days}}
+                    <td class="day text-center{{!
+                            This is the list of additional classes to display.
+
+                            This cell is for today.
+                            }}{{#istoday}} today{{/istoday}}{{!
+
+                            This day falls on a weekend.
+                            }}{{#isweekend}} weekend{{/isweekend}}{{!
+
+                            There is at least one event.
+                            }}{{#events.0}} hasevent{{/events.0}}{{!
+
+                            There are events on this day which span multiple days.
+                            }}{{#durationevents.0}} duration{{/durationevents.0}}{{!
+                            }}{{#durationevents}} duration_{{.}}{{/durationevents}}{{!
+                            }}{{#islastday}} duraction_finish{{/islastday}}{{!
+
+                            These event types fall on this day.
+                            }}{{#calendareventtypes}}{{!
+                            }} calendar_event_{{.}}{{!
+                            }}{{/calendareventtypes}}{{!
+
+                            This day contains the last day of an event.
+                            }}{{#haslastdayofevent}}{{!
+                            }} duration_finish{{!
+                            }}{{/haslastdayofevent}}{{!
+                        }}"{{!
+
+                        Add data-eventtype-[eventtype] data attributes.
+                        These are used to show and hide events using the filter.
+                        }}{{#calendareventtypes}}{{!
+                        }} data-eventtype-{{.}}="1"{{!
+                        }}{{/calendareventtypes}}{{!
+
+                        This is the timestamp for this month.
+                        }} data-day-timestamp="{{timestamp}}"{{!
+                    }}>{{!
+                        }}{{#popovertitle}}
+                            {{< core_calendar/minicalendar_day_link }}
+                                {{$day}}{{mday}}{{/day}}
+                                {{$url}}{{viewdaylink}}{{/url}}
+                                {{$title}}{{popovertitle}}{{/title}}
+                                {{$nocontent}}{{#str}}eventnone, calendar{{/str}}{{/nocontent}}
+                                {{$content}}
+                                    {{#events}}
+                                        <div data-popover-eventtype-{{calendareventtype}}="1">
+                                            {{#modulename}}
+                                                {{#pix}} icon, {{modulename}} {{/pix}}
+                                            {{/modulename}}
+                                            {{^modulename}}
+                                                {{#pix}} i/{{eventtype}}event, core {{/pix}}
+                                            {{/modulename}}
+                                            {{{popupname}}}
+                                        </div>
+                                    {{/events}}
+                                {{/content}}
+                            {{/ core_calendar/minicalendar_day_link }}
+                        {{/popovertitle}}{{!
+                        }}{{^popovertitle}}
+                            {{mday}}
+                        {{/popovertitle}}{{!
+                    }}</td>
+                {{/days}}
+                {{#postpadding}}
+                    <td class="dayblank">&nbsp;</td>
+                {{/postpadding}}
+            </tr>
+{{/weeks}}
+        </tbody>
+    </table>
+</div>
+{{#js}}
+require([
+    'jquery',
+    'core_calendar/selectors',
+    'core_calendar/events',
+], function(
+    $,
+    CalendarSelectors,
+    CalendarEvents
+) {
+
+    $('body').on(CalendarEvents.filterChanged, function(e, data) {
+        M.util.js_pending("month-mini-{{uniqid}}-filterChanged");
+        // A filter value has been changed.
+        // Find all matching cells in the popover data, and hide them.
+        $("#month-mini-{{uniqid}}-{{time}}")
+            .find(CalendarSelectors.popoverType[data.type])
+            .toggleClass('hidden', !!data.hidden);
+        M.util.js_complete("month-mini-{{uniqid}}-filterChanged");
+    });
+});
+{{/js}}
index ef8fa66..0bd835c 100644 (file)
@@ -84,9 +84,9 @@ class behat_calendar extends behat_base {
      * @param int $day The day of the current month
      */
     public function i_hover_over_day_of_this_month_in_calendar($day) {
-        $summarytitle = get_string('calendarheading', 'calendar', userdate(time(), get_string('strftimemonthyear')));
+        $summarytitle = userdate(time(), get_string('strftimemonthyear'));
         // The current month table.
-        $currentmonth = "table[contains(concat(' ', normalize-space(@summary), ' '), ' {$summarytitle} ')]";
+        $currentmonth = "table[descendant::*[self::caption[contains(concat(' ', normalize-space(.), ' '), ' {$summarytitle} ')]]]";
 
         // Strings for the class cell match.
         $cellclasses  = "contains(concat(' ', normalize-space(@class), ' '), ' day ')";
@@ -95,6 +95,7 @@ class behat_calendar extends behat_base {
         $dayofmonth   = "a[{$daycontains}]";
 
         $xpath = '//' . $currentmonth . '/descendant::' . $daycell . '/' . $dayofmonth;
+        $this->execute("behat_general::wait_until_the_page_is_ready");
         $this->execute("behat_general::i_hover", array($xpath, "xpath_element"));
 
     }
index ba0bc94..6447c8d 100644 (file)
@@ -161,5 +161,4 @@ echo html_writer::end_tag('div');
 list($data, $template) = calendar_get_footer_options($calendar);
 echo $renderer->render_from_template($template, $data);
 
-$PAGE->requires->js_call_amd('core_calendar/calendar', 'init');
 echo $OUTPUT->footer();
index d7e7b84..9a2a3c7 100644 (file)
@@ -206,7 +206,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
          */
         var findNextFocusable = function(mainElement) {
             var tabables = $("a:visible");
-            var isInside = false, foundElement = null;
+            var isInside = false;
+            var foundElement = null;
             tabables.each(function() {
                 if ($.contains(mainElement[0], this)) {
                     isInside = true;
index 833f7fb..8972162 100644 (file)
@@ -124,4 +124,41 @@ class mycourse extends \core_search\base {
     public function get_context_url(\core_search\document $doc) {
         return new \moodle_url('/course/view.php', array('id' => $doc->get('courseid')));
     }
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * Should be overridden by each search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array(
+                'overviewfiles',
+                'summary'// Fileareas.
+        );
+
+        return $fileareas;
+    }
+
+    /**
+     * Returns the moodle component name.
+     *
+     * It might be the plugin name (whole frankenstyle name) or the core subsystem name.
+     *
+     * @return string
+     */
+    public function get_component_name() {
+        return 'course';
+    }
 }
index 2ccc6f1..b38c641 100644 (file)
@@ -3894,16 +3894,14 @@ function course_get_user_navigation_options($context, $course = null) {
     // Frontpage settings?
     if ($isfrontpage) {
         // We are on the front page, so make sure we use the proper capability (site:viewparticipants).
-        $options->participants = has_capability('moodle/site:viewparticipants', $sitecontext) ||
-            has_capability('moodle/course:enrolreview', $sitecontext);
+        $options->participants = course_can_view_participants($sitecontext);
         $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
         $options->tags = !empty($CFG->usetags) && $isloggedin;
         $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
         $options->calendar = $isloggedin;
     } else {
         // We are in a course, so make sure we use the proper capability (course:viewparticipants).
-        $options->participants = has_capability('moodle/course:viewparticipants', $context) ||
-            has_capability('moodle/course:enrolreview', $context);
+        $options->participants = course_can_view_participants($context);
         $options->badges = !empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges) &&
                             has_capability('moodle/badges:viewbadges', $context);
         // Add view grade report is permitted.
@@ -4237,3 +4235,34 @@ function course_check_module_updates_since($cm, $from, $fileareas = array(), $fi
 
     return $updates;
 }
+
+/**
+ * Returns true if the user can view the participant page, false otherwise,
+ *
+ * @param context $context The context we are checking.
+ * @return bool
+ */
+function course_can_view_participants($context) {
+    $viewparticipantscap = 'moodle/course:viewparticipants';
+    if ($context->contextlevel == CONTEXT_SYSTEM) {
+        $viewparticipantscap = 'moodle/site:viewparticipants';
+    }
+
+    return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context);
+}
+
+/**
+ * Checks if a user can view the participant page, if not throws an exception.
+ *
+ * @param context $context The context we are checking.
+ * @throws required_capability_exception
+ */
+function course_require_view_participants($context) {
+    if (!course_can_view_participants($context)) {
+        $viewparticipantscap = 'moodle/course:viewparticipants';
+        if ($context->contextlevel == CONTEXT_SYSTEM) {
+            $viewparticipantscap = 'moodle/site:viewparticipants';
+        }
+        throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', '');
+    }
+}
index cc2750a..d3056ea 100644 (file)
@@ -27,6 +27,7 @@ if (!defined('MOODLE_INTERNAL')) {
     die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
 }
 
+require_once($CFG->dirroot . '/course/lib.php');
 require_once($CFG->libdir.'/formslib.php');
 
 class recent_form extends moodleform {
@@ -64,9 +65,9 @@ class recent_form extends moodleform {
         }
 
         if ($COURSE->id == SITEID) {
-            $viewparticipants = has_capability('moodle/site:viewparticipants', context_system::instance());
+            $viewparticipants = course_can_view_participants(context_system::instance());
         } else {
-            $viewparticipants = has_capability('moodle/course:viewparticipants', $context);
+            $viewparticipants = course_can_view_participants($context);
         }
 
         if ($viewparticipants) {
index 702771f..6d53ba5 100644 (file)
@@ -3904,4 +3904,174 @@ class core_course_courselib_testcase extends advanced_testcase {
         // Update the assign instances for this course.
         $this->assertTrue(course_module_bulk_update_calendar_events('assign', $course->id));
     }
+
+    /**
+     * Test that a student can view participants in a course they are enrolled in.
+     */
+    public function test_course_can_view_participants_as_student() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $this->setUser($user);
+
+        $this->assertTrue(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Test that a student in a course can not view participants on the site.
+     */
+    public function test_course_can_view_participants_as_student_on_site() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $this->setUser($user);
+
+        $this->assertFalse(course_can_view_participants(context_system::instance()));
+    }
+
+    /**
+     * Test that an admin can view participants on the site.
+     */
+    public function test_course_can_view_participants_as_admin_on_site() {
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+
+        $this->assertTrue(course_can_view_participants(context_system::instance()));
+    }
+
+    /**
+     * Test teachers can view participants in a course they are enrolled in.
+     */
+    public function test_course_can_view_participants_as_teacher() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        $this->assertTrue(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Check the teacher can still view the participants page without the 'viewparticipants' cap.
+     */
+    public function test_course_can_view_participants_as_teacher_without_view_participants_cap() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        // Disable one of the capabilties.
+        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
+
+        // Should still be able to view the page as they have the 'moodle/course:enrolreview' cap.
+        $this->assertTrue(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Check the teacher can still view the participants page without the 'moodle/course:enrolreview' cap.
+     */
+    public function test_course_can_view_participants_as_teacher_without_enrol_review_cap() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        // Disable one of the capabilties.
+        assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
+
+        // Should still be able to view the page as they have the 'moodle/course:viewparticipants' cap.
+        $this->assertTrue(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Check the teacher can not view the participants page without the required caps.
+     */
+    public function test_course_can_view_participants_as_teacher_without_required_caps() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
+
+        $this->setUser($user);
+
+        // Disable the capabilities.
+        assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
+        assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
+
+        $this->assertFalse(course_can_view_participants($coursecontext));
+    }
+
+    /**
+     * Check that an exception is not thrown if we can view the participants page.
+     */
+    public function test_course_require_view_participants() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $this->setUser($user);
+
+        course_require_view_participants($coursecontext);
+    }
+
+    /**
+     * Check that an exception is thrown if we can't view the participants page.
+     */
+    public function test_course_require_view_participants_as_student_on_site() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+
+        $this->setUser($user);
+
+        $this->expectException('required_capability_exception');
+        course_require_view_participants(context_system::instance());
+    }
 }
index 370141a..d11bfab 100644 (file)
@@ -86,6 +86,8 @@ class core_enrol_external extends external_api {
      */
     public static function get_enrolled_users_with_capability($coursecapabilities, $options) {
         global $CFG, $DB;
+
+        require_once($CFG->dirroot . '/course/lib.php');
         require_once($CFG->dirroot . "/user/lib.php");
 
         if (empty($coursecapabilities)) {
@@ -145,11 +147,8 @@ class core_enrol_external extends external_api {
                 throw new moodle_exception(get_string('errorcoursecontextnotvalid' , 'webservice', $exceptionparam));
             }
 
-            if ($courseid == SITEID) {
-                require_capability('moodle/site:viewparticipants', $context);
-            } else {
-                require_capability('moodle/course:viewparticipants', $context);
-            }
+            course_require_view_participants($context);
+
             // The accessallgroups capability is needed to use this option.
             if (!empty($groupid) && groups_is_member($groupid)) {
                 require_capability('moodle/site:accessallgroups', $coursecontext);
@@ -293,7 +292,9 @@ class core_enrol_external extends external_api {
      * @return array of courses
      */
     public static function get_users_courses($userid) {
-        global $USER, $DB;
+        global $CFG, $USER, $DB;
+
+        require_once($CFG->dirroot . '/course/lib.php');
 
         // Do basic automatic PARAM checks on incoming data, using params description
         // If any problems are found then exceptions are thrown with helpful error messages
@@ -312,7 +313,7 @@ class core_enrol_external extends external_api {
                 continue;
             }
 
-            if ($userid != $USER->id and !has_capability('moodle/course:viewparticipants', $context)) {
+            if ($userid != $USER->id and !course_can_view_participants($context)) {
                 // we need capability to view participants
                 continue;
             }
@@ -520,6 +521,8 @@ class core_enrol_external extends external_api {
      */
     public static function get_enrolled_users($courseid, $options = array()) {
         global $CFG, $USER, $DB;
+
+        require_once($CFG->dirroot . '/course/lib.php');
         require_once($CFG->dirroot . "/user/lib.php");
 
         $params = self::validate_parameters(
@@ -600,11 +603,8 @@ class core_enrol_external extends external_api {
             throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam);
         }
 
-        if ($courseid == SITEID) {
-            require_capability('moodle/site:viewparticipants', $context);
-        } else {
-            require_capability('moodle/course:viewparticipants', $context);
-        }
+        course_require_view_participants($context);
+
         // to overwrite this parameter, you need role:review capability
         if ($withcapability) {
             require_capability('moodle/role:review', $coursecontext);
index 6e8789e..9eb6d21 100644 (file)
@@ -30,8 +30,6 @@ $string['activitynext'] = 'Next activity';
 $string['activityprev'] = 'Previous activity';
 $string['breadcrumb'] = 'Breadcrumb trail';
 $string['hideblocka'] = 'Hide {$a} block';
-$string['monthnext'] = 'Next month';
-$string['monthprev'] = 'Previous month';
 $string['showblocka'] = 'Show {$a} block';
 $string['sitemap'] = 'Site map';
 $string['skipa'] = 'Skip {$a}';
@@ -41,3 +39,7 @@ $string['skipto'] = 'Skip to {$a}';
 $string['tocontent'] = 'Skip to main content';
 $string['tonavigation'] = 'Go to navigation';
 $string['youarehere'] = 'You are here';
+
+// Deprecated since Moodle 3.4.
+$string['monthnext'] = 'Next month';
+$string['monthprev'] = 'Previous month';
index 405046d..30a936c 100644 (file)
@@ -90,6 +90,7 @@ $string['eventdate'] = 'Date';
 $string['eventdescription'] = 'Description';
 $string['eventduration'] = 'Duration';
 $string['eventendtime'] = 'End time';
+$string['eventendtimewrapped'] = '{$a} (End time)';
 $string['eventinstanttime'] = 'Time';
 $string['eventkind'] = 'Type of event';
 $string['eventname'] = 'Event title';
@@ -135,10 +136,14 @@ $string['gotocalendar'] = 'Go to calendar';
 $string['group'] = 'Group';
 $string['groupevent'] = 'Group event';
 $string['groupevents'] = 'Group events';
-$string['hidecourseevents'] = 'Hide course events';
-$string['hideglobalevents'] = 'Hide global events';
-$string['hidegroupsevents'] = 'Hide group events';
-$string['hideuserevents'] = 'Hide user events';
+$string['eventtypeglobal'] = 'global';
+$string['eventtypesite'] = 'global';
+$string['eventtypecourse'] = 'course';
+$string['eventtypemodule'] = 'module';
+$string['eventtypegroup'] = 'group';
+$string['eventtypeuser'] = 'user';
+$string['hideeventtype'] = 'Hide {$a} events';
+$string['showeventtype'] = 'Show {$a} events';
 $string['hourly'] = 'Hourly';
 $string['ical'] = 'iCal';
 $string['importcalendar'] = 'Import calendar';
@@ -159,9 +164,10 @@ $string['monday'] = 'Monday';
 $string['monthly'] = 'Monthly';
 $string['monthlyview'] = 'Monthly view';
 $string['monthnext'] = 'Next month';
+$string['monthprev'] = 'Previous month';
 $string['monththis'] = 'This month';
 $string['more'] = 'More';
-$string['namewithsource'] = '{$a->name}({$a->source})';
+$string['namewithsource'] = '{$a->name} ({$a->source})';
 $string['never'] = 'Never';
 $string['newevent'] = 'New event';
 $string['notitle'] = 'no title';
@@ -194,10 +200,6 @@ $string['repeatweeksl'] = 'Repeat weekly, creating altogether';
 $string['repeatweeksr'] = 'events';
 $string['sat'] = 'Sat';
 $string['saturday'] = 'Saturday';
-$string['showcourseevents'] = 'Show course events';
-$string['showglobalevents'] = 'Show global events';
-$string['showgroupsevents'] = 'Show group events';
-$string['showuserevents'] = 'Show user events';
 $string['shown'] = 'shown';
 $string['siteevents'] = 'Site events';
 $string['spanningevents'] = 'Events underway';
@@ -214,17 +216,10 @@ $string['timeformat_12'] = '12-hour (am/pm)';
 $string['timeformat_24'] = '24-hour';
 $string['timeperiod'] = 'Time period';
 $string['today'] = 'Today';
+$string['todayplustitle'] = 'Today {$a}';
 $string['tomorrow'] = 'Tomorrow';
 $string['tt_deleteevent'] = 'Delete event';
 $string['tt_editevent'] = 'Edit event';
-$string['tt_hidecourse'] = 'Course events are shown (click to hide)';
-$string['tt_hideglobal'] = 'Global events are shown (click to hide)';
-$string['tt_hidegroups'] = 'Group events are shown (click to hide)';
-$string['tt_hideuser'] = 'User events are shown (click to hide)';
-$string['tt_showcourse'] = 'Course events are hidden (click to show)';
-$string['tt_showglobal'] = 'Global events are hidden (click to show)';
-$string['tt_showgroups'] = 'Group events are hidden (click to show)';
-$string['tt_showuser'] = 'User events are hidden (click to show)';
 $string['tue'] = 'Tue';
 $string['tuesday'] = 'Tuesday';
 $string['typeclose'] = 'Close event';
@@ -252,3 +247,21 @@ $string['youcandeleteallrepeats'] = 'This event is part of a repeating event ser
 
 // Deprecated since Moodle 3.2.
 $string['for'] = 'for';
+
+// Deprecated since Moodle 3.4.
+$string['tt_hidecourse'] = 'Course events are shown (click to hide)';
+$string['tt_hideglobal'] = 'Global events are shown (click to hide)';
+$string['tt_hidegroups'] = 'Group events are shown (click to hide)';
+$string['tt_hideuser'] = 'User events are shown (click to hide)';
+$string['tt_showcourse'] = 'Course events are hidden (click to show)';
+$string['tt_showglobal'] = 'Global events are hidden (click to show)';
+$string['tt_showgroups'] = 'Group events are hidden (click to show)';
+$string['tt_showuser'] = 'User events are hidden (click to show)';
+$string['hidecourseevents'] = 'Hide course events';
+$string['hideglobalevents'] = 'Hide global events';
+$string['hidegroupsevents'] = 'Hide group events';
+$string['hideuserevents'] = 'Hide user events';
+$string['showcourseevents'] = 'Show course events';
+$string['showglobalevents'] = 'Show global events';
+$string['showgroupsevents'] = 'Show group events';
+$string['showuserevents'] = 'Show user events';
index 55a0e70..cd7bb58 100644 (file)
@@ -61,3 +61,21 @@ error:backpackloginfailed,core_badges
 signinwithyouremail,core_badges
 sectionusedefaultname,core
 registermoochtips,core_hub
+monthnext,core_access
+monthprev,core_access
+tt_hidecourse,core_calendar
+tt_hideglobal,core_calendar
+tt_hidegroups,core_calendar
+tt_hideuser,core_calendar
+tt_showcourse,core_calendar
+tt_showglobal,core_calendar
+tt_showgroups,core_calendar
+tt_showuser,core_calendar
+hidecourseevents,core_calendar
+hideglobalevents,core_calendar
+hidegroupsevents,core_calendar
+hideuserevents,core_calendar
+showcourseevents,core_calendar
+showglobalevents,core_calendar
+showgroupsevents,core_calendar
+showuserevents,core_calendar
index ce69373..3b75249 100644 (file)
Binary files a/lib/amd/build/form-autocomplete.min.js and b/lib/amd/build/form-autocomplete.min.js differ
index 60c678f..76294eb 100644 (file)
@@ -37,6 +37,8 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
         UP: 38
     };
 
+    var uniqueId = $.now();
+
     /**
      * Make an item in the selection list "active".
      *
@@ -786,11 +788,15 @@ define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification']
             // Find or generate some ids.
             var state = {
                 selectId: originalSelect.attr('id'),
-                inputId: 'form_autocomplete_input-' + $.now(),
-                suggestionsId: 'form_autocomplete_suggestions-' + $.now(),
-                selectionId: 'form_autocomplete_selection-' + $.now(),
-                downArrowId: 'form_autocomplete_downarrow-' + $.now()
+                inputId: 'form_autocomplete_input-' + uniqueId,
+                suggestionsId: 'form_autocomplete_suggestions-' + uniqueId,
+                selectionId: 'form_autocomplete_selection-' + uniqueId,
+                downArrowId: 'form_autocomplete_downarrow-' + uniqueId
             };
+
+            // Increment the unique counter so we don't get duplicates ever.
+            uniqueId++;
+
             options.multiple = originalSelect.attr('multiple');
 
             if (typeof closeSuggestionsOnSelect !== "undefined") {
index 8dae596..27adb6a 100644 (file)
@@ -438,7 +438,7 @@ define(['core/mustache',
                 treated += content.substring(0, index);
                 content = content.substr(index);
                 strIndex = '';
-                walker = 4;  // 4 is the length of '[[_s'.
+                walker = 4; // 4 is the length of '[[_s'.
 
                 // Walk the characters to manually extract the index of the string from the placeholder.
                 char = content.substr(walker, 1);
@@ -455,7 +455,7 @@ define(['core/mustache',
                     strFinal = '';
                 }
                 treated += strFinal;
-                content = content.substr(6 + strIndex.length);  // 6 is the length of the placeholder without the index: '[[_s]]'.
+                content = content.substr(6 + strIndex.length); // 6 is the length of the placeholder without the index: '[[_s]]'.
 
                 // Find the next placeholder.
                 index = content.search(pattern);
index 7d3714f..bacd39a 100644 (file)
@@ -63,6 +63,9 @@ class no_teaching extends \core_analytics\local\target\binary {
      * @return \core_analytics\prediction_action[]
      */
     public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
+        global $CFG;
+
+        require_once($CFG->dirroot . '/course/lib.php');
 
         // No need to call the parent as the parent's action is view details and this target only have 1 feature.
         $actions = array();
@@ -75,7 +78,7 @@ class no_teaching extends \core_analytics\local\target\binary {
         $actions['viewcourse'] = new \core_analytics\prediction_action('viewcourse', $prediction,
             $url, $pix, get_string('view'));
 
-        if (has_any_capability(['moodle/course:viewparticipants', 'moodle/course:enrolreview'], $sampledata['context'])) {
+        if (course_can_view_participants($sampledata['context'])) {
             $url = new \moodle_url('/user/index.php', array('id' => $course->id));
             $pix = new \pix_icon('i/cohort', get_string('participants'));
             $actions['viewparticipants'] = new \core_analytics\prediction_action('viewparticipants', $prediction,
index 8f99d9d..11b68c8 100644 (file)
@@ -60,6 +60,9 @@ require_once("$CFG->libdir/filebrowser/file_info_context_module.php");
 */
 class file_browser {
 
+    /** @var array cached list of enrolled courses.  */
+    protected $enrolledcourses = null;
+
     /**
      * Looks up file_info instance
      *
@@ -190,7 +193,7 @@ class file_browser {
     /**
      * Returns info about the files at Course category context
      *
-     * @param stdClass $context context object
+     * @param context $context context object
      * @param string $component component
      * @param string $filearea file area
      * @param int $itemid item ID
@@ -199,39 +202,36 @@ class file_browser {
      * @return file_info|null file_info instance or null if not found or access not allowed
      */
     private function get_file_info_context_module($context, $component, $filearea, $itemid, $filepath, $filename) {
-        global $COURSE, $DB, $CFG;
-
-        static $cachedmodules = array();
-
-        if (!array_key_exists($context->instanceid, $cachedmodules)) {
-            $cachedmodules[$context->instanceid] = get_coursemodule_from_id('', $context->instanceid);
-        }
-
-        if (!($cm = $cachedmodules[$context->instanceid])) {
+        if (!($context instanceof context_module)) {
             return null;
         }
+        $coursecontext = $context->get_course_context();
+        $modinfo = get_fast_modinfo($coursecontext->instanceid);
+        $cm = $modinfo->get_cm($context->instanceid);
 
-        if ($cm->course == $COURSE->id) {
-            $course = $COURSE;
-        } else if (!$course = $DB->get_record('course', array('id'=>$cm->course))) {
+        if (empty($cm->uservisible)) {
             return null;
         }
 
-        $modinfo = get_fast_modinfo($course);
-        if (empty($modinfo->cms[$cm->id]->uservisible)) {
-            return null;
-        }
-
-        $modname = $modinfo->cms[$cm->id]->modname;
-
-        if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
-            return null;
-        }
-
-        // ok, we know that module exists, and user may access it
-
-        $level = new file_info_context_module($this, $context, $course, $cm, $modname);
+        $level = new file_info_context_module($this, $context, $cm->get_course(), $cm, $cm->modname);
         return $level->get_file_info($component, $filearea, $itemid, $filepath, $filename);
     }
 
+    /**
+     * Check if user is enrolled into the course
+     *
+     * This function keeps a cache of enrolled courses because it may be called multiple times for many courses in one request
+     *
+     * @param int $courseid
+     * @return bool
+     */
+    public function is_enrolled($courseid) {
+        if ($this->enrolledcourses === null || PHPUNIT_TEST) {
+            // Since get_file_browser() returns a statically cached object we can't rely on cache
+            // inside the file_browser class in the unittests.
+            // TODO MDL-59964 remove this caching when it's implemented inside enrol_get_my_courses().
+            $this->enrolledcourses = enrol_get_my_courses(['id']);
+        }
+        return array_key_exists($courseid, $this->enrolledcourses);
+    }
 }
index 67f842c..3e9ffa3 100644 (file)
@@ -36,6 +36,9 @@ class file_info_context_course extends file_info {
     /** @var stdClass course object */
     protected $course;
 
+    /** @var file_info_context_module[] cached child modules. See {@link get_child_module()} */
+    protected $childrenmodules = [];
+
     /**
      * Constructor
      *
@@ -68,7 +71,7 @@ class file_info_context_course extends file_info {
             return null;
         }
 
-        if (!is_viewing($this->context) and !is_enrolled($this->context)) {
+        if (!is_viewing($this->context) and !$this->browser->is_enrolled($this->course->id)) {
             // no peaking here if not enrolled or inspector
             return null;
         }
@@ -86,6 +89,41 @@ class file_info_context_course extends file_info {
         return null;
     }
 
+    /**
+     * Returns list of areas inside this course
+     *
+     * @param string $extensions Only return areas that have files with these extensions
+     * @param bool $returnemptyfolders return all areas always, if true it will ignore the previous argument
+     * @return array
+     */
+    protected function get_course_areas($extensions = '*', $returnemptyfolders = false) {
+        global $DB;
+
+        $allareas = [
+            'course_summary',
+            'course_overviewfiles',
+            'course_section',
+            'backup_section',
+            'backup_course',
+            'backup_automated',
+            'course_legacy'
+        ];
+
+        if ($returnemptyfolders) {
+            return $allareas;
+        }
+
+        $params1 = ['contextid' => $this->context->id, 'emptyfilename' => '.'];
+        $sql1 = "SELECT " . $DB->sql_concat('f.component', "'_'", 'f.filearea') . "
+            FROM {files} f
+            WHERE f.filename <> :emptyfilename AND f.contextid = :contextid ";
+        $sql3 = ' GROUP BY f.component, f.filearea';
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $areaswithfiles = $DB->get_fieldset_sql($sql1 . $sql2 . $sql3, array_merge($params1, $params2));
+
+        return array_intersect($allareas, $areaswithfiles);
+    }
+
     /**
      * Gets a stored file for the course summary filearea directory
      *
@@ -387,6 +425,28 @@ class file_info_context_course extends file_info {
         return $this->get_filtered_children('*', false, true);
     }
 
+    /**
+     * Returns the child module if it is accessible by the current user
+     *
+     * @param cm_info|int $cm
+     * @return file_info_context_module|null
+     */
+    protected function get_child_module($cm) {
+        $cmid = is_object($cm) ? $cm->id : $cm;
+        if (!array_key_exists($cmid, $this->childrenmodules)) {
+            $this->childrenmodules[$cmid] = null;
+            if (!($cm instanceof cm_info)) {
+                $cms = get_fast_modinfo($this->course)->cms;
+                $cm = array_key_exists($cmid, $cms) ? $cms[$cmid] : null;
+            }
+            if ($cm && $cm->uservisible) {
+                $this->childrenmodules[$cmid] = new file_info_context_module($this->browser,
+                    $cm->context, $this->course, $cm, $cm->modname);
+            }
+        }
+        return $this->childrenmodules[$cmid];
+    }
+
     /**
      * Help function to return files matching extensions or their count
      *
@@ -397,46 +457,52 @@ class file_info_context_course extends file_info {
      * @return array|int array of file_info instances or the count
      */
     private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
-        $areas = array(
-            array('course', 'summary'),
-            array('course', 'overviewfiles'),
-            array('course', 'section'),
-            array('backup', 'section'),
-            array('backup', 'course'),
-            array('backup', 'automated'),
-            array('course', 'legacy')
-        );
         $children = array();
-        foreach ($areas as $area) {
+
+        $courseareas = $this->get_course_areas($extensions, $returnemptyfolders);
+        foreach ($courseareas as $areaname) {
+            $area = explode('_', $areaname, 2);
             if ($child = $this->get_file_info($area[0], $area[1], 0, '/', '.')) {
-                if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
-                    $children[] = $child;
-                    if (($countonly !== false) && count($children) >= $countonly) {
-                        return $countonly;
-                    }
+                $children[] = $child;
+                if (($countonly !== false) && count($children) >= $countonly) {
+                    return $countonly;
                 }
             }
         }
 
+        $cnt = count($children);
         if (!has_capability('moodle/course:managefiles', $this->context)) {
             // 'managefiles' capability is checked in every activity module callback.
             // Don't even waste time on retrieving the modules if we can't browse the files anyway
         } else {
-            // now list all modules
-            $modinfo = get_fast_modinfo($this->course);
-            foreach ($modinfo->cms as $cminfo) {
-                if (empty($cminfo->uservisible)) {
-                    continue;
-                }
-                $modcontext = context_module::instance($cminfo->id, IGNORE_MISSING);
-                if ($child = $this->browser->get_file_info($modcontext)) {
-                    if ($returnemptyfolders || $child->count_non_empty_children($extensions)) {
+            if ($returnemptyfolders) {
+                $modinfo = get_fast_modinfo($this->course);
+                foreach ($modinfo->cms as $cminfo) {
+                    if ($child = $this->get_child_module($cminfo)) {
                         $children[] = $child;
-                        if (($countonly !== false) && count($children) >= $countonly) {
-                            return $countonly;
+                        $cnt++;
+                    }
+                }
+            } else if ($moduleareas = $this->get_module_areas_with_files($extensions)) {
+                // We found files in some of the modules.
+                // Create array of children modules ordered with the same way as cms in modinfo.
+                $modulechildren = array_fill_keys(array_keys(get_fast_modinfo($this->course)->get_cms()), null);
+                foreach ($moduleareas as $area) {
+                    if ($modulechildren[$area->cmid]) {
+                        // We already found non-empty area within the same module, do not analyse other areas.
+                        continue;
+                    }
+                    if ($child = $this->get_child_module($area->cmid)) {
+                        if ($child->get_file_info($area->component, $area->filearea, $area->itemid, null, null)) {
+                            $modulechildren[$area->cmid] = $child;
+                            $cnt++;
+                            if (($countonly !== false) && $cnt >= $countonly) {
+                                return $cnt;
+                            }
                         }
                     }
                 }
+                $children = array_merge($children, array_values(array_filter($modulechildren)));
             }
         }
 
@@ -446,6 +512,52 @@ class file_info_context_course extends file_info {
         return $children;
     }
 
+    /**
+     * Returns list of areas inside the course modules that have files with the given extension
+     *
+     * @param string $extensions
+     * @return array
+     */
+    protected function get_module_areas_with_files($extensions = '*') {
+        global $DB;
+
+        $params1 = ['contextid' => $this->context->id,
+            'emptyfilename' => '.',
+            'contextlevel' => CONTEXT_MODULE,
+            'depth' => $this->context->depth + 1,
+            'pathmask' => $this->context->path . '/%'];
+        $sql1 = "SELECT ctx.id AS contextid, f.component, f.filearea, f.itemid, ctx.instanceid AS cmid, " .
+                context_helper::get_preload_record_columns_sql('ctx') . "
+            FROM {files} f
+            INNER JOIN {context} ctx ON ctx.id = f.contextid
+            WHERE f.filename <> :emptyfilename
+              AND ctx.contextlevel = :contextlevel
+              AND ctx.depth = :depth
+              AND " . $DB->sql_like('ctx.path', ':pathmask') . " ";
+        $sql3 = ' GROUP BY ctx.id, f.component, f.filearea, f.itemid, ctx.instanceid,
+              ctx.path, ctx.depth, ctx.contextlevel
+            ORDER BY ctx.id, f.component, f.filearea, f.itemid';
+        list($sql2, $params2) = $this->build_search_files_sql($extensions);
+        $areas = [];
+        if ($rs = $DB->get_recordset_sql($sql1. $sql2 . $sql3, array_merge($params1, $params2))) {
+            foreach ($rs as $record) {
+                context_helper::preload_from_record($record);
+                $areas[] = $record;
+            }
+            $rs->close();
+        }
+
+        // Sort areas so 'backup' and 'intro' are in the beginning of the list, they are the easiest to check access to.
+        usort($areas, function($a, $b) {
+            $aeasy = ($a->filearea === 'intro' && substr($a->component, 0, 4) === 'mod_') ||
+                ($a->filearea === 'activity' && $a->component === 'backup');
+            $beasy = ($b->filearea === 'intro' && substr($b->component, 0, 4) === 'mod_') ||
+                ($b->filearea === 'activity' && $b->component === 'backup');
+            return $aeasy == $beasy ? 0 : ($aeasy ? -1 : 1);
+        });
+        return $areas;
+    }
+
     /**
      * Returns list of children which are either files matching the specified extensions
      * or folders that contain at least one such file.
index 7c354bd..d0b4746 100644 (file)
@@ -97,6 +97,10 @@ class file_info_context_coursecat extends file_info {
     protected function get_area_coursecat_description($itemid, $filepath, $filename) {
         global $CFG;
 
+        if (!$this->category->id) {
+            // No coursecat description area for "system".
+            return null;
+        }
         if (!$this->category->visible and !has_capability('moodle/category:viewhiddencategories', $this->context)) {
             return null;
         }
@@ -158,37 +162,92 @@ class file_info_context_coursecat extends file_info {
      * @return array of file_info instances
      */
     public function get_children() {
-        global $DB;
-
         $children = array();
 
         if ($child = $this->get_area_coursecat_description(0, '/', '.')) {
             $children[] = $child;
         }
 
-        $course_cats = $DB->get_records('course_categories', array('parent'=>$this->category->id), 'sortorder', 'id,visible');
-        foreach ($course_cats as $category) {
+        list($coursecats, $hiddencats) = $this->get_categories();
+        foreach ($coursecats as $category) {
             $context = context_coursecat::instance($category->id);
-            if (!$category->visible and !has_capability('moodle/category:viewhiddencategories', $context)) {
-                continue;
-            }
-            if ($child = $this->browser->get_file_info($context)) {
-                $children[] = $child;
-            }
+            $children[] = new self($this->browser, $context, $category);
         }
 
-        $courses = $DB->get_records('course', array('category'=>$this->category->id), 'sortorder', 'id,visible');
+        $courses = $this->get_courses($hiddencats);
         foreach ($courses as $course) {
-            $context = context_course::instance($course->id);
-            if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
-                continue;
-            }
-            if ($child = $this->browser->get_file_info($context)) {
-                $children[] = $child;
+            $children[] = $this->get_child_course($course);
+        }
+
+        return array_filter($children);
+    }
+
+    /**
+     * List of courses in this category and in hidden subcategories
+     *
+     * @param array $hiddencats list of categories that are hidden from current user and returned by {@link get_categories()}
+     * @return array list of courses
+     */
+    protected function get_courses($hiddencats) {
+        global $DB, $CFG;
+        require_once($CFG->libdir.'/modinfolib.php');
+
+        $params = array('category' => $this->category->id, 'contextlevel' => CONTEXT_COURSE);
+        $sql = 'c.category = :category';
+
+        foreach ($hiddencats as $category) {
+            $catcontext = context_coursecat::instance($category->id);
+            $sql .= ' OR ' . $DB->sql_like('ctx.path', ':path' . $category->id);
+            $params['path' . $category->id] = $catcontext->path . '/%';
+        }
+
+        // Let's retrieve only minimum number of fields from course table -
+        // what is needed to check access or call get_fast_modinfo().
+        $coursefields = array_merge(['id', 'visible'], course_modinfo::$cachedfields);
+        $fields = 'c.' . join(',c.', $coursefields) . ', ' .
+            context_helper::get_preload_record_columns_sql('ctx');
+        return $DB->get_records_sql('SELECT ' . $fields . ' FROM {course} c
+                JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)
+                WHERE ('.$sql.') ORDER BY c.sortorder', $params);
+    }
+
+    /**
+     * Finds accessible and non-accessible direct subcategories
+     *
+     * @return array [$coursecats, $hiddencats] - child categories that are visible to the current user and not visible
+     */
+    protected function get_categories() {
+        global $DB;
+        $fields = 'c.*, ' . context_helper::get_preload_record_columns_sql('ctx');
+        $coursecats = $DB->get_records_sql('SELECT ' . $fields . ' FROM {course_categories} c
+                LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)
+                WHERE c.parent = :parent ORDER BY c.sortorder',
+            array('parent' => $this->category->id, 'contextlevel' => CONTEXT_COURSECAT));
+
+        $hiddencats = [];
+
+        foreach ($coursecats as $id => &$category) {
+            context_helper::preload_from_record($category);
+            $context = context_coursecat::instance($category->id);
+            if (!$category->visible && !has_capability('moodle/category:viewhiddencategories', $context)) {
+                $hiddencats[$id] = $coursecats[$id];
+                unset($coursecats[$id]);
             }
         }
+        return [$coursecats, $hiddencats];
+    }
 
-        return $children;
+    /**
+     * Returns the file info element for a given course or null if course is not accessible
+     *
+     * @param stdClass $course may contain context fields for preloading
+     * @return file_info_context_course|null
+     */
+    protected function get_child_course($course) {
+        context_helper::preload_from_record($course);
+        $context = context_course::instance($course->id);
+        $child = new file_info_context_course($this->browser, $context, $course);
+        return $child->get_file_info(null, null, null, null, null);
     }
 
     /**
@@ -200,53 +259,33 @@ class file_info_context_coursecat extends file_info {
      * @return int
      */
     public function count_non_empty_children($extensions = '*', $limit = 1) {
-        global $DB;
         $cnt = 0;
-        if (($child = $this->get_area_coursecat_description(0, '/', '.'))
-                && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
-            return $cnt;
+        if ($child = $this->get_area_coursecat_description(0, '/', '.')) {
+            $cnt += $child->count_non_empty_children($extensions) ? 1 : 0;
+            if ($cnt >= $limit) {
+                return $cnt;
+            }
         }
 
-        $rs = $DB->get_recordset_sql('SELECT ctx.id AS contextid, c.visible
-                FROM {context} ctx, {course} c
-                WHERE ctx.instanceid = c.id
-                AND ctx.contextlevel = :courselevel
-                AND c.category = :categoryid
-                ORDER BY c.visible DESC', // retrieve visible courses first
-                array('categoryid' => $this->category->id, 'courselevel' => CONTEXT_COURSE));
-        foreach ($rs as $record) {
-            $context = context::instance_by_id($record->contextid);
-            if (!$record->visible and !has_capability('moodle/course:viewhiddencourses', $context)) {
-                continue;
-            }
-            if (($child = $this->browser->get_file_info($context))
-                    && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
-                break;
+        list($coursecats, $hiddencats) = $this->get_categories();
+        foreach ($coursecats as $category) {
+            $context = context_coursecat::instance($category->id);
+            $child = new file_info_context_coursecat($this->browser, $context, $category);
+            $cnt += $child->count_non_empty_children($extensions) ? 1 : 0;
+            if ($cnt >= $limit) {
+                return $cnt;
             }
         }
-        $rs->close();
-        if ($cnt >= $limit) {
-            return $cnt;
-        }
 
-        $rs = $DB->get_recordset_sql('SELECT ctx.id AS contextid, cat.visible
-                FROM {context} ctx, {course_categories} cat
-                WHERE ctx.instanceid = cat.id
-                AND ctx.contextlevel = :catlevel
-                AND cat.parent = :categoryid
-                ORDER BY cat.visible DESC', // retrieve visible categories first
-                array('categoryid' => $this->category->id, 'catlevel' => CONTEXT_COURSECAT));
-        foreach ($rs as $record) {
-            $context = context::instance_by_id($record->contextid);
-            if (!$record->visible and !has_capability('moodle/category:viewhiddencategories', $context)) {
-                continue;
-            }
-            if (($child = $this->browser->get_file_info($context))
-                    && $child->count_non_empty_children($extensions) && (++$cnt) >= $limit) {
-                break;
+        $courses = $this->get_courses($hiddencats);
+        foreach ($courses as $course) {
+            if ($child = $this->get_child_course($course)) {
+                $cnt += $child->count_non_empty_children($extensions) ? 1 : 0;
+                if ($cnt >= $limit) {
+                    return $cnt;
+                }
             }
         }
-        $rs->close();
 
         return $cnt;
     }
index c62f83a..c2c068f 100644 (file)
@@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
 class file_info_context_module extends file_info {
     /** @var stdClass Course object */
     protected $course;
-    /** @var stdClass Course module object */
+    /** @var cm_info Course module object */
     protected $cm;
     /** @var string Module name */
     protected $modname;
@@ -58,23 +58,17 @@ class file_info_context_module extends file_info {
 
         parent::__construct($browser, $context);
         $this->course  = $course;
-        $this->cm      = $cm;
-        $this->modname = $modname;
+        $this->cm      = cm_info::create($cm);
+        $this->modname = $this->cm->modname;
         $this->nonemptychildren = null;
 
-        include_once("$CFG->dirroot/mod/$modname/lib.php");
-
-        //find out all supported areas
-        $functionname     = 'mod_'.$modname.'_get_file_areas';
-        $functionname_old = $modname.'_get_file_areas';
-
-        if (function_exists($functionname)) {
+        if ($functionname = component_callback_exists('mod_'.$modname, 'get_file_areas')) {
+            $cm = $this->cm->get_course_module_record();
             $this->areas = $functionname($course, $cm, $context);
-        } else if (function_exists($functionname_old)) {
-            $this->areas = $functionname_old($course, $cm, $context);
         } else {
             $this->areas = array();
         }
+
         unset($this->areas['intro']); // hardcoded, ignore attempts to override it
     }
 
@@ -99,14 +93,12 @@ class file_info_context_module extends file_info {
             return null;
         }
 
-        if (!is_viewing($this->context) and !is_enrolled($this->context)) {
+        if (!is_viewing($this->context) and !$this->browser->is_enrolled($this->course->id)) {
             // no peaking here if not enrolled or inspector
             return null;
         }
 
-        $modinfo = get_fast_modinfo($this->course);
-        $cminfo = $modinfo->get_cm($this->cm->id);
-        if (!$cminfo->uservisible) {
+        if (!$this->cm->uservisible) {
             // activity hidden sorry
             return null;
         }
@@ -121,13 +113,10 @@ class file_info_context_module extends file_info {
             return $this->get_area_backup($itemid, $filepath, $filename);
         }
 
-        $functionname     = 'mod_'.$this->modname.'_get_file_info';
-        $functionname_old = $this->modname.'_get_file_info';
-
-        if (function_exists($functionname)) {
-            return $functionname($this->browser, $this->areas, $this->course, $this->cm, $this->context, $filearea, $itemid, $filepath, $filename);
-        } else if (function_exists($functionname_old)) {
-            return $functionname_old($this->browser, $this->areas, $this->course, $this->cm, $this->context, $filearea, $itemid, $filepath, $filename);
+        if ($functionname = component_callback_exists('mod_'.$this->modname, 'get_file_info')) {
+            $cm = $this->cm->get_course_module_record();
+            return $functionname($this->browser, $this->areas, $this->course, $cm,
+                $this->context, $filearea, $itemid, $filepath, $filename);
         }
 
         return null;
@@ -206,7 +195,7 @@ class file_info_context_module extends file_info {
      * @return string
      */
     public function get_visible_name() {
-        return $this->cm->name.' ('.get_string('modulename', $this->cm->modname).')';
+        return $this->cm->get_formatted_name().' ('.$this->cm->get_module_type_name().')';
     }
 
     /**
index 27144c0..a7c4f28 100644 (file)
@@ -25,6 +25,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once($CFG->libdir.'/filebrowser/file_info_context_coursecat.php');
+
 /**
  * Represents the system context in the tree navigated by {@link file_browser}.
  *
@@ -32,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2008 Petr Skoda (http://skodak.org)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class file_info_context_system extends file_info {
+class file_info_context_system extends file_info_context_coursecat {
 
     /**
      * Constructor
@@ -41,7 +43,7 @@ class file_info_context_system extends file_info {
      * @param stdClass $context context object
      */
     public function __construct($browser, $context) {
-        parent::__construct($browser, $context);
+        parent::__construct($browser, $context, (object)['id' => 0, 'parent' => 0, 'visible' => 1]);
     }
 
     /**
@@ -139,71 +141,6 @@ class file_info_context_system extends file_info {
         return true;
     }
 
-    /**
-     * Returns list of children.
-     *
-     * @return array of file_info instances
-     */
-    public function get_children() {
-        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);
-            if (!$category->visible and !has_capability('moodle/category:viewhiddencategories', $context)) {
-                continue;
-            }
-            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
      *
diff --git a/lib/filebrowser/tests/file_browser_test.php b/lib/filebrowser/tests/file_browser_test.php
new file mode 100644 (file)
index 0000000..3ce0570
--- /dev/null
@@ -0,0 +1,244 @@
+<?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/>.
+
+/**
+ * Unit tests for file browser
+ *
+ * @package    core_files
+ * @copyright  2017 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Unit tests for file browser
+ *
+ * @package    core_files
+ * @copyright  2017 Marina Glancy
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class file_browser_testcase extends advanced_testcase {
+
+    /** @var stdClass */
+    protected $course1;
+    /** @var stdClass */
+    protected $course2;
+    /** @var stdClass */
+    protected $module1;
+    /** @var stdClass */
+    protected $module2;
+    /** @var stdClass */
+    protected $course1filerecord;
+    /** @var stdClass */
+    protected $teacher;
+    /** @var stdClass */
+    protected $teacherrole;
+
+    /**
+     * Set up
+     */
+    public function setUp() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+
+        $this->getDataGenerator()->create_category(); // Empty category.
+        $this->course1 = $this->getDataGenerator()->create_course(); // Empty course.
+
+        $this->course2 = $this->getDataGenerator()->create_course();
+
+        // Add a file to course1 summary.
+        $coursecontext1 = context_course::instance($this->course1->id);
+        $this->course1filerecord = array('contextid' => $coursecontext1->id,
+            'component' => 'course',
+            'filearea' => 'summary',
+            'itemid' => '0',
+            'filepath' => '/',
+            'filename' => 'summaryfile.jpg');
+        $fs = get_file_storage();
+        $fs->create_file_from_string($this->course1filerecord, 'IMG');
+
+        $this->module1 = $this->getDataGenerator()->create_module('resource', ['course' => $this->course2->id]); // Contains 1 file.
+        $this->module2 = $this->getDataGenerator()->create_module('assign', ['course' => $this->course2->id]); // Contains no files.
+
+        $this->teacher = $this->getDataGenerator()->create_user();
+        $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+        $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course1->id, $this->teacherrole->id);
+        $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course2->id, $this->teacherrole->id);
+
+        $this->setUser($this->teacher);
+    }
+
+    /**
+     * Test "Server files" from the system context
+     */
+    public function test_file_info_context_system() {
+
+        // There is one non-empty category child and two category children.
+
+        $browser = get_file_browser();
+        $fileinfo = $browser->get_file_info(context_system::instance());
+        $this->assertNotEmpty($fileinfo->count_non_empty_children());
+        $this->assertEquals(1, count($fileinfo->get_non_empty_children()));
+        $categorychildren = array_filter($fileinfo->get_children(), function($a) {
+            return $a instanceof file_info_context_coursecat;
+        });
+        $this->assertEquals(2, count($categorychildren));
+    }
+
+    /**
+     * Test "Server files" from the system context, hide Misc category
+     */
+    public function test_file_info_context_system_hidden() {
+
+        // Hide the course category that contains our two courses. Teacher does not have cap to view hidden categories.
+        coursecat::get($this->course1->category)->update(['visible' => 0]);
+
+        // We should have two non-empty children in system context (courses).
+        $browser = get_file_browser();
+        $fileinfo = $browser->get_file_info(context_system::instance());
+        $this->assertNotEmpty($fileinfo->count_non_empty_children());
+        $this->assertEquals(2, count($fileinfo->get_non_empty_children()));
+
+        // Should be 1 category children (empty category).
+        $categorychildren = array_filter($fileinfo->get_children(), function($a) {
+            return $a instanceof file_info_context_coursecat;
+        });
+        $this->assertEquals(1, count($categorychildren));
+
+        // Should be 2 course children - courses that belonged to hidden subcategory are now direct children of "System".
+        $coursechildren = array_filter($fileinfo->get_children(), function($a) {
+            return $a instanceof file_info_context_course;
+        });
+        $this->assertEquals(2, count($coursechildren));
+    }
+
+    /**
+     * Test "Server files" from the course category context
+     */
+    public function test_file_info_context_coursecat() {
+
+        // There are two non-empty courses.
+
+        $browser = get_file_browser();
+        $fileinfo = $browser->get_file_info(context_coursecat::instance($this->course2->category));
+        $this->assertNotEmpty($fileinfo->count_non_empty_children());
+        $this->assertEquals(2, count($fileinfo->get_non_empty_children()));
+        $coursechildren = array_filter($fileinfo->get_children(), function($a) {
+            return $a instanceof file_info_context_course;
+        });
+        $this->assertEquals(2, count($coursechildren));
+    }
+
+    /**
+     * Test "Server files" from the course category context, only look for .jpg
+     */
+    public function test_file_info_context_coursecat_jpg() {
+
+        // There is one non-empty category child and two category children.
+
+        $browser = get_file_browser();
+        $fileinfo = $browser->get_file_info(context_system::instance());
+        $this->assertNotEmpty($fileinfo->count_non_empty_children(['.jpg']));
+        $this->assertEquals(1, count($fileinfo->get_non_empty_children(['.jpg'])));
+    }
+
+    /**
+     * Test "Server files" from the course context (course1)
+     */
+    public function test_file_info_context_course_1() {
+
+        $browser = get_file_browser();
+        $fileinfo = $browser->get_file_info(context_course::instance($this->course1->id));
+        // Fileinfo element has only one non-empty child - "Course summary" file area.
+        $this->assertNotEmpty($fileinfo->count_non_empty_children());
+        $nonemptychildren = $fileinfo->get_non_empty_children();
+        $this->assertEquals(1, count($nonemptychildren));
+        $child = reset($nonemptychildren);
+        $this->assertTrue($child instanceof file_info_stored);
+        $this->assertEquals(['filename' => '.'] + $this->course1filerecord, $child->get_params());
+        // Filearea "Course summary" has a child that is the actual image file.
+        $this->assertEquals($this->course1filerecord, $child->get_children()[0]->get_params());
+
+        // There are six course-level file areas and no modules in this course.
+        $allchildren = $fileinfo->get_children();
+        $this->assertEquals(6, count($allchildren));
+        $modulechildren = array_filter($allchildren, function($a) {
+            return $a instanceof file_info_context_module;
+        });
+        $this->assertEquals(0, count($modulechildren));
+
+        // Admin can see seven course-level file areas.
+        $this->setAdminUser();
+        $fileinfo = $browser->get_file_info(context_course::instance($this->course1->id));
+        $this->assertEquals(7, count($fileinfo->get_children()));
+    }
+
+    /**
+     * Test "Server files" from the course context (course1)
+     */
+    public function test_file_info_context_course_2() {
+
+        // 2. Start from the course level.
+        $browser = get_file_browser();
+        $fileinfo = $browser->get_file_info(context_course::instance($this->course2->id));
+        $this->assertNotEmpty($fileinfo->count_non_empty_children());
+        $nonemptychildren = $fileinfo->get_non_empty_children();
+        $this->assertEquals(1, count($nonemptychildren));
+        $child = reset($nonemptychildren);
+        $this->assertTrue($child instanceof file_info_context_module);
+        $this->assertEquals($this->module1->name.' (File)', $child->get_visible_name());
+        $this->assertEquals(1, count($child->get_non_empty_children()));
+        $this->assertEquals(1, $child->count_non_empty_children());
+        $modulechildren = array_filter($fileinfo->get_children(), function($a) {
+            return $a instanceof file_info_context_module;
+        });
+        $this->assertEquals(2, count($modulechildren));
+    }
+
+    /**
+     * Test "Server files" from the course context (module1)
+     */
+    public function test_file_info_context_module_1() {
+
+        $module1context = context_module::instance($this->module1->cmid);
+        $browser = get_file_browser();
+        $fileinfo = $browser->get_file_info($module1context);
+        $this->assertEquals($this->module1->name . ' (File)', $fileinfo->get_visible_name());
+        $this->assertNotEmpty($fileinfo->count_non_empty_children());
+        $nonemptychildren = $fileinfo->get_non_empty_children();
+        $this->assertEquals(1, count($nonemptychildren));
+        $child = reset($nonemptychildren);
+        $this->assertTrue($child instanceof file_info_stored);
+    }
+
+    /**
+     * Test "Server files" from the course context (module1)
+     */
+    public function test_file_info_context_module_2() {
+
+        $module2context = context_module::instance($this->module2->cmid);
+        $browser = get_file_browser();
+        $fileinfo = $browser->get_file_info($module2context);
+        $this->assertEquals($this->module2->name.' (Assignment)', $fileinfo->get_visible_name());
+        $this->assertEmpty($fileinfo->count_non_empty_children());
+        $nonemptychildren = $fileinfo->get_non_empty_children();
+        $this->assertEquals(0, count($nonemptychildren));
+
+    }
+}
index afcd47e..da9cb84 100644 (file)
@@ -2212,6 +2212,8 @@ class global_navigation extends navigation_node {
     protected function load_for_user($user=null, $forceforcontext=false) {
         global $DB, $CFG, $USER, $SITE;
 
+        require_once($CFG->dirroot . '/course/lib.php');
+
         if ($user === null) {
             // We can't require login here but if the user isn't logged in we don't
             // want to show anything
@@ -2258,7 +2260,7 @@ class global_navigation extends navigation_node {
         } else if ($USER->id != $user->id) {
             // This is the site so add a users node to the root branch.
             $usersnode = $this->rootnodes['users'];
-            if (has_capability('moodle/course:viewparticipants', $coursecontext)) {
+            if (course_can_view_participants($coursecontext)) {
                 $usersnode->action = new moodle_url('/user/index.php', array('id' => $course->id));
             }
             $userviewurl = new moodle_url('/user/profile.php', $baseargs);
@@ -2715,7 +2717,8 @@ class global_navigation extends navigation_node {
             // Just a link to course competency.
             $title = get_string('competencies', 'core_competency');
             $path = new moodle_url("/admin/tool/lp/coursecompetencies.php", array('courseid' => $course->id));
-            $coursenode->add($title, $path, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/competencies', ''));
+            $coursenode->add($title, $path, navigation_node::TYPE_SETTING, null, 'competencies',
+                    new pix_icon('i/competencies', ''));
         }
         if ($navoptions->grades) {
             $url = new moodle_url('/grade/report/index.php', array('id'=>$course->id));
index a6c2cf3..616e860 100644 (file)
@@ -56,7 +56,7 @@
 
         <div class="subcontent loginsub">
             {{#error}}
-                <div class="loginerrors">
+                <div class="loginerrors" role="alert">
                     <a href="#" id="loginerrormessage" class="accesshide">{{error}}</a>
                     {{{errorformatted}}}
                 </div>
index cd4d742..38772ea 100644 (file)
@@ -44,6 +44,8 @@ information provided here is intended especially for developers.
 * user_can_view_profile() now also checks the moodle/user:viewalldetails capability.
 * The core/modal_confirm dialogue has been deprecated. Please use the core/modal_save_cancel dialogue instead. Please ensure you
   update to use the ModalEvents.save and ModalEvents.cancel events instead of their yes/no counterparts.
+* Instead of checking the 'moodle/course:viewparticipants' and 'moodle/site:viewparticipants' capabilities use the
+  new functions course_can_view_participants() and course_require_view_participants().
 
 === 3.3.1 ===
 
index 642353f..2065529 100644 (file)
@@ -199,6 +199,10 @@ class api {
         // Make sure to limit searches to enrolled courses.
         $enrolledcourses = enrol_get_my_courses(array('id', 'cacherev'));
         $courses = array();
+        // Really we want the user to be able to view the participants if they have the capability
+        // 'moodle/course:viewparticipants' or 'moodle/course:enrolreview', but since the search_courses function
+        // only takes required parameters we can't. However, the chance of a user having 'moodle/course:enrolreview' but
+        // *not* 'moodle/course:viewparticipants' are pretty much zero, so it is not worth addressing.
         if ($arrcourses = \coursecat::search_courses(array('search' => $search), array('limit' => $limitnum),
                 array('moodle/course:viewparticipants'))) {
             foreach ($arrcourses as $course) {
index 0d80644..27f7568 100644 (file)
@@ -249,7 +249,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/custom_interactio
         // If the element isn't in the view window.
         if (relativeTop > container.innerHeight()) {
             var height = notificationElement.outerHeight();
-            // offset enough to make sure the notification will be in view.
+            // Offset enough to make sure the notification will be in view.
             height = height * 4;
             var scrollTo = position.top - height;
             container.scrollTop(scrollTo);
index ea5a69c..7517d9c 100644 (file)
@@ -36,6 +36,7 @@ require_once($CFG->dirroot . '/mod/assign/locallib.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
     /**
      * Returns true if this area uses file indexing.
      *
@@ -46,22 +47,14 @@ class activity extends \core_search\base_activity {
     }
 
     /**
-     * Add the attached description files.
+     * Return the context info required to index files for
+     * this search area.
      *
-     * @param document $document The current document
-     * @return null
+     * @return array
      */
-    public function attach_files($document) {
-        $fs = get_file_storage();
-
-        $cm = $this->get_cm($this->get_module_name(), $document->get('itemid'), $document->get('courseid'));
-        $context = \context_module::instance($cm->id);
-
-        $files = $fs->get_area_files($context->id, 'mod_assign', ASSIGN_INTROATTACHMENT_FILEAREA, 0,
-                'sortorder DESC, id ASC', false);
+    public function get_search_fileareas() {
+        $fileareas = array('intro', ASSIGN_INTROATTACHMENT_FILEAREA); // Fileareas.
 
-        foreach ($files as $file) {
-            $document->add_stored_file($file);
-        }
+        return $fileareas;
     }
 }
index 14103ad..0ae808e 100644 (file)
@@ -170,6 +170,12 @@ class assign {
     /** @var array cached list of IDs of users who share group membership with the user. The cache key will be the user. */
     private $sharedgroupmembers = array();
 
+    /**
+     * @var stdClass The most recent team submission. Used to determine additional attempt numbers and whether
+     * to update the gradebook.
+     */
+    private $mostrecentteamsubmission = null;
+
     /**
      * Constructor for the base assign class.
      *
@@ -2478,13 +2484,16 @@ class assign {
 
         $submission = null;
         if ($this->get_instance()->teamsubmission) {
-            $submission = $this->get_group_submission($grade->userid, 0, false);
+            if (isset($this->mostrecentteamsubmission)) {
+                $submission = $this->mostrecentteamsubmission;
+            } else {
+                $submission = $this->get_group_submission($grade->userid, 0, false);
+            }
         } else {
             $submission = $this->get_user_submission($grade->userid, false);
         }
 
-        // Only push to gradebook if the update is for the latest attempt.
-        // Not the latest attempt.
+        // Only push to gradebook if the update is for the most recent attempt.
         if ($submission && $submission->attemptnumber != $grade->attemptnumber) {
             return true;
         }
@@ -7935,6 +7944,12 @@ class assign {
         $instance = $this->get_instance();
         $submission = null;
         if ($instance->teamsubmission) {
+            // We need to know what the most recent group submission is.
+            // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
+            // and when deciding if we need to update the gradebook with an edited grade.
+            $mostrecentsubmission = $this->get_group_submission($userid, 0, false, -1);
+            $this->set_most_recent_team_submission($mostrecentsubmission);
+            // Get the submission that we are saving grades for. The data attempt number determines which submission attempt.
             $submission = $this->get_group_submission($userid, 0, false, $data->attemptnumber);
         } else {
             $submission = $this->get_user_submission($userid, false, $data->attemptnumber);
@@ -7949,8 +7964,10 @@ class assign {
             }
             $members = $this->get_submission_group_members($groupid, true, $this->show_only_active_users());
             foreach ($members as $member) {
-                // User may exist in multple groups (which should put them in the default group).
-                $this->apply_grade_to_user($data, $member->id, $data->attemptnumber);
+                // We only want to update the grade for this group submission attempt. The data attempt number could be
+                // -1 which may end up in additional attempts being created for each group member instead of just one
+                // additional attempt for the group.
+                $this->apply_grade_to_user($data, $member->id, $submission->attemptnumber);
                 $this->process_outcomes($member->id, $data, $userid);
             }
         } else {
@@ -8121,6 +8138,11 @@ class assign {
             }
 
             if (empty($groupsprocessed[$groupid])) {
+                // We need to know what the most recent group submission is.
+                // Specifically when determining if we are adding another attempt (we only want to add one attempt per team),
+                // and when deciding if we need to update the gradebook with an edited grade.
+                $currentsubmission = $this->get_group_submission($userid, 0, false, -1);
+                $this->set_most_recent_team_submission($currentsubmission);
                 $result = $this->process_add_attempt($userid) && $result;
                 $groupsprocessed[$groupid] = true;
             }
@@ -8171,7 +8193,18 @@ class assign {
 
         // Create the new submission record for the group/user.
         if ($this->get_instance()->teamsubmission) {
-            $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
+            if (isset($this->mostrecentteamsubmission)) {
+                // Team submissions can end up in this function for each user (via save_grade). We don't want to create
+                // more than one attempt for the whole team.
+                if ($this->mostrecentteamsubmission->attemptnumber == $oldsubmission->attemptnumber) {
+                    $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
+                } else {
+                    $newsubmission = $this->get_group_submission($userid, 0, false, $oldsubmission->attemptnumber);
+                }
+            } else {
+                debugging('Please use set_most_recent_team_submission() before calling add_attempt', DEBUG_DEVELOPER);
+                $newsubmission = $this->get_group_submission($userid, 0, true, $oldsubmission->attemptnumber + 1);
+            }
         } else {
             $newsubmission = $this->get_user_submission($userid, true, $oldsubmission->attemptnumber + 1);
         }
@@ -8635,6 +8668,18 @@ class assign {
 
         return $o;
     }
+
+    /**
+     * Set the most recent submission for the team.
+     * The most recent team submission is used to determine if another attempt should be created when allowing another
+     * attempt on a group assignment, and whether the gradebook should be updated.
+     *
+     * @since Moodle 3.4
+     * @param stdClass $submission The most recent submission of the group.
+     */
+    public function set_most_recent_team_submission($submission) {
+        $this->mostrecentteamsubmission = $submission;
+    }
 }
 
 /**
index ba940f5..29fdb6b 100644 (file)
@@ -126,42 +126,43 @@ Feature: In an assignment, students start a new attempt based on their previous
       | operation | Allow another attempt |
     And I click on "Go" "button" confirming the dialogue
     And I should not see "The grades were not saved because someone has modified one or more records more recently than when you loaded the page."
-# Behat tests for the group submission, should be uncommented once the MDL-48216 is fixed.
-#    And I log out
-#    And I log in as "student3"
-#    And I am on "Course 1" course homepage
-#    And I follow "Test assignment name"
-#    #And I should see "This is attempt 1 ( 3 attempts allowed )."
-#    And I press "Add submission"
-#    And I set the following fields to these values:
-#      | Online text | I'm the student's 3 group 2 first attempt |
-#    And I press "Save changes"
-#    And I log out
-#    And I log in as "teacher1"
-#    And I am on "Course 1" course homepage
-#    And I follow "Test assignment name"
-#    And I navigate to "View all submissions" in current page administration
-#    And "Student 1" row "Status" column of "generaltable" table should contain "Reopened"
-#    And "Student 2" row "Status" column of "generaltable" table should contain "Reopened"
-#    And "Student 3" row "Status" column of "generaltable" table should contain "Submitted for grading"
-#    And "Student 4" row "Status" column of "generaltable" table should contain "Submitted for grading"
-#    And I click on "Grade " "link" in the "Student 3" "table_row"
-#    And I set the following fields to these values:
-#      | Allow another attempt | 1 |
-#    And I press "Save changes"
-#    And I log out
-#    And I log in as "student4"
-#    And I am on "Course 1" course homepage
-#    And I follow "Test assignment name"
-#    #And I should see "This is attempt 2 ( 3 attempts allowed )."
-#    And I press "Add submission"
-#    And I set the following fields to these values:
-#      | Online text | I'm the student's 4 group 2 second attempt |
-#    And I press "Save changes"
-#    And I log out
-#    And I log in as "teacher1"
-#    And I am on "Course 1" course homepage
-#    And I follow "Test assignment name"
-#    I navigate to "View all submissions" in current page administration
-#    And I click on "Grade" "link" in the "Student 1" "table_row"
-    #And I should see "This is attempt 2 (3 attempts allowed)"
+    And I log out
+    And I log in as "student3"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment name"
+    And I should see "This is attempt 1 ( 3 attempts allowed )."
+    And I press "Add submission"
+    And I set the following fields to these values:
+      | Online text | I'm the student's 3 group 2 first attempt |
+    And I press "Save changes"
+    And I log out
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment name"
+    And I navigate to "View all submissions" in current page administration
+    And "Student 1" row "Status" column of "generaltable" table should contain "Reopened"
+    And "Student 2" row "Status" column of "generaltable" table should contain "Reopened"
+    And "Student 3" row "Status" column of "generaltable" table should contain "Submitted for grading"
+    And "Student 4" row "Status" column of "generaltable" table should contain "Submitted for grading"
+    And I click on "Grade" "link" in the "Student 3" "table_row"
+    And I set the following fields to these values:
+      | Allow another attempt | 1 |
+    And I press "Save changes"
+    And I press "Ok"
+    And I follow "Assignment: Test assignment name"
+    And I log out
+    And I log in as "student4"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment name"
+    And I should see "This is attempt 2 ( 3 attempts allowed )."
+    And I press "Add a new attempt"
+    And I set the following fields to these values:
+      | Online text | I'm the student's 4 group 2 second attempt |
+    And I press "Save changes"
+    And I log out
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I follow "Test assignment name"
+    And I select "Group 2" from the "group" singleselect
+    And I click on "Grade" "link" in the ".submissionlinks" "css_element"
+    And I should see "2" in the "#id_attemptsettings" "css_element"
index 0d99f07..e6cef21 100644 (file)
@@ -1,5 +1,9 @@
 This files describes API changes in the assign code.
 
+=== 3.4 ===
+* assign::add_attempt requires that set_most_recent_team_submission() be called if attempting to use this function with a team
+  submission.
+
 === 3.3.2 ===
 * assign_refresh_events() Now takes two additional parameters to refine the update to a specific instance. This function
   now optionally takes the module instance object or ID, and the course module object or ID. Please try to send the full
index 862e42e..dac6b7b 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 021b58c..3acd186 100644 (file)
@@ -158,4 +158,25 @@ class chapter extends \core_search\base_mod {
         $contextmodule = \context::instance_by_id($doc->get('contextid'));
         return new \moodle_url('/mod/book/view.php', array('id' => $contextmodule->instanceid));
     }
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array('chapter'); // Filearea.
+
+        return $fileareas;
+    }
 }
index ffc7eb7..4072c78 100644 (file)
@@ -579,8 +579,8 @@ function book_export_contents($cm, $baseurl) {
         $chapterindexfile['filesize']     = 0;
         $chapterindexfile['fileurl']      = moodle_url::make_webservice_pluginfile_url(
                     $context->id, 'mod_book', 'chapter', $chapter->id, '/', 'index.html')->out(false);
-        $chapterindexfile['timecreated']  = $book->timecreated;
-        $chapterindexfile['timemodified'] = $book->timemodified;
+        $chapterindexfile['timecreated']  = $chapter->timecreated;
+        $chapterindexfile['timemodified'] = $chapter->timemodified;
         $chapterindexfile['content']      = format_string($chapter->title, true, array('context' => $context));
         $chapterindexfile['sortorder']    = 0;
         $chapterindexfile['userid']       = null;
index 067dd6e..98bed37 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index af2bd91..0c7eb2f 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 7f1f609..75fc280 100644 (file)
@@ -34,4 +34,14 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
 }
index 84755b4..eee1f83 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 0809337..51175e2 100644 (file)
@@ -34,6 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
     /**
      * Returns true if this area uses file indexing.
      *
@@ -44,21 +45,14 @@ class activity extends \core_search\base_activity {
     }
 
     /**
-     * Add all the folder files to the index.
+     * Return the context info required to index files for
+     * this search area.
      *
-     * @param document $document The current document
-     * @return null
+     * @return array
      */
-    public function attach_files($document) {
-        $fs = get_file_storage();
-
-        $cm = $this->get_cm($this->get_module_name(), $document->get('itemid'), $document->get('courseid'));
-        $context = \context_module::instance($cm->id);
-
-        $files = $fs->get_area_files($context->id, 'mod_folder', 'content', 0, 'sortorder DESC, id ASC', false);
+    public function get_search_fileareas() {
+        $fileareas = array('intro', 'content'); // Fileareas.
 
-        foreach ($files as $file) {
-            $document->add_stored_file($file);
-        }
+        return $fileareas;
     }
 }
index 6435104..8239758 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 8c43e4e..bb48a03 100644 (file)
@@ -120,6 +120,21 @@ class post extends \core_search\base_mod {
         return true;
     }
 
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array(
+            'attachment',
+            'post'
+        );
+
+        return $fileareas;
+    }
+
     /**
      * Add the forum post attachments.
      *
@@ -142,14 +157,21 @@ class post extends \core_search\base_mod {
         // Because this is used during indexing, we don't want to cache posts. Would result in memory leak.
         unset($this->postsdata[$postid]);
 
-        $cm = $this->get_cm('forum', $post->forum, $document->get('courseid'));
+        $cm = $this->get_cm($this->get_module_name(), $post->forum, $document->get('courseid'));
         $context = \context_module::instance($cm->id);
+        $contextid = $context->id;
+
+        $fileareas = $this->get_search_fileareas();
+        $component = $this->get_component_name();
 
         // Get the files and attach them.
-        $fs = get_file_storage();
-        $files = $fs->get_area_files($context->id, 'mod_forum', 'attachment', $postid, "filename", false);
-        foreach ($files as $file) {
-            $document->add_stored_file($file);
+        foreach ($fileareas as $filearea) {
+            $fs = get_file_storage();
+            $files = $fs->get_area_files($contextid, $component, $filearea, $postid, '', false);
+
+            foreach ($files as $file) {
+                $document->add_stored_file($file);
+            }
         }
     }
 
index 0f080a9..03ecffd 100644 (file)
@@ -5362,6 +5362,8 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $
                                         $currentgroup = -1, $groupmode = -1, $page = -1, $perpage = 100, $cm = null) {
     global $CFG, $USER, $OUTPUT;
 
+    require_once($CFG->dirroot . '/course/lib.php');
+
     if (!$cm) {
         if (!$cm = get_coursemodule_from_instance('forum', $forum->id, $forum->course)) {
             print_error('invalidcoursemodule');
@@ -5498,7 +5500,7 @@ function forum_print_latest_discussions($course, $forum, $maxdiscussions = -1, $
         }
     }
 
-    $canviewparticipants = has_capability('moodle/course:viewparticipants',$context);
+    $canviewparticipants = course_can_view_participants($context);
     $canviewhiddentimedposts = has_capability('mod/forum:viewhiddentimedposts', $context);
 
     $strdatestring = get_string('strftimerecentfull');
index 82b34e2..d7daef8 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 930ec32..791e341 100644 (file)
@@ -193,4 +193,25 @@ class entry extends \core_search\base_mod {
         }
         return $this->entriesdata[$entryid];
     }
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array('attachment', 'entry'); // Fileareas.
+
+        return $fileareas;
+    }
 }
index 04aa1b8..9a1a052 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 2ddd706..a8f7159 100644 (file)
@@ -38,6 +38,15 @@ defined('MOODLE_INTERNAL') || die();
  */
 class activity extends \core_search\base_activity {
 
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
     /**
      * Overwritten as labels are displayed in-course.
      *
index 1f93104..bb24fcf 100644 (file)
@@ -34,4 +34,25 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array('intro', 'page_contents'); // Fileareas.
+
+        return $fileareas;
+    }
 }
index 47d8177..269ad7a 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index c66c66f..6a7b41c 100644 (file)
@@ -35,7 +35,6 @@ defined('MOODLE_INTERNAL') || die();
  */
 class activity extends \core_search\base_activity {
 
-
     /**
      * Returns the document associated with this activity.
      *
@@ -74,4 +73,25 @@ class activity extends \core_search\base_activity {
 
         return $doc;
     }
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array('intro', 'content'); // Fileareas.
+
+        return $fileareas;
+    }
 }
index a4bb3f6..bdb90c2 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 7490ab5..2af10ad 100644 (file)
@@ -34,6 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
     /**
      * Returns true if this area uses file indexing.
      *
@@ -44,24 +45,14 @@ class activity extends \core_search\base_activity {
     }
 
     /**
-     * Add the main file to the index.
+     * Return the context info required to index files for
+     * this search area.
      *
-     * @param document $document The current document
-     * @return null
+     * @return array
      */
-    public function attach_files($document) {
-        $fs = get_file_storage();
-
-        $cm = $this->get_cm($this->get_module_name(), $document->get('itemid'), $document->get('courseid'));
-        $context = \context_module::instance($cm->id);
+    public function get_search_fileareas() {
+        $fileareas = array('intro', 'content'); // Fileareas.
 
-        // Order by sortorder desc, the first is consided the main file.
-        $files = $fs->get_area_files($context->id, 'mod_resource', 'content', 0, 'sortorder DESC, id ASC', false);
-
-        $mainfile = $files ? reset($files) : null;
-        if ($mainfile && $mainfile->get_sortorder() > 0) {
-            $document->add_stored_file($mainfile);
-        }
+        return $fileareas;
     }
-
 }
index b4b7d09..c5d04dd 100644 (file)
@@ -84,7 +84,7 @@ class mod_resource_search_testcase extends advanced_testcase {
         );
         $fs->create_file_from_string($filerecord, 'Test resource file');
 
-        // Attach a second file that shouldn't be returned with the search doc.
+        // Attach a second file.
         $filerecord['filename'] = 'extrafile';
         $filerecord['sortorder'] = 0;
         $fs->create_file_from_string($filerecord, 'Test resource file 2');
@@ -101,10 +101,15 @@ class mod_resource_search_testcase extends advanced_testcase {
             $searcharea->attach_files($doc);
             $files = $doc->get_files();
 
-            // Resources should only return their main file.
-            $this->assertCount(1, $files);
-            $file = reset($files);
-            $this->assertEquals('mainfile', $file->get_filename());
+            // Resources should return all added files.
+            $this->assertCount(2, $files);
+
+            $filenames = array();
+            foreach ($files as $file) {
+                $filenames[] = $file->get_filename();
+            }
+            $this->assertContains('mainfile', $filenames);
+            $this->assertContains('extrafile', $filenames);
 
             $nrecords++;
         }
index 75ad2a1..906066b 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 783d542..82d650a 100644 (file)
@@ -331,15 +331,19 @@ class mod_scorm_mod_form extends moodleform_mod {
         }
 
         // Set some completion default data.
-        if (!empty($defaultvalues['completionstatusrequired']) && !is_array($defaultvalues['completionstatusrequired'])) {
+        $cvalues = array();
+        if (empty($this->_instance)) {
+            // When in add mode, set a default completion rule that requires the SCORM's status be set to "Completed".
+            $cvalues[4] = 1;
+        } else if (!empty($defaultvalues['completionstatusrequired']) && !is_array($defaultvalues['completionstatusrequired'])) {
             // Unpack values.
-            $cvalues = array();
             foreach (scorm_status_options() as $key => $value) {
                 if (($defaultvalues['completionstatusrequired'] & $key) == $key) {
                     $cvalues[$key] = 1;
                 }
             }
-
+        }
+        if (!empty($cvalues)) {
             $defaultvalues['completionstatusrequired'] = $cvalues;
         }
 
@@ -481,7 +485,6 @@ class mod_scorm_mod_form extends moodleform_mod {
             }
         }
 
-        $this->data_preprocessing($defaultvalues);
         parent::set_data($defaultvalues);
     }
 
@@ -513,10 +516,6 @@ class mod_scorm_mod_form extends moodleform_mod {
                 $firstkey = $key;
             }
             $mform->addElement('checkbox', $key, $name, $value);
-            // Default completion rule that requires the SCORM's status be set to "Completed".
-            if ($key === 'completionstatusrequired[4]') {
-                $mform->setDefault($key, 1);
-            }
             $mform->setType($key, PARAM_BOOL);
             $items[] = $key;
         }
index 6375182..fd79141 100644 (file)
@@ -35,6 +35,15 @@ defined('MOODLE_INTERNAL') || die();
  */
 class activity extends \core_search\base_activity {
 
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
     /**
      * Returns recordset containing required data for indexing activities.
      *
index 97f3515..c5baae2 100644 (file)
@@ -35,6 +35,15 @@ defined('MOODLE_INTERNAL') || die();
  */
 class activity extends \core_search\base_activity {
 
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
     /**
      * Returns the document associated with this activity.
      *
index e3812b5..d1c6feb 100644 (file)
@@ -34,4 +34,13 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
 }
index 987b1b7..be3a16a 100644 (file)
@@ -175,4 +175,25 @@ class collaborative_page extends \core_search\base_mod {
         $contextmodule = \context::instance_by_id($doc->get('contextid'));
         return new \moodle_url('/mod/wiki/view.php', array('id' => $contextmodule->instanceid));
     }
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array('attachments'); // Filearea.
+
+        return $fileareas;
+    }
 }
index 2844588..5ebec2d 100644 (file)
@@ -34,4 +34,25 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class activity extends \core_search\base_activity {
+
+    /**
+     * Returns true if this area uses file indexing.
+     *
+     * @return bool
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the context info required to index files for
+     * this search area.
+     *
+     * @return array
+     */
+    public function get_search_fileareas() {
+        $fileareas = array('intro', 'instructauthors', 'instructreviewers', 'conclusion'); // Fileareas.
+
+        return $fileareas;
+    }
 }
index 3e0efc4..cb184b5 100644 (file)
@@ -16,6 +16,7 @@
 
 require_once('../config.php');
 require_once('lib.php');
+require_once($CFG->dirroot . '/course/lib.php');
 
 $noteid = required_param('id', PARAM_INT);
 
@@ -59,9 +60,7 @@ if (data_submitted() && confirm_sesskey()) {
 
     // Output HTML.
     $link = null;
-    if (has_capability('moodle/course:viewparticipants', $context)
-        || has_capability('moodle/site:viewparticipants', context_system::instance())) {
-
+    if (course_can_view_participants($context) || course_can_view_participants(context_system::instance())) {
         $link = new moodle_url('/user/index.php', array('id' => $course->id));
     }
     $PAGE->navbar->add(get_string('participants'), $link);
index 88a697f..7b97ecc 100644 (file)
@@ -17,6 +17,7 @@
 require_once('../config.php');
 require_once('lib.php');
 require_once('edit_form.php');
+require_once($CFG->dirroot . '/course/lib.php');
 
 $noteid = optional_param('id', 0, PARAM_INT);
 
@@ -95,9 +96,7 @@ if ($noteid) {
 
 // Output HTML.
 $link = null;
-if (has_capability('moodle/course:viewparticipants', $context)
-    || has_capability('moodle/site:viewparticipants', context_system::instance())) {
-
+if (course_can_view_participants($context) || course_can_view_participants(context_system::instance())) {
     $link = new moodle_url('/user/index.php', array('id' => $course->id));
 }
 $PAGE->navbar->add(get_string('participants'), $link);
index d9f9775..3c5cd28 100644 (file)
@@ -22,6 +22,7 @@
  */
 require_once('../config.php');
 require_once('lib.php');
+require_once($CFG->dirroot . '/course/lib.php');
 
 $courseid     = optional_param('course', SITEID, PARAM_INT);
 $userid       = optional_param('user', 0, PARAM_INT);
@@ -110,9 +111,7 @@ if ($userid && $course->id == SITEID) {
     $PAGE->set_context(context_course::instance($courseid));
 } else {
     $link = null;
-    if (has_capability('moodle/course:viewparticipants', $coursecontext)
-        || has_capability('moodle/site:viewparticipants', $systemcontext)) {
-
+    if (course_can_view_participants($coursecontext) || course_can_view_participants($systemcontext)) {
         $link = new moodle_url('/user/index.php', array('id' => $course->id));
     }
 }
index 8d98416..2a5b0a6 100644 (file)
@@ -10,9 +10,9 @@
       "dev": true
     },
     "acorn": {
-      "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.0.3.tgz",
-      "integrity": "sha1-xGDfCEkUY/AozLguqzcwvwEIez0=",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz",
+      "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==",
       "dev": true
     },
     "acorn-jsx": {
@@ -72,9 +72,9 @@
       "dev": true
     },
     "ansi-escapes": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
-      "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-2.0.0.tgz",
+      "integrity": "sha1-W65SvkJIeN2Xg+iRDj/Cki6DyBs=",
       "dev": true
     },
     "ansi-regex": {
       "dev": true,
       "optional": true
     },
+    "babel-code-frame": {
+      "version": "6.22.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz",
+      "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=",
+      "dev": true,
+      "requires": {
+        "chalk": "1.1.3",
+        "esutils": "2.0.2",
+        "js-tokens": "3.0.2"
+      }
+    },
     "balanced-match": {
       "version": "0.4.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
       }
     },
     "cli-cursor": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
-      "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+      "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
       "dev": true,
       "requires": {
-        "restore-cursor": "1.0.1"
+        "restore-cursor": "2.0.0"
       }
     },
     "cli-width": {
       "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
       "dev": true
     },
-    "code-point-at": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
-      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
-      "dev": true
-    },
     "coffee-script": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.10.0.tgz",
         }
       }
     },
+    "cross-spawn": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+      "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+      "dev": true,
+      "requires": {
+        "lru-cache": "4.1.1",
+        "shebang-command": "1.2.0",
+        "which": "1.2.14"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
+          "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==",
+          "dev": true,
+          "requires": {
+            "pseudomap": "1.0.2",
+            "yallist": "2.1.2"
+          }
+        }
+      }
+    },
     "cryptiles": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
         "array-find-index": "1.0.2"
       }
     },
-    "d": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
-      "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
-      "dev": true,
-      "requires": {
-        "es5-ext": "0.10.21"
-      }
-    },
     "dashdash": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
       "dev": true
     },
     "doctrine": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz",
-      "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz",
+      "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=",
       "dev": true,
       "requires": {
         "esutils": "2.0.2",
         "is-arrayish": "0.2.1"
       }
     },
-    "es5-ext": {
-      "version": "0.10.21",
-      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.21.tgz",
-      "integrity": "sha1-Gacl+eUdAwC7wejoIRCf2dr1WSU=",
-      "dev": true,
-      "requires": {
-        "es6-iterator": "2.0.1",
-        "es6-symbol": "3.1.1"
-      }
-    },
-    "es6-iterator": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz",
-      "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=",
-      "dev": true,
-      "requires": {
-        "d": "1.0.0",
-        "es5-ext": "0.10.21",
-        "es6-symbol": "3.1.1"
-      }
-    },
-    "es6-map": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz",
-      "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=",
-      "dev": true,
-      "requires": {
-        "d": "1.0.0",
-        "es5-ext": "0.10.21",
-        "es6-iterator": "2.0.1",
-        "es6-set": "0.1.5",
-        "es6-symbol": "3.1.1",
-        "event-emitter": "0.3.5"
-      }
-    },
-    "es6-set": {
-      "version": "0.1.5",
-      "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz",
-      "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=",
-      "dev": true,
-      "requires": {
-        "d": "1.0.0",
-        "es5-ext": "0.10.21",
-        "es6-iterator": "2.0.1",
-        "es6-symbol": "3.1.1",
-        "event-emitter": "0.3.5"
-      }
-    },
-    "es6-symbol": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
-      "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
-      "dev": true,
-      "requires": {
-        "d": "1.0.0",
-        "es5-ext": "0.10.21"
-      }
-    },
-    "es6-weak-map": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz",
-      "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
-      "dev": true,
-      "requires": {
-        "d": "1.0.0",
-        "es5-ext": "0.10.21",
-        "es6-iterator": "2.0.1",
-        "es6-symbol": "3.1.1"
-      }
-    },
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
         }
       }
     },
-    "escope": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz",
-      "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=",
-      "dev": true,
-      "requires": {
-        "es6-map": "0.1.5",
-        "es6-weak-map": "2.0.2",
-        "esrecurse": "4.1.0",
-        "estraverse": "4.2.0"
-      }
-    },
     "eslint": {
-      "version": "3.7.1",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.7.1.tgz",
-      "integrity": "sha1-f6qEWZ4P6kIvBLwy20kFQFGj8Ro=",
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.4.1.tgz",
+      "integrity": "sha1-mc1+r8/8ov+Zpcj18qR01jZLS9M=",
       "dev": true,
       "requires": {
+        "ajv": "5.2.2",
+        "babel-code-frame": "6.22.0",
         "chalk": "1.1.3",
         "concat-stream": "1.6.0",
+        "cross-spawn": "5.1.0",
         "debug": "2.6.8",
-        "doctrine": "1.5.0",
-        "escope": "3.6.0",
-        "espree": "3.4.3",
+        "doctrine": "2.0.0",
+        "eslint-scope": "3.7.1",
+        "espree": "3.5.0",
+        "esquery": "1.0.0",
         "estraverse": "4.2.0",
         "esutils": "2.0.2",
         "file-entry-cache": "2.0.0",
+        "functional-red-black-tree": "1.0.1",
         "glob": "7.1.2",
         "globals": "9.17.0",
         "ignore": "3.3.3",
         "imurmurhash": "0.1.4",
-        "inquirer": "0.12.0",
-        "is-my-json-valid": "2.16.0",
+        "inquirer": "3.2.1",
         "is-resolvable": "1.0.0",
-        "js-yaml": "3.8.4",
+        "js-yaml": "3.9.1",
         "json-stable-stringify": "1.0.1",
         "levn": "0.3.0",
         "lodash": "4.17.4",
+        "minimatch": "3.0.4",
         "mkdirp": "0.5.1",
         "natural-compare": "1.4.0",
         "optionator": "0.8.2",
         "path-is-inside": "1.0.2",
-        "pluralize": "1.2.1",
-        "progress": "1.1.8",
+        "pluralize": "4.0.0",
+        "progress": "2.0.0",
         "require-uncached": "1.0.3",
-        "shelljs": "0.6.1",
-        "strip-bom": "3.0.0",
-        "strip-json-comments": "1.0.4",
-        "table": "3.8.3",
-        "text-table": "0.2.0",
-        "user-home": "2.0.0"
+        "semver": "5.3.0",
+        "strip-json-comments": "2.0.1",
+        "table": "4.0.1",
+        "text-table": "0.2.0"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "5.2.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz",
+          "integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=",
+          "dev": true,
+          "requires": {
+            "co": "4.6.0",
+            "fast-deep-equal": "1.0.0",
+            "json-schema-traverse": "0.3.1",
+            "json-stable-stringify": "1.0.1"
+          }
+        },
+        "esprima": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
+          "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==",
+          "dev": true
+        },
+        "js-yaml": {
+          "version": "3.9.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz",
+          "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==",
+          "dev": true,
+          "requires": {
+            "argparse": "1.0.9",
+            "esprima": "4.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+          "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+          "dev": true
+        }
       }
     },
     "eslint-plugin-promise": {
       "integrity": "sha1-ePu2/+BHIBYnVp6FpsU3OvKmj8o=",
       "dev": true
     },
+    "eslint-scope": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
+      "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=",
+      "dev": true,
+      "requires": {
+        "esrecurse": "4.1.0",
+        "estraverse": "4.2.0"
+      }
+    },
     "espree": {
-      "version": "3.4.3",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-3.4.3.tgz",
-      "integrity": "sha1-KRC1zNSc6JPC//+qtP2LOjG4I3Q=",
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.0.tgz",
+      "integrity": "sha1-mDWGJb3QVYYeon4oZ+pyn69GPY0=",
       "dev": true,
       "requires": {
-        "acorn": "5.0.3",
+        "acorn": "5.1.1",
         "acorn-jsx": "3.0.1"
       }
     },
       "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=",
       "dev": true
     },
+    "esquery": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz",
+      "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=",
+      "dev": true,
+      "requires": {
+        "estraverse": "4.2.0"
+      }
+    },
     "esrecurse": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz",
       "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
       "dev": true
     },
-    "event-emitter": {
-      "version": "0.3.5",
-      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
-      "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
-      "dev": true,
-      "requires": {
-        "d": "1.0.0",
-        "es5-ext": "0.10.21"
-      }
-    },
     "eventemitter2": {
       "version": "0.4.14",
       "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
       "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
       "dev": true
     },
-    "exit-hook": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
-      "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=",
-      "dev": true
-    },
     "expand-brackets": {
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
       "dev": true,
       "optional": true
     },
+    "external-editor": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.4.tgz",
+      "integrity": "sha1-HtkZnanL/i7y96MbL96LDRI2iXI=",
+      "dev": true,
+      "requires": {
+        "iconv-lite": "0.4.17",
+        "jschardet": "1.5.1",
+        "tmp": "0.0.31"
+      }
+    },
     "extglob": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
       "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=",
       "dev": true
     },
+    "fast-deep-equal": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
+      "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=",
+      "dev": true
+    },
     "fast-levenshtein": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
       "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
       "dev": true
     },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "dev": true
+    },
     "gather-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/gather-stream/-/gather-stream-1.0.0.tgz",
         }
       }
     },
-    "generate-function": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
-      "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
-      "dev": true
-    },
-    "generate-object-property": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
-      "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
-      "dev": true,
-      "requires": {
-        "is-property": "1.0.2"
-      }
-    },
     "get-stdin": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
       }
     },
     "grunt-eslint": {
-      "version": "19.0.0",
-      "resolved": "https://registry.npmjs.org/grunt-eslint/-/grunt-eslint-19.0.0.tgz",
-      "integrity": "sha1-u3TDeQYVmc7B9mFp3vKonYYthhs=",
+      "version": "20.0.0",
+      "resolved": "https://registry.npmjs.org/grunt-eslint/-/grunt-eslint-20.0.0.tgz",
+      "integrity": "sha512-jQ2GBIYUkfVict7WcSBH7mAukTJ7Cz5TwJUCQ8XxzVTTyAcxC+1MGM3rdEuQbtsLUNKqy9xr0ai/l14WkxVtkw==",
       "dev": true,
       "requires": {
         "chalk": "1.1.3",
-        "eslint": "3.7.1"
+        "eslint": "4.4.1"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz",
+          "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==",
+          "dev": true
+        },
+        "ajv": {
+          "version": "5.2.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz",
+          "integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=",
+          "dev": true,
+          "requires": {
+            "co": "4.6.0",
+            "fast-deep-equal": "1.0.0",
+            "json-schema-traverse": "0.3.1",
+            "json-stable-stringify": "1.0.1"
+          }
+        },
+        "ansi-escapes": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-2.0.0.tgz",
+          "integrity": "sha1-W65SvkJIeN2Xg+iRDj/Cki6DyBs=",
+          "dev": true
+        },
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
+          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+          "dev": true,
+          "requires": {
+            "color-convert": "1.9.0"
+          }
+        },
+        "cli-cursor": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+          "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+          "dev": true,
+          "requires": {
+            "restore-cursor": "2.0.0"
+          }
+        },
+        "doctrine": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz",
+          "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=",
+          "dev": true,
+          "requires": {
+            "esutils": "2.0.2",
+            "isarray": "1.0.0"
+          }
+        },
+        "eslint": {
+          "version": "4.4.1",
+          "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.4.1.tgz",
+          "integrity": "sha1-mc1+r8/8ov+Zpcj18qR01jZLS9M=",
+          "dev": true,
+          "requires": {
+            "ajv": "5.2.2",
+            "babel-code-frame": "6.22.0",
+            "chalk": "1.1.3",
+            "concat-stream": "1.6.0",
+            "cross-spawn": "5.1.0",
+            "debug": "2.6.8",
+            "doctrine": "2.0.0",
+            "eslint-scope": "3.7.1",
+            "espree": "3.5.0",
+            "esquery": "1.0.0",
+            "estraverse": "4.2.0",
+            "esutils": "2.0.2",
+            "file-entry-cache": "2.0.0",
+            "functional-red-black-tree": "1.0.1",
+            "glob": "7.1.2",
+            "globals": "9.17.0",
+            "ignore": "3.3.3",
+            "imurmurhash": "0.1.4",
+            "inquirer": "3.2.1",
+            "is-resolvable": "1.0.0",
+            "js-yaml": "3.9.1",
+            "json-stable-stringify": "1.0.1",
+            "levn": "0.3.0",
+            "lodash": "4.17.4",
+            "minimatch": "3.0.4",
+            "mkdirp": "0.5.1",
+            "natural-compare": "1.4.0",
+            "optionator": "0.8.2",
+            "path-is-inside": "1.0.2",
+            "pluralize": "4.0.0",
+            "progress": "2.0.0",
+            "require-uncached": "1.0.3",
+            "semver": "5.3.0",
+            "strip-json-comments": "2.0.1",
+            "table": "4.0.1",
+            "text-table": "0.2.0"
+          }
+        },
+        "espree": {
+          "version": "3.5.0",
+          "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.0.tgz",
+          "integrity": "sha1-mDWGJb3QVYYeon4oZ+pyn69GPY0=",
+          "dev": true,
+          "requires": {
+            "acorn": "5.1.1",
+            "acorn-jsx": "3.0.1"
+          }
+        },
+        "esprima": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
+          "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==",
+          "dev": true
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "1.0.5"
+          }
+        },
+        "inquirer": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.2.1.tgz",
+          "integrity": "sha512-QgW3eiPN8gpj/K5vVpHADJJgrrF0ho/dZGylikGX7iqAdRgC9FVKYKWFLx6hZDBFcOLEoSqINYrVPeFAeG/PdA==",
+          "dev": true,
+          "requires": {
+            "ansi-escapes": "2.0.0",
+            "chalk": "2.1.0",
+            "cli-cursor": "2.1.0",
+            "cli-width": "2.1.0",
+            "external-editor": "2.0.4",
+            "figures": "2.0.0",
+            "lodash": "4.17.4",
+            "mute-stream": "0.0.7",
+            "run-async": "2.3.0",
+            "rx-lite": "4.0.8",
+            "rx-lite-aggregates": "4.0.8",
+            "string-width": "2.1.1",
+            "strip-ansi": "4.0.0",
+            "through": "2.3.8"
+          },
+          "dependencies": {
+            "chalk": {
+              "version": "2.1.0",
+              "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
+              "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
+              "dev": true,
+              "requires": {
+                "ansi-styles": "3.2.0",
+                "escape-string-regexp": "1.0.5",
+                "supports-color": "4.2.1"
+              }
+            }
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "js-yaml": {
+          "version": "3.9.1",
+          "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz",
+          "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==",
+          "dev": true,
+          "requires": {
+            "argparse": "1.0.9",
+            "esprima": "4.0.0"
+          }
+        },
+        "mute-stream": {
+          "version": "0.0.7",
+          "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+          "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+          "dev": true
+        },
+        "onetime": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+          "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+          "dev": true,
+          "requires": {
+            "mimic-fn": "1.1.0"
+          }
+        },
+        "pluralize": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-4.0.0.tgz",
+          "integrity": "sha1-WbcIwcAZCi9pLxx2GMRGsFL9F2I=",
+          "dev": true
+        },
+        "progress": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
+          "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
+          "dev": true
+        },
+        "restore-cursor": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+          "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+          "dev": true,
+          "requires": {
+            "onetime": "2.0.1",
+            "signal-exit": "3.0.2"
+          }
+        },
+        "run-async": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+          "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+          "dev": true,
+          "requires": {
+            "is-promise": "2.1.0"
+          }
+        },
+        "rx-lite": {
+          "version": "4.0.8",
+          "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
+          "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "2.0.0",
+            "strip-ansi": "4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "3.0.0"
+          }
+        },
+        "strip-json-comments": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+          "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+          "dev": true
+        },
+        "supports-color": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz",
+          "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==",
+          "dev": true,
+          "requires": {
+            "has-flag": "2.0.0"
+          }
+        },
+        "table": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/table/-/table-4.0.1.tgz",
+          "integrity": "sha1-qBFsEz+sLGH0pCCrbN9cTWHw5DU=",
+          "dev": true,
+          "requires": {
+            "ajv": "4.11.8",
+            "ajv-keywords": "1.5.1",
+            "chalk": "1.1.3",
+            "lodash": "4.17.4",
+            "slice-ansi": "0.0.4",
+            "string-width": "2.1.1"
+          },
+          "dependencies": {
+            "ajv": {
+              "version": "4.11.8",
+              "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
+              "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
+              "dev": true,
+              "requires": {
+                "co": "4.6.0",
+                "json-stable-stringify": "1.0.1"
+              }
+            }
+          }
+        }
       }
     },
     "grunt-known-options": {
       "dev": true
     },
     "inquirer": {
-      "version": "0.12.0",
-      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz",
-      "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=",
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.2.1.tgz",
+      "integrity": "sha512-QgW3eiPN8gpj/K5vVpHADJJgrrF0ho/dZGylikGX7iqAdRgC9FVKYKWFLx6hZDBFcOLEoSqINYrVPeFAeG/PdA==",
       "dev": true,
       "requires": {
-        "ansi-escapes": "1.4.0",
-        "ansi-regex": "2.1.1",
-        "chalk": "1.1.3",
-        "cli-cursor": "1.0.2",
+        "ansi-escapes": "2.0.0",
+        "chalk": "2.1.0",
+        "cli-cursor": "2.1.0",
         "cli-width": "2.1.0",
-        "figures": "1.7.0",
+        "external-editor": "2.0.4",
+        "figures": "2.0.0",
         "lodash": "4.17.4",
-        "readline2": "1.0.1",
-        "run-async": "0.1.0",
-        "rx-lite": "3.1.2",
-        "string-width": "1.0.2",
-        "strip-ansi": "3.0.1",
+        "mute-stream": "0.0.7",
+        "run-async": "2.3.0",
+        "rx-lite": "4.0.8",
+        "rx-lite-aggregates": "4.0.8",
+        "string-width": "2.1.1",
+        "strip-ansi": "4.0.0",
         "through": "2.3.8"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
+          "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
+          "dev": true,
+          "requires": {
+            "color-convert": "1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz",
+          "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "3.2.0",
+            "escape-string-regexp": "1.0.5",
+            "supports-color": "4.2.1"
+          }
+        },
+        "figures": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+          "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+          "dev": true,
+          "requires": {
+            "escape-string-regexp": "1.0.5"
+          }
+        },
+        "rx-lite": {
+          "version": "4.0.8",
+          "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
+          "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "3.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "4.2.1",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz",
+          "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==",
+          "dev": true,
+          "requires": {
+            "has-flag": "2.0.0"
+          }
+        }
       }
     },
     "irregular-plurals": {
       }
     },
     "is-fullwidth-code-point": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
-      "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
-      "dev": true,
-      "requires": {
-        "number-is-nan": "1.0.1"
-      }
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+      "dev": true
     },
     "is-glob": {
       "version": "2.0.1",
         "is-extglob": "1.0.0"
       }
     },
-    "is-my-json-valid": {
-      "version": "2.16.0",
-      "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz",
-      "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=",
-      "dev": true,
-      "requires": {
-        "generate-function": "2.0.0",
-        "generate-object-property": "1.2.0",
-        "jsonpointer": "4.0.1",
-        "xtend": "4.0.1"
-      }
-    },
     "is-number": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
       "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
       "dev": true
     },
-    "is-property": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
-      "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
       "dev": true
     },
     "is-regexp": {
       "integrity": "sha1-8OgK4DmkvWVLXygfyT8EqRSn/M4=",
       "dev": true
     },
+    "js-tokens": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
+      "dev": true
+    },
     "js-yaml": {
       "version": "3.8.4",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz",
       "dev": true,
       "optional": true
     },
+    "jschardet": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.5.1.tgz",
+      "integrity": "sha512-vE2hT1D0HLZCLLclfBSfkfTTedhVj0fubHpJBHKwwUWX0nSbhPAfk+SG9rTX95BYNmau8rGFfCeaT6T5OW1C2A==",
+      "dev": true
+    },
     "jshint": {
       "version": "0.9.1",
       "resolved": "https://registry.npmjs.org/jshint/-/jshint-0.9.1.tgz",
       "dev": true,
       "optional": true
     },
+    "json-schema-traverse": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+      "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
+      "dev": true
+    },
     "json-stable-stringify": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
       "integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ=",
       "dev": true
     },
-    "jsonpointer": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
-      "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
-      "dev": true
-    },
     "JSONStream": {
       "version": "0.8.4",
       "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.8.4.tgz",
         "mime-db": "1.27.0"
       }
     },
+    "mimic-fn": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz",
+      "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=",
+      "dev": true
+    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       }
     },
     "mute-stream": {
-      "version": "0.0.5",
-      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
-      "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=",
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+      "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
       "dev": true
     },
     "natural-compare": {
       "dev": true
     },
     "onetime": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
-      "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=",
-      "dev": true
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+      "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "1.1.0"
+      }
     },
     "optimist": {
       "version": "0.3.7",
       "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
       "dev": true
     },
+    "os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
+      "dev": true
+    },
     "pako": {
       "version": "0.2.9",
       "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
       }
     },
     "pluralize": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz",
-      "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-4.0.0.tgz",
+      "integrity": "sha1-WbcIwcAZCi9pLxx2GMRGsFL9F2I=",
       "dev": true
     },
     "postcss": {
       "dev": true
     },
     "progress": {
-      "version": "1.1.8",
-      "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
-      "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
+      "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
       "dev": true
     },
     "promise": {
       "dev": true,
       "optional": true
     },
+    "pseudomap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+      "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
+      "dev": true
+    },
     "punycode": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
         "util-deprecate": "1.0.2"
       }
     },
-    "readline2": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz",
-      "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=",
-      "dev": true,
-      "requires": {
-        "code-point-at": "1.1.0",
-        "is-fullwidth-code-point": "1.0.0",
-        "mute-stream": "0.0.5"
-      }
-    },
     "redent": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
       "dev": true
     },
     "restore-cursor": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
-      "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+      "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
       "dev": true,
       "requires": {
-        "exit-hook": "1.1.1",
-        "onetime": "1.1.0"
+        "onetime": "2.0.1",
+        "signal-exit": "3.0.2"
       }
     },
     "right-align": {
       }
     },
     "run-async": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz",
-      "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+      "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
       "dev": true,
       "requires": {
-        "once": "1.4.0"
+        "is-promise": "2.1.0"
       }
     },
     "rx-lite": {
       "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=",
       "dev": true
     },
+    "rx-lite-aggregates": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
+      "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=",
+      "dev": true,
+      "requires": {
+        "rx-lite": "3.1.2"
+      }
+    },
     "safe-buffer": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz",
       "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
       "dev": true
     },
-    "shelljs": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz",
-      "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=",
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
       "dev": true
     },
     "shifter": {
       }
     },
     "string-width": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
-      "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+      "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
       "dev": true,
       "requires": {
-        "code-point-at": "1.1.0",
-        "is-fullwidth-code-point": "1.0.0",
-        "strip-ansi": "3.0.1"
+        "is-fullwidth-code-point": "2.0.0",
+        "strip-ansi": "4.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "3.0.0"
+          }
+        }
       }
     },
     "stringstream": {
         "ansi-regex": "2.1.1"
       }
     },
-    "strip-bom": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
-      "dev": true
-    },
     "strip-indent": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
       }
     },
     "table": {
-      "version": "3.8.3",
-      "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz",
-      "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/table/-/table-4.0.1.tgz",
+      "integrity": "sha1-qBFsEz+sLGH0pCCrbN9cTWHw5DU=",
       "dev": true,
       "requires": {
         "ajv": "4.11.8",
         "chalk": "1.1.3",
         "lodash": "4.17.4",
         "slice-ansi": "0.0.4",
-        "string-width": "2.0.0"
-      },
-      "dependencies": {
-        "is-fullwidth-code-point": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
-          "dev": true
-        },
-        "string-width": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.0.0.tgz",
-          "integrity": "sha1-Y1xUNsxypuDDh87KJ41OLuxSaH4=",
-          "dev": true,
-          "requires": {
-            "is-fullwidth-code-point": "2.0.0",
-            "strip-ansi": "3.0.1"
-          }
-        }
+        "string-width": "2.1.1"
       }
     },
     "text-table": {
         }
       }
     },
+    "tmp": {
+      "version": "0.0.31",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz",
+      "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "1.0.2"
+      }
+&nb