Merge branch 'MDL-59382-master-4' of git://github.com/ryanwyllie/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 3 Aug 2017 01:31:44 +0000 (09:31 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 3 Aug 2017 01:31:44 +0000 (09:31 +0800)
51 files changed:
blocks/calendar_month/tests/behat/block_calendar_month.feature
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_course.feature
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_dashboard.feature
blocks/calendar_upcoming/tests/behat/block_calendar_upcoming_frontpage.feature
calendar/amd/build/calendar.min.js
calendar/amd/build/calendar_events.min.js [deleted file]
calendar/amd/build/event_form.min.js [new file with mode: 0644]
calendar/amd/build/events.min.js [new file with mode: 0644]
calendar/amd/build/modal_event_form.min.js [new file with mode: 0644]
calendar/amd/build/repository.min.js [moved from calendar/amd/build/calendar_repository.min.js with 54% similarity]
calendar/amd/build/summary_modal.min.js
calendar/amd/src/calendar.js
calendar/amd/src/event_form.js [new file with mode: 0644]
calendar/amd/src/events.js [moved from calendar/amd/src/calendar_events.js with 77% similarity]
calendar/amd/src/modal_event_form.js [new file with mode: 0644]
calendar/amd/src/repository.js [moved from calendar/amd/src/calendar_repository.js with 75% similarity]
calendar/amd/src/summary_modal.js
calendar/classes/external/event_exporter.php
calendar/classes/local/event/forms/create.php [new file with mode: 0644]
calendar/classes/local/event/forms/update.php [new file with mode: 0644]
calendar/classes/local/event/mappers/create_update_form_mapper.php [new file with mode: 0644]
calendar/classes/local/event/mappers/create_update_form_mapper_interface.php [new file with mode: 0644]
calendar/classes/local/event/mappers/event_mapper.php
calendar/externallib.php
calendar/lib.php
calendar/renderer.php
calendar/templates/modal_event_form.mustache [new file with mode: 0644]
calendar/tests/behat/behat_calendar.php
calendar/tests/behat/calendar.feature
calendar/tests/behat/calendar_import.feature
calendar/tests/behat/calendar_lookahead.feature
calendar/tests/lib_test.php
lang/en/calendar.php
lang/en/moodle.php
lib/amd/build/modal.min.js
lib/amd/build/modal_factory.min.js
lib/amd/src/modal.js
lib/amd/src/modal_factory.js
lib/db/services.php
lib/form/templatable_form_element.php
lib/grouplib.php
lib/tests/grouplib_test.php
theme/boost/templates/core/modal.mustache
theme/boost/templates/core_form/element-template-inline.mustache
theme/bootstrapbase/less/bootstrap/variables.less
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/calendar.less
theme/bootstrapbase/less/moodle/modal.less
theme/bootstrapbase/less/moodle/variables.less [new file with mode: 0644]
theme/bootstrapbase/style/moodle.css
version.php

index 15e30e1..8753167 100644 (file)
@@ -138,7 +138,6 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I add the "Calendar" block
     And I create a calendar event with form data:
       | id_eventtype | Group |
-      | id_groupid | Group 1 |
       | id_name | Group Event |
     And I log out
     Then I log in as "student1"
@@ -176,7 +175,6 @@ Feature: Enable the calendar block in a course and test it's functionality
     And I am on "Course 1" course homepage
     And I create a calendar event with form data:
       | id_eventtype | Group |
-      | id_groupid | Group 1 |
       | id_name | Group Event 1 |
     And I log out
     Then I log in as "student1"
index d517c5f..9e092bf 100644 (file)
@@ -4,7 +4,7 @@ Feature: Enable the upcoming events block in a course
   As a teacher
   I can view the event in the upcoming events block
 
-  Scenario: View a global event in the upcoming events block in a course
+  Background:
     Given the following "users" exist:
       | username | firstname | lastname | email | idnumber |
       | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
@@ -14,7 +14,10 @@ Feature: Enable the upcoming events block in a course
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
-    When I log in as "admin"
+
+  @javascript
+  Scenario: View a global event in the calendar block
+    Given I log in as "admin"
     And I create a calendar event with form data:
       | id_eventtype | Site |
       | id_name | My Site Event |
index ceb1898..0b7c739 100644 (file)
@@ -3,12 +3,14 @@ Feature: View a upcoming site event on the dashboard
   In order to view a site event
   As a student
   I can view the event in the upcoming events block
-
-  Scenario: View a global event in the upcoming events block on the dashboard
+  Background:
     Given the following "users" exist:
       | username | firstname | lastname | email | idnumber |
       | student1 | Student | 1 | student1@example.com | S1 |
-    And I log in as "admin"
+
+  @javascript
+  Scenario: View a global event in the upcoming events block on the dashboard
+    Given I log in as "admin"
     And I create a calendar event with form data:
       | id_eventtype | Site |
       | id_name | My Site Event |
index a101952..448b710 100644 (file)
@@ -4,11 +4,14 @@ Feature: View a site event on the frontpage
   As a teacher
   I can view the event in the upcoming events block
 
-  Scenario: View a global event in the upcoming events block on the frontpage
+  Background:
     Given the following "users" exist:
       | username | firstname | lastname | email | idnumber |
       | teacher1 | Teacher | 1 | teacher1@example.com | T1 |
-    And I log in as "admin"
+
+  @javascript
+  Scenario: View a global event in the upcoming events block on the frontpage
+    Given I log in as "admin"
     And I create a calendar event with form data:
       | id_eventtype | Site |
       | id_name | My Site Event |
