MDL-59382 calendar: add modal to create and update events

This commit is contained in:
Ryan Wyllie 2017-07-24 08:01:14 +00:00
parent 6103fd2efe
commit aa0912258d
23 changed files with 2392 additions and 157 deletions

View file

@ -1 +1 @@
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(a,b,c,d,e,f,g,h,i,j){var k={ROOT:"[data-region='calendar']",EVENT_LINK:"[data-action='view-event']"},l=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},m=function(a){return c.get_string("subsource","core_calendar",a).then(function(b){return a.url?'<a href="'+a.url+'">'+b+"</a>":b})},n=function(b){j.getEventById(b).then(function(c){if(!c.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+b);var d=c.event,e=l(d.eventtype);if(d.displayeventsource){d.subscription=JSON.parse(d.subscription);var f={url:d.subscription.url,name:d.subscription.name},g=m(f);return a.when(e,g).then(function(a,b){return d.eventtype=a,d.source=b,d})}return e.then(function(a){return d.eventtype=a,d})}).then(function(a){var b={title:a.name,type:h.TYPE,body:d.render("core_calendar/event_summary_body",a)};return a.caneditevent||(b.footer=""),g.create(b)}).done(function(a){a.getRoot().on(i.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},o=function(){a(k.EVENT_LINK).click(function(b){b.preventDefault();var c=a(this).attr("data-event-id");n(c)})};return{init:function(){o()}}});
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(a,b,c,d,e,f,g,h,i,j,k,l){var m={ROOT:"[data-region='calendar']",EVENT_LINK:"[data-action='view-event']",NEW_EVENT_BUTTON:"[data-action='new-event-button']"},n=function(a){var b="type"+a;return c.get_string(b,"core_calendar").then(function(a){return a})},o=function(a){return c.get_string("subsource","core_calendar",a).then(function(b){return a.url?'<a href="'+a.url+'">'+b+"</a>":b})},p=function(b){k.getEventById(b).then(function(c){if(!c.event)throw new Error("Error encountered while trying to fetch calendar event with ID: "+b);var d=c.event,e=n(d.eventtype);if(d.displayeventsource){d.subscription=JSON.parse(d.subscription);var f={url:d.subscription.url,name:d.subscription.name},g=o(f);return a.when(e,g).then(function(a,b){return d.eventtype=a,d.source=b,d})}return e.then(function(a){return d.eventtype=a,d})}).then(function(a){var b={title:a.name,type:j.TYPE,body:d.render("core_calendar/event_summary_body",a)};return a.caneditevent||(b.footer=""),h.create(b)}).done(function(a){a.getRoot().on(g.hidden,function(){a.destroy()}),a.show()}).fail(e.exception)},q=function(a){var b=a.find(m.NEW_EVENT_BUTTON),c=b.attr("data-context-id");return h.create({type:i.TYPE,large:!0,templateContext:{contextid:c}},b)},r=function(b,c){var d=a("body");d.on(l.created,function(){window.location.reload()}),d.on(l.deleted,function(){window.location.reload()}),d.on(l.updated,function(){window.location.reload()}),c.then(function(a){d.on(l.editEvent,function(b,c){a.setEventId(c),a.show()})})},s=function(){var b=a(m.ROOT);a(m.EVENT_LINK).click(function(b){b.preventDefault();var c=a(this).attr("data-event-id");p(c)});var c=q(b);r(b,c)};return{init:function(){s()}}});

1
calendar/amd/build/event_form.min.js vendored Normal file
View file

