From 9d3a665d9cf7fd3e658dae457a0ed2a4dbed5d82 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 28 Jan 2015 17:46:03 +0100 Subject: [PATCH] Modify calendar UI to properly handle updates on recurring events with attandees (#4318) Since the Kolab stack doesn't yet fully support invitations for recurring events, the calendar client prevents the user from modifying single recurrence instances if attandees are involved: options to update the "current" or "future" items are disabled and deleting a single event will update the main event and notify all attendees. --- plugins/calendar/calendar.php | 31 ++++++++-- plugins/calendar/calendar_ui.js | 58 ++++++++++++++----- .../calendar/drivers/kolab/kolab_driver.php | 13 +++-- plugins/calendar/localization/en_US.inc | 1 + plugins/calendar/skins/larry/calendar.css | 1 + 5 files changed, 80 insertions(+), 24 deletions(-) diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index db61d474..59008dbe 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -842,7 +842,7 @@ class calendar extends rcube_plugin $success = $reload = $got_msg = false; // don't notify if modifying a recurring instance (really?) - if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify']) + if ($event['_savemode'] && in_array($event['_savemode'], array('current','future')) && $event['_notify'] && $action != 'remove') unset($event['_notify']); // force notify if hidden + active else if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1) @@ -866,20 +866,35 @@ class calendar extends rcube_plugin case "edit": $this->write_preprocess($event, $action); - if ($success = $this->driver->edit_event($event)) - $this->cleanup_event($event); + if ($success = $this->driver->edit_event($event)) { + $this->cleanup_event($event); + if ($success !== true) { + $event['id'] = $success; + $old = null; + } + } $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1; break; case "resize": $this->write_preprocess($event, $action); - $success = $this->driver->resize_event($event); + if ($success = $this->driver->resize_event($event)) { + if ($success !== true) { + $event['id'] = $success; + $old = null; + } + } $reload = $event['_savemode'] ? 2 : 1; break; case "move": $this->write_preprocess($event, $action); - $success = $this->driver->move_event($event); + if ($success = $this->driver->move_event($event)) { + if ($success !== true) { + $event['id'] = $success; + $old = null; + } + } $reload = $success && $event['_savemode'] ? 2 : 1; break; @@ -1110,6 +1125,12 @@ class calendar extends rcube_plugin // make sure we have the complete record $event = $action == 'remove' ? $old : $this->driver->get_event($event); + // sending notification on a recurrence instance -> re-send the main event + if ($event['recurrence_id']) { + $event = $this->driver->get_event(array('id' => $event['recurrence_id'], 'cal' => $event['calendar'])); + $action = 'edit'; + } + // only notify if data really changed (TODO: do diff check on client already) if (!$old || $action == 'remove' || self::event_diff($event, $old)) { $sent = $this->notify_attendees($event, $old, $action, $event['_comment']); diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 8eb51c69..598a4d7a 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -742,9 +742,11 @@ function rcube_calendar_ui(settings) // show warning if editing a recurring event if (event.id && event.recurrence) { - var sel = event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'); + var allow_exceptions = !has_attendees(event) || !is_organizer(event), + sel = event._savemode || (allow_exceptions && event.thisandfuture ? 'future' : (allow_exceptions && event.isexception ? 'current' : 'all')); $('#edit-recurring-warning').show(); $('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true); + $('input.edit-recurring-savemode[value="current"], input.edit-recurring-savemode[value="future"]').prop('disabled', !allow_exceptions); } else $('#edit-recurring-warning').hide(); @@ -764,6 +766,11 @@ function rcube_calendar_ui(settings) if (event.attendees) { for (j=0; j < event.attendees.length; j++) { data = event.attendees[j]; + // reset attendee status + if (event._savemode == 'new' && data.role != 'ORGANIZER') { + data.status = 'NEEDS-ACTION'; + delete data.noreply; + } add_attendee(data, !allow_invitations); if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply) reply_selected++; @@ -2519,12 +2526,13 @@ function rcube_calendar_ui(settings) var update_event_confirm = function(action, event, data) { if (!data) data = event; - var decline = false, notify = false, html = '', cal = me.calendars[event.calendar]; + var decline = false, notify = false, html = '', cal = me.calendars[event.calendar], + _has_attendees = has_attendees(event), _is_organizer = is_organizer(event); // event has attendees, ask whether to notify them - if (has_attendees(event)) { + if (_has_attendees) { var checked = (settings.itip_notify & 1 ? ' checked="checked"' : ''); - if (is_organizer(event)) { + if (_is_organizer) { notify = true; if (settings.itip_notify & 2) { html += '
' + @@ -2550,11 +2558,25 @@ function rcube_calendar_ui(settings) // recurring event: user needs to select the savemode if (event.recurrence) { + var disabled_state = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'); + + if (_has_attendees) { + if (action == 'remove') { + if (!_is_organizer) { + message_label = 'removerecurringallonly'; + disabled_state = ' disabled'; + } + } + else if (is_organizer(event)) { + disabled_state = ' disabled'; + } + } + html += '
' + - rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '
' + + rcmail.gettext(message_label, 'calendar') + '
' + '
' + - '' + rcmail.gettext('currentevent', 'calendar') + '' + - '' + rcmail.gettext('futurevents', 'calendar') + '' + + '' + rcmail.gettext('currentevent', 'calendar') + '' + + '' + rcmail.gettext('futurevents', 'calendar') + '' + '' + rcmail.gettext('allevents', 'calendar') + '' + (action != 'remove' ? '' + rcmail.gettext('saveasnew', 'calendar') + '' : '') + '
'; @@ -2564,14 +2586,24 @@ function rcube_calendar_ui(settings) if (html) { var $dialog = $('
').html(html); - $dialog.find('a.button').button().click(function(e) { + $dialog.find('a.button').button().filter(':not(.disabled)').click(function(e) { data._savemode = String(this.href).replace(/.+#/, ''); data._notify = settings.itip_notify; - if ($dialog.find('input.confirm-attendees-donotify').length) - data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0; - if (decline && $dialog.find('input.confirm-attendees-decline:checked').length) - data.decline = 1; - update_event(action, data); + + // open event edit dialog when saving as new + if (data._savemode == 'new') { + event._savemode = 'new'; + event_edit_dialog('edit', event); + fc.fullCalendar('refetchEvents'); + } + else { + if ($dialog.find('input.confirm-attendees-donotify').length) + data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0; + if (decline && $dialog.find('input.confirm-attendees-decline:checked').length) + data.decline = 1; + update_event(action, data); + } + $dialog.dialog("close"); return false; }); diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 04e96dc4..8a484055 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -889,15 +889,16 @@ class kolab_driver extends calendar_driver // save submitted data as new (non-recurring) event $event['recurrence'] = array(); $event['uid'] = $this->cal->generate_uid(); - unset($event['recurrence_id'], $event['id']); + unset($event['recurrence_id'], $event['id'], $event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']); // copy attachment data to new event foreach ((array)$event['attachments'] as $idx => $attachment) { - if (!$attachment['data']) - $attachment['data'] = $fromcalendar->get_attachment_body($attachment['id'], $event); + if (!$attachment['content']) + $event['attachments'][$idx]['content'] = $this->get_attachment_body($attachment['id'], $master); } - - $success = $storage->insert_event($event); + + if ($success = $storage->insert_event($event)) + $success = $event['uid']; break; case 'future': @@ -907,7 +908,7 @@ class kolab_driver extends calendar_driver $event['thisandfuture'] = $savemode == 'future'; // remove some internal properties which should not be saved - unset($event['_savemode'], $event['_fromcalendar'], $event['_identity']); + unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_notify']); // save properties to a recurrence exception instance if ($old['recurrence_id']) { diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index ebea976a..85e7e7c2 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -261,6 +261,7 @@ $labels['changeeventconfirm'] = 'Change event'; $labels['removeeventconfirm'] = 'Delete event'; $labels['changerecurringeventwarning'] = 'This is a recurring event. Would you like to edit the current event only, this and all future occurences, all occurences or save it as a new event?'; $labels['removerecurringeventwarning'] = 'This is a recurring event. Would you like to delete the current event only, this and all future occurences or all occurences of this event?'; +$labels['removerecurringallonly'] = 'This is a recurring event. As a participant, you can only delete the entire event with all occurences.'; $labels['currentevent'] = 'Current'; $labels['futurevents'] = 'Future'; $labels['allevents'] = 'All'; diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index a239f2bc..50f8b645 100644 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -1056,6 +1056,7 @@ td.topalign { .event-update-confirm a.button { margin: 0 0.5em 0 0.2em; min-width: 5em; + text-align: center; } #event-rsvp,