index d54e1ca..fab055d 100644 (file)
Binary files a/calendar/amd/build/calendar.min.js and b/calendar/amd/build/calendar.min.js differ
diff --git a/calendar/amd/build/calendar_events.min.js b/calendar/amd/build/calendar_events.min.js
deleted file mode 100644 (file)
index 5496507..0000000
Binary files a/calendar/amd/build/calendar_events.min.js and /dev/null differ
diff --git a/calendar/amd/build/event_form.min.js b/calendar/amd/build/event_form.min.js
new file mode 100644 (file)
index 0000000..49c3a88
Binary files /dev/null and b/calendar/amd/build/event_form.min.js differ
diff --git a/calendar/amd/build/events.min.js b/calendar/amd/build/events.min.js
new file mode 100644 (file)
index 0000000..938b804
Binary files /dev/null and b/calendar/amd/build/events.min.js differ
diff --git a/calendar/amd/build/modal_event_form.min.js b/calendar/amd/build/modal_event_form.min.js
new file mode 100644 (file)
index 0000000..78b804a
Binary files /dev/null and b/calendar/amd/build/modal_event_form.min.js differ
similarity index 54%
rename from calendar/amd/build/calendar_repository.min.js
rename to calendar/amd/build/repository.min.js
index bfb5d0d..27c0538 100644 (file)
Binary files a/calendar/amd/build/calendar_repository.min.js and b/calendar/amd/build/repository.min.js differ
index 3c917ad..9c0bb22 100644 (file)
Binary files a/calendar/amd/build/summary_modal.min.js and b/calendar/amd/build/summary_modal.min.js differ
index 5de6318..1843984 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * A javascript module to calendar events.
+ * 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 Simey Lameze <simey@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/str', 'core/templates', 'core/notification', 'core/custom_interaction_events',
-        'core/modal_factory', 'core_calendar/summary_modal', 'core/modal_events', 'core_calendar/calendar_repository'],
-    function($, Ajax, Str, Templates, Notification, CustomEvents, ModalFactory, SummaryModal, ModalEvents, CalendarRepository) {
-
-        var SELECTORS = {
-            ROOT: "[data-region='calendar']",
-            EVENT_LINK: "[data-action='view-event']",
-        };
-
-        /**
-         * Get the event type lang string.
-         *
-         * @param {String} eventType The event type.
-         * @return {promise} The lang string promise.
-         */
-        var getEventType = function(eventType) {
-            var lang = 'type' + eventType;
-            return Str.get_string(lang, 'core_calendar').then(function(langStr) {
-                return langStr;
-            });
-        };
-
-        /**
-         * Get the event source.
-         *
-         * @param {Object} subscription The event subscription object.
-         * @return {promise} The lang string promise.
-         */
-        var getEventSource = function(subscription) {
-            return Str.get_string('subsource', 'core_calendar', subscription).then(function(langStr) {
-                if (subscription.url) {
-                    return '<a href="' + subscription.url + '">' + langStr + '</a>';
-                }
-                return langStr;
-            });
-        };
-
-        /**
-         * Render the event summary modal.
-         *
-         * @param {Number} eventId The calendar event id.
-         */
-        var renderEventSummaryModal = function(eventId) {
-            // Calendar repository promise.
-            CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
-                if (!getEventResponse.event) {
-                    throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
-                }
-                var eventData = getEventResponse.event;
-                var eventTypePromise = getEventType(eventData.eventtype);
-
-                // If the calendar event has event source, get the source's language string/link.
-                if (eventData.displayeventsource) {
-                    eventData.subscription = JSON.parse(eventData.subscription);
-                    var eventSourceParams = {
-                        url: eventData.subscription.url,
-                        name: eventData.subscription.name
-                    };
-                    var eventSourcePromise = getEventSource(eventSourceParams);
-
-                    // Return event data with event type and event source info.
-                    return $.when(eventTypePromise, eventSourcePromise).then(function(eventType, eventSource) {
-                        eventData.eventtype = eventType;
-                        eventData.source = eventSource;
-                        return eventData;
-                    });
-                }
+define([
+            'jquery',
+            'core/ajax',
+            'core/str',
+            'core/templates',
+            'core/notification',
+            'core/custom_interaction_events',
+            'core/modal_events',
+            'core/modal_factory',
+            'core_calendar/modal_event_form',
+            'core_calendar/summary_modal',
+            'core_calendar/repository',
+            'core_calendar/events'
+        ],
+        function(
+            $,
+            Ajax,
+            Str,
+            Templates,
+            Notification,
+            CustomEvents,
+            ModalEvents,
+            ModalFactory,
+            ModalEventForm,
+            SummaryModal,
+            CalendarRepository,
+            CalendarEvents
+        ) {
+
+    var SELECTORS = {
+        ROOT: "[data-region='calendar']",
+        EVENT_LINK: "[data-action='view-event']",
+        NEW_EVENT_BUTTON: "[data-action='new-event-button']"
+    };
+
+    /**
+     * Get the event type lang string.
+     *
+     * @param {String} eventType The event type.
+     * @return {promise} The lang string promise.
+     */
+    var getEventType = function(eventType) {
+        var lang = 'type' + eventType;
+        return Str.get_string(lang, 'core_calendar').then(function(langStr) {
+            return langStr;
+        });
+    };
+
+    /**
+     * Get the event source.
+     *
+     * @param {Object} subscription The event subscription object.
+     * @return {promise} The lang string promise.
+     */
+    var getEventSource = function(subscription) {
+        return Str.get_string('subsource', 'core_calendar', subscription).then(function(langStr) {
+            if (subscription.url) {
+                return '<a href="' + subscription.url + '">' + langStr + '</a>';
+            }
+            return langStr;
+        });
+    };
 
-                // Return event data with event type info.
-                return eventTypePromise.then(function(eventType) {
+    /**
+     * Render the event summary modal.
+     *
+     * @param {Number} eventId The calendar event id.
+     */
+    var renderEventSummaryModal = function(eventId) {
+        // Calendar repository promise.
+        CalendarRepository.getEventById(eventId).then(function(getEventResponse) {
+            if (!getEventResponse.event) {
+                throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId);
+            }
+            var eventData = getEventResponse.event;
+            var eventTypePromise = getEventType(eventData.eventtype);
+
+            // If the calendar event has event source, get the source's language string/link.
+            if (eventData.displayeventsource) {
+                eventData.subscription = JSON.parse(eventData.subscription);
+                var eventSourceParams = {
+                    url: eventData.subscription.url,
+                    name: eventData.subscription.name
+                };
+                var eventSourcePromise = getEventSource(eventSourceParams);
+
+                // Return event data with event type and event source info.
+                return $.when(eventTypePromise, eventSourcePromise).then(function(eventType, eventSource) {
                     eventData.eventtype = eventType;
+                    eventData.source = eventSource;
                     return eventData;
                 });
+            }
 
-            }).then(function(eventData) {
-                // Build the modal parameters from the event data.
-                var modalParams = {
-                    title: eventData.name,
-                    type: SummaryModal.TYPE,
-                    body: Templates.render('core_calendar/event_summary_body', eventData)
-                };
-                if (!eventData.caneditevent) {
-                    modalParams.footer = '';
+            // Return event data with event type info.
+            return eventTypePromise.then(function(eventType) {
+                eventData.eventtype = eventType;
+                return eventData;
+            });
+
+        }).then(function(eventData) {
+            // Build the modal parameters from the event data.
+            var modalParams = {
+                title: eventData.name,
+                type: SummaryModal.TYPE,
+                body: Templates.render('core_calendar/event_summary_body', eventData)
+            };
+            if (!eventData.caneditevent) {
+                modalParams.footer = '';
+            }
+            // Create the modal.
+            return ModalFactory.create(modalParams);
+
+        }).done(function(modal) {
+            // Handle hidden event.
+            modal.getRoot().on(ModalEvents.hidden, function() {
+                // Destroy when hidden.
+                modal.destroy();
+            });
+
+            // Finally, render the modal!
+            modal.show();
+
+        }).fail(Notification.exception);
+    };
+
+    /**
+     * Create the event form modal for creating new events and
+     * editing existing events.
+     *
+     * @method registerEventFormModal
+     * @param {object} root The calendar root element
+     * @return {object} The create modal promise
+     */
+    var registerEventFormModal = function(root) {
+        var newEventButton = root.find(SELECTORS.NEW_EVENT_BUTTON);
+        var contextId = newEventButton.attr('data-context-id');
+
+        return ModalFactory.create(
+            {
+                type: ModalEventForm.TYPE,
+                large: true,
+                templateContext: {
+                    contextid: contextId
                 }
-                // Create the modal.
-                return ModalFactory.create(modalParams);
-
-            }).done(function(modal) {
-                // Handle hidden event.
-                modal.getRoot().on(ModalEvents.hidden, function() {
-                    // Destroy when hidden.
-                    modal.destroy();
-                });
+            },
+            newEventButton
+        );
+    };
 
-                // Finally, render the modal!
-                modal.show();
+    /**
+     * Listen to and handle any calendar events fired by the calendar UI.
+     *
+     * @method registerCalendarEventListeners
+     * @param {object} root The calendar root element
+     * @param {object} eventFormModalPromise A promise reolved with the event form modal
+     */
+    var registerCalendarEventListeners = function(root, eventFormModalPromise) {
+        var body = $('body');
+
+        // TODO: Replace these with actual logic to update
+        // the UI without having to force a page reload.
+        body.on(CalendarEvents.created, function() {
+            window.location.reload();
+        });
+        body.on(CalendarEvents.deleted, function() {
+            window.location.reload();
+        });
+        body.on(CalendarEvents.updated, function() {
+            window.location.reload();
+        });
 
-            }).fail(Notification.exception);
-        };
-
-        /**
-         * Register event listeners for the module.
-         */
-        var registerEventListeners = function() {
-            // Bind click events to event links.
-            $(SELECTORS.EVENT_LINK).click(function(e) {
-                e.preventDefault();
-                var eventId = $(this).attr('data-event-id');
-                renderEventSummaryModal(eventId);
+        eventFormModalPromise.then(function(modal) {
+            // When something within the calendar tells us the user wants
+            // to edit an event then show the event form modal.
+            body.on(CalendarEvents.editEvent, function(e, eventId) {
+                modal.setEventId(eventId);
+                modal.show();
             });
-        };
 
-        return {
-            init: function() {
-                registerEventListeners();
-            }
-        };
-    });
+            return;
+        });
+    };
+
+    /**
+     * Register event listeners for the module.
+     */
+    var registerEventListeners = function() {
+        var root = $(SELECTORS.ROOT);
+
+        // Bind click events to event links.
+        $(SELECTORS.EVENT_LINK).click(function(e) {
+            e.preventDefault();
+            var eventId = $(this).attr('data-event-id');
+            renderEventSummaryModal(eventId);
+        });
+
+        var eventFormPromise = registerEventFormModal(root);
+        registerCalendarEventListeners(root, eventFormPromise);
+    };
+
+    return {
+        init: function() {
+            registerEventListeners();
+        }
+    };
+});
diff --git a/calendar/amd/src/event_form.js b/calendar/amd/src/event_form.js
new file mode 100644 (file)
index 0000000..e62ee63
--- /dev/null
@@ -0,0 +1,282 @@
+// 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/>.
+
+/**
+ * A javascript module to enhance the event form.
+ *
+ * @module     core_calendar/event_form
+ * @package    core_calendar
+ * @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) {
+
+    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 EVENT_TYPES = {
+        USER: 'user',
+        SITE: 'site',
+        COURSE: 'course',
+        GROUP: 'group'
+    };
+
+    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);
+        });
+    };
+
+    /**
+     * Parse the group id select element in the event form and pull out
+     * the course id from the value to allow us to toggle other select
+     * elements based on the course id for the group a user selects.
+     *
+     * This is a little hacky but I couldn't find a better way to pass
+     * the course id for each group id with the limitations of mforms.
+     *
+     * The group id options are rendered with a value like:
+     * "<courseid>-<groupid>"
+     * E.g.
+     * For a group with id 10 in a course with id 3 the value of the
+     * option will be 3-10.
+     *
+     * @method parseGroupSelect
+     * @param {object} formElement The root form element
+     */
+    var parseGroupSelect = function(formElement) {
+        formElement.find(SELECTORS.EVENT_GROUP_ID)
+            .find(SELECTORS.SELECT_OPTION)
+            .each(function(index, element) {
+                element = $(element);
+                var value = element.attr('value');
+                var splits = value.split('-');
+                var courseId = splits[0];
+
+                element.attr('data-course-id', courseId);
+            });
+    };
+
+    /**
+     * Toggle the visibility of the secondary select elements based on
+     * the event type the user has selected.
+     *
+     * There are 3 secondary select elements within the form:
+     *      - course: a list of all courses a user can add course events to
+     *      - group course: a list of all courses a user can add group events to.
+     *                      this list can be different from the course list above.
+     *      - group: a list of all groups a user can add an event to. This list will
+     *               be filtered further based on the group course selected.
+     *
+     *  There are 4 event types:
+     *      - user: none of the secondary selects should be visible.
+     *      - site: none of the secondary selects should be visible.
+     *      - course: "course" select should be visible and both "group course"
+     *                and "group" should be hidden.
+     *      - group: "group course" and "group" should be visible and "course"
+     *               should be hidden.
+     *
+     * @method hideTypeSubSelects
+     * @param {object} formElement The root form element
+     */
+    var hideTypeSubSelects = function(formElement) {
+        var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
+        var eventType = typeSelect.val();
+        var courseIdSelect = formElement.find(SELECTORS.EVENT_COURSE_ID)
+            .closest(SELECTORS.FORM_GROUP)
+            .removeClass('hidden');
+        var groupCourseIdSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID)
+            .closest(SELECTORS.FORM_GROUP)
+            .removeClass('hidden');
+        var groupIdSelect = formElement.find(SELECTORS.EVENT_GROUP_ID)
+            .closest(SELECTORS.FORM_GROUP)
+            .removeClass('hidden');
+
+        // Hide the unreleated selectors for the given event type.
+        switch (eventType) {
+            case EVENT_TYPES.COURSE:
+                groupCourseIdSelect.addClass('hidden');
+                groupIdSelect.addClass('hidden');
+                break;
+            case EVENT_TYPES.GROUP:
+                courseIdSelect.addClass('hidden');
+                break;
+            default:
+                courseIdSelect.addClass('hidden');
+                groupCourseIdSelect.addClass('hidden');
+                groupIdSelect.addClass('hidden');
+        }
+    };
+
+    /**
+     * Listen for when the user changes the event type select in the
+     * form and then toggle the visibility of the appropriate secondary
+     * select elements.
+     *
+     * See: hideTypeSubSelects.
+     *
+     * @method addTypeSelectListeners
+     * @param {object} formElement The root form element
+     */
+    var addTypeSelectListeners = function(formElement) {
+        var typeSelect = formElement.find(SELECTORS.EVENT_TYPE);
+
+        typeSelect.on('change', function() {
+            hideTypeSubSelects(formElement);
+        });
+    };
+
+    /**
+     * Listen for when the user changes the group course when configuring
+     * a group event and filter the options in the group select to only
+     * show the groups available within the course the user has selected.
+     *
+     * @method addCourseGroupSelectListeners
+     * @param {object} formElement The root form element
+     */
+    var addCourseGroupSelectListeners = function(formElement) {
+        var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID);
+        var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID);
+        var groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION);
+        var filterGroupSelectOptions = function() {
+            var selectedCourseId = courseGroupSelect.val();
+            var selectedIndex = null;
+
+            groupSelectOptions.each(function(index, element) {
+                element = $(element);
+
+                if (element.attr('data-course-id') == selectedCourseId) {
+                    element.removeClass('hidden');
+                    element.prop('disabled', false);
+
+                    if (selectedIndex === null) {
+                        selectedIndex = index;
+                    }
+                } else {
+                    element.addClass('hidden');
+                    element.prop('disabled', true);
+                }
+            });
+
+            groupSelect.prop('selectedIndex', selectedIndex);
+        };
+
+        courseGroupSelect.on('change', filterGroupSelectOptions);
+        filterGroupSelectOptions();
+    };
+
+    /**
+     * Initialise all of the form enhancementds.
+     *
+     * @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 formElement = $('#' + formId);
+
+        listenForShowHideEvents(formElement);
+        destroyOldMoreLessToggle(formElement);
+        hideTypeSubSelects(formElement);
+        parseGroupSelect(formElement);
+        addTypeSelectListeners(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,
+    };
+});
similarity index 77%
rename from calendar/amd/src/calendar_events.js
rename to calendar/amd/src/events.js
index bb88d81..80ec8ca 100644 (file)
@@ -14,9 +14,9 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Contain the events a modal can fire.
+ * Contain the events the calendar component can fire.
  *
- * @module     core_calendar/calendar_events
+ * @module     core_calendar/events
  * @class      calendar_events
  * @package    core_calendar
  * @copyright  2017 Simey Lameze <simey@moodle.com>
@@ -24,6 +24,9 @@
  */
 define([], function() {
     return {
-        deleted: 'calendar-events:deleted'
+        created: 'calendar-events:created',
+        deleted: 'calendar-events:deleted',
+        updated: 'calendar-events:updated',
+        editEvent: 'calendar-events:edit_event'
     };
 });
