From 4c21f13eafc89df8b8612abc3e7d60d183fcf010 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 1 Jun 2011 18:35:10 +0200 Subject: [PATCH] Handle editing/moving/resizing/deleting recurring events in UI and database backend --- plugins/calendar/calendar.js | 122 ++++++-- plugins/calendar/calendar.php | 25 +- plugins/calendar/config.inc.php.dist | 16 +- plugins/calendar/drivers/calendar_driver.php | 1 + .../drivers/database/database_driver.php | 282 ++++++++++++------ plugins/calendar/lib/calendar_ui.php | 16 + plugins/calendar/localization/en_US.inc | 10 + plugins/calendar/skins/default/calendar.css | 32 ++ .../skins/default/templates/calendar.html | 2 + 9 files changed, 378 insertions(+), 128 deletions(-) diff --git a/plugins/calendar/calendar.js b/plugins/calendar/calendar.js index 7b1070a2..8324ea32 100644 --- a/plugins/calendar/calendar.js +++ b/plugins/calendar/calendar.js @@ -52,7 +52,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { var ignore_click = false; // create a nice human-readable string for the date/time range - var event_date_text = function(event) { + var event_date_text = function(event) + { var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000; if (event.allDay) fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + (duration > 86400 ? ' — ' + $.fullCalendar.formatDate(event.end, settings['date_format']) : ''); @@ -67,7 +68,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { }; // event details dialog (show only) - var event_show_dialog = function(event) { + var event_show_dialog = function(event) + { var $dialog = $("#eventshow"); var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false }; @@ -142,7 +144,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { }; // bring up the event dialog (jquery-ui popup) - var event_edit_dialog = function(action, event) { + var event_edit_dialog = function(action, event) + { // close show dialog first $("#eventshow").dialog('close'); @@ -225,16 +228,10 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { var wdays = event.recurrence.BYDAY.split(','); $('input.edit-recurrence-weekly-byday').val(wdays); } - else if (event.start) { - $('input.edit-recurrence-weekly-byday').val([weekdays[event.start.getDay()]]); - } if (event.recurrence && event.recurrence.BYMONTHDAY) { $('input.edit-recurrence-monthly-bymonthday').val(String(event.recurrence.BYMONTHDAY).split(',')); $('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']); } - else if (event.start) { - $('input.edit-recurrence-monthly-bymonthday').val([event.start.getDate()]); - } if (event.recurrence && event.recurrence.BYDAY && (event.recurrence.FREQ == 'MONTHLY' || event.recurrence.FREQ == 'YEARLY')) { var byday, section = event.recurrence.FREQ.toLowerCase(); if ((byday = String(event.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) { @@ -253,6 +250,14 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { $('input.edit-recurrence-yearly-bymonth').val([String(event.start.getMonth()+1)]); } + // show warning if editing a recurring event + if (event.id && event.recurrence) { + $('#edit-recurring-warning').show(); + $('input.edit-recurring-savemode[value="all"]').prop('checked', true); + } + else + $('#edit-recurring-warning').hide(); + // buttons var buttons = {}; @@ -260,6 +265,12 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { var start = me.parse_datetime(starttime.val(), startdate.val()); var end = me.parse_datetime(endtime.val(), enddate.val()); + // basic input validatetion + if (start.getTime() > end.getTime()) { + alert(rcmail.gettext('invalideventdates', 'calendar')); + return false; + } + // post data to server var data = { start: start.getTime()/1000, @@ -304,13 +315,15 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { if (freq == 'WEEKLY') { var byday = []; $('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); }); - data.recurrence.BYDAY = byday.join(','); + if (byday.length) + data.recurrence.BYDAY = byday.join(','); } else if (freq == 'MONTHLY') { var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = []; if (mode == 'BYMONTHDAY') { - $('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); }); - data.recurrence.BYMONTHDAY = bymonday.join(','); + $('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); }); + if (bymonday.length) + data.recurrence.BYMONTHDAY = bymonday.join(','); } else data.recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val(); @@ -318,14 +331,18 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { else if (freq == 'YEARLY') { var byday, bymonth = []; $('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); }); - data.recurrence.BYMONTH = bymonth.join(','); + if (bymonth.length) + data.recurrence.BYMONTH = bymonth.join(','); if ((byday = $('#edit-recurrence-yearly-byday').val())) data.recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday; } } - if (event.id) + if (event.id) { data.id = event.id; + if (event.recurrence) + data.savemode = $('input.edit-recurring-savemode:checked').val(); + } else data.calendar = calendars.val(); @@ -368,7 +385,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { }; // mouse-click handler to check if the show dialog is still open and prevent default action - var dialog_check = function(e) { + var dialog_check = function(e) + { var showd = $("#eventshow"); if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) { showd.dialog('close'); @@ -382,6 +400,47 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { } return true; }; + + // display confirm dialog when modifying/deleting a recurring event where the user needs to select the savemode + var recurring_edit_confirm = function(event, action) { + var $dialog = $('
').addClass('edit-recurring-warning'); + $dialog.html('
' + + rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '
' + + ''); + + $dialog.find('a.button').button().click(function(e){ + event.savemode = String(this.href).replace(/.+#/, ''); + rcmail.http_post('plugin.event', { action:action, e:event }); + $dialog.dialog("destroy").hide(); + return false; + }); + + $dialog.dialog({ + modal: true, + width: 420, + dialogClass: 'warning', + title: rcmail.gettext((action == 'remove' ? 'removerecurringevent' : 'changerecurringevent'), 'calendar'), + buttons: [ + { + text: rcmail.gettext('cancel', 'calendar'), + click: function() { + $(this).dialog("close"); + } + } + ], + close: function(){ + $dialog.dialog("destroy").hide(); + $('#calendar').fullCalendar('refetchEvents'); + } + }).show(); + + return true; + }; // general datepicker settings this.datepicker_settings = { @@ -423,6 +482,10 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { // delete the given event after showing a confirmation dialog this.delete_event = function(event) { + // show extended confirm dialog for recurring events, use jquery UI dialog + if (event.recurrence) + return recurring_edit_confirm({ id:event.id }, 'remove'); + // send remove request to plugin if (confirm(rcmail.gettext('deleteventconfirm', 'calendar'))) { rcmail.http_post('plugin.event', { action:'remove', e:{ id:event.id } }); @@ -475,7 +538,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { resizable: true, closeOnEscape: false, dialogClass: 'alarm', - title: rcmail.gettext('alarmtitle', 'calendar'), + title: '' + rcmail.gettext('alarmtitle', 'calendar'), buttons: buttons, close: function() { $('#alarm-snooze-dropdown').hide(); @@ -564,7 +627,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { }).data('id', id); } - if (!cal.readonly) { + if (!cal.readonly && !this.selected_calendar) { this.selected_calendar = id; rcmail.enable_command('plugin.addevent', true); } @@ -594,6 +657,11 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { week: 'ddd ' + settings['date_short'], // Mon 9/7 day: 'dddd ' + settings['date_short'] // Monday 9/7 }, + titleFormat: { + month: 'MMMM yyyy', + week: settings['date_long'].replace(/ yyyy/, '[ yyyy]') + "{ '—' " + settings['date_long'] + "}", + day: 'dddd ' + settings['date_long'] + }, defaultView: settings['default_view'], allDayText: rcmail.gettext('all-day', 'calendar'), buttonText: { @@ -658,17 +726,23 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { end: event.end.getTime()/1000, allday: allDay?1:0 }; - rcmail.http_post('plugin.event', { action:'move', e:data }); + if (event.recurrence) + recurring_edit_confirm(data, 'move'); + else + rcmail.http_post('plugin.event', { action:'move', e:data }); }, // callback for event resizing eventResize : function(event, delta) { // send resize request to server var data = { - id: event.id, + id: event.id, start: event.start.getTime()/1000, - end: event.end.getTime()/1000, + end: event.end.getTime()/1000, }; - rcmail.http_post('plugin.event', { action:'resize', e:data }); + if (event.recurrence) + recurring_edit_confirm(data, 'resize'); + else + rcmail.http_post('plugin.event', { action:'resize', e:data }); } }); @@ -843,10 +917,4 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { }); $('#edit-recurrence-enddate').datepicker(cal.datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) }); - // avoid unselecting all weekdays, monthdays and months - $('input.edit-recurrence-weekly-byday, input.edit-recurrence-monthly-bymonthday, input.edit-recurrence-yearly-bymonth').click(function(){ - if (!$('input.'+this.className+':checked').length) - this.checked = true; - }); - }); diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 037e89e9..c973d171 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -135,6 +135,7 @@ class calendar extends rcube_plugin $this->register_handler('plugin.alarm_select', array($this->ui, 'alarm_select')); $this->register_handler('plugin.snooze_select', array($this->ui, 'snooze_select')); $this->register_handler('plugin.recurrence_form', array($this->ui, 'recurrence_form')); + $this->register_handler('plugin.edit_recurring_warning', array($this->ui, 'recurring_event_warning')); $this->rc->output->set_env('calendar_settings', $this->load_settings()); $this->rc->output->add_label('low','normal','high'); @@ -344,7 +345,7 @@ class calendar extends rcube_plugin switch ($action) { case "new": // create UID for new event - $events['uid'] = strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); + $event['uid'] = $this->generate_uid(); $success = $this->driver->new_event($event); $reload = true; break; @@ -370,12 +371,13 @@ class calendar extends rcube_plugin break; } - if (!$success) { + if ($success) + $this->rc->output->show_message('successfullysaved', 'confirmation'); + else $this->rc->output->show_message('calendar.errorsaving', 'error'); - } - else if ($reload) { + + if ($success && $reload) $this->rc->output->command('plugin.reload_calendar', array()); - } } /** @@ -433,6 +435,7 @@ class calendar extends rcube_plugin $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', "agendaWeek"); $settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', "yyyy/MM/dd"); $settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', "M/d"); + $settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', "M d yyyy"); $settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', "HH:mm"); $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', 2); $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', 1); @@ -516,10 +519,6 @@ class calendar extends rcube_plugin if ($event['recurrence']) $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']); - // TEMPORARY: recurring instances are immutable - if ($event['recurrence_id']) - $event['editable'] = false; - $json[] = array( 'start' => date('c', $event['start']), // ISO 8601 date (added in PHP 5) 'end' => date('c', $event['end']), // ISO 8601 date (added in PHP 5) @@ -619,6 +618,14 @@ class calendar extends rcube_plugin return rtrim($freq . $details . ', ' . $until); } + /** + * Generate a unique identifier for an event + */ + public function generate_uid() + { + return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); + } + /** * Helper function to convert alarm trigger strings * into two-field values (e.g. "-45M" => 45, "-M") diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist index 4b8cb0b1..8abc924a 100644 --- a/plugins/calendar/config.inc.php.dist +++ b/plugins/calendar/config.inc.php.dist @@ -2,7 +2,7 @@ /* +-------------------------------------------------------------------------+ | Configuration for the Calendar plugin | - | Version 0.3 alpha | + | Version 0.3 beta | | | | This program is free software; you can redistribute it and/or modify | | it under the terms of the GNU General Public License version 2 | @@ -37,6 +37,9 @@ $rcmail_config['calendar_time_format'] = "HH:mm"; // short date format (used for column titles) $rcmail_config['calendar_date_short'] = 'M-d'; +// long date format (used for calendar title) +$rcmail_config['calendar_date_long'] = 'MMM d yyyy'; + // timeslots per hour (1, 2, 3, 4, 6) $rcmail_config['calendar_timeslots'] = 2; @@ -47,8 +50,11 @@ $rcmail_config['calendar_first_day'] = 1; $rcmail_config['calendar_first_hour'] = 6; // event categories -$rcmail_config['calendar_categories'] = array('Personal' => 'c0c0c0', - 'Work' => 'ff0000', - 'Family' => '00ff00', - 'Holiday' => 'ff6600'); +$rcmail_config['calendar_categories'] = array( + 'Personal' => 'c0c0c0', + 'Work' => 'ff0000', + 'Family' => '00ff00', + 'Holiday' => 'ff6600', +); + ?> \ No newline at end of file diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index 044a361b..be50e300 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -50,6 +50,7 @@ * 'priority' => 1|0|2, // Event priority (0=low, 1=normal, 2=high) * 'sensitivity' => 0|1|2, // Event sensitivity (0=public, 1=private, 2=confidential) * 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event) + * 'savemode' => 'all|future|current|new', // How changes on recurring event should be handled * ); */ diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index 0378b35a..e86da7d2 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -152,7 +152,7 @@ class database_driver extends calendar_driver ), $event['calendar'], strval($event['uid']), - intval($event['allday']), + intval($event['all_day']), $event['recurrence'], strval($event['title']), strval($event['description']), @@ -178,37 +178,88 @@ class database_driver extends calendar_driver * Update an event entry with the given data * * @param array Hash array with event properties - * @see Driver:new_event() + * @see Driver:edit_event() */ public function edit_event($event) { if (!empty($this->calendars)) { - $event = $this->_save_preprocess($event); - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_events . " - SET changed=%s, start=%s, end=%s, all_day=?, recurrence=?, title=?, description=?, location=?, categories=?, free_busy=?, priority=?, sensitivity=?, alarms=?, notifyat=? - WHERE event_id=? - AND calendar_id IN (" . $this->calendar_ids . ")", - $this->rc->db->now(), - $this->rc->db->fromunixtime($event['start']), - $this->rc->db->fromunixtime($event['end']) - ), - intval($event['allday']), - $event['recurrence'], - strval($event['title']), - strval($event['description']), - strval($event['location']), - strval($event['categories']), - intval($event['free_busy']), - intval($event['sensitivity']), - intval($event['priority']), - $event['alarms'], - $event['notifyat'], - $event['id'] - ); + $update_master = false; + $update_recurring = true; + $old = $this->get_event($event['id']); - if ($success = $this->rc->db->affected_rows($query)) - $this->_update_recurring($event); + // modify a recurring event, check submitted savemode to do the right things + if ($old['recurrence'] || $old['recurrence_id']) { + $master = $old['recurrence_id'] ? $this->get_event($old['recurrence_id']) : $old; + + switch ($event['savemode']) { + case 'new': + $event['uid'] = $this->cal->generate_uid(); + return $this->new_event($event); + + case 'current': + // add exception to master event + $master['recurrence']['EXDATE'][] = $old['start']; + $update_master = true; + + // just update this occurence (decouple from master) + $update_recurring = false; + $event['recurrence_id'] = 0; + $event['recurrence'] = array(); + break; + + case 'future': + if ($master['id'] != $event['id']) { + // set until-date on master event, then save this instance as new recurring event + $master['recurrence']['UNTIL'] = $event['start'] - 86400; + unset($master['recurrence']['COUNT']); + $update_master = true; + + // if recurrence COUNT, update value to the correct number of future occurences + if ($event['recurrence']['COUNT']) { + $sqlresult = $this->rc->db->query(sprintf( + "SELECT event_id FROM " . $this->db_events . " + WHERE calendar_id IN (%s) + AND start >= %s + AND recurrence_id=?", + $this->calendar_ids, + $this->rc->db->fromunixtime($event['start']) + ), + $master['id']); + if ($count = $this->rc->db->num_rows($sqlresult)) + $event['recurrence']['COUNT'] = $count; + } + + $update_recurring = true; + $event['recurrence_id'] = 0; + break; + } + // else: 'future' == 'all' if modifying the master event + + default: // 'all' is default + $event['id'] = $master['id']; + $event['recurrence_id'] = 0; + + // use start date from master but try to be smart on time or duration changes + $old_start_date = date('Y-m-d', $old['start']); + $old_start_time = date('H:i:s', $old['start']); + $old_duration = $old['end'] - $old['start']; + + $new_start_date = date('Y-m-d', $event['start']); + $new_start_time = date('H:i:s', $event['start']); + $new_duration = $event['end'] - $event['start']; + + // shifted or resized + if ($old_start_date == $new_start_date || $old_duration == $new_duration) { + $event['start'] = $master['start'] + ($event['start'] - $old['start']); + $event['end'] = $event['start'] + $new_duration; + } + break; + } + } + + $success = $this->_update_event($event, $update_recurring); + if ($success && $update_master) + $this->_update_event($master, true); return $success; } @@ -226,14 +277,16 @@ class database_driver extends calendar_driver $event['_exdates'] = (array)$event['recurrence']['EXDATE']; $event['recurrence'] = rtrim($rrule, ';'); $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]); - $event['allday'] = $event['allday'] ? 1 : 0; + + if (isset($event['allday'])) + $event['all_day'] = $event['allday'] ? 1 : 0; // compute absolute time to notify the user $event['notifyat'] = $this->_get_notification($event); return $event; } - + /** * Compute absolute time to notify the user */ @@ -267,6 +320,43 @@ class database_driver extends calendar_driver return null; } + /** + * Save the given event record to database + * + * @param array Event data, already passed through self::_save_preprocess() + * @param boolean True if recurring events instances should be updated, too + */ + private function _update_event($event, $update_recurring = true) + { + $event = $this->_save_preprocess($event); + + $sql_set = array(); + $set_cols = array('all_day', 'recurrence', 'recurrence_id', 'title', 'description', 'location', 'categories', 'free_busy', 'priority', 'sensitivity', 'alarms', 'notifyat'); + foreach ($set_cols as $col) { + if (isset($event[$col])) + $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]); + } + + $query = $this->rc->db->query(sprintf( + "UPDATE " . $this->db_events . " + SET changed=%s, start=%s, end=%s %s + WHERE event_id=? + AND calendar_id IN (" . $this->calendar_ids . ")", + $this->rc->db->now(), + $this->rc->db->fromunixtime($event['start']), + $this->rc->db->fromunixtime($event['end']), + ($sql_set ? ', ' . join(', ', $sql_set) : '') + ), + $event['id'] + ); + + $success = $this->rc->db->affected_rows($query); + if ($success && $update_recurring) + $this->_update_recurring($event); + + return $success; + } + /** * Insert "fake" entries for recurring occurences of this event */ @@ -320,7 +410,6 @@ class database_driver extends calendar_driver break; } } - } /** @@ -331,29 +420,8 @@ class database_driver extends calendar_driver */ public function move_event($event) { - if (!empty($this->calendars)) { - $event = $this->_save_preprocess($event + (array)$this->get_event($event['id'])); - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_events . " - SET changed=%s, start=%s, end=%s, all_day=?, notifyat=? - WHERE event_id=? - AND calendar_id IN (" . $this->calendar_ids . ")", - $this->rc->db->now(), - $this->rc->db->fromunixtime($event['start']), - $this->rc->db->fromunixtime($event['end']) - ), - $event['allday'] ? 1 : 0, - $event['notifyat'], - $event['id'] - ); - - if ($success = $this->rc->db->affected_rows($query)) - $this->_update_recurring($event); - - return $success; - } - - return false; + // let edit_event() do all the magic + return $this->edit_event($event + (array)$this->get_event($event['id'])); } /** @@ -364,28 +432,8 @@ class database_driver extends calendar_driver */ public function resize_event($event) { - if (!empty($this->calendars)) { - $event = $this->_save_preprocess($event + (array)$this->get_event($event['id'])); - $query = $this->rc->db->query(sprintf( - "UPDATE " . $this->db_events . " - SET changed=%s, start=%s, end=%s, notifyat=? - WHERE event_id=? - AND calendar_id IN (" . $this->calendar_ids . ")", - $this->rc->db->now(), - $this->rc->db->fromunixtime($event['start']), - $this->rc->db->fromunixtime($event['end']) - ), - $event['notifyat'], - $event['id'] - ); - - if ($success = $this->rc->db->affected_rows($query)) - $this->_update_recurring($event); - - return $success; - } - - return false; + // let edit_event() do all the magic + return $this->edit_event($event + (array)$this->get_event($event['id'])); } /** @@ -397,14 +445,67 @@ class database_driver extends calendar_driver public function remove_event($event) { if (!empty($this->calendars)) { - $query = $this->rc->db->query( - "DELETE FROM " . $this->db_events . " - WHERE (event_id=? OR recurrence_id=?) - AND calendar_id IN (" . $this->calendar_ids . ")", - $event['id'], - $event['id'] - ); - return $this->rc->db->affected_rows($query); + $event += (array)$this->get_event($event['id']); + $master = $event; + $update_master = false; + $savemode = 'all'; + + // read master if deleting a recurring event + if ($event['recurrence'] || $event['recurrence_id']) { + $master = $event['recurrence_id'] ? $this->get_event($old['recurrence_id']) : $event; + $savemode = $event['savemode']; + } + + switch ($savemode) { + case 'current': + // add exception to master event + $master['recurrence']['EXDATE'][] = $event['start']; + $update_master = true; + + // just delete this single occurence + $query = $this->rc->db->query( + "DELETE FROM " . $this->db_events . " + WHERE calendar_id IN (" . $this->calendar_ids . ") + AND event_id=?", + $event['id'] + ); + break; + + case 'future': + if ($master['id'] != $event['id']) { + // set until-date on master event + $master['recurrence']['UNTIL'] = $event['start'] - 86400; + unset($master['recurrence']['COUNT']); + $update_master = true; + + // delete this and all future instances + $query = $this->rc->db->query( + "DELETE FROM " . $this->db_events . " + WHERE calendar_id IN (" . $this->calendar_ids . ") + AND start >= " . $this->rc->db->fromunixtime($old['start']) . " + AND recurrence_id=?", + $master['id'] + ); + break; + } + // else: future == all if modifying the master event + + default: // 'all' is default + $query = $this->rc->db->query( + "DELETE FROM " . $this->db_events . " + WHERE (event_id=? OR recurrence_id=?) + AND calendar_id IN (" . $this->calendar_ids . ")", + $master['id'], + $master['id'] + ); + break; + } + + $success = $this->rc->db->affected_rows($query); + if ($success && $update_master) + $this->_update_event($master, true); +console($savemode, $master['id'], $success); + return $success; } return false; @@ -417,6 +518,11 @@ class database_driver extends calendar_driver */ public function get_event($id) { + static $cache = array(); + + if ($cache[$id]) + return $cache[$id]; + $result = $this->rc->db->query(sprintf( "SELECT * FROM " . $this->db_events . " WHERE calendar_id IN (%s) @@ -425,8 +531,10 @@ class database_driver extends calendar_driver ), $id); - if ($result && ($event = $this->rc->db->fetch_assoc($result))) - return $this->_read_postprocess($event); + if ($result && ($event = $this->rc->db->fetch_assoc($result))) { + $cache[$id] = $this->_read_postprocess($event); + return $cache[$id]; + } return false; } @@ -493,7 +601,7 @@ class database_driver extends calendar_driver } } - unset($event['event_id'], $event['calendar_id']); + unset($event['event_id'], $event['calendar_id'], $event['notifyat']); return $event; } @@ -528,7 +636,7 @@ class database_driver extends calendar_driver $result = $this->rc->db->query(sprintf( "SELECT * FROM " . $this->db_events . " WHERE calendar_id IN (%s) - AND notifyat <= %s AND end <= %s", + AND notifyat <= %s AND end > %s", join(',', $calendar_ids), $this->rc->db->fromunixtime($time), $this->rc->db->fromunixtime($time) diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 461a92ba..8ccccc48 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -277,6 +277,22 @@ class calendar_ui return html::tag('ul', $attrib, join("\n", $items)); } + /** + * Generate the form for recurrence settings + */ + function recurring_event_warning($attrib = array()) + { + $attrib['id'] = 'edit-recurring-warning'; + + $radio = new html_radiobutton(array('name' => 'savemode', 'class' => 'edit-recurring-savemode')); + $form = html::label(null, $radio->show('', array('value' => 'current')) . $this->calendar->gettext('currentevent')) . ' ' . + html::label(null, $radio->show('', array('value' => 'future')) . $this->calendar->gettext('futurevents')) . ' ' . + html::label(null, $radio->show('all', array('value' => 'all')) . $this->calendar->gettext('allevents')) . ' ' . + html::label(null, $radio->show('', array('value' => 'new')) . $this->calendar->gettext('saveasnew')); + + return html::div($attrib, html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->calendar->gettext('changerecurringeventwarning')) . html::div('savemode', $form)); + } + /** * Generate the form for recurrence settings */ diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index ec8947e8..8872f798 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -89,6 +89,7 @@ $labels['tabattachments'] = 'Attachments'; $labels['deleteventconfirm'] = "Do you really want to delete this event?"; $labels['errorsaving'] = "Failed to save changes"; $labels['operationfailed'] = "The requested operation failed"; +$labels['invalideventdates'] = "Invalid dates entered! Please check your input."; // recurrence form $labels['repeat'] = 'Repeat'; @@ -117,4 +118,13 @@ $labels['fourth'] = 'fourth'; $labels['last'] = 'last'; $labels['dayofmonth'] = 'Day of month'; +$labels['changerecurringevent'] = 'Change recurring event'; +$labels['removerecurringevent'] = 'Remove recurring 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 remove the current event only, this and all future occurences or all occurences of this event?'; +$labels['currentevent'] = 'Current'; +$labels['futurevents'] = 'Future'; +$labels['allevents'] = 'All'; +$labels['saveasnew'] = 'Save as new'; + ?> \ No newline at end of file diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index a9607578..4400c3a1 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -240,6 +240,7 @@ pre { } #eventedit { + position: relative; padding: 0.5em 0.1em; } @@ -321,6 +322,36 @@ td.topalign { padding: 0.2em 0; } +.edit-recurring-warning { + margin-top: 0.5em; + padding: 0.8em; + background-color: #F7FDCB; + border: 1px solid #C2D071; +} + +.edit-recurring-warning .message { + margin-bottom: 0.5em; +} + +.edit-recurring-warning .savemode { + padding-left: 20px; +} + +.edit-recurring-warning span.ui-icon { + float: left; + margin: 0 7px 20px 0; +} + +.edit-recurring-warning label { + min-width: 3em; + padding-right: 1em; +} + +.edit-recurring-warning a.button { + margin: 0 0.5em 0 0.2em; + min-width: 5em; +} + span.edit-alarm-set { white-space: nowrap; } @@ -400,6 +431,7 @@ a.alarm-action-snooze:after { height: 160px; } + /* fullcalendar style overrides */ .fc-event-title { diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html index b4efed38..1d20e522 100644 --- a/plugins/calendar/skins/default/templates/calendar.html +++ b/plugins/calendar/skins/default/templates/calendar.html @@ -160,6 +160,8 @@
+ +