// === Possible Errors ===
'comma-dangle': 'off',
+ 'no-compare-neg-zero': 'error',
'no-cond-assign': 'error',
'no-console': 'error',
'no-constant-condition': 'error',
'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'
+ }
+ }
+ ]
}
// 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: {
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;
}
notification.confirm(
strings[0], // Confirm.
strings[1], // Reopen plan X?
- strings[2], // reopen.
+ strings[2], // Reopen.
strings[3], // Cancel.
function() {
self._doReopenPlan(planData);
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);
if ($nameformat == B_ACTIVITYRESULTS_NAME_FORMAT_FULL) {
if (has_capability('moodle/course:managegroups', $context)) {
$grouplink = $CFG->wwwroot.'/group/overview.php?id='.$courseid.'&group=';
- } else if (has_capability('moodle/course:viewparticipants', $context)) {
+ } else if (course_can_view_participants($context)) {
$grouplink = $CFG->wwwroot.'/user/index.php?id='.$courseid.'&group=';
} else {
$grouplink = '';
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;
}
}
-
-
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
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
| 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
// 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
* @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');
$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;
}
/**
* 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();
};
return {
- init: function() {
- CalendarViewManager.init();
- registerEventListeners();
+ init: function(root) {
+ root = $(root);
+
+ CalendarViewManager.init(root);
+ registerEventListeners(root);
}
};
});
--- /dev/null
+// 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);
+ }
+ };
+});
--- /dev/null
+// 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);
+ }
+ };
+});
* @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);
- });
};
/**
*
* @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,
};
});
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',
};
});
'core/modal_registry',
'core/fragment',
'core_calendar/events',
- 'core_calendar/repository',
- 'core_calendar/event_form'
+ 'core_calendar/repository'
],
function(
$,
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"]',
};
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';
*/
ModalEventForm.prototype.disableButtons = function() {
this.saveButton.prop('disabled', true);
- this.moreLessButton.prop('disabled', true);
};
/**
*/
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();
- }
};
/**
*
* @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;
}
args.formdata = formData;
}
- args.haserror = (typeof hasError == 'undefined') ? false : hasError;
-
this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
this.setBody(this.bodyPromise);
// 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();
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
--- /dev/null
+// 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]",
+ },
+ };
+});
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();
});
* @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);
* @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() {
if (!courseId) {
courseId = root.find(SELECTORS.CALENDAR_MONTH_WRAPPER).data('courseid');
}
- return refreshMonthContent(time, courseId);
+ return refreshMonthContent(root, time, courseId);
};
/**
};
return {
- init: function() {
- registerEventListeners(SELECTORS.ROOT);
+ init: function(root) {
+ registerEventListeners(root);
},
reloadCurrentMonth: reloadCurrentMonth,
changeMonth: changeMonth,
$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;
}
* @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();
$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;
}
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;
+ }
}
*/
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.
*
'yday' => [
'type' => PARAM_INT,
],
+ // These are additional params.
+ 'istoday' => [
+ 'type' => PARAM_BOOL,
+ 'default' => false,
+ ],
+ 'isweekend' => [
+ 'type' => PARAM_BOOL,
+ 'default' => false,
+ ],
];
}
'neweventtimestamp' => [
'type' => PARAM_INT,
],
- 'istoday' => [
- 'type' => PARAM_BOOL,
- 'default' => false,
- ],
- 'isweekend' => [
- 'type' => PARAM_BOOL,
- 'default' => false,
- ],
'viewdaylink' => [
'type' => PARAM_URL,
'optional' => true,
'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,
+ ],
];
}
$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;
}
'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;
+ }
}
$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,
+ ],
+ ];
}
/**
'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,
+ ],
];
}
* @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(),
];
}
$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);
}
*/
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']);
--- /dev/null
+<?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;
+ }
+}
*/
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);
}
'multiple' => true,
],
'days' => [
- 'type' => day_exporter::read_properties_definition(),
+ 'type' => week_day_exporter::read_properties_definition(),
'multiple' => true,
],
];
* @return array Keys are the property names, values are their values.
*/
protected function get_other_values(renderer_base $output) {
+ global $CFG;
$return = [
'prepadding' => [],
'postpadding' => [],
}
$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) {
$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'],
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();
$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')]);
}
/**
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);
// 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'])) {
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.
*
$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) {
$month = new \core_calendar\external\month_exporter($calendar, $type, $related);
$data = $month->export($renderer);
- $template = 'core_calendar/month_detailed';
return [$data, $template];
}
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;
}
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);
+}
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)
*
$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);
+ }
}
+++ /dev/null
-<?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);
--- /dev/null
+{{!
+ 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}}
--- /dev/null
+{{!
+ 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}}
--- /dev/null
+{{!
+ 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}}
--- /dev/null
+{{!
+ 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>
}}
{{< 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 }}
{{< 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}}"
<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}}
--- /dev/null
+{{!
+ 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"> </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"> </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}}
* @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 ')";
$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"));
}
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();
*/
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;
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';
+ }
}
// 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.
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', '');
+ }
+}
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 {
}
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) {
// 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());
+ }
}
*/
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)) {
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);
* @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
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;
}
*/
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(
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);
$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}';
$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';
$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';
$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';
$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';
$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';
$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';
// 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';
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
UP: 38
};
+ var uniqueId = $.now();
+
/**
* Make an item in the selection list "active".
*
// 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") {
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);
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);
* @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();
$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,
*/
class file_browser {
+ /** @var array cached list of enrolled courses. */
+ protected $enrolledcourses = null;
+
/**
* Looks up file_info instance
*
/**
* 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
* @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);
+ }
}
/** @var stdClass course object */
protected $course;
+ /** @var file_info_context_module[] cached child modules. See {@link get_child_module()} */
+ protected $childrenmodules = [];
+
/**
* Constructor
*
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;
}
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
*
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
*
* @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)));
}
}
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.
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;
}
* @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);
}
/**
* @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;
}
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;
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
}
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;
}
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;
* @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().')';
}
/**
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}.
*
* @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
* @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]);
}
/**
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
*
--- /dev/null
+<?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));
+
+ }
+}
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
} 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);
// 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));
<div class="subcontent loginsub">
{{#error}}
- <div class="loginerrors">
+ <div class="loginerrors" role="alert">
<a href="#" id="loginerrormessage" class="accesshide">{{error}}</a>
{{{errorformatted}}}
</div>
* 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 ===
// 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) {
// 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);
* @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.
*
}
/**
- * 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;
}
}
/** @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.
*
$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;
}
$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);
}
$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 {
}
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;
}
// 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);
}
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;
+ }
}
/**
| 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"
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
* @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;
+ }
}
$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;
+ }
}
$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;
* @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;
+ }
}
* @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;
+ }
}
* @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;
+ }
+
}
* @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;
+ }
}
* @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.
*
}
/**
- * 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;
}
}
* @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 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.
*
// 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);
+ }
}
}
$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');
}
}
- $canviewparticipants = has_capability('moodle/course:viewparticipants',$context);
+ $canviewparticipants = course_can_view_participants($context);
$canviewhiddentimedposts = has_capability('mod/forum:viewhiddentimedposts', $context);
$strdatestring = get_string('strftimerecentfull');
* @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 $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;
+ }
}
* @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;
+ }
}
*/
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.
*
* @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;
+ }
}
* @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;
+ }
}
*/
class activity extends \core_search\base_activity {
-
/**
* Returns the document associated with this 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;
+ }
}
* @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;
+ }
}
* @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.
*
}
/**
- * 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;
}
-
}
);
$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');
$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++;
}
* @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;
+ }
}
}
// 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;
}
}
}
- $this->data_preprocessing($defaultvalues);
parent::set_data($defaultvalues);
}
$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;
}
*/
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.
*
*/
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.
*
* @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;
+ }
}
$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;
+ }
}
* @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;
+ }
}
require_once('../config.php');
require_once('lib.php');
+require_once($CFG->dirroot . '/course/lib.php');
$noteid = required_param('id', PARAM_INT);
// 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);
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);
// 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);
*/
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);
$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));
}
}
"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": {
"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