diff --git a/calendar/amd/src/modal_event_form.js b/calendar/amd/src/modal_event_form.js
new file mode 100644 (file)
index 0000000..bad5ca4
--- /dev/null
@@ -0,0 +1,433 @@
+// 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/>.
+
+/**
+ * Contain the logic for the quick add or update event modal.
+ *
+ * @module     calendar/modal_quick_add_event
+ * @class      modal_quick_add_event
+ * @package    core
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+            'jquery',
+            'core/event',
+            'core/str',
+            'core/notification',
+            'core/templates',
+            'core/custom_interaction_events',
+            'core/modal',
+            'core/modal_registry',
+            'core/fragment',
+            'core_calendar/events',
+            'core_calendar/repository',
+            'core_calendar/event_form'
+        ],
+        function(
+            $,
+            Event,
+            Str,
+            Notification,
+            Templates,
+            CustomEvents,
+            Modal,
+            ModalRegistry,
+            Fragment,
+            CalendarEvents,
+            Repository,
+            EventForm
+        ) {
+
+    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"]',
+    };
+
+    /**
+     * Constructor for the Modal.
+     *
+     * @param {object} root The root jQuery element for the modal
+     */
+    var ModalEventForm = function(root) {
+        Modal.call(this, root);
+        this.eventId = null;
+        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 = Object.create(Modal.prototype);
+    ModalEventForm.prototype.constructor = ModalEventForm;
+
+    /**
+     * Set the event id to the given value.
+     *
+     * @method setEventId
+     * @param {int} id The event id
+     */
+    ModalEventForm.prototype.setEventId = function(id) {
+        this.eventId = id;
+    };
+
+    /**
+     * Retrieve the current event id, if any.
+     *
+     * @method getEventId
+     * @return {int|null} The event id
+     */
+    ModalEventForm.prototype.getEventId = function() {
+        return this.eventId;
+    };
+
+    /**
+     * Check if the modal has an event id.
+     *
+     * @method hasEventId
+     * @return {bool}
+     */
+    ModalEventForm.prototype.hasEventId = function() {
+        return this.eventId !== null;
+    };
+
+    /**
+     * Get the form element from the modal.
+     *
+     * @method getForm
+     * @return {object}
+     */
+    ModalEventForm.prototype.getForm = function() {
+        return this.getBody().find('form');
+    };
+
+    /**
+     * Disable the buttons in the footer.
+     *
+     * @method disableButtons
+     */
+    ModalEventForm.prototype.disableButtons = function() {
+        this.saveButton.prop('disabled', true);
+        this.moreLessButton.prop('disabled', true);
+    };
+
+    /**
+     * Enable the buttons in the footer.
+     *
+     * @method enableButtons
+     */
+    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();
+        }
+    };
+
+    /**
+     * Reload the title for the modal to the appropriate value
+     * depending on whether we are creating a new event or
+     * editing an existing event.
+     *
+     * @method reloadTitleContent
+     * @return {object} A promise resolved with the new title text
+     */
+    ModalEventForm.prototype.reloadTitleContent = function() {
+        if (this.reloadingTitle) {
+            return this.titlePromise;
+        }
+
+        this.reloadingTitle = true;
+
+        if (this.hasEventId()) {
+            this.titlePromise = Str.get_string('editevent', 'calendar');
+        } else {
+            this.titlePromise = Str.get_string('newevent', 'calendar');
+        }
+
+        this.titlePromise.then(function(string) {
+            this.setTitle(string);
+            return string;
+        }.bind(this))
+        .always(function() {
+            this.reloadingTitle = false;
+            return;
+        }.bind(this));
+
+        return this.titlePromise;
+    };
+
+    /**
+     * Send a request to the server to get the event_form in a fragment
+     * and render the result in the body of the modal.
+     *
+     * If serialised form data is provided then it will be sent in the
+     * request to the server to have the form rendered with the data. This
+     * is used when the form had a server side error and we need the server
+     * to re-render it for us to display the error to the user.
+     *
+     * @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) {
+        if (this.reloadingBody) {
+            return this.bodyPromise;
+        }
+
+        this.reloadingBody = true;
+        this.disableButtons();
+
+        var contextId = this.saveButton.attr('data-context-id');
+        var args = {};
+
+        if (this.hasEventId()) {
+            args.eventid = this.getEventId();
+        }
+
+        if (typeof formData !== 'undefined') {
+            args.formdata = formData;
+        }
+
+        args.haserror = (typeof hasError == 'undefined') ? false : hasError;
+
+        this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
+
+        this.setBody(this.bodyPromise);
+
+        this.bodyPromise.then(function() {
+            this.enableButtons();
+            return;
+        }.bind(this))
+        .catch(Notification.exception)
+        .always(function() {
+            this.reloadingBody = false;
+            return;
+        }.bind(this));
+
+        return this.bodyPromise;
+    };
+
+    /**
+     * Reload both the title and body content.
+     *
+     * @method reloadAllContent
+     * @return {object} promise
+     */
+    ModalEventForm.prototype.reloadAllContent = function() {
+        return $.when(this.reloadTitleContent(), this.reloadBodyContent());
+    };
+
+    /**
+     * Kick off a reload the modal content before showing it. This
+     * is to allow us to re-use the same modal for creating and
+     * editing different events within the page.
+     *
+     * We do the reload when showing the modal rather than hiding it
+     * to save a request to the server if the user closes the modal
+     * and never re-opens it.
+     *
+     * @method show
+     */
+    ModalEventForm.prototype.show = function() {
+        this.reloadAllContent();
+        Modal.prototype.show.call(this);
+    };
+
+    /**
+     * Clear the event id from the modal when it's closed so
+     * that it is loaded fresh next time it's displayed.
+     *
+     * The event id will be set by the calling code if it wants
+     * to edit a specific event.
+     *
+     * @method hide
+     */
+    ModalEventForm.prototype.hide = function() {
+        Modal.prototype.hide.call(this);
+        this.setEventId(null);
+    };
+
+    /**
+     * Get the serialised form data.
+     *
+     * @method getFormData
+     * @return {string} serialised form data
+     */
+    ModalEventForm.prototype.getFormData = function() {
+        return this.getForm().serialize();
+    };
+
+    /**
+     * Send the form data to the server to create or update
+     * an event.
+     *
+     * If there is a server side validation error then we re-request the
+     * rendered form (with the data) from the server in order to get the
+     * server side errors to display.
+     *
+     * On success the modal is hidden and the page is reloaded so that the
+     * new event will display.
+     *
+     * @method save
+     * @return {object} A promise
+     */
+    ModalEventForm.prototype.save = function() {
+        var loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
+
+        loadingContainer.removeClass('hidden');
+        this.disableButtons();
+
+        var formData = this.getFormData();
+        // Send the form data to the server for processing.
+        return Repository.submitCreateUpdateForm(formData)
+            .then(function(response) {
+                if (response.validationerror) {
+                    // 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);
+                } else {
+                    // No problemo! Our work here is done.
+                    this.hide();
+
+                    // Trigger the appropriate calendar event so that the view can
+                    // be updated.
+                    if (this.hasEventId()) {
+                        $('body').trigger(CalendarEvents.updated, [response.event]);
+                    } else {
+                        $('body').trigger(CalendarEvents.created, [response.event]);
+                    }
+                }
+
+                return;
+            }.bind(this))
+            .always(function() {
+                // Regardless of success or error we should always stop
+                // the loading icon and re-enable the buttons.
+                loadingContainer.addClass('hidden');
+                this.enableButtons();
+            }.bind(this))
+            .catch(Notification.exception);
+    };
+
+    /**
+     * Set up all of the event handling for the modal.
+     *
+     * @method registerEventListeners
+     */
+    ModalEventForm.prototype.registerEventListeners = function() {
+        // Apply parent event listeners.
+        Modal.prototype.registerEventListeners.call(this);
+
+        // When the user clicks the save button we trigger the form submission. We need to
+        // trigger an actual submission because there is some JS code in the form that is
+        // listening for this event and doing some stuff (e.g. saving draft areas etc).
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
+            this.getForm().submit();
+            data.originalEvent.preventDefault();
+            e.stopPropagation();
+        }.bind(this));
+
+        // Catch the submit event before it is actually processed by the browser and
+        // prevent the submission. We'll take it from here.
+        this.getModal().on('submit', function(e) {
+            this.save();
+
+            // Stop the form from actually submitting and prevent it's
+            // propagation because we have already handled the event.
+            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
+    // of this type using the modal factory.
+    if (!registered) {
+        ModalRegistry.register(ModalEventForm.TYPE, ModalEventForm, 'calendar/modal_event_form');
+        registered = true;
+    }
+
+    return ModalEventForm;
+});
similarity index 75%
rename from calendar/amd/src/calendar_repository.js
rename to calendar/amd/src/repository.js
index 2de4c41..0f96079 100644 (file)
@@ -16,7 +16,7 @@
 /**
  * A javascript module to handle calendar ajax actions.
  *
- * @module     core_calendar/calendar_repository
+ * @module     core_calendar/repository
  * @class      repository
  * @package    core_calendar
  * @copyright  2017 Simey Lameze <lameze@moodle.com>
@@ -65,8 +65,27 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
         return Ajax.call([request])[0];
     };
 
+    /**
+     * Submit the form data for the event form.
+     *
+     * @method submitCreateUpdateForm
+     * @param {string} formdata The URL encoded values from the form
+     * @return {promise} Resolved with the new or edited event
+     */
+    var submitCreateUpdateForm = function(formdata) {
+        var request = {
+            methodname: 'core_calendar_submit_create_update_form',
+            args: {
+                formdata: formdata
+            }
+        };
+
+        return Ajax.call([request])[0];
+    };
+
     return {
         getEventById: getEventById,
-        deleteEvent: deleteEvent
+        deleteEvent: deleteEvent,
+        submitCreateUpdateForm: submitCreateUpdateForm
     };
 });
