From c56dd950ebb95ef1f739df2d2bd168bbc9b9185f Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Mon, 18 Sep 2017 03:56:20 +0000 Subject: [PATCH] MDL-60058 calendar: add visual indicator to UI for valid drop zones --- .../amd/build/drag_drop_data_store.min.js | 2 +- .../build/month_navigation_drag_drop.min.js | 2 +- .../amd/build/month_view_drag_drop.min.js | 2 +- calendar/amd/src/drag_drop_data_store.js | 114 ++++++++++ .../amd/src/month_navigation_drag_drop.js | 15 ++ calendar/amd/src/month_view_drag_drop.js | 199 ++++++++++++++++-- .../external/calendar_event_exporter.php | 130 +++++++++++- calendar/templates/month_detailed.mustache | 12 ++ .../tests/calendar_event_exporter_test.php | 150 +++++++++++++ .../bootstrapbase/less/moodle/bs4-compat.less | 4 + theme/bootstrapbase/style/moodle.css | 3 + 11 files changed, 613 insertions(+), 20 deletions(-) create mode 100644 calendar/tests/calendar_event_exporter_test.php diff --git a/calendar/amd/build/drag_drop_data_store.min.js b/calendar/amd/build/drag_drop_data_store.min.js index e2cc7decf47..5185c8b54d2 100644 --- a/calendar/amd/build/drag_drop_data_store.min.js +++ b/calendar/amd/build/drag_drop_data_store.min.js @@ -1 +1 @@ -define([],function(){var a=null,b=null,c=function(b){a=b},d=function(){return a},e=function(){return null!==a},f=function(a){b=a},g=function(){return b},h=function(){c(null),f(null)};return{setEventId:c,getEventId:d,hasEventId:e,setDurationDays:f,getDurationDays:g,clearAll:h}}); \ No newline at end of file +define([],function(){var a=null,b=null,c=null,d=null,e=null,f=null,g=function(b){a=b},h=function(){return a},i=function(){return null!==a},j=function(a){b=a},k=function(){return b},l=function(a){c=a},m=function(){return c},n=function(){return null!==c},o=function(a){d=a},p=function(){return d},q=function(){return null!==d},r=function(a){e=a},s=function(){return e},t=function(a){f=a},u=function(){return f},v=function(){g(null),j(null),l(null),o(null),r(null),t(null)};return{setEventId:g,getEventId:h,hasEventId:i,setDurationDays:j,getDurationDays:k,setMinTimestart:l,getMinTimestart:m,hasMinTimestart:n,setMaxTimestart:o,getMaxTimestart:p,hasMaxTimestart:q,setMinError:r,getMinError:s,setMaxError:t,getMaxError:u,clearAll:v}}); \ No newline at end of file diff --git a/calendar/amd/build/month_navigation_drag_drop.min.js b/calendar/amd/build/month_navigation_drag_drop.min.js index a8888bf3f49..6e6cbc371e1 100644 --- a/calendar/amd/build/month_navigation_drag_drop.min.js +++ b/calendar/amd/build/month_navigation_drag_drop.min.js @@ -1 +1 @@ -define(["jquery","core_calendar/drag_drop_data_store"],function(a,b){var c={DRAGGABLE:'[draggable="true"][data-region="event-item"]',DROP_ZONE:'[data-drop-zone="nav-link"]'},d="bg-primary text-white",e="drop-target",f=1e3,g=!1,h=null,i=null,j=function(a,b){b?a.addClass(d):a.removeClass(d)},k=function(){i.find(c.DROP_ZONE).addClass(e)},l=function(){i.find(c.DROP_ZONE).removeClass(e)},m=function(b){var d=a(b.target).closest(c.DROP_ZONE);return d.length?d:null},n=function(b){var d=a(b.target).closest(c.DRAGGABLE);d.length&&k()},o=function(a){a.preventDefault();var c=m(a);c&&b.hasEventId()&&(h||(h=setTimeout(function(){c.click(),h=null},f)),j(c,!0),l())},p=function(a){var b=m(a);b&&(h&&(clearTimeout(h),h=null),j(b,!1),k(),a.preventDefault())},q=function(a){l();var b=m(a);b&&(j(b,!1),a.preventDefault())};return{init:function(c){g||(document.addEventListener("dragstart",n,!1),document.addEventListener("dragover",o,!1),document.addEventListener("dragleave",p,!1),document.addEventListener("drop",q,!1),document.addEventListener("dragend",l,!1),g=!0),i=a(c),b.hasEventId()&&k()}}}); \ No newline at end of file +define(["jquery","core_calendar/drag_drop_data_store"],function(a,b){var c={DRAGGABLE:'[draggable="true"][data-region="event-item"]',DROP_ZONE:'[data-drop-zone="nav-link"]'},d="bg-primary text-white",e="drop-target",f=1e3,g=!1,h=null,i=null,j=function(a,b){b?a.addClass(d):a.removeClass(d)},k=function(){i.find(c.DROP_ZONE).addClass(e)},l=function(){i.find(c.DROP_ZONE).removeClass(e)},m=function(b){var d=a(b.target).closest(c.DROP_ZONE);return d.length?d:null},n=function(b){var d=a(b.target).closest(c.DRAGGABLE);d.length&&k()},o=function(a){if(b.hasEventId()){a.preventDefault();var c=m(a);c&&b.hasEventId()&&(h||(h=setTimeout(function(){c.click(),h=null},f)),j(c,!0),l())}},p=function(a){if(b.hasEventId()){var c=m(a);c&&(h&&(clearTimeout(h),h=null),j(c,!1),k(),a.preventDefault())}},q=function(a){if(b.hasEventId()){l();var c=m(a);c&&(j(c,!1),a.preventDefault())}};return{init:function(c){g||(document.addEventListener("dragstart",n,!1),document.addEventListener("dragover",o,!1),document.addEventListener("dragleave",p,!1),document.addEventListener("drop",q,!1),document.addEventListener("dragend",l,!1),g=!0),i=a(c),b.hasEventId()&&k()}}}); \ No newline at end of file diff --git a/calendar/amd/build/month_view_drag_drop.min.js b/calendar/amd/build/month_view_drag_drop.min.js index b2ee13dec67..53812700409 100644 --- a/calendar/amd/build/month_view_drag_drop.min.js +++ b/calendar/amd/build/month_view_drag_drop.min.js @@ -1 +1 @@ -define(["jquery","core_calendar/events","core_calendar/drag_drop_data_store"],function(a,b,c){var d={ROOT:"[data-region='calendar']",DRAGGABLE:'[draggable="true"][data-region="event-item"]',DROP_ZONE:'[data-drop-zone="month-view-day"]',WEEK:'[data-region="month-view-week"]'},e="bg-primary text-white",f=!1,g=function(b){var c=a(b.target).closest(d.DROP_ZONE);return c.length?c:null},h=function(a,b,f){if("undefined"==typeof f&&(f=c.getDurationDays()),b?a.addClass(e):a.removeClass(e),f--,f>0){var g=a.next();if(!g.length){var i=a.closest(d.WEEK).next();i.length&&(g=i.children(d.DROP_ZONE).first())}g.length&&h(g,b,f)}},i=function(b){var e=a(b.target).closest(d.DRAGGABLE);if(e.length){e=e.find("[data-event-id]");var f=e.attr("data-event-id"),g=d.ROOT+' [data-event-id="'+f+'"]',h=a(g).length;c.setEventId(f),c.setDurationDays(h),b.dataTransfer.effectAllowed="move",b.dataTransfer.dropEffect="move",b.dataTransfer.setData("text/plain",f),b.dropEffect="move"}},j=function(a){a.preventDefault();var b=g(a);b&&h(b,!0)},k=function(a){var b=g(a);b&&(h(b,!1),a.preventDefault())},l=function(e){var f=g(e);if(!f)return void c.clearAll();var i=c.getEventId(),j=d.ROOT+' [data-event-id="'+i+'"]',k=a(j),l=null,m=a(e.target).closest(d.DROP_ZONE);k.length&&(l=k.closest(d.DROP_ZONE)),h(f,!1),a("body").trigger(b.moveEvent,[i,l,m]),c.clearAll(),e.preventDefault()};return{init:function(){f||(document.addEventListener("dragstart",i,!1),document.addEventListener("dragover",j,!1),document.addEventListener("dragleave",k,!1),document.addEventListener("drop",l,!1),f=!0)}}}); \ No newline at end of file +define(["jquery","core/notification","core/str","core_calendar/events","core_calendar/drag_drop_data_store"],function(a,b,c,d,e){var f={ROOT:"[data-region='calendar']",DRAGGABLE:'[draggable="true"][data-region="event-item"]',DROP_ZONE:'[data-drop-zone="month-view-day"]',WEEK:'[data-region="month-view-week"]'},g="bg-faded",h="bg-danger text-white",i="bg-primary text-white",j=g+" "+h+" "+i,k=!1,l=function(b){var c=a(b.target).closest(f.DROP_ZONE);return c.length?c:null},m=function(a){var b=a.attr("data-day-timestamp"),c=e.getMinTimestart(),d=e.getMaxTimestart();return!(c&&c>b)&&!(d&&db?e.getMinError():d&&d0){var k=a.next();if(!k.length){var l=a.closest(f.WEEK).next();l.length&&(k=l.children(f.DROP_ZONE).first())}k.length&&p(k,b,c)}},q=function(){a(f.ROOT).find(f.DROP_ZONE).each(function(b,c){c=a(c),m(c)||p(c,!1)})},r=function(b){var c=a(b.target),d=c.closest(f.DRAGGABLE);if(d.length){var g=d.find("[data-event-id]"),h=g.attr("data-event-id"),i=d.attr("data-min-day-timestamp"),j=d.attr("data-max-day-timestamp"),k=d.attr("data-min-day-error"),l=d.attr("data-max-day-error"),m=f.ROOT+' [data-event-id="'+h+'"]',n=a(m).length;e.setEventId(h),e.setDurationDays(n),i&&e.setMinTimestart(i),j&&e.setMaxTimestart(j),k&&e.setMinError(k),l&&e.setMaxError(l),b.dataTransfer.effectAllowed="move",b.dataTransfer.dropEffect="move",b.dataTransfer.setData("text/plain",h),b.dropEffect="move",q()}},s=function(a){if(e.hasEventId()){a.preventDefault();var b=l(a);b&&p(b,!0)}},t=function(a){if(e.hasEventId()){var b=l(a);b&&(p(b,!1),a.preventDefault())}},u=function(g){if(e.hasEventId()){var h=l(g);if(!h)return e.clearAll(),void o();if(m(h)){var i=e.getEventId(),j=f.ROOT+' [data-event-id="'+i+'"]',k=a(j),p=null;k.length&&(p=k.closest(f.DROP_ZONE)),a("body").trigger(d.moveEvent,[i,p,h])}else{var q=n(h);c.get_string("errorinvaliddate","calendar").then(function(a){b.exception({name:a,message:q||a})})}e.clearAll(),o(),g.preventDefault()}},v=function(){e.clearAll(),o()},w=function(){q()};return{init:function(){k||(document.addEventListener("dragstart",r,!1),document.addEventListener("dragover",s,!1),document.addEventListener("dragleave",t,!1),document.addEventListener("drop",u,!1),document.addEventListener("dragend",v,!1),a("body").on(d.monthChanged,w),k=!0)}}}); \ No newline at end of file diff --git a/calendar/amd/src/drag_drop_data_store.js b/calendar/amd/src/drag_drop_data_store.js index 6414f8ef88e..893d11abd65 100644 --- a/calendar/amd/src/drag_drop_data_store.js +++ b/calendar/amd/src/drag_drop_data_store.js @@ -30,6 +30,14 @@ define([], function() { var eventId = null; /* @var {int|null} durationDays How many days the event spans */ var durationDays = null; + /* @var {int|null} minTimestart The earliest valid timestart */ + var minTimestart = null; + /* @var {int|null} maxTimestart The latest valid tiemstart */ + var maxTimestart = null; + /* @var {string|null} minError Error message for min timestamp violation */ + var minError = null; + /* @var {string|null} maxError Error message for max timestamp violation */ + var maxError = null; /** * Store the id of the event being dragged. @@ -76,12 +84,108 @@ define([], function() { return durationDays; }; + /** + * Store the minimum timestart valid for an event being dragged. + * + * @param {int} timestamp The unix timstamp + */ + var setMinTimestart = function(timestamp) { + minTimestart = timestamp; + }; + + /** + * Get the minimum valid timestart. + * + * @return {int|null} + */ + var getMinTimestart = function() { + return minTimestart; + }; + + /** + * Check if a minimum timestamp is set. + * + * @return {bool} + */ + var hasMinTimestart = function() { + return minTimestart !== null; + }; + + /** + * Store the maximum timestart valid for an event being dragged. + * + * @param {int} timestamp The unix timstamp + */ + var setMaxTimestart = function(timestamp) { + maxTimestart = timestamp; + }; + + /** + * Get the maximum valid timestart. + * + * @return {int|null} + */ + var getMaxTimestart = function() { + return maxTimestart; + }; + + /** + * Check if a maximum timestamp is set. + * + * @return {bool} + */ + var hasMaxTimestart = function() { + return maxTimestart !== null; + }; + + /** + * Store the error string to display if trying to drag an event + * earlier than the minimum allowed date. + * + * @param {string} message The error message + */ + var setMinError = function(message) { + minError = message; + }; + + /** + * Get the error message for a minimum time start violation. + * + * @return {string|null} + */ + var getMinError = function() { + return minError; + }; + + /** + * Store the error string to display if trying to drag an event + * later than the maximum allowed date. + * + * @param {string} message The error message + */ + var setMaxError = function(message) { + maxError = message; + }; + + /** + * Get the error message for a maximum time start violation. + * + * @return {string|null} + */ + var getMaxError = function() { + return maxError; + }; + /** * Reset all of the stored values. */ var clearAll = function() { setEventId(null); setDurationDays(null); + setMinTimestart(null); + setMaxTimestart(null); + setMinError(null); + setMaxError(null); }; return { @@ -90,6 +194,16 @@ define([], function() { hasEventId: hasEventId, setDurationDays: setDurationDays, getDurationDays: getDurationDays, + setMinTimestart: setMinTimestart, + getMinTimestart: getMinTimestart, + hasMinTimestart: hasMinTimestart, + setMaxTimestart: setMaxTimestart, + getMaxTimestart: getMaxTimestart, + hasMaxTimestart: hasMaxTimestart, + setMinError: setMinError, + getMinError: getMinError, + setMaxError: setMaxError, + getMaxError: getMaxError, clearAll: clearAll }; }); diff --git a/calendar/amd/src/month_navigation_drag_drop.js b/calendar/amd/src/month_navigation_drag_drop.js index 0e192df437c..dca8f6178e0 100644 --- a/calendar/amd/src/month_navigation_drag_drop.js +++ b/calendar/amd/src/month_navigation_drag_drop.js @@ -119,6 +119,11 @@ define([ * @param {event} e The dragover event */ var dragoverHandler = function(e) { + // Ignore dragging of non calendar events. + if (!DataStore.hasEventId()) { + return; + } + e.preventDefault(); var target = getTargetFromEvent(e); @@ -153,6 +158,11 @@ define([ * @param {event} e The dragstart event */ var dragleaveHandler = function(e) { + // Ignore dragging of non calendar events. + if (!DataStore.hasEventId()) { + return; + } + var target = getTargetFromEvent(e); if (!target) { @@ -176,6 +186,11 @@ define([ * @param {event} e The drop event */ var dropHandler = function(e) { + // Ignore dragging of non calendar events. + if (!DataStore.hasEventId()) { + return; + } + removeDropZoneIndicator(); var target = getTargetFromEvent(e); diff --git a/calendar/amd/src/month_view_drag_drop.js b/calendar/amd/src/month_view_drag_drop.js index 05b37331215..b94af90d155 100644 --- a/calendar/amd/src/month_view_drag_drop.js +++ b/calendar/amd/src/month_view_drag_drop.js @@ -25,11 +25,15 @@ */ define([ 'jquery', + 'core/notification', + 'core/str', 'core_calendar/events', 'core_calendar/drag_drop_data_store' ], function( $, + Notification, + Str, CalendarEvents, DataStore ) { @@ -40,7 +44,10 @@ define([ DROP_ZONE: '[data-drop-zone="month-view-day"]', WEEK: '[data-region="month-view-week"]', }; - var HOVER_CLASS = 'bg-primary text-white'; + var INVALID_DROP_ZONE_CLASS = 'bg-faded'; + var INVALID_HOVER_CLASS = 'bg-danger text-white'; + var VALID_HOVER_CLASS = 'bg-primary text-white'; + var ALL_CLASSES = INVALID_DROP_ZONE_CLASS + ' ' + INVALID_HOVER_CLASS + ' ' + VALID_HOVER_CLASS; /* @var {bool} registered If the event listeners have been added */ var registered = false; @@ -56,10 +63,73 @@ define([ return (dropZone.length) ? dropZone : null; }; + /** + * Determine if the given dropzone element is within the acceptable + * time range. + * + * The drop zone timestamp is midnight on that day so we should check + * that the event's acceptable timestart value + * + * @param {object} dropZone The drop zone day from the calendar + * @return {bool} + */ + var isValidDropZone = function(dropZone) { + var dropTimestamp = dropZone.attr('data-day-timestamp'); + var minTimestart = DataStore.getMinTimestart(); + var maxTimestart = DataStore.getMaxTimestart(); + + if (minTimestart && minTimestart > dropTimestamp) { + return false; + } + + if (maxTimestart && maxTimestart < dropTimestamp) { + return false; + } + + return true; + }; + + /** + * Get the error string to display for a given drop zone element + * if it is invalid. + * + * @param {object} dropZone The drop zone day from the calendar + * @return {string} + */ + var getDropZoneError = function(dropZone) { + var dropTimestamp = dropZone.attr('data-day-timestamp'); + var minTimestart = DataStore.getMinTimestart(); + var maxTimestart = DataStore.getMaxTimestart(); + + if (minTimestart && minTimestart > dropTimestamp) { + return DataStore.getMinError(); + } + + if (maxTimestart && maxTimestart < dropTimestamp) { + return DataStore.getMaxError(); + } + + return null; + }; + + /** + * Remove all of the styling from each of the drop zones in the calendar. + */ + var clearAllDropZonesState = function() { + $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) { + dropZone = $(dropZone); + dropZone.removeClass(ALL_CLASSES); + }); + }; + /** * Update the hover state for the event in the calendar to reflect * which days the event will be moved to. * + * If the drop zone is not being hovered then it will apply some + * styling to reflect whether the drop zone is a valid or invalid + * drop place for the current dragging event. + * * This funciton supports events spanning multiple days and will * recurse to highlight (or remove highlight) each of the days * that the event will be moved to. @@ -79,10 +149,22 @@ define([ count = DataStore.getDurationDays(); } + var valid = isValidDropZone(dropZone); + dropZone.removeClass(ALL_CLASSES); + if (hovered) { - dropZone.addClass(HOVER_CLASS); + + if (valid) { + dropZone.addClass(VALID_HOVER_CLASS); + } else { + dropZone.addClass(INVALID_HOVER_CLASS); + } } else { - dropZone.removeClass(HOVER_CLASS); + dropZone.removeClass(VALID_HOVER_CLASS + ' ' + INVALID_HOVER_CLASS); + + if (!valid) { + dropZone.addClass(INVALID_DROP_ZONE_CLASS); + } } count--; @@ -110,6 +192,21 @@ define([ } }; + /** + * Find all of the calendar event drop zones in the calendar and update the display + * for the user to indicate which zones are valid and invalid. + */ + var updateAllDropZonesState = function() { + $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) { + dropZone = $(dropZone); + + if (!isValidDropZone(dropZone)) { + updateHoverState(dropZone, false); + } + }); + }; + + /** * Set up the module level variables to track which event is being * dragged and how many days it spans. @@ -117,27 +214,49 @@ define([ * @param {event} e The dragstart event */ var dragstartHandler = function(e) { - var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE); + var target = $(e.target); + var draggableElement = target.closest(SELECTORS.DRAGGABLE); - if (!eventElement.length) { + if (!draggableElement.length) { return; } - eventElement = eventElement.find('[data-event-id]'); - + var eventElement = draggableElement.find('[data-event-id]'); var eventId = eventElement.attr('data-event-id'); + var minTimestart = draggableElement.attr('data-min-day-timestamp'); + var maxTimestart = draggableElement.attr('data-max-day-timestamp'); + var minError = draggableElement.attr('data-min-day-error'); + var maxError = draggableElement.attr('data-max-day-error'); var eventsSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]'; var duration = $(eventsSelector).length; DataStore.setEventId(eventId); DataStore.setDurationDays(duration); + if (minTimestart) { + DataStore.setMinTimestart(minTimestart); + } + + if (maxTimestart) { + DataStore.setMaxTimestart(maxTimestart); + } + + if (minError) { + DataStore.setMinError(minError); + } + + if (maxError) { + DataStore.setMaxError(maxError); + } + e.dataTransfer.effectAllowed = "move"; e.dataTransfer.dropEffect = "move"; // Firefox requires a value to be set here or the drag won't // work and the dragover handler won't fire. e.dataTransfer.setData('text/plain', eventId); e.dropEffect = "move"; + + updateAllDropZonesState(); }; /** @@ -150,6 +269,11 @@ define([ * @param {event} e The dragstart event */ var dragoverHandler = function(e) { + // Ignore dragging of non calendar events. + if (!DataStore.hasEventId()) { + return; + } + e.preventDefault(); var dropZone = getDropZoneFromEvent(e); @@ -171,6 +295,11 @@ define([ * @param {event} e The dragstart event */ var dragleaveHandler = function(e) { + // Ignore dragging of non calendar events. + if (!DataStore.hasEventId()) { + return; + } + var dropZone = getDropZoneFromEvent(e); if (!dropZone) { @@ -193,30 +322,66 @@ define([ * @param {event} e The dragstart event */ var dropHandler = function(e) { + // Ignore dragging of non calendar events. + if (!DataStore.hasEventId()) { + return; + } + var dropZone = getDropZoneFromEvent(e); if (!dropZone) { DataStore.clearAll(); + clearAllDropZonesState(); return; } - var eventId = DataStore.getEventId(); - var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]'; - var eventElement = $(eventElementSelector); - var origin = null; - var destination = $(e.target).closest(SELECTORS.DROP_ZONE); + if (isValidDropZone(dropZone)) { + var eventId = DataStore.getEventId(); + var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]'; + var eventElement = $(eventElementSelector); + var origin = null; - if (eventElement.length) { - origin = eventElement.closest(SELECTORS.DROP_ZONE); + if (eventElement.length) { + origin = eventElement.closest(SELECTORS.DROP_ZONE); + } + + $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, dropZone]); + } else { + // If the drop zone is not valid then there is not need for us to + // try to process it. Instead we can just show an error to the user. + var message = getDropZoneError(dropZone); + Str.get_string('errorinvaliddate', 'calendar').then(function(string) { + Notification.exception({ + name: string, + message: message || string + }); + }); } - updateHoverState(dropZone, false); - $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, destination]); DataStore.clearAll(); + clearAllDropZonesState(); e.preventDefault(); }; + /** + * Clear the data store and remove the drag indicators from the UI + * when the drag event has finished. + */ + var dragendHandler = function() { + DataStore.clearAll(); + clearAllDropZonesState(); + }; + + /** + * Re-render the drop zones in the new month to highlight + * which areas are or aren't acceptable to drop the calendar + * event. + */ + var calendarMonthChangedHandler = function() { + updateAllDropZonesState(); + }; + return { /** * Initialise the event handlers for the drag events. @@ -231,6 +396,8 @@ define([ document.addEventListener('dragover', dragoverHandler, false); document.addEventListener('dragleave', dragleaveHandler, false); document.addEventListener('drop', dropHandler, false); + document.addEventListener('dragend', dragendHandler, false); + $('body').on(CalendarEvents.monthChanged, calendarMonthChangedHandler); registered = true; } }, diff --git a/calendar/classes/external/calendar_event_exporter.php b/calendar/classes/external/calendar_event_exporter.php index 423cd92cc75..051d04957e8 100644 --- a/calendar/classes/external/calendar_event_exporter.php +++ b/calendar/classes/external/calendar_event_exporter.php @@ -26,6 +26,7 @@ namespace core_calendar\external; defined('MOODLE_INTERNAL') || die(); +use \core_calendar\local\event\container; use \core_course\external\course_summary_exporter; use \renderer_base; require_once($CFG->dirroot . '/course/lib.php'); @@ -57,6 +58,22 @@ class calendar_event_exporter extends event_exporter_base { $values['popupname'] = [ 'type' => PARAM_RAW, ]; + $values['mindaytimestamp'] = [ + 'type' => PARAM_INT, + 'optional' => true + ]; + $values['mindayerror'] = [ + 'type' => PARAM_TEXT, + 'optional' => true + ]; + $values['maxdaytimestamp'] = [ + 'type' => PARAM_INT, + 'optional' => true + ]; + $values['maxdayerror'] = [ + 'type' => PARAM_TEXT, + 'optional' => true + ]; return $values; } @@ -89,9 +106,9 @@ class calendar_event_exporter extends event_exporter_base { } else { // TODO MDL-58866 We do not have any way to find urls for events outside of course modules. $course = $event->get_course()->get('id') ?: SITEID; - $url = course_get_url($course); } + $values['url'] = $url->out(false); $values['islastday'] = false; $today = $this->related['type']->timestamp_to_date_array($this->related['today']); @@ -153,6 +170,10 @@ class calendar_event_exporter extends event_exporter_base { $values['calendareventtype'] = $this->get_calendar_event_type(); + if ($event->get_course_module()) { + $values = array_merge($values, $this->get_module_timestamp_limits($event)); + } + return $values; } @@ -184,4 +205,111 @@ class calendar_event_exporter extends event_exporter_base { return $type; } + + /** + * Return the set of minimum and maximum date timestamp values + * for the given event. + * + * @param event_interface $event + * @return array + */ + protected function get_module_timestamp_limits($event) { + $values = []; + $mapper = container::get_event_mapper(); + $starttime = $event->get_times()->get_start_time(); + + list($min, $max) = component_callback( + 'mod_' . $event->get_course_module()->get('modname'), + 'core_calendar_get_valid_event_timestart_range', + [$mapper->from_event_to_legacy_event($event)], + [null, null] + ); + + if ($min) { + $values = array_merge($values, $this->get_module_timestamp_min_limit($starttime, $min)); + } + + if ($max) { + $values = array_merge($values, $this->get_module_timestamp_max_limit($starttime, $max)); + } + + return $values; + } + + /** + * Get the correct minimum midnight day limit based on the event start time + * and the module's minimum timestamp limit. + * + * @param DateTimeInterface $starttime The event start time + * @param array $min The module's minimum limit for the event + */ + protected function get_module_timestamp_min_limit(\DateTimeInterface $starttime, $min) { + // We need to check that the minimum valid time is earlier in the + // day than the current event time so that if the user drags and drops + // the event to this day (which changes the date but not the time) it + // will result in a valid time start for the event. + // + // For example: + // An event that starts on 2017-01-10 08:00 with a minimum cutoff + // of 2017-01-05 09:00 means that 2017-01-05 is not a valid start day + // for the drag and drop because it would result in the event start time + // being set to 2017-01-05 08:00, which is invalid. Instead the minimum + // valid start day would be 2017-01-06. + $values = []; + $timestamp = $min[0]; + $errorstring = $min[1]; + $mindate = (new \DateTimeImmutable())->setTimestamp($timestamp); + $minstart = $mindate->setTime( + $starttime->format('H'), + $starttime->format('i'), + $starttime->format('s') + ); + $midnight = usergetmidnight($timestamp); + + if ($mindate <= $minstart) { + $values['mindaytimestamp'] = $midnight; + } else { + $tomorrow = (new \DateTime())->setTimestamp($midnight)->modify('+1 day'); + $values['mindaytimestamp'] = $tomorrow->getTimestamp(); + } + + // Get the human readable error message to display if the min day + // timestamp is violated. + $values['mindayerror'] = $errorstring; + return $values; + } + + /** + * Get the correct maximum midnight day limit based on the event start time + * and the module's maximum timestamp limit. + * + * @param DateTimeInterface $starttime The event start time + * @param array $max The module's maximum limit for the event + */ + protected function get_module_timestamp_max_limit(\DateTimeInterface $starttime, $max) { + // We're doing a similar calculation here as we are for the minimum + // day timestamp. See the explanation above. + $values; + $timestamp = $max[0]; + $errorstring = $max[1]; + $maxdate = (new \DateTimeImmutable())->setTimestamp($timestamp); + $maxstart = $maxdate->setTime( + $starttime->format('H'), + $starttime->format('i'), + $starttime->format('s') + ); + $midnight = usergetmidnight($timestamp); + + if ($maxdate >= $maxstart) { + $values['maxdaytimestamp'] = $midnight; + } else { + $yesterday = (new \DateTime())->setTimestamp($midnight)->modify('-1 day'); + $values['maxdaytimestamp'] = $yesterday->getTimestamp(); + } + + // Get the human readable error message to display if the max day + // timestamp is violated. + $values['maxdayerror'] = $errorstring; + return $values; + } } diff --git a/calendar/templates/month_detailed.mustache b/calendar/templates/month_detailed.mustache index 942e459120a..8f8fca5f29b 100644 --- a/calendar/templates/month_detailed.mustache +++ b/calendar/templates/month_detailed.mustache @@ -91,6 +91,18 @@ {{#canedit}} draggable="true" data-drag-type="move" + {{#mindaytimestamp}} + data-min-day-timestamp="{{.}}" + {{/mindaytimestamp}} + {{#mindayerror}} + data-min-day-error="{{.}}" + {{/mindayerror}} + {{#maxdaytimestamp}} + data-max-day-timestamp="{{.}}" + {{/maxdaytimestamp}} + {{#maxdayerror}} + data-max-day-error="{{.}}" + {{/maxdayerror}} {{/canedit}}> {{name}} diff --git a/calendar/tests/calendar_event_exporter_test.php b/calendar/tests/calendar_event_exporter_test.php new file mode 100644 index 00000000000..5ae3edbfd7b --- /dev/null +++ b/calendar/tests/calendar_event_exporter_test.php @@ -0,0 +1,150 @@ +. + +/** + * Calendar event exporter tests tests. + * + * @package core_calendar + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +use core_calendar\external\calendar_event_exporter; +use core_calendar\local\event\container; + +/** + * Calendar event exporter testcase. + * + * @copyright 2017 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_calendar_event_exporter_testcase extends advanced_testcase { + /** + * Data provider for the module timestamp min limit test case to confirm + * that the minimum time limit is set correctly on the boundary cases. + */ + public function get_module_timestamp_min_limit_test_cases() { + $now = time(); + $todaymidnight = usergetmidnight($now); + $tomorrowmidnight = $todaymidnight + DAYSECS; + $eightam = $todaymidnight + (60 * 60 * 8); + $starttime = (new DateTime())->setTimestamp($eightam); + + return [ + 'before min' => [ + $starttime, + [ + ($starttime->getTimestamp() + 1), + 'some error' + ], + $tomorrowmidnight + ], + 'equal min' => [ + $starttime, + [ + $starttime->getTimestamp(), + 'some error' + ], + $todaymidnight + ], + 'after min' => [ + $starttime, + [ + ($starttime->getTimestamp() - 1), + 'some error' + ], + $todaymidnight + ] + ]; + } + + /** + * @dataProvider get_module_timestamp_min_limit_test_cases() + */ + public function test_get_module_timestamp_min_limit($starttime, $min, $expected) { + $class = \core_calendar\external\calendar_event_exporter::class; + $mock = $this->getMockBuilder($class) + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + $reflector = new ReflectionClass($class); + $method = $reflector->getMethod('get_module_timestamp_min_limit'); + $method->setAccessible(true); + + $result = $method->invoke($mock, $starttime, $min); + $this->assertEquals($expected, $result['mindaytimestamp']); + $this->assertEquals($min[1], $result['mindayerror']); + } + + /** + * Data provider for the module timestamp min limit test case to confirm + * that the minimum time limit is set correctly on the boundary cases. + */ + public function get_module_timestamp_max_limit_test_cases() { + $now = time(); + $todaymidnight = usergetmidnight($now); + $yesterdaymidnight = $todaymidnight - DAYSECS; + $eightam = $todaymidnight + (60 * 60 * 8); + $starttime = (new DateTime())->setTimestamp($eightam); + + return [ + 'before max' => [ + $starttime, + [ + ($starttime->getTimestamp() + 1), + 'some error' + ], + $todaymidnight + ], + 'equal max' => [ + $starttime, + [ + $starttime->getTimestamp(), + 'some error' + ], + $todaymidnight + ], + 'after max' => [ + $starttime, + [ + ($starttime->getTimestamp() - 1), + 'some error' + ], + $yesterdaymidnight + ] + ]; + } + + /** + * @dataProvider get_module_timestamp_max_limit_test_cases() + */ + public function test_get_module_timestamp_max_limit($starttime, $max, $expected) { + $class = \core_calendar\external\calendar_event_exporter::class; + $mock = $this->getMockBuilder($class) + ->disableOriginalConstructor() + ->setMethods(null) + ->getMock(); + $reflector = new ReflectionClass($class); + $method = $reflector->getMethod('get_module_timestamp_max_limit'); + $method->setAccessible(true); + + $result = $method->invoke($mock, $starttime, $max); + $this->assertEquals($expected, $result['maxdaytimestamp']); + $this->assertEquals($max[1], $result['maxdayerror']); + } +} diff --git a/theme/bootstrapbase/less/moodle/bs4-compat.less b/theme/bootstrapbase/less/moodle/bs4-compat.less index ccc168ee750..8d4842e0695 100644 --- a/theme/bootstrapbase/less/moodle/bs4-compat.less +++ b/theme/bootstrapbase/less/moodle/bs4-compat.less @@ -297,3 +297,7 @@ background-color: darken(@blue, 10%); } } + +.bg-faded { + background-color: @grayLighter; +} diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index e178774d2da..a1aad5ce136 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -21921,3 +21921,6 @@ ul.indented-list { .bg-primary[href] { background-color: #0378a9; } +.bg-faded { + background-color: #eee; +}