@ -0,0 +1 @@
define(["jquery","core/templates"],function(a,b){var c={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"},d={USER:"user",SITE:"site",COURSE:"course",GROUP:"group"},e={SHOW_ADVANCED:"event_form-show-advanced",HIDE_ADVANCED:"event_form-hide-advanced",ADVANCED_SHOWN:"event_form-advanced-shown",ADVANCED_HIDDEN:"event_form-advanced-hidden"},f=function(a){a.find(c.FIELDSET_ADVANCED_ELEMENTS).removeClass("containsadvancedelements");var d=a.find(c.MORELESS_TOGGLE);b.replaceNode(d,"","")},g=function(a){a.find(c.ADVANCED_ELEMENT).removeClass("hidden"),a.trigger(e.ADVANCED_SHOWN)},h=function(a){a.find(c.ADVANCED_ELEMENT).addClass("hidden"),a.trigger(e.ADVANCED_HIDDEN)},i=function(a){a.on(e.SHOW_ADVANCED,function(){g(a)}),a.on(e.HIDE_ADVANCED,function(){h(a)})},j=function(b){b.find(c.EVENT_GROUP_ID).find(c.SELECT_OPTION).each(function(b,c){var c=a(c),d=c.attr("value"),e=d.split("-"),f=e[0];c.attr("data-course-id",f)})},k=function(a){var b=a.find(c.EVENT_TYPE),e=b.val(),f=a.find(c.EVENT_COURSE_ID).closest(c.FORM_GROUP).removeClass("hidden"),g=a.find(c.EVENT_GROUP_COURSE_ID).closest(c.FORM_GROUP).removeClass("hidden"),h=a.find(c.EVENT_GROUP_ID).closest(c.FORM_GROUP).removeClass("hidden");switch(e){case d.COURSE:g.addClass("hidden"),h.addClass("hidden");break;case d.GROUP:f.addClass("hidden");break;default:f.addClass("hidden"),g.addClass("hidden"),h.addClass("hidden")}},l=function(a){var b=a.find(c.EVENT_TYPE);b.on("change",function(){k(a)})},m=function(b){var d=b.find(c.EVENT_GROUP_COURSE_ID),e=b.find(c.EVENT_GROUP_ID),f=e.find(c.SELECT_OPTION),g=function(){var b=d.val(),c=null;f.each(function(d,e){e=a(e),e.attr("data-course-id")==b?(e.removeClass("hidden"),e.prop("disabled",!1),null===c&&(c=d)):(e.addClass("hidden"),e.prop("disabled",!0))}),e.prop("selectedIndex",c)};d.on("change",g),g()},n=function(b,c){var d=a("#"+b);i(d),f(d),k(d),j(d),l(d),m(d),c?g(d):h(d)};return{init:n,events:e}});

View file

@ -0,0 +1 @@
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(a,b,c,d,e,f,g,h,i,j,k,l){var m=!1,n={MORELESS_BUTTON:'[data-action="more-less-toggle"]',SAVE_BUTTON:'[data-action="save"]',LOADING_ICON_CONTAINER:'[data-region="loading-icon-container"]'},o=function(a){g.call(this,a),this.eventId=null,this.reloadingBody=!1,this.reloadingTitle=!1,this.saveButton=this.getFooter().find(n.SAVE_BUTTON),this.moreLessButton=this.getFooter().find(n.MORELESS_BUTTON)};return o.TYPE="core_calendar-modal_event_form",o.prototype=Object.create(g.prototype),o.prototype.constructor=o,o.prototype.setEventId=function(a){this.eventId=a},o.prototype.getEventId=function(){return this.eventId},o.prototype.hasEventId=function(){return null!==this.eventId},o.prototype.getForm=function(){return this.getBody().find("form")},o.prototype.disableButtons=function(){this.saveButton.prop("disabled",!0),this.moreLessButton.prop("disabled",!0)},o.prototype.enableButtons=function(){this.saveButton.prop("disabled",!1),this.moreLessButton.prop("disabled",!1)},o.prototype.setMoreButton=function(){this.moreLessButton.attr("data-collapsed","true"),c.get_string("more","calendar").then(function(a){this.moreLessButton.text(a)}.bind(this))},o.prototype.setLessButton=function(){this.moreLessButton.attr("data-collapsed","false"),c.get_string("less","calendar").then(function(a){this.moreLessButton.text(a)}.bind(this))},o.prototype.toggleMoreLessButton=function(){var a=this.getForm();"true"==this.moreLessButton.attr("data-collapsed")?(a.trigger(l.events.SHOW_ADVANCED),this.setLessButton()):(a.trigger(l.events.HIDE_ADVANCED),this.setMoreButton())},o.prototype.reloadTitleContent=function(){return this.reloadingTitle?this.titlePromise:(this.reloadingTitle=!0,this.hasEventId()?this.titlePromise=c.get_string("editevent","calendar"):this.titlePromise=c.get_string("newevent","calendar"),this.titlePromise.then(function(a){return this.setTitle(a),a}.bind(this)).always(function(){this.reloadingTitle=!1}.bind(this)),this.titlePromise)},o.prototype.reloadBodyContent=function(a,b){if(this.reloadingBody)return this.bodyPromise;this.reloadingBody=!0,this.disableButtons();var c=this.saveButton.attr("data-context-id"),e={};return this.hasEventId()&&(e.eventid=this.getEventId()),"undefined"!=typeof a&&(e.formdata=a),e.haserror="undefined"!=typeof b&&b,this.bodyPromise=i.loadFragment("calendar","event_form",c,e),this.setBody(this.bodyPromise),this.bodyPromise.then(function(){this.enableButtons()}.bind(this))["catch"](d.exception).always(function(){this.reloadingBody=!1}.bind(this)),this.bodyPromise},o.prototype.reloadAllContent=function(){return a.when(this.reloadTitleContent(),this.reloadBodyContent())},o.prototype.show=function(){this.reloadAllContent(),g.prototype.show.call(this)},o.prototype.hide=function(){g.prototype.hide.call(this),this.setEventId(null)},o.prototype.getFormData=function(){return this.getForm().serialize()},o.prototype.save=function(){var b=this.saveButton.find(n.LOADING_ICON_CONTAINER);b.removeClass("hidden"),this.disableButtons();var c=this.getFormData();return k.submitCreateUpdateForm(c).then(function(b){return b.validationerror?this.reloadBodyContent(c,!0):(this.hide(),void(this.hasEventId()?a("body").trigger(j.updated,[b.event]):a("body").trigger(j.created,[b.event])))}.bind(this)).always(function(){b.addClass("hidden"),this.enableButtons()}.bind(this))["catch"](d.exception)},o.prototype.registerEventListeners=function(){g.prototype.registerEventListeners.call(this),this.getModal().on(f.events.activate,n.SAVE_BUTTON,function(a,b){this.getForm().submit(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on("submit",function(a){this.save(),a.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on(f.events.activate,n.MORELESS_BUTTON,function(a,b){this.toggleMoreLessButton(),b.originalEvent.preventDefault(),a.stopPropagation()}.bind(this)),this.getModal().on(l.events.ADVANCED_SHOWN,function(){this.setLessButton()}.bind(this)),this.getModal().on(l.events.ADVANCED_HIDDEN,function(){this.setMoreButton()}.bind(this))},m||(h.register(o.TYPE,o,"calendar/modal_event_form"),m=!0),o});

View file

@ -1 +1 @@
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"],function(a,b,c,d,e,f,g,h,i,j){var k=!1,l={ROOT:"[data-region='summary-modal-container']",EDIT_BUTTON:'[data-action="edit"]',DELETE_BUTTON:'[data-action="delete"]',EVENT_LINK:'[data-action="event-link"]'},m=function(a){e.call(this,a),this.getFooter().find(l.EDIT_BUTTON).length||c.exception({message:"No edit button found"}),this.getFooter().find(l.DELETE_BUTTON).length||c.exception({message:"No delete button found"})};return m.TYPE="core_calendar-event_summary",m.prototype=Object.create(e.prototype),m.prototype.constructor=m,m.prototype.registerEventListeners=function(){e.prototype.registerEventListeners.call(this);var a=g.create({type:g.types.CONFIRM},this.getFooter().find(l.DELETE_BUTTON)).then(function(a){return b.get_string("confirm").then(function(b){a.setTitle(b)}.bind(this))["catch"](c.exception),a.getRoot().on(h.yes,function(){var b=this.getBody().find(l.ROOT).attr("data-event-id");i.deleteEvent(b).done(function(){a.getRoot().trigger(j.deleted,b),window.location.reload()}).fail(c.exception)}.bind(this)),a}.bind(this));this.getRoot().on(h.bodyRendered,function(){var c=this.getBody().find(l.ROOT).attr("data-event-title");a.then(function(a){a.setBody(b.get_string("confirmeventdelete","core_calendar",c))})}.bind(this))},k||(f.register(m.TYPE,m,"core_calendar/event_summary_modal"),k=!0),m});
define(["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/repository","core_calendar/events"],function(a,b,c,d,e,f,g,h,i,j){var k=!1,l={ROOT:"[data-region='summary-modal-container']",EDIT_BUTTON:'[data-action="edit"]',DELETE_BUTTON:'[data-action="delete"]'},m=function(a){e.call(this,a),this.getEditButton().length||c.exception({message:"No edit button found"}),this.getDeleteButton().length||c.exception({message:"No delete button found"})};return m.TYPE="core_calendar-event_summary",m.prototype=Object.create(e.prototype),m.prototype.constructor=m,m.prototype.getEditButton=function(){return"undefined"==typeof this.editButton&&(this.editButton=this.getFooter().find(l.EDIT_BUTTON)),this.editButton},m.prototype.getDeleteButton=function(){return"undefined"==typeof this.deleteButton&&(this.deleteButton=this.getFooter().find(l.DELETE_BUTTON)),this.deleteButton},m.prototype.getEventId=function(){return this.getBody().find(l.ROOT).attr("data-event-id")},m.prototype.registerEventListeners=function(){e.prototype.registerEventListeners.call(this);var f=g.create({type:g.types.CONFIRM},this.getDeleteButton()).then(function(b){return b.getRoot().on(h.yes,function(){var b=this.getEventId();i.deleteEvent(b).then(function(){a("body").trigger(j.deleted,[b]),this.hide()}.bind(this))["catch"](c.exception)}.bind(this)),b}.bind(this));this.getRoot().on(h.bodyRendered,function(){var a=this.getBody().find(l.ROOT).attr("data-event-title");f.then(function(c){c.setBody(b.get_string("confirmeventdelete","core_calendar",a))})}.bind(this)),d.define(this.getEditButton(),[d.events.activate]),this.getEditButton().on(d.events.activate,function(b,c){a("body").trigger(j.editEvent,[this.getEventId()]),this.hide(),b.preventDefault(),b.stopPropagation(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()}.bind(this))},k||(f.register(m.TYPE,m,"core_calendar/event_summary_modal"),k=!0),m});

View file

@ -14,128 +14,215 @@
// 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) {
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']",
};
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 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;
});
};
/**
* 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);
/**
* 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);
// 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;
});
}
// Return event data with event type info.
return eventTypePromise.then(function(eventType) {
// 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 = '';
}
// 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);
};
/**
* 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);
});
};
return {
init: function() {
registerEventListeners();
}
};
});
// 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
}
},
newEventButton
);
};
/**
* 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(); });
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;
});
};
/**
* 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();
}
};
});

View file

@ -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) {
var 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,
};
});

View file

@ -0,0 +1,429 @@
// 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);
}.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);
}.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]);
}
}
}.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;
});

View 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,51 @@ 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

View file

@ -0,0 +1,283 @@
<?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
* @package calendar
*/
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 chosen 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');
}
}

View file

@ -0,0 +1,60 @@
<?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
* @package calendar
*/
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.
*
* @method add_event_repeat_elements
* @param MoodleQuickForm $mform
* @param stdClass $event The event properties
*/
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');
}
}

View file

@ -0,0 +1,121 @@
<?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.
*
* @method from_legacy_event_to_data
* @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.
*
* @method from_data_to_event_properties
* @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($data->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.
*
* @method get_time_duration_from_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;
}
}
}

View file

@ -0,0 +1,55 @@
<?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.
*
* @method from_legacy_event_to_data
* @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.
*
* @method from_data_to_event_properties
* @param stdClass $data
* @return stdClass
*/
public function from_data_to_event_properties(\stdClass $data);
}

View 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,90 @@ 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);
}
$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),
)
);
}
}

View 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'.
*
* @param array The available types for the logged in user
*/
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,70 @@ function calendar_get_legacy_events($tstart, $tend, $users, $groups, $courses, $
return $carry + [$event->get_id() => $mapper->from_event_to_stdclass($event)];
}, []);
}
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;
}

View 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);
}
/**

View file

@ -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 }}

View 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);
}
}
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);
}
}