index e344d62..b758f45 100644 (file)
@@ -22,8 +22,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_events', 'core/modal',
-    'core/modal_registry', 'core/modal_factory', 'core/modal_events', 'core_calendar/calendar_repository',
-    'core_calendar/calendar_events'],
+    'core/modal_registry', 'core/modal_factory', 'core/modal_events', 'core_calendar/repository',
+    'core_calendar/events'],
     function($, Str, Notification, CustomEvents, Modal, ModalRegistry, ModalFactory, ModalEvents, CalendarRepository,
              CalendarEvents) {
 
@@ -32,7 +32,6 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
         ROOT: "[data-region='summary-modal-container']",
         EDIT_BUTTON: '[data-action="edit"]',
         DELETE_BUTTON: '[data-action="delete"]',
-        EVENT_LINK: '[data-action="event-link"]'
     };
 
     /**
@@ -43,11 +42,11 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
     var ModalEventSummary = function(root) {
         Modal.call(this, root);
 
-        if (!this.getFooter().find(SELECTORS.EDIT_BUTTON).length) {
+        if (!this.getEditButton().length) {
             Notification.exception({message: 'No edit button found'});
         }
 
-        if (!this.getFooter().find(SELECTORS.DELETE_BUTTON).length) {
+        if (!this.getDeleteButton().length) {
             Notification.exception({message: 'No delete button found'});
         }
     };
@@ -56,6 +55,48 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
     ModalEventSummary.prototype = Object.create(Modal.prototype);
     ModalEventSummary.prototype.constructor = ModalEventSummary;
 
+    /**
+     * Get the edit button element from the footer. The button is cached
+     * as it's not expected to change.
+     *
+     * @method getEditButton
+     * @return {object} button element
+     */
+    ModalEventSummary.prototype.getEditButton = function() {
+        if (typeof this.editButton == 'undefined') {
+            this.editButton = this.getFooter().find(SELECTORS.EDIT_BUTTON);
+        }
+
+        return this.editButton;
+    };
+
+    /**
+     * Get the delete button element from the footer. The button is cached
+     * as it's not expected to change.
+     *
+     * @method getDeleteButton
+     * @return {object} button element
+     */
+    ModalEventSummary.prototype.getDeleteButton = function() {
+        if (typeof this.deleteButton == 'undefined') {
+            this.deleteButton = this.getFooter().find(SELECTORS.DELETE_BUTTON);
+        }
+
+        return this.deleteButton;
+    };
+
+    /**
+     * Get the id for the event being shown in this modal. This value is
+     * not cached because it will change depending on which event is
+     * being displayed.
+     *
+     * @method getEventId
+     * @return {int}
+     */
+    ModalEventSummary.prototype.getEventId = function() {
+        return this.getBody().find(SELECTORS.ROOT).attr('data-event-id');
+    };
+
     /**
      * Set up all of the event handling for the modal.
      *
@@ -64,28 +105,53 @@ define(['jquery', 'core/str', 'core/notification', 'core/custom_interaction_even
     ModalEventSummary.prototype.registerEventListeners = function() {
         // Apply parent event listeners.
         Modal.prototype.registerEventListeners.call(this);
-        var confirmPromise = ModalFactory.create({
-            type: ModalFactory.types.CONFIRM,
-        }, this.getFooter().find(SELECTORS.DELETE_BUTTON)).then(function(modal) {
-            Str.get_string('confirm').then(function(languagestring) {
-                modal.setTitle(languagestring);
-            }.bind(this)).catch(Notification.exception);
+
+        var confirmPromise = ModalFactory.create(
+            {
+                type: ModalFactory.types.CONFIRM
+            },
+            this.getDeleteButton()
+        ).then(function(modal) {
             modal.getRoot().on(ModalEvents.yes, function() {
-                var eventId = this.getBody().find(SELECTORS.ROOT).attr('data-event-id');
-                CalendarRepository.deleteEvent(eventId).done(function() {
-                    modal.getRoot().trigger(CalendarEvents.deleted, eventId);
-                    window.location.reload();
-                }).fail(Notification.exception);
+                var eventId = this.getEventId();
+
+                CalendarRepository.deleteEvent(eventId)
+                    .then(function() {
+                        $('body').trigger(CalendarEvents.deleted, [eventId]);
+                        this.hide();
+                    }.bind(this))
+                    .catch(Notification.exception);
             }.bind(this));
+
             return modal;
         }.bind(this));
 
+        // We have to wait for the modal to finish rendering in order to ensure that
+        // the data-event-title property is available to use as the modal title.
         this.getRoot().on(ModalEvents.bodyRendered, function() {
             var eventTitle = this.getBody().find(SELECTORS.ROOT).attr('data-event-title');
             confirmPromise.then(function(modal) {
                 modal.setBody(Str.get_string('confirmeventdelete', 'core_calendar', eventTitle));
             });
         }.bind(this));
+
+        CustomEvents.define(this.getEditButton(), [
+            CustomEvents.events.activate
+        ]);
+
+        this.getEditButton().on(CustomEvents.events.activate, function(e, data) {
+            // When the edit button is clicked we fire an event for the calendar UI to handle.
+            // We don't care how the UI chooses to handle it.
+            $('body').trigger(CalendarEvents.editEvent, [this.getEventId()]);
+            // There is nothing else for us to do so let's hide.
+            this.hide();
+
+            // We've handled this event so no need to propagate it.
+            e.preventDefault();
+            e.stopPropagation();
+            data.originalEvent.preventDefault();
+            data.originalEvent.stopPropagation();
+        }.bind(this));
     };
 
     // Automatically register with the modal registry the first time this module is imported so that you can create modals
index baa73f6..ec2d938 100644 (file)
@@ -71,7 +71,7 @@ class event_exporter extends exporter {
         $data->timestart = $starttimestamp;
         $data->timeduration = $endtimestamp - $starttimestamp;
         $data->timesort = $event->get_times()->get_sort_time()->getTimestamp();
-        $data->visible = $event->is_visible();
+        $data->visible = $event->is_visible() ? 1 : 0;
         $data->timemodified = $event->get_times()->get_modified_time()->getTimestamp();
 
         if ($repeats = $event->get_repeats()) {
diff --git a/calendar/classes/local/event/forms/create.php b/calendar/classes/local/event/forms/create.php
new file mode 100644 (file)
index 0000000..c5e21e2
--- /dev/null
@@ -0,0 +1,281 @@
+<?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/>.
+
+/**
+ * The mform for creating a calendar event. Based on the
+ * old event form.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_calendar\local\event\forms;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+
+/**
+ * The mform class for creating a calendar event.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class create extends \moodleform {
+    /**
+     * The form definition
+     */
+    public function definition () {
+        global $PAGE;
+
+        $mform = $this->_form;
+        $haserror = !empty($this->_customdata['haserror']);
+        $eventtypes = calendar_get_all_allowed_types();
+
+        $mform->setDisableShortforms();
+        $mform->disable_form_change_checker();
+
+        // Empty string so that the element doesn't get rendered.
+        $mform->addElement('header', 'general', '');
+
+        $this->add_default_hidden_elements($mform);
+
+        // Event name field.
+        $mform->addElement('text', 'name', get_string('eventname', 'calendar'), 'size="50"');
+        $mform->addRule('name', get_string('required'), 'required', null, 'client');
+        $mform->setType('name', PARAM_TEXT);
+
+        // Event time start field.
+        $mform->addElement('date_time_selector', 'timestart', get_string('date'));
+
+        // Add the select elements for the available event types.
+        $this->add_event_type_elements($mform, $eventtypes);
+
+        // Start of advanced elements.
+        // Advanced elements are not visible to the user by default.
+        // They are displayed through the user of a show more / less button.
+        $mform->addElement('editor', 'description', get_string('eventdescription', 'calendar'), ['rows' => 3]);
+        $mform->setType('description', PARAM_RAW);
+        $mform->setAdvanced('description');
+
+        // Add the variety of elements allowed for selecting event duration.
+        $this->add_event_duration_elements($mform);
+
+        // Add the form elements for repeating events.
+        $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]);
+    }
+
+    /**
+     * A bit of custom validation for this form
+     *
+     * @param array $data An assoc array of field=>value
+     * @param array $files An array of files
+     * @return array
+     */
+    public function validation($data, $files) {
+        global $DB, $CFG;
+
+        $errors = parent::validation($data, $files);
+        $coursekey = isset($data['groupcourseid']) ? 'groupcourseid' : 'courseid';
+
+        if (isset($data[$coursekey]) && $data[$coursekey] > 0) {
+            if ($course = $DB->get_record('course', ['id' => $data[$coursekey]])) {
+                if ($data['timestart'] < $course->startdate) {
+                    $errors['timestart'] = get_string('errorbeforecoursestart', 'calendar');
+                }
+            } else {
+                $errors[$coursekey] = get_string('invalidcourse', 'error');
+            }
+        }
+
+        if ($data['duration'] == 1 && $data['timestart'] > $data['timedurationuntil']) {
+            $errors['durationgroup'] = get_string('invalidtimedurationuntil', 'calendar');
+        } else if ($data['duration'] == 2 && (trim($data['timedurationminutes']) == '' || $data['timedurationminutes'] < 1)) {
+            $errors['durationgroup'] = get_string('invalidtimedurationminutes', 'calendar');
+        }
+
+        return $errors;
+    }
+
+    /**
+     * Add the list of hidden elements that should appear in this form each
+     * time. These elements will never be visible to the user.
+     *
+     * @method add_default_hidden_elements
+     * @param MoodleQuickForm $mform
+     */
+    protected function add_default_hidden_elements($mform) {
+        global $USER;
+
+        // Add some hidden fields.
+        $mform->addElement('hidden', 'id');
+        $mform->setType('id', PARAM_INT);
+        $mform->setDefault('id', 0);
+
+        $mform->addElement('hidden', 'userid');
+        $mform->setType('userid', PARAM_INT);
+        $mform->setDefault('userid', $USER->id);
+
+        $mform->addElement('hidden', 'modulename');
+        $mform->setType('modulename', PARAM_INT);
+        $mform->setDefault('modulename', '');
+
+        $mform->addElement('hidden', 'instance');
+        $mform->setType('instance', PARAM_INT);
+        $mform->setDefault('instance', 0);
+
+        $mform->addElement('hidden', 'visible');
+        $mform->setType('visible', PARAM_INT);
+        $mform->setDefault('visible', 1);
+    }
+
+    /**
+     * Add the appropriate elements for the available event types.
+     *
+     * If the only event type available is 'user' then we add a hidden
+     * element because there is nothing for the user to choose.
+     *
+     * If more than one type is available then we add the elements as
+     * follows:
+     *      - Always add the event type selector
+     *      - Elements per type:
+     *          - course: add an additional select element with each
+     *                    course as an option.
+     *          - group: add a select element for the course (different
+     *                   from the above course select) and a select
+     *                   element for the group.
+     *
+     * @method add_event_type_elements
+     * @param MoodleQuickForm $mform
+     * @param array $eventtypes The available event types for the user
+     */
+    protected function add_event_type_elements($mform, $eventtypes) {
+        $options = [];
+
+        if (isset($eventtypes['user'])) {
+            $options['user'] = get_string('user');
+        }
+        if (isset($eventtypes['group'])) {
+            $options['group'] = get_string('group');
+        }
+        if (isset($eventtypes['course'])) {
+            $options['course'] = get_string('course');
+        }
+        if (isset($eventtypes['site'])) {
+            $options['site'] = get_string('site');
+        }
+
+        // If we only have one event type and it's 'user' event then don't bother
+        // rendering the select boxes because there is no choice for the user to
+        // make.
+        if (count(array_keys($eventtypes)) == 1 && isset($eventtypes['user'])) {
+            $mform->addElement('hidden', 'eventtype');
+            $mform->setType('eventtype', PARAM_TEXT);
+            $mform->setDefault('eventtype', 'user');
+
+            // Render a static element to tell the user what type of event will
+            // be created.
+            $mform->addElement('static', 'staticeventtype', get_string('eventkind', 'calendar'), $options['user']);
+            return;
+        } else {
+            $mform->addElement('select', 'eventtype', get_string('eventkind', 'calendar'), $options);
+        }
+
+        if (isset($eventtypes['course'])) {
+            $courseoptions = [];
+            foreach ($eventtypes['course'] as $course) {
+                $courseoptions[$course->id] = format_string($course->fullname, true,
+                    ['context' => \context_course::instance($course->id)]);
+            }
+
+            $mform->addElement('select', 'courseid', get_string('course'), $courseoptions);
+            $mform->disabledIf('courseid', 'eventtype', 'noteq', 'course');
+        }
+
+        if (isset($eventtypes['group'])) {
+            $courseoptions = [];
+            foreach ($eventtypes['groupcourses'] as $course) {
+                $courseoptions[$course->id] = format_string($course->fullname, true,
+                    ['context' => \context_course::instance($course->id)]);
+            }
+
+            $mform->addElement('select', 'groupcourseid', get_string('course'), $courseoptions);
+            $mform->disabledIf('groupcourseid', 'eventtype', 'noteq', 'group');
+
+            $groupoptions = [];
+            foreach ($eventtypes['group'] as $group) {
+                // We are formatting it this way in order to provide the javascript both
+                // the course and group ids so that it can enhance the form for the user.
+                $index = "{$group->courseid}-{$group->id}";
+                $groupoptions[$index] = format_string($group->name, true,
+                    ['context' => \context_course::instance($group->courseid)]);
+            }
+
+            $mform->addElement('select', 'groupid', get_string('group'), $groupoptions);
+            $mform->disabledIf('groupid', 'eventtype', 'noteq', 'group');
+        }
+    }
+
+    /**
+     * Add the various elements to express the duration options available
+     * for an event.
+     *
+     * @method add_event_duration_elements
+     * @param MoodleQuickForm $mform
+     */
+    protected function add_event_duration_elements($mform) {
+        $group = [];
+        $group[] = $mform->createElement('radio', 'duration', null, get_string('durationnone', 'calendar'), 0);
+        $group[] = $mform->createElement('radio', 'duration', null, get_string('durationuntil', 'calendar'), 1);
+        $group[] = $mform->createElement('date_time_selector', 'timedurationuntil', '');
+        $group[] = $mform->createElement('radio', 'duration', null, get_string('durationminutes', 'calendar'), 2);
+        $group[] = $mform->createElement('text', 'timedurationminutes', get_string('durationminutes', 'calendar'));
+
+        $mform->addGroup($group, 'durationgroup', get_string('eventduration', 'calendar'), '<br />', false);
+        $mform->setAdvanced('durationgroup');
+
+        $mform->disabledIf('timedurationuntil',         'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[day]',    'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[month]',  'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[year]',   'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[hour]',   'duration', 'noteq', 1);
+        $mform->disabledIf('timedurationuntil[minute]', 'duration', 'noteq', 1);
+
+        $mform->setType('timedurationminutes', PARAM_INT);
+        $mform->disabledIf('timedurationminutes', 'duration', 'noteq', 2);
+
+        $mform->setDefault('duration', 0);
+    }
+
+    /**
+     * Add the repeat elements for the form when creating a new event.
+     *
+     * @method add_event_repeat_elements
+     * @param MoodleQuickForm $mform
+     */
+    protected function add_event_repeat_elements($mform) {
+        $mform->addElement('checkbox', 'repeat', get_string('repeatevent', 'calendar'), null);
+        $mform->addElement('text', 'repeats', get_string('repeatweeksl', 'calendar'), 'maxlength="10" size="10"');
+        $mform->setType('repeats', PARAM_INT);
+        $mform->setDefault('repeats', 1);
+        $mform->disabledIf('repeats', 'repeat', 'notchecked');
+        $mform->setAdvanced('repeat');
+        $mform->setAdvanced('repeats');
+    }
+}
diff --git a/calendar/classes/local/event/forms/update.php b/calendar/classes/local/event/forms/update.php
new file mode 100644 (file)
index 0000000..9096b65
--- /dev/null
@@ -0,0 +1,57 @@
+<?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/>.
+
+/**
+ * The mform for updating a calendar event. Based on the
+ * old event form.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core_calendar\local\event\forms;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+
+/**
+ * The mform class for updating a calendar event.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class update extends create {
+    /**
+     * Add the repeat elements for the form when editing an existing event.
+     *
+     * @param MoodleQuickForm $mform
+     */
+    protected function add_event_repeat_elements($mform) {
+        $event = $this->_customdata['event'];
+
+        $mform->addElement('hidden', 'repeatid');
+        $mform->setType('repeatid', PARAM_INT);
+
+        $group = [];
+        $group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditall', 'calendar',
+                $event->eventrepeats), 1);
+        $group[] = $mform->createElement('radio', 'repeateditall', null, get_string('repeateditthis', 'calendar'), 0);
+        $mform->addGroup($group, 'repeatgroup', get_string('repeatedevents', 'calendar'), '<br />', false);
+
+        $mform->setDefault('repeateditall', 1);
+        $mform->setAdvanced('repeatgroup');
+    }
+}
diff --git a/calendar/classes/local/event/mappers/create_update_form_mapper.php b/calendar/classes/local/event/mappers/create_update_form_mapper.php
new file mode 100644 (file)
index 0000000..bedcccc
--- /dev/null
@@ -0,0 +1,118 @@
+<?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/>.
+
+/**
+ * Event create form and update form mapper.
+ *
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\local\event\mappers;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+/**
+ * Event create form and update form mapper class.
+ *
+ * This class will perform the necessary data transformations to take
+ * a legacy event and build the appropriate data structure for both the
+ * create and update event forms.
+ *
+ * It will also do the reverse transformation
+ * and take the returned form data and provide a data structure that can
+ * be used to set legacy event properties.
+ *
+ * @copyright 2017 Ryan Wyllie <ryan@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class create_update_form_mapper implements create_update_form_mapper_interface {
+
+    /**
+     * Generate the appropriate data for the form from a legacy event.
+     *
+     * @param \calendar_event $legacyevent
+     * @return stdClass
+     */
+    public function from_legacy_event_to_data(\calendar_event $legacyevent) {
+        $legacyevent->count_repeats();
+        $data = $legacyevent->properties(true);
+        $data->timedurationuntil = $legacyevent->timestart + $legacyevent->timeduration;
+        $data->duration = (empty($legacyevent->timeduration)) ? 0 : 1;
+
+        if ($legacyevent->eventtype == 'group') {
+            // Set up the correct value for the to display on the form.
+            $data->groupid = "{$legacyevent->courseid}-{$legacyevent->groupid}";
+            $data->groupcourseid = $legacyevent->courseid;
+        }
+
+        return $data;
+    }
+
+    /**
+     * Generate the appropriate calendar_event properties from the form data.
+     *
+     * @param \stdClass $data
+     * @return stdClass
+     */
+    public function from_data_to_event_properties(\stdClass $data) {
+        $properties = clone($data);
+
+        // Undo the form definition work around to allow us to have two different
+        // course selectors present depending on which event type the user selects.
+        if (isset($data->groupcourseid)) {
+            $properties->courseid = $data->groupcourseid;
+            unset($properties->groupcourseid);
+        }
+
+        // Pull the group id back out of the value. The form saves the value
+        // as "<courseid>-<groupid>" to allow the javascript to work correctly.
+        if (isset($data->groupid)) {
+            list($courseid, $groupid) = explode('-', $data->groupid);
+            $properties->groupid = $groupid;
+        }
+
+        // Default course id if none is set.
+        if (!isset($properties->courseid)) {
+            $properties->courseid = 0;
+        }
+
+        // Decode the form fields back into valid event property.
+        $properties->timeduration = $this->get_time_duration_from_form_data($data);
+
+        return $properties;
+    }
+
+    /**
+     * A helper function to calculate the time duration for an event based on
+     * the event_form data.
+     *
+     * @param \stdClass $data event_form data
+     * @return int
+     */
+    private function get_time_duration_from_form_data(\stdClass $data) {
+        if ($data->duration == 1) {
+            return $data->timedurationuntil - $data->timestart;
+        } else if ($data->duration == 2) {
+            return $data->timedurationminutes * MINSECS;
+        } else {
+            return 0;
+        }
+    }
+}
diff --git a/calendar/classes/local/event/mappers/create_update_form_mapper_interface.php b/calendar/classes/local/event/mappers/create_update_form_mapper_interface.php
new file mode 100644 (file)
index 0000000..6b38c7c
--- /dev/null
@@ -0,0 +1,53 @@
+<?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/>.
+
+/**
+ * Create update form mapper interface.
+ *
+ * @package    core_calendar
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_calendar\local\event\mappers;
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot . '/calendar/lib.php');
+
+/**
+ * Interface for a create_update_form_mapper class
+ *
+ * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface create_update_form_mapper_interface {
+    /**
+     * Generate the appropriate data for the form from a legacy event.
+     *
+     * @param \calendar_event $legacyevent
+     * @return stdClass
+     */
+    public function from_legacy_event_to_data(\calendar_event $legacyevent);
+
+    /**
+     * Generate the appropriate calendar_event properties from the form data.
+     *
+     * @param \stdClass $data
+     * @return stdClass
+     */
+    public function from_data_to_event_properties(\stdClass $data);
+}
index a4191f2..8e544dd 100644 (file)
@@ -54,28 +54,34 @@ class event_mapper implements event_mapper_interface {
 
     public function from_legacy_event_to_event(\calendar_event $legacyevent) {
         $coalesce = function($property) use ($legacyevent) {
-            return property_exists($legacyevent, $property) ? $legacyevent->{$property} : null;
+            try {
+                return $legacyevent->$property;
+            } catch (\coding_exception $e) {
+                // The magic setter throews an exception if the
+                // property doesn't exist.
+                return null;
+            }
         };
 
         return $this->factory->create_instance(
             (object)[
-                $coalesce('id'),
-                $coalesce('name'),
-                $coalesce('description'),
-                $coalesce('format'),
-                $coalesce('courseid'),
-                $coalesce('groupid'),
-                $coalesce('userid'),
-                $coalesce('repeatid'),
-                $coalesce('modulename'),
-                $coalesce('instance'),
-                $coalesce('type'),
-                $coalesce('timestart'),
-                $coalesce('timeduration'),
-                $coalesce('timemodified'),
-                $coalesce('timesort'),
-                $coalesce('visible'),
-                $coalesce('subscription')
+                'id' => $coalesce('id'),
+                'name' => $coalesce('name'),
+                'description' => $coalesce('description'),
+                'format' => $coalesce('format'),
+                'courseid' => $coalesce('courseid'),
+                'groupid' => $coalesce('groupid'),
+                'userid' => $coalesce('userid'),
+                'repeatid' => $coalesce('repeatid'),
+                'modulename' => $coalesce('modulename'),
+                'instance' => $coalesce('instance'),
+                'eventtype' => $coalesce('eventtype'),
+                'timestart' => $coalesce('timestart'),
+                'timeduration' => $coalesce('timeduration'),
+                'timemodified' => $coalesce('timemodified'),
+                'timesort' => $coalesce('timesort'),
+                'visible' => $coalesce('visible'),
+                'subscriptionid' => $coalesce('subscriptionid')
             ]
         );
     }
index 6fd68d4..0edab2f 100644 (file)
@@ -30,6 +30,11 @@ defined('MOODLE_INTERNAL') || die;
 require_once("$CFG->libdir/externallib.php");
 
 use \core_calendar\local\api as local_api;
+use \core_calendar\local\event\container as event_container;
+use \core_calendar\local\event\forms\create as create_event_form;
+use \core_calendar\local\event\forms\update as update_event_form;
+use \core_calendar\local\event\mappers\create_update_form_mapper;
+use \core_calendar\external\event_exporter;
 use \core_calendar\external\events_exporter;
 use \core_calendar\external\events_grouped_by_course_exporter;
 use \core_calendar\external\events_related_objects_cache;
@@ -779,4 +784,91 @@ class core_calendar_external extends external_api {
             )
         );
     }
+
+    /**
+     * Returns description of method parameters.
+     *
+     * @return external_function_parameters.
+     */
+    public static function submit_create_update_form_parameters() {
+        return new external_function_parameters(
+            [
+                'formdata' => new external_value(PARAM_RAW, 'The data from the event form'),
+            ]
+        );
+    }
+
+    /**
+     * Handles the event form submission.
+     *
+     * @param string $formdata The event form data in a URI encoded param string
+     * @return array The created or modified event
+     * @throws moodle_exception
+     */
+    public static function submit_create_update_form($formdata) {
+        global $CFG, $USER, $PAGE;
+        require_once($CFG->dirroot."/calendar/lib.php");
+
+        // Parameter validation.
+        $params = self::validate_parameters(self::submit_create_update_form_parameters(), ['formdata' => $formdata]);
+        $context = \context_user::instance($USER->id);
+        $data = [];
+
+        self::validate_context($context);
+        parse_str($params['formdata'], $data);
+
+        if (!empty($data['id'])) {
+            $eventid = clean_param($data['id'], PARAM_INT);
+            $legacyevent = calendar_event::load($eventid);
+            $legacyevent->count_repeats();
+            $formoptions = ['event' => $legacyevent];
+            $mform = new update_event_form(null, $formoptions, 'post', '', null, true, $data);
+        } else {
+            $legacyevent = null;
+            $mform = new create_event_form(null, null, 'post', '', null, true, $data);
+        }
+
+        if ($validateddata = $mform->get_data()) {
+            $formmapper = new create_update_form_mapper();
+            $properties = $formmapper->from_data_to_event_properties($validateddata);
+
+            if (is_null($legacyevent)) {
+                $legacyevent = new \calendar_event($properties);
+            }
+
+            $properties = $legacyevent->properties(true);
+            $legacyevent->update($properties);
+
+            $eventmapper = event_container::get_event_mapper();
+            $event = $eventmapper->from_legacy_event_to_event($legacyevent);
+            $cache = new events_related_objects_cache([$event]);
+            $relatedobjects = [
+                'context' => $cache->get_context($event),
+                'course' => $cache->get_course($event),
+            ];
+            $exporter = new event_exporter($event, $relatedobjects);
+            $renderer = $PAGE->get_renderer('core_calendar');
+
+            return [ 'event' => $exporter->export($renderer) ];
+        } else {
+            return [ 'validationerror' => true ];
+        }
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return external_description.
+     */
+    public static function  submit_create_update_form_returns() {
+        $eventstructure = event_exporter::get_read_structure();
+        $eventstructure->required = VALUE_OPTIONAL;
+
+        return new external_single_structure(
+            array(
+                'event' => $eventstructure,
+                'validationerror' => new external_value(PARAM_BOOL, 'Invalid form data', VALUE_DEFAULT, false),
+            )
+        );
+    }
 }
index 94e617b..45d1297 100644 (file)
@@ -2686,8 +2686,9 @@ function calendar_set_event_type_display($type, $display = null, $user = null) {
  *
  * @param stdClass $allowed list of allowed edit for event  type
  * @param stdClass|int $course object of a course or course id
+ * @param array $groups array of groups for the given course
  */
-function calendar_get_allowed_types(&$allowed, $course = null) {
+function calendar_get_allowed_types(&$allowed, $course = null, $groups = null) {
     global $USER, $DB;
 
     $allowed = new \stdClass();
@@ -2695,6 +2696,23 @@ function calendar_get_allowed_types(&$allowed, $course = null) {
     $allowed->groups = false;
     $allowed->courses = false;
     $allowed->site = has_capability('moodle/calendar:manageentries', \context_course::instance(SITEID));
+    $getgroupsfunc = function($course, $context, $user) use ($groups) {
+        if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
+            if (has_capability('moodle/site:accessallgroups', $context)) {
+                return is_null($groups) ? groups_get_all_groups($course->id) : $groups;
+            } else {
+                if (is_null($groups)) {
+                    return groups_get_all_groups($course->id, $user->id);
+                } else {
+                    return array_filter($groups, function($group) use ($user) {
+                        return isset($group->members[$user->id]);
+                    });
+                }
+            }
+        }
+
+        return false;
+    };
 
     if (!empty($course)) {
         if (!is_object($course)) {
@@ -2706,27 +2724,84 @@ function calendar_get_allowed_types(&$allowed, $course = null) {
 
             if (has_capability('moodle/calendar:manageentries', $coursecontext)) {
                 $allowed->courses = array($course->id => 1);
-
-                if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
-                    if (has_capability('moodle/site:accessallgroups', $coursecontext)) {
-                        $allowed->groups = groups_get_all_groups($course->id);
-                    } else {
-                        $allowed->groups = groups_get_all_groups($course->id, $USER->id);
-                    }
-                }
+                $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
             } else if (has_capability('moodle/calendar:managegroupentries', $coursecontext)) {
-                if ($course->groupmode != NOGROUPS || !$course->groupmodeforce) {
-                    if (has_capability('moodle/site:accessallgroups', $coursecontext)) {
-                        $allowed->groups = groups_get_all_groups($course->id);
-                    } else {
-                        $allowed->groups = groups_get_all_groups($course->id, $USER->id);
-                    }
-                }
+                $allowed->groups = $getgroupsfunc($course, $coursecontext, $USER);
             }
         }
     }
 }
 
+/**
+ * Get all of the allowed types for all of the courses and groups
+ * the logged in user belongs to.
+ *
+ * The returned array will optionally have 5 keys:
+ *      'user' : true if the logged in user can create user events
+ *      'site' : true if the logged in user can create site events
+ *      'course' : array of courses that the user can create events for
+ *      'group': array of groups that the user can create events for
+ *      'groupcourses' : array of courses that the groups belong to (can
+ *                       be different from the list in 'course'.
+ *
+ * @return array The array of allowed types.
+ */
+function calendar_get_all_allowed_types() {
+    global $CFG, $USER;
+
+    require_once($CFG->libdir . '/enrollib.php');
+
+    $types = [];
+
+    calendar_get_allowed_types($allowed);
+
+    if ($allowed->user) {
+        $types['user'] = true;
+    }
+
+    if ($allowed->site) {
+        $types['site'] = true;
+    }
+
+    // This function warms the context cache for the course so the calls
+    // to load the course context in calendar_get_allowed_types don't result
+    // in additional DB queries.
+    $courses = enrol_get_users_courses($USER->id, true);
+    // We want to pre-fetch all of the groups for each course in a single
+    // query to avoid calendar_get_allowed_types from hitting the DB for
+    // each separate course.
+    $groups = groups_get_all_groups_for_courses($courses);
+
+    foreach ($courses as $course) {
+        $coursegroups = isset($groups[$course->id]) ? $groups[$course->id] : null;
+        calendar_get_allowed_types($allowed, $course, $coursegroups);
+
+        if (!empty($allowed->courses)) {
+            if (!isset($types['course'])) {
+                $types['course'] = [$course];
+            } else {
+                $types['course'][] = $course;
+            }
+        }
+
+        if (!empty($allowed->groups)) {
+            if (!isset($types['groupcourses'])) {
+                $types['groupcourses'] = [$course];
+            } else {
+                $types['groupcourses'][] = $course;
+            }
+
+            if (!isset($types['group'])) {
+                $types['group'] = array_values($allowed->groups);
+            } else {
+                $types['group'] = array_merge($types['group'], array_values($allowed->groups));
+            }
+        }
+    }
+
+    return $types;
+}
+
 /**
  * See if user can add calendar entries at all used to print the "New Event" button.
  *
@@ -3340,3 +3415,76 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $
         return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
     }, []);
 }
+
+/**
+ * Request and render event form fragment.
+ *
+ * @param array $args The fragment arguments.
+ * @return string The rendered mform fragment.
+ */
+function calendar_output_fragment_event_form($args) {
+    global $CFG, $OUTPUT;
+    require_once($CFG->dirroot.'/calendar/event_form.php');
+
+    $html = '';
+    $data = null;
+    $eventid = isset($args['eventid']) ? clean_param($args['eventid'], PARAM_INT) : null;
+    $event = null;
+    $hasformdata = isset($args['formdata']) && !empty($args['formdata']);
+    $formoptions = [];
+
+    if ($hasformdata) {
+        parse_str(clean_param($args['formdata'], PARAM_TEXT), $data);
+    }
+
+    if (isset($args['haserror'])) {
+        $formoptions['haserror'] = clean_param($args['haserror'], PARAM_BOOL);
+    }
+
+    if (is_null($eventid)) {
+        $mform = new \core_calendar\local\event\forms\create(
+            null,
+            $formoptions,
+            'post',
+            '',
+            null,
+            true,
+            $data
+        );
+    } else {
+        $event = calendar_event::load($eventid);
+        $event->count_repeats();
+        $formoptions['event'] = $event;
+        $mform = new \core_calendar\local\event\forms\update(
+            null,
+            $formoptions,
+            'post',
+            '',
+            null,
+            true,
+            $data
+        );
+    }
+
+    if ($hasformdata) {
+        $mform->is_validated();
+    } else if (!is_null($event)) {
+        $mapper = new \core_calendar\local\event\mappers\create_update_form_mapper();
+        $data = $mapper->from_legacy_event_to_data($event);
+        $mform->set_data($data);
+
+        // Check to see if this event is part of a subscription or import.
+        // If so display a warning on edit.
+        if (isset($event->subscriptionid) && ($event->subscriptionid != null)) {
+            $renderable = new \core\output\notification(
+                get_string('eventsubscriptioneditwarning', 'calendar'),
+                \core\output\notification::NOTIFY_INFO
+            );
+
+            $html .= $OUTPUT->render($renderable);
+        }
+    }
+
+    $html .= $mform->render();
+    return $html;
+}
index ee450e1..940be31 100644 (file)
@@ -142,18 +142,13 @@ class core_calendar_renderer extends plugin_renderer_base {
             $time = time();
         }
 
-        $output = html_writer::start_tag('div', array('class'=>'buttons'));
-        $output .= html_writer::start_tag('form', array('action' => CALENDAR_URL . 'event.php', 'method' => 'get'));
-        $output .= html_writer::start_tag('div');
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name' => 'action', 'value' => 'new'));
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name' => 'course', 'value' => $courseid));
-        $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name' => 'time', 'value' => $time));
-        $attributes = array('type' => 'submit', 'value' => get_string('newevent', 'calendar'), 'class' => 'btn btn-secondary');
-        $output .= html_writer::empty_tag('input', $attributes);
-        $output .= html_writer::end_tag('div');
-        $output .= html_writer::end_tag('form');
-        $output .= html_writer::end_tag('div');
-        return $output;
+        $coursecontext = \context_course::instance($courseid);
+        $attributes = [
+            'class' => 'btn btn-secondary pull-xs-right pull-right',
+            'data-context-id' => $coursecontext->id,
+            'data-action' => 'new-event-button'
+        ];
+        return html_writer::tag('button', get_string('newevent', 'calendar'), $attributes);
     }
 
     /**
diff --git a/calendar/templates/modal_event_form.mustache b/calendar/templates/modal_event_form.mustache
new file mode 100644 (file)
index 0000000..e99c491
--- /dev/null
@@ -0,0 +1,61 @@
+{{!
+    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/modal_event_form
+
+    Moodle modal template with save and cancel buttons.
+
+    The purpose of this template is to render a modal.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * title A cleaned string (use clean_text()) to display.
+    * body HTML content for the boday
+
+    Example context (json):
+    {
+        "title": "Example save cancel modal",
+        "body": "Some example content for the body"
+    }
+}}
+
+{{< 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}}"
+                data-action="save">
+
+            {{#str}} save {{/str}}
+            <span class="hidden" data-region="loading-icon-container">
+                {{> core/loading }}
+            </span>
+        </button>
+    {{/footer}}
+{{/ core/modal }}
index 2d4ecab..c77b547 100644 (file)
@@ -63,6 +63,9 @@ class behat_calendar extends behat_base {
         $eventname = $data->getRow(1);
         $eventname = $eventname[1];
 
+        // Click to create new event.
+        $this->execute("behat_general::i_wait_seconds", 1);
+
         // Click to create new event.
         $this->execute("behat_general::i_click_on", array(get_string('newevent', 'calendar'), "button"));
 
@@ -70,11 +73,7 @@ class behat_calendar extends behat_base {
         $this->execute("behat_forms::i_set_the_following_fields_to_these_values", $data);
 
         // Save event.
-        $this->execute("behat_forms::press_button", get_string('savechanges'));
-
-        // Check if event is created. Being last step, don't need to wait or check for exceptions.
-        $this->execute("behat_general::assert_page_contains_text", $eventname);
-
+        $this->execute("behat_forms::press_button", get_string('save'));
     }
 
     /**
index 81689db..291c83e 100644 (file)
@@ -1,4 +1,4 @@
-@core @core_calendar
+@core @core_calendar @javascript
 Feature: Perform basic calendar functionality
   In order to ensure the calendar works as expected
   As an admin
@@ -10,6 +10,7 @@ Feature: Perform basic calendar functionality
       | student1 | Student | 1 | student1@example.com |
       | student2 | Student | 2 | student2@example.com |
       | student3 | Student | 3 | student3@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
     And the following "courses" exist:
       | fullname | shortname | format |
       | Course 1 | C1 | topics |
@@ -17,17 +18,19 @@ Feature: Perform basic calendar functionality
       | user | course | role |
       | student1 | C1 | student |
       | student3 | C1 | student |
+      | teacher1 | C1 | teacher |
     And the following "groups" exist:
       | name | course | idnumber |
       | Group 1 | C1 | G1 |
     And the following "group members" exist:
       | user | group |
       | student1 | G1 |
-    When I log in as "admin"
-    And I am on "Course 1" course homepage with editing mode on
-    And I add the "Calendar" block
+      | teacher1 | G1 |
 
   Scenario: Create a site event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
     And I create a calendar event with form data:
       | Type of event | site |
       | Event title | Really awesome event! |
@@ -41,8 +44,14 @@ Feature: Perform basic calendar functionality
     And I log in as "student2"
     And I follow "This month"
     And I should see "Really awesome event!"
+    And I log out
 
   Scenario: Create a course event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
+    And I log out
+    And I log in as "teacher1"
     And I create a calendar event with form data:
       | Type of event | course |
       | Event title | Really awesome event! |
@@ -56,11 +65,16 @@ Feature: Perform basic calendar functionality
     And I log in as "student2"
     And I follow "This month"
     And I should not see "Really awesome event!"
+    And I log out
 
   Scenario: Create a group event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
+    And I log out
+    And I log in as "teacher1"
     And I create a calendar event with form data:
       | Type of event | group |
-      | Group | Group 1 |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event |
     And I log out
@@ -68,13 +82,11 @@ Feature: Perform basic calendar functionality
     And I am on "Course 1" course homepage
     And I follow "This month"
     And I follow "Really awesome event!"
-    And "Group 1" "text" should exist in the ".eventlist" "css_element"
-    And I log out
-    And I log in as "student3"
-    And I follow "This month"
-    And I should not see "Really awesome event!"
 
   Scenario: Create a user event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
     And I create a calendar event with form data:
       | Type of event | user |
       | Event title | Really awesome event! |
@@ -86,22 +98,37 @@ Feature: Perform basic calendar functionality
     And I should not see "Really awesome event!"
 
   Scenario: Delete an event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
+    And I log out
+    And I log in as "teacher1"
     And I create a calendar event with form data:
       | Type of event | user |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event, sucka! |
-    And I click on "Delete event" "link" in the ".event div.commands" "css_element"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    And I click on "Really awesome event!" "link"
     And I click on "Delete" "button"
+    And I click on "Yes" "button"
+    And I wait to be redirected
     And I should not see "Really awesome event!"
 
   Scenario: Edit an event
+    Given I log in as "admin"
+    And I am on "Course 1" course homepage with editing mode on
+    And I add the "Calendar" block
     And I create a calendar event with form data:
       | Type of event | user |
       | Event title | Really awesome event! |
       | Description | Come join this awesome event, sucka! |
-    And I click on "Edit event" "link" in the ".event div.commands" "css_element"
+    And I am on "Course 1" course homepage
+    And I follow "This month"
+    And I click on "Really awesome event!" "link"
+    And I click on "Edit" "button"
     And I set the following fields to these values:
       | Event title | Mediocre event :( |
       | Description | Wait, this event isn't that great. |
-    And I press "Save changes"
+    And I press "Save"
     And I should see "Mediocre event"
index a9cedb1..c9a3829 100644 (file)
@@ -30,5 +30,19 @@ Feature: Import and edit calendar events
     And I should see "February 2017"
     And I should see "Event on 2-15-2017"
     And I should see "Event on 2-25-2017"
-    And I follow "Event on 2-15-2017"
-    And I should see "Event source: Test Import"
+    And I click on "Event on 2-15-2017" "link"
+    And I click on "Edit" "button"
+    And I set the following fields to these values:
+      | Event title    | Event on 2-20-2017 |
+      | Description    | Event on 2-20-2017 |
+      | timestart[day] | 20 |
+    And I press "Save"
+    When I view the calendar for "2" "2017"
+    Then I should see "Event on 2-20-2017"
+    And I should see "Event on 2-25-2017"
+    And I should not see "Event on 2-15-2017"
+    And I press "Manage subscriptions"
+    And I press "Remove"
+    And I view the calendar for "2" "2017"
+    And I should not see "Event on 2-25-2017"
+    And I should not see "Event on 2-20-2017"
index 2f82882..339fd65 100644 (file)
@@ -1,4 +1,4 @@
-@core @core_calendar
+@core @core_calendar @javascript
 Feature: Limit displayed upcoming events
   In order to filter what is displayed on the calendar
   As a user
@@ -23,10 +23,9 @@ Feature: Limit displayed upcoming events
     And I follow "This month"
     And I click on "a.next" "css_element"
     And I click on "a.next" "css_element"
-    And I create a calendar event:
+    When I create a calendar event:
       | Type of event     | course |
       | Event title       | Two months away event |
-    When I am on "Course 1" course homepage
     Then I should not see "Two months away event"
     And I am on site homepage
     And I follow "Preferences" in the user menu
index 2f627ed..23efe02 100644 (file)
@@ -409,4 +409,267 @@ class core_calendar_lib_testcase extends advanced_testcase {
         $events = calendar_get_legacy_events($timestart, $timeend, true, true, true);
         $this->assertCount(3, $events);
     }
-}
\ No newline at end of file
+
+    public function test_calendar_get_all_allowed_types_no_types() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $systemcontext = context_system::instance();
+        $sitecontext = context_course::instance(SITEID);
+        $roleid = $generator->create_role();
+
+        $generator->role_assign($roleid, $user->id, $systemcontext->id);
+        $generator->role_assign($roleid, $user->id, $sitecontext->id);
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $sitecontext, true);
+        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $systemcontext, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertEmpty($types);
+    }
+
+    public function test_calendar_get_all_allowed_types_user() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $context = context_system::instance();
+        $roleid = $generator->create_role();
+
+        $generator->role_assign($roleid, $user->id, $context->id);
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageownentries', CAP_ALLOW, $roleid, $context, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertTrue($types['user']);
+
+        assign_capability('moodle/calendar:manageownentries', CAP_PROHIBIT, $roleid, $context, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertArrayNotHasKey('user', $types);
+    }
+
+    public function test_calendar_get_all_allowed_types_site() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $context = context_course::instance(SITEID);
+        $roleid = $generator->create_role();
+
+        $generator->role_assign($roleid, $user->id, $context->id);
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertTrue($types['site']);
+
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context, true);
+
+        $types = calendar_get_all_allowed_types();
+        $this->assertArrayNotHasKey('site', $types);
+    }
+
+    public function test_calendar_get_all_allowed_types_course() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course1 = $generator->create_course(); // Has capability.
+        $course2 = $generator->create_course(); // Doesn't have capability.
+        $course3 = $generator->create_course(); // Not enrolled.
+        $context1 = context_course::instance($course1->id);
+        $context2 = context_course::instance($course2->id);
+        $context3 = context_course::instance($course3->id);
+        $roleid = $generator->create_role();
+        $contexts = [$context1, $context2, $context3];
+        $enrolledcourses = [$course1, $course2];
+
+        foreach ($enrolledcourses as $course) {
+            $generator->enrol_user($user->id, $course->id, 'student');
+        }
+
+        foreach ($contexts as $context) {
+            $generator->role_assign($roleid, $user->id, $context->id);
+        }
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context1, true);
+        assign_capability('moodle/calendar:manageentries', CAP_PROHIBIT, $roleid, $context2, true);
+
+        // The user only has the correct capability in course 1 so that is the only
+        // one that should be in the results.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        $this->assertCount(1, $typecourses);
+        $this->assertEquals($course1->id, $typecourses[0]->id);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
+
+        // The user only now has the correct capability in both course 1 and 2 so we
+        // expect both to be in the results.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        // Sort the results by id ascending to ensure the test is consistent
+        // and repeatable.
+        usort($typecourses, function($a, $b) {
+            $aid = $a->id;
+            $bid = $b->id;
+
+            if ($aid == $bid) {
+                return 0;
+            }
+            return ($aid < $bid) ? -1 : 1;
+        });
+
+        $this->assertCount(2, $typecourses);
+        $this->assertEquals($course1->id, $typecourses[0]->id);
+        $this->assertEquals($course2->id, $typecourses[1]->id);
+    }
+
+    public function test_calendar_get_all_allowed_types_group_no_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $roleid = $generator->create_role();
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+
+        // The user has the correct capability in the course but there are
+        // no groups so we shouldn't see a group type.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        $this->assertCount(1, $typecourses);
+        $this->assertEquals($course->id, $typecourses[0]->id);
+        $this->assertArrayNotHasKey('group', $types);
+        $this->assertArrayNotHasKey('groupcourses', $types);
+    }
+
+    public function test_calendar_get_all_allowed_types_group_no_acces_to_diff_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $group1 = $generator->create_group(array('courseid' => $course->id));
+        $group2 = $generator->create_group(array('courseid' => $course->id));
+        $roleid = $generator->create_role();
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
+
+        // The user has the correct capability in the course but they aren't a member
+        // of any of the groups and don't have the accessallgroups capability.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        $this->assertCount(1, $typecourses);
+        $this->assertEquals($course->id, $typecourses[0]->id);
+        $this->assertArrayNotHasKey('group', $types);
+        $this->assertArrayNotHasKey('groupcourses', $types);
+    }
+
+    public function test_calendar_get_all_allowed_types_group_access_all_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course1 = $generator->create_course();
+        $course2 = $generator->create_course();
+        $context1 = context_course::instance($course1->id);
+        $context2 = context_course::instance($course2->id);
+        $group1 = $generator->create_group(array('courseid' => $course1->id));
+        $group2 = $generator->create_group(array('courseid' => $course1->id));
+        $roleid = $generator->create_role();
+
+        $generator->enrol_user($user->id, $course1->id, 'student');
+        $generator->enrol_user($user->id, $course2->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context1->id);
+        $generator->role_assign($roleid, $user->id, $context2->id);
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context1, true);
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context2, true);
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context1, true);
+        assign_capability('moodle/site:accessallgroups', CAP_ALLOW, $roleid, $context2, true);
+
+        // The user has the correct capability in the course and has
+        // the accessallgroups capability.
+        $types = calendar_get_all_allowed_types();
+        $typecourses = $types['course'];
+        $typegroups = $types['group'];
+        $typegroupcourses = $types['groupcourses'];
+        $idascfunc = function($a, $b) {
+            $aid = $a->id;
+            $bid = $b->id;
+
+            if ($aid == $bid) {
+                return 0;
+            }
+            return ($aid < $bid) ? -1 : 1;
+        };
+        // Sort the results by id ascending to ensure the test is consistent
+        // and repeatable.
+        usort($typecourses, $idascfunc);
+        usort($typegroups, $idascfunc);
+
+        $this->assertCount(2, $typecourses);
+        $this->assertEquals($course1->id, $typecourses[0]->id);
+        $this->assertEquals($course2->id, $typecourses[1]->id);
+        $this->assertCount(1, $typegroupcourses);
+        $this->assertEquals($course1->id, $typegroupcourses[0]->id);
+        $this->assertCount(2, $typegroups);
+        $this->assertEquals($group1->id, $typegroups[0]->id);
+        $this->assertEquals($group2->id, $typegroups[1]->id);
+    }
+
+    public function test_calendar_get_all_allowed_types_group_no_access_all_groups() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $group1 = $generator->create_group(array('courseid' => $course->id));
+        $group2 = $generator->create_group(array('courseid' => $course->id));
+        $group3 = $generator->create_group(array('courseid' => $course->id));
+        $roleid = $generator->create_role();
+
+        $generator->enrol_user($user->id, $course->id, 'student');
+        $generator->role_assign($roleid, $user->id, $context->id);
+        $generator->create_group_member(array('groupid' => $group1->id, 'userid' => $user->id));
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user->id));
+
+        $this->setUser($user);
+
+        assign_capability('moodle/calendar:manageentries', CAP_ALLOW, $roleid, $context, true);
+        assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $roleid, $context, true);
+
+        // The user has the correct capability in the course but can't access
+        // groups that they are not a member of.
+        $types = calendar_get_all_allowed_types();
+        $typegroups = $types['group'];
+        $typegroupcourses = $types['groupcourses'];
+        $idascfunc = function($a, $b) {
+            $aid = $a->id;
+            $bid = $b->id;
+
+            if ($aid == $bid) {
+                return 0;
+            }
+            return ($aid < $bid) ? -1 : 1;
+        };
+        // Sort the results by id ascending to ensure the test is consistent
+        // and repeatable.
+        usort($typegroups, $idascfunc);
+
+        $this->assertCount(1, $typegroupcourses);
+        $this->assertEquals($course->id, $typegroupcourses[0]->id);
+        $this->assertCount(2, $typegroups);
+        $this->assertEquals($group1->id, $typegroups[0]->id);
+        $this->assertEquals($group2->id, $typegroups[1]->id);
+    }
+}
index 5fca2dc..60b8075 100644 (file)
@@ -149,6 +149,7 @@ $string['importfrominstructions'] = 'Please provide either a URL to a remote cal
 $string['invalidtimedurationminutes'] = 'The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.';
 $string['invalidtimedurationuntil'] = 'The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.';
 $string['iwanttoexport'] = 'Export';
+$string['less'] = 'Less';
 $string['managesubscriptions'] = 'Manage subscriptions';
 $string['manyevents'] = '{$a} events';
 $string['mon'] = 'Mon';
@@ -157,6 +158,7 @@ $string['monthly'] = 'Monthly';
 $string['monthlyview'] = 'Monthly view';
 $string['monthnext'] = 'Next month';
 $string['monththis'] = 'This month';
+$string['more'] = 'More';
 $string['namewithsource'] = '{$a->name}({$a->source})';
 $string['never'] = 'Never';
 $string['newevent'] = 'New event';
index 231ad97..e8928a9 100644 (file)
@@ -1632,6 +1632,7 @@ $string['rsserrorauth'] = 'Your RSS link does not contain a valid authentication
 $string['rsserrorguest'] = 'This feed uses guest access to access the data, but guest does not have permission to read the data. Visit the original location that this feed comes from (URL) as a valid user and get a new RSS link from there.';
 $string['rsskeyshelp'] = '<p>To ensure security and privacy, RSS feed URLs contain a special token that identifies the user they are for. This prevents other users from accessing areas of the site where they are not allowed.</p><p>The token is automatically created the first time you access an area that produces an RSS feed. If you think that your RSS feed token has been compromised, you can request a new one by clicking the reset link. Please note that your current RSS feed URLs will then become invalid.</p>';
 $string['rsstype'] = 'RSS feed for this activity';
+$string['save'] = 'Save';
 $string['saveandnext'] = 'Save and show next';
 $string['savedat'] = 'Saved at:';
 $string['savechanges'] = 'Save changes';
index a469dda..d1b6f8e 100644 (file)
Binary files a/lib/amd/build/modal.min.js and b/lib/amd/build/modal.min.js differ
index 715247b..06f8a5f 100644 (file)
Binary files a/lib/amd/build/modal_factory.min.js and b/lib/amd/build/modal_factory.min.js differ
index 6cca43b..53ae0b7 100644 (file)
@@ -315,7 +315,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
             return;
         }
 
-        this.getRoot().addClass('large');
+        this.getModal().addClass('modal-lg');
     };
 
     /**
@@ -325,7 +325,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
      * @return {bool}
      */
     Modal.prototype.isLarge = function() {
-        return this.getRoot().hasClass('large');
+        return this.getModal().hasClass('modal-lg');
     };
 
     /**
@@ -338,7 +338,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
             return;
         }
 
-        this.getRoot().removeClass('large');
+        this.getModal().removeClass('modal-lg');
     };
 
     /**
@@ -348,7 +348,7 @@ define(['jquery', 'core/templates', 'core/notification', 'core/key_codes',
      * @return {bool}
      */
     Modal.prototype.isSmall = function() {
-        return !this.getRoot().hasClass('large');
+        return !this.getModal().hasClass('modal-lg');
     };
 
     /**
index 68dedf2..52839b1 100644 (file)
@@ -101,10 +101,10 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
      * @param {object} triggerElement The trigger HTML jQuery object
      * @return {promise} Resolved with a Modal instance
      */
-    var createFromType = function(registryConf, triggerElement) {
+    var createFromType = function(registryConf, templateContext, triggerElement) {
         var templateName = registryConf.template;
 
-        return Templates.render(templateName, {})
+        return Templates.render(templateName, templateContext)
             .then(function(html) {
                 var modalElement = $(html);
                 return createFromElement(registryConf, modalElement, triggerElement);
@@ -124,6 +124,7 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
         var type = modalConfig.type || TYPES.DEFAULT;
         var isLarge = modalConfig.large ? true : false;
         var registryConf = null;
+        var templateContext = {};
 
         registryConf = ModalRegistry.get(type);
 
@@ -131,7 +132,11 @@ define(['jquery', 'core/modal_events', 'core/modal_registry', 'core/modal',
             Notification.exception({message: 'Unable to find modal of type: ' + type});
         }
 
-        return createFromType(registryConf, triggerElement)
+        if (typeof modalConfig.templateContext != 'undefined') {
+            templateContext = modalConfig.templateContext;
+        }
+
+        return createFromType(registryConf, templateContext, triggerElement)
             .then(function(modal) {
                 if (typeof modalConfig.title != 'undefined') {
                     modal.setTitle(modalConfig.title);
index 14385ae..909b7fa 100644 (file)
@@ -126,6 +126,15 @@ $functions = array(
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_calendar_submit_create_update_form' => array(
+        'classname' => 'core_calendar_external',
+        'methodname' => 'submit_create_update_form',
+        'description' => 'Submit form data for event form',
+        'classpath' => 'calendar/externallib.php',
+        'type' => 'write',
+        'capabilities' => 'moodle/calendar:manageentries, moodle/calendar:manageownentries, moodle/calendar:managegroupentries',
+        'ajax' => true,
+    ),
     'core_cohort_add_cohort_members' => array(
         'classname' => 'core_cohort_external',
         'methodname' => 'add_cohort_members',
index 7d8c2ab..beb7173 100644 (file)
@@ -78,6 +78,7 @@ trait templatable_form_element {
                 $otherattributes[] = $attr . '="' . s($value) . '"';
             }
         }
+        $context['name'] = $context['name'] ?: $this->getName();
         $context['extraclasses'] = $extraclasses;
         $context['type'] = $this->getType();
         $context['attributes'] = implode(' ', $otherattributes);
index 08bee9a..8893d13 100644 (file)
@@ -294,6 +294,151 @@ function groups_get_all_groups($courseid, $userid=0, $groupingid=0, $fields='g.*
     return $results;
 }
 
+/**
+ * Gets array of all groups in a set of course.
+ *
+ * @category group
+ * @param array $courses Array of course objects or course ids.
+ * @return array Array of groups indexed by course id.
+ */
+function groups_get_all_groups_for_courses($courses) {
+    global $DB;
+
+    if (empty($courses)) {
+        return [];
+    }
+
+    $groups = [];
+    $courseids = [];
+
+    foreach ($courses as $course) {
+        $courseid = is_object($course) ? $course->id : $course;
+        $groups[$courseid] = [];
+        $courseids[] = $courseid;
+    }
+
+    $groupfields = [
+        'g.id as gid',
+        'g.courseid',
+        'g.idnumber',
+        'g.name',
+        'g.description',
+        'g.descriptionformat',
+        'g.enrolmentkey',
+        'g.picture',
+        'g.hidepicture',
+        'g.timecreated',
+        'g.timemodified'
+    ];
+
+    $groupsmembersfields = [
+        'gm.id as gmid',
+        'gm.groupid',
+        'gm.userid',
+        'gm.timeadded',
+        'gm.component',
+        'gm.itemid'
+    ];
+
+    $concatidsql = $DB->sql_concat_join("'-'", ['g.id', 'COALESCE(gm.id, 0)']) . ' AS uniqid';
+    list($courseidsql, $params) = $DB->get_in_or_equal($courseids);
+    $groupfieldssql = implode(',', $groupfields);
+    $groupmembersfieldssql = implode(',', $groupsmembersfields);
+    $sql = "SELECT {$concatidsql}, {$groupfieldssql}, {$groupmembersfieldssql}
+              FROM {groups} g
+         LEFT JOIN {groups_members} gm
+                ON gm.groupid = g.id
+             WHERE g.courseid {$courseidsql}";
+
+    $results = $DB->get_records_sql($sql, $params);
+
+    // The results will come back as a flat dataset thanks to the left
+    // join so we will need to do some post processing to blow it out
+    // into a more usable data structure.
+    //
+    // This loop will extract the distinct groups from the result set
+    // and add it's list of members to the object as a property called
+    // 'members'. Then each group will be added to the result set indexed
+    // by it's course id.
+    //
+    // The resulting data structure for $groups should be:
+    // $groups = [
+    //      '1' = [
+    //          '1' => (object) [
+    //              'id' => 1,
+    //              <rest of group properties>
+    //              'members' => [
+    //                  '1' => (object) [
+    //                      <group member properties>
+    //                  ],
+    //                  '2' => (object) [
+    //                      <group member properties>
+    //                  ]
+    //              ]
+    //          ],
+    //          '2' => (object) [
+    //              'id' => 2,
+    //              <rest of group properties>
+    //              'members' => [
+    //                  '1' => (object) [
+    //                      <group member properties>
+    //                  ],
+    //                  '3' => (object) [
+    //                      <group member properties>
+    //                  ]
+    //              ]
+    //          ]
+    //      ]
+    // ]
+    //
+    foreach ($results as $key => $result) {
+        $groupid = $result->gid;
+        $courseid = $result->courseid;
+        $coursegroups = $groups[$courseid];
+        $groupsmembersid = $result->gmid;
+        $reducefunc = function($carry, $field) use ($result) {
+            // Iterate over the groups properties and pull
+            // them out into a separate object.
+            list($prefix, $field) = explode('.', $field);
+
+            if (property_exists($result, $field)) {
+                $carry[$field] = $result->{$field};
+            }
+
+            return $carry;
+        };
+
+        if (isset($coursegroups[$groupid])) {
+            $group = $coursegroups[$groupid];
+        } else {
+            $initial = [
+                'id' => $groupid,
+                'members' => []
+            ];
+            $group = (object) array_reduce(
+                $groupfields,
+                $reducefunc,
+                $initial
+            );
+        }
+
+        if (!empty($groupsmembersid)) {
+            $initial = ['id' => $groupsmembersid];
+            $groupsmembers = (object) array_reduce(
+                $groupsmembersfields,
+                $reducefunc,
+                $initial
+            );
+
+            $group->members[$groupsmembers->userid] = $groupsmembers;
+        }
+
+        $coursegroups[$groupid] = $group;
+        $groups[$courseid] = $coursegroups;
+    }
+
+    return $groups;
+}
 
 /**
  * Gets array of all groups in current user.
index f9bfbee..e8aa123 100644 (file)
@@ -1547,4 +1547,154 @@ class core_grouplib_testcase extends advanced_testcase {
         $this->assertCount(2, $members);    // Now I see members of group 3.
         $this->assertEquals([$user1->id, $user3->id], array_keys($members), '', 0.0, 10, true);
     }
+
+    /**
+     * Test groups_get_all_groups_for_courses() method.
+     */
+    public function test_groups_get_all_groups_for_courses_no_courses() {
+        $this->resetAfterTest(true);
+        $generator = $this->getDataGenerator();
+
+        $this->assertEquals([], groups_get_all_groups_for_courses([]));
+    }
+
+    /**
+     * Test groups_get_all_groups_for_courses() method.
+     */
+    public function test_groups_get_all_groups_for_courses_with_courses() {
+        $this->resetAfterTest(true);
+        $generator = $this->getDataGenerator();
+
+        // Create courses.
+        $course1 = $generator->create_course(); // no groups.
+        $course2 = $generator->create_course(); // one group, no members.
+        $course3 = $generator->create_course(); // one group, one member.
+        $course4 = $generator->create_course(); // one group, multiple members.
+        $course5 = $generator->create_course(); // two groups, no members.
+        $course6 = $generator->create_course(); // two groups, one member.
+        $course7 = $generator->create_course(); // two groups, multiple members.
+
+        $courses = [$course1, $course2, $course3, $course4, $course5, $course6, $course7];
+        // Create users.
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $user3 = $generator->create_user();
+        $user4 = $generator->create_user();
+
+        // Enrol users.
+        foreach ($courses as $course) {
+            $generator->enrol_user($user1->id, $course->id);
+            $generator->enrol_user($user2->id, $course->id);
+            $generator->enrol_user($user3->id, $course->id);
+            $generator->enrol_user($user4->id, $course->id);
+        }
+
+        // Create groups.
+        $group1 = $generator->create_group(array('courseid' => $course2->id)); // no members.
+        $group2 = $generator->create_group(array('courseid' => $course3->id)); // one member.
+        $group3 = $generator->create_group(array('courseid' => $course4->id)); // multiple members.
+        $group4 = $generator->create_group(array('courseid' => $course5->id)); // no members.
+        $group5 = $generator->create_group(array('courseid' => $course5->id)); // no members.
+        $group6 = $generator->create_group(array('courseid' => $course6->id)); // one member.
+        $group7 = $generator->create_group(array('courseid' => $course6->id)); // one member.
+        $group8 = $generator->create_group(array('courseid' => $course7->id)); // multiple members.
+        $group9 = $generator->create_group(array('courseid' => $course7->id)); // multiple members.
+
+        // Assign users to groups.
+        $generator->create_group_member(array('groupid' => $group2->id, 'userid' => $user1->id));
+        $generator->create_group_member(array('groupid' => $group3->id, 'userid' => $user1->id));
+        $generator->create_group_member(array('groupid' => $group3->id, 'userid' => $user2->id));
+        $generator->create_group_member(array('groupid' => $group6->id, 'userid' => $user1->id));
+        $generator->create_group_member(array('groupid' => $group7->id, 'userid' => $user1->id));
+        $generator->create_group_member(array('groupid' => $group8->id, 'userid' => $user1->id));
+        $generator->create_group_member(array('groupid' => $group8->id, 'userid' => $user2->id));
+        $generator->create_group_member(array('groupid' => $group9->id, 'userid' => $user1->id));
+        $generator->create_group_member(array('groupid' => $group9->id, 'userid' => $user2->id));
+
+        $result = groups_get_all_groups_for_courses($courses);
+        $assertpropertiesmatch = function($expected, $actual) {
+            $props = get_object_vars($expected);
+
+            foreach ($props as $name => $val) {
+                $got = $actual->{$name};
+                $this->assertEquals(
+                    $val,
+                    $actual->{$name},
+                    "Failed asserting that {$got} equals {$val} for property {$name}"
+                );
+            }
+        };
+
+        // Course 1 has no groups.
+        $this->assertEquals([], $result[$course1->id]);
+
+        // Course 2 has one group with no members.
+        $coursegroups = $result[$course2->id];
+        $coursegroup = $coursegroups[$group1->id];
+        $this->assertCount(1, $coursegroups);
+        $this->assertEquals([], $coursegroup->members);
+        $assertpropertiesmatch($group1, $coursegroup);
+
+        // Course 3 has one group with one member.
+        $coursegroups = $result[$course3->id];
+        $coursegroup = $coursegroups[$group2->id];
+        $groupmember1 = $coursegroup->members[$user1->id];
+        $this->assertCount(1, $coursegroups);
+        $this->assertCount(1, $coursegroup->members);
+        $assertpropertiesmatch($group2, $coursegroup);
+        $this->assertEquals($user1->id, $groupmember1->userid);
+
+        // Course 4 has one group with multiple members.
+        $coursegroups = $result[$course4->id];
+        $coursegroup = $coursegroups[$group3->id];
+        $groupmember1 = $coursegroup->members[$user1->id];
+        $groupmember2 = $coursegroup->members[$user2->id];
+        $this->assertCount(1, $coursegroups);
+        $this->assertCount(2, $coursegroup->members);
+        $assertpropertiesmatch($group3, $coursegroup);
+        $this->assertEquals($user1->id, $groupmember1->userid);
+        $this->assertEquals($user2->id, $groupmember2->userid);
+
+        // Course 5 has multiple groups with no members.
+        $coursegroups = $result[$course5->id];
+        $coursegroup1 = $coursegroups[$group4->id];
+        $coursegroup2 = $coursegroups[$group5->id];
+        $this->assertCount(2, $coursegroups);
+        $this->assertEquals([], $coursegroup1->members);
+        $this->assertEquals([], $coursegroup2->members);
+        $assertpropertiesmatch($group4, $coursegroup1);
+        $assertpropertiesmatch($group5, $coursegroup2);
+
+        // Course 6 has multiple groups with one member.
+        $coursegroups = $result[$course6->id];
+        $coursegroup1 = $coursegroups[$group6->id];
+        $coursegroup2 = $coursegroups[$group7->id];
+        $group1member1 = $coursegroup1->members[$user1->id];
+        $group2member1 = $coursegroup2->members[$user1->id];
+        $this->assertCount(2, $coursegroups);
+        $this->assertCount(1, $coursegroup1->members);
+        $this->assertCount(1, $coursegroup2->members);
+        $assertpropertiesmatch($group6, $coursegroup1);
+        $assertpropertiesmatch($group7, $coursegroup2);
+        $this->assertEquals($user1->id, $group1member1->userid);
+        $this->assertEquals($user1->id, $group2member1->userid);
+
+        // Course 7 has multiple groups with multiple members.
+        $coursegroups = $result[$course7->id];
+        $coursegroup1 = $coursegroups[$group8->id];
+        $coursegroup2 = $coursegroups[$group9->id];
+        $group1member1 = $coursegroup1->members[$user1->id];
+        $group1member2 = $coursegroup1->members[$user2->id];
+        $group2member1 = $coursegroup2->members[$user1->id];
+        $group2member2 = $coursegroup2->members[$user2->id];
+        $this->assertCount(2, $coursegroups);
+        $this->assertCount(2, $coursegroup1->members);
+        $this->assertCount(2, $coursegroup2->members);
+        $assertpropertiesmatch($group8, $coursegroup1);
+        $assertpropertiesmatch($group9, $coursegroup2);
+        $this->assertEquals($user1->id, $group1member1->userid);
+        $this->assertEquals($user2->id, $group1member2->userid);
+        $this->assertEquals($user1->id, $group2member1->userid);
+        $this->assertEquals($user2->id, $group2member2->userid);
+    }
 }
index 5b4bd0c..7cdb760 100644 (file)
@@ -48,7 +48,7 @@
                   <span aria-hidden="true">&times;</span>
                 </button>
                 {{$header}}
-                    <h4 id="{{uniqid}}-modal-title" data-region="title" tabindex="0">{{title}}</h4>
+                    <h4 id="{{uniqid}}-modal-title" data-region="title" tabindex="0">{{$title}}{{title}}{{/title}}</h4>
                 {{/header}}
             </div>
             <div class="modal-body" data-region="body">
index 88593fc..55e16c5 100644 (file)
@@ -4,7 +4,7 @@
         {{#required}}<abbr class="initialism text-danger" title="{{#str}}required{{/str}}">{{#pix}}req, core, {{#str}}required{{/str}}{{/pix}}</abbr>{{/required}}
         {{#advanced}}<abbr class="initialism text-info" title="{{#str}}advanced{{/str}}">!</abbr>{{/advanced}}
     </label>
-    <span data-fieldtype="{{element.type}}">
+    <span id="{{element.id}}" data-fieldtype="{{element.type}}">
         {{$ element }}
             <!-- Element goes here -->
         {{/ element }}
index 220bbde..a9b4591 100644 (file)
 @zindexModalBackdrop:     1040;
 @zindexModal:             1050;
 
-
 // Sprite icons path
 // -------------------------
 @iconSpritePath:          "../img/glyphicons-halflings.png";
index 0a980d0..0bc21d0 100644 (file)
@@ -3,6 +3,9 @@
 @import "fontawesome/font-awesome";
 @import "fontawesome/moodle-path";
 
+// Import the Moodle variables.
+@import "moodle/variables.less";
+
 // Old Moodle stuff from base theme.
 // Massive, needs broken up.
 @import "moodle/core";
index f8f9ca5..cbb29dc 100644 (file)
@@ -22,6 +22,9 @@
 
 // Calendar restyling.
 .path-calendar {
+    #dateselector-calendar-panel {
+        z-index: @zindexModalContainer+4;
+    }
     .calendartable {
         width: 100%;
         th,
index 716de8b..32f89c1 100644 (file)
@@ -10,7 +10,7 @@ body {
     right: 0;
     bottom: 0;
     left: 0;
-    z-index: 4050;
+    z-index: @zindexModalContainer;
     outline: 0;
     overflow-x: hidden;
     overflow-y: auto;
@@ -28,6 +28,10 @@ body {
         border-radius: 10px;
         border: none;
 
+        &.modal-lg {
+            max-width: 900px;
+        }
+
         .modal-header {
             min-height: 13px;
             padding: 5px;
@@ -98,16 +102,10 @@ body {
             border-radius: 0 0 10px 10px;
         }
     }
-
-    &.large {
-        .modal {
-            max-width: 900px;
-        }
-    }
 }
 
 .modal-backdrop {
-    z-index: 4049;
+    z-index: @zindexModalContainerBackdrop;
     background-color: #aaa;
     opacity: 0.4;
 }
diff --git a/theme/bootstrapbase/less/moodle/variables.less b/theme/bootstrapbase/less/moodle/variables.less
new file mode 100644 (file)
index 0000000..b86ad2f
--- /dev/null
@@ -0,0 +1,7 @@
+// Global variables for use within Moodle's less style sheets.
+// These should be unique and not override the variables defined
+// in Bootstrap.
+
+@zindexModalContainer: 4050;
+@zindexModalContainerBackdrop: 4049;
+
index eaed20d..e93a190 100644 (file)
@@ -5551,6 +5551,9 @@ img.iconsmall {
 .calendar_event_user {
   background-color: #dce7ec;
 }
+.path-calendar #dateselector-calendar-panel {
+  z-index: 4054;
+}
 .path-calendar .calendartable {
   width: 100%;
 }
@@ -16985,6 +16988,9 @@ body.modal-open {
   border-radius: 10px;
   border: none;
 }
+.modal-container .modal.modal-lg {
+  max-width: 900px;
+}
 .modal-container .modal .modal-header {
   min-height: 13px;
   padding: 5px;
@@ -17044,9 +17050,6 @@ body.modal-open {
   box-shadow: none;
   border-radius: 0 0 10px 10px;
 }
-.modal-container.large .modal {
-  max-width: 900px;
-}
 .modal-backdrop {
   z-index: 4049;
   background-color: #aaa;
index d58a311..33da10c 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2017072700.02;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2017080300.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.