Align event start date with the first occurrence
Summary: When a recurring event start date does not match a recurrence pattern (e.g. an event recurring on Fridays is created on Thursday), we move the start date to the date of the first occurrence. There's also a checkbox to keep the old behavior where the start date was not modified. Reviewers: vanmeeuwen Reviewed By: vanmeeuwen Differential Revision: https://git.kolab.org/D536
This commit is contained in:
parent
6b3ac66afc
commit
a1cd95152c
10 changed files with 484 additions and 33 deletions
|
@ -887,8 +887,10 @@ class calendar extends rcube_plugin
|
||||||
case "new":
|
case "new":
|
||||||
// create UID for new event
|
// create UID for new event
|
||||||
$event['uid'] = $this->generate_uid();
|
$event['uid'] = $this->generate_uid();
|
||||||
$this->write_preprocess($event, $action);
|
if (!$this->write_preprocess($event, $action)) {
|
||||||
if ($success = $this->driver->new_event($event)) {
|
$got_msg = true;
|
||||||
|
}
|
||||||
|
else if ($success = $this->driver->new_event($event)) {
|
||||||
$event['id'] = $event['uid'];
|
$event['id'] = $event['uid'];
|
||||||
$event['_savemode'] = 'all';
|
$event['_savemode'] = 'all';
|
||||||
$this->cleanup_event($event);
|
$this->cleanup_event($event);
|
||||||
|
@ -898,8 +900,10 @@ class calendar extends rcube_plugin
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "edit":
|
case "edit":
|
||||||
$this->write_preprocess($event, $action);
|
if (!$this->write_preprocess($event, $action)) {
|
||||||
if ($success = $this->driver->edit_event($event)) {
|
$got_msg = true;
|
||||||
|
}
|
||||||
|
else if ($success = $this->driver->edit_event($event)) {
|
||||||
$this->cleanup_event($event);
|
$this->cleanup_event($event);
|
||||||
$this->event_save_success($event, $old, $action, $success);
|
$this->event_save_success($event, $old, $action, $success);
|
||||||
}
|
}
|
||||||
|
@ -907,16 +911,20 @@ class calendar extends rcube_plugin
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "resize":
|
case "resize":
|
||||||
$this->write_preprocess($event, $action);
|
if (!$this->write_preprocess($event, $action)) {
|
||||||
if ($success = $this->driver->resize_event($event)) {
|
$got_msg = true;
|
||||||
|
}
|
||||||
|
else if ($success = $this->driver->resize_event($event)) {
|
||||||
$this->event_save_success($event, $old, $action, $success);
|
$this->event_save_success($event, $old, $action, $success);
|
||||||
}
|
}
|
||||||
$reload = $event['_savemode'] ? 2 : 1;
|
$reload = $event['_savemode'] ? 2 : 1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "move":
|
case "move":
|
||||||
$this->write_preprocess($event, $action);
|
if (!$this->write_preprocess($event, $action)) {
|
||||||
if ($success = $this->driver->move_event($event)) {
|
$got_msg = true;
|
||||||
|
}
|
||||||
|
else if ($success = $this->driver->move_event($event)) {
|
||||||
$this->event_save_success($event, $old, $action, $success);
|
$this->event_save_success($event, $old, $action, $success);
|
||||||
}
|
}
|
||||||
$reload = $success && $event['_savemode'] ? 2 : 1;
|
$reload = $success && $event['_savemode'] ? 2 : 1;
|
||||||
|
@ -1184,7 +1192,7 @@ class calendar extends rcube_plugin
|
||||||
// unlock client
|
// unlock client
|
||||||
$this->rc->output->command('plugin.unlock_saving');
|
$this->rc->output->command('plugin.unlock_saving');
|
||||||
|
|
||||||
// update event object on the client or trigger a complete refretch if too complicated
|
// update event object on the client or trigger a complete refresh if too complicated
|
||||||
if ($reload) {
|
if ($reload) {
|
||||||
$args = array('source' => $event['calendar']);
|
$args = array('source' => $event['calendar']);
|
||||||
if ($reload > 1)
|
if ($reload > 1)
|
||||||
|
@ -1993,12 +2001,31 @@ class calendar extends rcube_plugin
|
||||||
|
|
||||||
// start/end is all we need for 'move' action (#1480)
|
// start/end is all we need for 'move' action (#1480)
|
||||||
if ($action == 'move') {
|
if ($action == 'move') {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert the submitted recurrence settings
|
// convert the submitted recurrence settings
|
||||||
if (is_array($event['recurrence'])) {
|
if (is_array($event['recurrence'])) {
|
||||||
$event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']);
|
$event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']);
|
||||||
|
|
||||||
|
// align start date with the first occurrence
|
||||||
|
if (!empty($event['recurrence']) && !empty($event['syncstart'])
|
||||||
|
&& (empty($event['_savemode']) || $event['_savemode'] == 'all')
|
||||||
|
) {
|
||||||
|
$next = $this->find_first_occurrence($event);
|
||||||
|
|
||||||
|
if (!$next) {
|
||||||
|
$this->rc->output->show_message('calendar.recurrenceerror', 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if ($event['start'] != $next) {
|
||||||
|
$diff = $event['start']->diff($event['end'], true);
|
||||||
|
|
||||||
|
$event['start'] = $next;
|
||||||
|
$event['end'] = clone $next;
|
||||||
|
$event['end']->add($diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert the submitted alarm values
|
// convert the submitted alarm values
|
||||||
|
@ -2075,6 +2102,8 @@ class calendar extends rcube_plugin
|
||||||
$event['url'] = $event['vurl'];
|
$event['url'] = $event['vurl'];
|
||||||
unset($event['vurl']);
|
unset($event['vurl']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3447,6 +3476,35 @@ class calendar extends rcube_plugin
|
||||||
return $this->driver->user_delete($args);
|
return $this->driver->user_delete($args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find first occurrence of a recurring event excluding start date
|
||||||
|
*
|
||||||
|
* @param array $event Event data (with 'start' and 'recurrence')
|
||||||
|
*
|
||||||
|
* @return DateTime Date of the first occurrence
|
||||||
|
*/
|
||||||
|
public function find_first_occurrence($event)
|
||||||
|
{
|
||||||
|
// Make sure libkolab plugin is loaded in case of Kolab driver
|
||||||
|
$this->load_driver();
|
||||||
|
|
||||||
|
// Use libkolab to compute recurring events (and libkolab plugin)
|
||||||
|
// Horde-based fallback has many bugs
|
||||||
|
if (class_exists('kolabformat') && class_exists('kolabcalendaring') && class_exists('kolab_date_recurrence')) {
|
||||||
|
$object = kolab_format::factory('event', 3.0);
|
||||||
|
$object->set($event);
|
||||||
|
|
||||||
|
$recurrence = new kolab_date_recurrence($object);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// fallback to libcalendaring (Horde-based) recurrence implementation
|
||||||
|
require_once(__DIR__ . '/lib/calendar_recurrence.php');
|
||||||
|
$recurrence = new calendar_recurrence($this, $event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $recurrence->first_occurrence();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Magic getter for public access to protected members
|
* Magic getter for public access to protected members
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -678,7 +678,7 @@ function rcube_calendar_ui(settings)
|
||||||
var freebusy = $('#edit-free-busy').val(event.free_busy);
|
var freebusy = $('#edit-free-busy').val(event.free_busy);
|
||||||
var priority = $('#edit-priority').val(event.priority);
|
var priority = $('#edit-priority').val(event.priority);
|
||||||
var sensitivity = $('#edit-sensitivity').val(event.sensitivity);
|
var sensitivity = $('#edit-sensitivity').val(event.sensitivity);
|
||||||
|
var syncstart = $('#edit-recurrence-syncstart input');
|
||||||
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
|
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
|
||||||
var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
|
var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
|
||||||
var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
|
var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
|
||||||
|
@ -898,6 +898,9 @@ function rcube_calendar_ui(settings)
|
||||||
data._fromcalendar = event.calendar;
|
data._fromcalendar = event.calendar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.recurrence && syncstart.is(':checked'))
|
||||||
|
data.syncstart = 1;
|
||||||
|
|
||||||
update_event(action, data);
|
update_event(action, data);
|
||||||
$dialog.dialog("close");
|
$dialog.dialog("close");
|
||||||
} // end click:
|
} // end click:
|
||||||
|
@ -3974,8 +3977,15 @@ function rcube_calendar_ui(settings)
|
||||||
$('#edit-attendees-form .attendees-invitebox').show();
|
$('#edit-attendees-form .attendees-invitebox').show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset autocompletion on tab change (#3389)
|
// reset autocompletion on tab change (#3389)
|
||||||
rcmail.ksearch_blur();
|
rcmail.ksearch_blur();
|
||||||
|
|
||||||
|
// display recurrence warning in recurrence tab only
|
||||||
|
if (tab == 'recurrence')
|
||||||
|
$('#edit-recurrence-frequency').change();
|
||||||
|
else
|
||||||
|
$('#edit-recurrence-syncstart').hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$('#edit-enddate').datepicker(datepicker_settings);
|
$('#edit-enddate').datepicker(datepicker_settings);
|
||||||
|
|
|
@ -659,14 +659,7 @@ class kolab_calendar extends kolab_storage_folder_api
|
||||||
}
|
}
|
||||||
|
|
||||||
// use libkolab to compute recurring events
|
// use libkolab to compute recurring events
|
||||||
if (class_exists('kolabcalendaring')) {
|
|
||||||
$recurrence = new kolab_date_recurrence($object);
|
$recurrence = new kolab_date_recurrence($object);
|
||||||
}
|
|
||||||
else {
|
|
||||||
// fallback to local recurrence implementation
|
|
||||||
require_once($this->cal->home . '/lib/calendar_recurrence.php');
|
|
||||||
$recurrence = new calendar_recurrence($this->cal, $event);
|
|
||||||
}
|
|
||||||
|
|
||||||
$i = 0;
|
$i = 0;
|
||||||
while ($next_event = $recurrence->next_instance()) {
|
while ($next_event = $recurrence->next_instance()) {
|
||||||
|
|
|
@ -1784,15 +1784,15 @@ class kolab_driver extends calendar_driver
|
||||||
*/
|
*/
|
||||||
private function get_recurrence_count($event, $dtstart)
|
private function get_recurrence_count($event, $dtstart)
|
||||||
{
|
{
|
||||||
|
// load the given event data into a libkolabxml container
|
||||||
|
if (!$event['_formatobj']) {
|
||||||
|
$event_xml = new kolab_format_event();
|
||||||
|
$event_xml->set($event);
|
||||||
|
$event['_formatobj'] = $event_xml;
|
||||||
|
}
|
||||||
|
|
||||||
// use libkolab to compute recurring events
|
// use libkolab to compute recurring events
|
||||||
if (class_exists('kolabcalendaring') && $event['_formatobj']) {
|
|
||||||
$recurrence = new kolab_date_recurrence($event['_formatobj']);
|
$recurrence = new kolab_date_recurrence($event['_formatobj']);
|
||||||
}
|
|
||||||
else {
|
|
||||||
// fallback to local recurrence implementation
|
|
||||||
require_once($this->cal->home . '/lib/calendar_recurrence.php');
|
|
||||||
$recurrence = new calendar_recurrence($this->cal, $event);
|
|
||||||
}
|
|
||||||
|
|
||||||
$count = 0;
|
$count = 0;
|
||||||
while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
|
while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
|
||||||
|
|
|
@ -92,6 +92,7 @@ class calendar_ui
|
||||||
$this->cal->register_handler('plugin.resource_calendar', array($this, 'resource_calendar'));
|
$this->cal->register_handler('plugin.resource_calendar', array($this, 'resource_calendar'));
|
||||||
$this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
|
$this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
|
||||||
$this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
|
$this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
|
||||||
|
$this->cal->register_handler('plugin.edit_recurrence_sync', array($this, 'edit_recurrence_sync'));
|
||||||
$this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
|
$this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
|
||||||
$this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
|
$this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
|
||||||
$this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
|
$this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
|
||||||
|
@ -473,7 +474,7 @@ class calendar_ui
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Render HTML for attendee notification warning
|
||||||
*/
|
*/
|
||||||
function edit_attendees_notify($attrib = array())
|
function edit_attendees_notify($attrib = array())
|
||||||
{
|
{
|
||||||
|
@ -481,6 +482,15 @@ class calendar_ui
|
||||||
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
|
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render HTML for recurrence option to align start date with the recurrence rule
|
||||||
|
*/
|
||||||
|
function edit_recurrence_sync($attrib = array())
|
||||||
|
{
|
||||||
|
$checkbox = new html_checkbox(array('name' => '_start_sync', 'value' => 1));
|
||||||
|
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('eventstartsync')));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the form for recurrence settings
|
* Generate the form for recurrence settings
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -125,6 +125,7 @@ $labels['invitationspending'] = 'Pending invitations';
|
||||||
$labels['invitationsdeclined'] = 'Declined invitations';
|
$labels['invitationsdeclined'] = 'Declined invitations';
|
||||||
$labels['changepartstat'] = 'Change participant status';
|
$labels['changepartstat'] = 'Change participant status';
|
||||||
$labels['rsvpcomment'] = 'Invitation text';
|
$labels['rsvpcomment'] = 'Invitation text';
|
||||||
|
$labels['eventstartsync'] = 'Move the event start date to the first occurrence';
|
||||||
|
|
||||||
// agenda view
|
// agenda view
|
||||||
$labels['listrange'] = 'Range to display:';
|
$labels['listrange'] = 'Range to display:';
|
||||||
|
@ -267,6 +268,7 @@ $labels['currentevent'] = 'Current';
|
||||||
$labels['futurevents'] = 'Future';
|
$labels['futurevents'] = 'Future';
|
||||||
$labels['allevents'] = 'All';
|
$labels['allevents'] = 'All';
|
||||||
$labels['saveasnew'] = 'Save as new';
|
$labels['saveasnew'] = 'Save as new';
|
||||||
|
$labels['recurrenceerror'] = 'Unable to resolve recurrence rule for specified start date.';
|
||||||
|
|
||||||
// birthdays calendar
|
// birthdays calendar
|
||||||
$labels['birthdays'] = 'Birthdays';
|
$labels['birthdays'] = 'Birthdays';
|
||||||
|
|
|
@ -127,6 +127,7 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<roundcube:object name="plugin.edit_recurrence_sync" id="edit-recurrence-syncstart" class="event-dialog-message" style="display:none" />
|
||||||
<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="event-dialog-message" style="display:none" />
|
<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="event-dialog-message" style="display:none" />
|
||||||
<roundcube:object name="plugin.edit_recurring_warning" class="event-dialog-message edit-recurring-warning" style="display:none" />
|
<roundcube:object name="plugin.edit_recurring_warning" class="event-dialog-message edit-recurring-warning" style="display:none" />
|
||||||
<div id="edit-localchanges-warning" class="event-dialog-message" style="display:none"><roundcube:label name="calendar.localchangeswarning" /></div>
|
<div id="edit-localchanges-warning" class="event-dialog-message" style="display:none"><roundcube:label name="calendar.localchangeswarning" /></div>
|
||||||
|
|
|
@ -152,4 +152,83 @@ class libcalendaring_recurrence
|
||||||
return $last;
|
return $last;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find date/time of the first occurrence (excluding start date)
|
||||||
|
*/
|
||||||
|
public function first_occurrence()
|
||||||
|
{
|
||||||
|
$start = clone $this->start;
|
||||||
|
$orig_start = clone $this->start;
|
||||||
|
$r = $this->recurrence;
|
||||||
|
$interval = intval($r['INTERVAL'] ?: 1);
|
||||||
|
|
||||||
|
switch ($this->recurrence['FREQ']) {
|
||||||
|
case 'WEEKLY':
|
||||||
|
if (empty($this->recurrence['BYDAY'])) {
|
||||||
|
return $start;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start->sub(new DateInterval("P{$interval}W"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MONTHLY':
|
||||||
|
if (empty($this->recurrence['BYDAY']) && empty($this->recurrence['BYMONTHDAY'])) {
|
||||||
|
return $start;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start->sub(new DateInterval("P{$interval}M"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'YEARLY':
|
||||||
|
if (empty($this->recurrence['BYDAY']) && empty($this->recurrence['BYMONTH'])) {
|
||||||
|
return $start;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start->sub(new DateInterval("P{$interval}Y"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return $start;
|
||||||
|
}
|
||||||
|
|
||||||
|
$r = $this->recurrence;
|
||||||
|
$r['INTERVAL'] = $interval;
|
||||||
|
if ($r['COUNT']) {
|
||||||
|
// Increase count so we do not stop the loop to early
|
||||||
|
$r['COUNT'] += 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create recurrence that starts in the past
|
||||||
|
$recurrence = new self($this->lib);
|
||||||
|
$recurrence->init($r, $start);
|
||||||
|
|
||||||
|
// find the first occurrence
|
||||||
|
$found = false;
|
||||||
|
while ($next = $recurrence->next()) {
|
||||||
|
$start = $next;
|
||||||
|
if ($next >= $orig_start) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$found) {
|
||||||
|
rcube::raise_error(array(
|
||||||
|
'file' => __FILE__,
|
||||||
|
'line' => __LINE__,
|
||||||
|
'message' => sprintf("Failed to find a first occurrence. Start: %s, Recurrence: %s",
|
||||||
|
$orig_start->format(DateTime::ISO8601), json_encode($r)),
|
||||||
|
), true);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($start Instanceof Horde_Date) {
|
||||||
|
$start = $start->toDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
$start->_dateonly = $this->dateonly;
|
||||||
|
|
||||||
|
return $start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,4 +138,89 @@ class kolab_date_recurrence
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find date/time of the first occurrence (excluding start date)
|
||||||
|
*/
|
||||||
|
public function first_occurrence()
|
||||||
|
{
|
||||||
|
$event = $this->object->to_array();
|
||||||
|
$start = clone $this->start;
|
||||||
|
$orig_start = clone $this->start;
|
||||||
|
$interval = intval($event['recurrence']['INTERVAL'] ?: 1);
|
||||||
|
|
||||||
|
switch ($event['recurrence']['FREQ']) {
|
||||||
|
case 'WEEKLY':
|
||||||
|
if (empty($event['recurrence']['BYDAY'])) {
|
||||||
|
return $orig_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start->sub(new DateInterval("P{$interval}W"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'MONTHLY':
|
||||||
|
if (empty($event['recurrence']['BYDAY']) && empty($event['recurrence']['BYMONTHDAY'])) {
|
||||||
|
return $orig_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start->sub(new DateInterval("P{$interval}M"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'YEARLY':
|
||||||
|
if (empty($event['recurrence']['BYDAY']) && empty($event['recurrence']['BYMONTH'])) {
|
||||||
|
return $orig_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start->sub(new DateInterval("P{$interval}Y"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'DAILY':
|
||||||
|
if (!empty($event['recurrence']['BYMONTH'])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return $orig_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
$event['start'] = $start;
|
||||||
|
$event['recurrence']['INTERVAL'] = $interval;
|
||||||
|
if ($event['recurrence']['COUNT']) {
|
||||||
|
// Increase count so we do not stop the loop to early
|
||||||
|
$event['recurrence']['COUNT'] += 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create recurrence that starts in the past
|
||||||
|
$object_type = $this->object instanceof kolab_format_task ? 'task' : 'event';
|
||||||
|
$object = kolab_format::factory($object_type, 3.0);
|
||||||
|
$object->set($event);
|
||||||
|
$recurrence = new self($object);
|
||||||
|
|
||||||
|
// find the first occurrence
|
||||||
|
$found = false;
|
||||||
|
while ($next = $recurrence->next_start()) {
|
||||||
|
$start = $next;
|
||||||
|
if ($next >= $orig_start) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$found) {
|
||||||
|
rcube::raise_error(array(
|
||||||
|
'file' => __FILE__,
|
||||||
|
'line' => __LINE__,
|
||||||
|
'message' => sprintf("Failed to find a first occurrence. Start: %s, Recurrence: %s",
|
||||||
|
$orig_start->format(DateTime::ISO8601), json_encode($event['recurrence'])),
|
||||||
|
), true);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($orig_start->_dateonly) {
|
||||||
|
$start->_dateonly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
213
plugins/libkolab/tests/kolab_date_recurrence.php
Normal file
213
plugins/libkolab/tests/kolab_date_recurrence.php
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kolab_date_recurrence tests
|
||||||
|
*
|
||||||
|
* @author Aleksander Machniak <machniak@kolabsys.com>
|
||||||
|
*
|
||||||
|
* Copyright (C) 2017, Kolab Systems AG <contact@kolabsys.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program 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 Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class kolab_date_recurrence_test extends PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
function setUp()
|
||||||
|
{
|
||||||
|
$rcube = rcmail::get_instance();
|
||||||
|
$rcube->plugins->load_plugin('libkolab', true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kolab_date_recurrence::first_occurrence()
|
||||||
|
*
|
||||||
|
* @dataProvider data_first_occurrence
|
||||||
|
*/
|
||||||
|
function test_first_occurrence($recurrence_data, $start, $expected)
|
||||||
|
{
|
||||||
|
$start = new DateTime($start);
|
||||||
|
if (!empty($recurrence_data['UNTIL'])) {
|
||||||
|
$recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$event = array('start' => $start, 'recurrence' => $recurrence_data);
|
||||||
|
$object = kolab_format::factory('event', 3.0);
|
||||||
|
$object->set($event);
|
||||||
|
|
||||||
|
$recurrence = new kolab_date_recurrence($object);
|
||||||
|
$first = $recurrence->first_occurrence();
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for test_first_occurrence()
|
||||||
|
*/
|
||||||
|
function data_first_occurrence()
|
||||||
|
{
|
||||||
|
// TODO: BYYEARDAY, BYWEEKNO, BYSETPOS, WKST
|
||||||
|
|
||||||
|
return array(
|
||||||
|
// non-recurring
|
||||||
|
array(
|
||||||
|
array(), // recurrence data
|
||||||
|
'2017-08-31 11:00:00', // start date
|
||||||
|
'2017-08-31 11:00:00', // expected result
|
||||||
|
),
|
||||||
|
// daily
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'DAILY', 'INTERVAL' => '1'), // recurrence data
|
||||||
|
'2017-08-31 11:00:00', // start date
|
||||||
|
'2017-08-31 11:00:00', // expected result
|
||||||
|
),
|
||||||
|
// TODO: this one is not supported by the Calendar UI
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'DAILY', 'INTERVAL' => '1', 'BYMONTH' => 1),
|
||||||
|
'2017-08-31 11:00:00',
|
||||||
|
'2018-01-01 11:00:00',
|
||||||
|
),
|
||||||
|
// weekly
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1'),
|
||||||
|
'2017-08-31 11:00:00', // Thursday
|
||||||
|
'2017-08-31 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE'),
|
||||||
|
'2017-08-31 11:00:00', // Thursday
|
||||||
|
'2017-09-06 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'TH'),
|
||||||
|
'2017-08-31 11:00:00', // Thursday
|
||||||
|
'2017-08-31 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'FR'),
|
||||||
|
'2017-08-31 11:00:00', // Thursday
|
||||||
|
'2017-09-01 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'WEEKLY', 'INTERVAL' => '2'),
|
||||||
|
'2017-08-31 11:00:00', // Thursday
|
||||||
|
'2017-08-31 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'WEEKLY', 'INTERVAL' => '3', 'BYDAY' => 'WE'),
|
||||||
|
'2017-08-31 11:00:00', // Thursday
|
||||||
|
'2017-09-20 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE', 'COUNT' => 1),
|
||||||
|
'2017-08-31 11:00:00', // Thursday
|
||||||
|
'2017-09-06 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE', 'UNTIL' => '2017-09-01'),
|
||||||
|
'2017-08-31 11:00:00', // Thursday
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
// monthly
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1'),
|
||||||
|
'2017-09-08 11:00:00',
|
||||||
|
'2017-09-08 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYMONTHDAY' => '8,9'),
|
||||||
|
'2017-08-31 11:00:00',
|
||||||
|
'2017-09-08 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYMONTHDAY' => '8,9'),
|
||||||
|
'2017-09-08 11:00:00',
|
||||||
|
'2017-09-08 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYDAY' => '1WE'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-09-06 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYDAY' => '-1WE'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-08-30 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'MONTHLY', 'INTERVAL' => '2'),
|
||||||
|
'2017-09-08 11:00:00',
|
||||||
|
'2017-09-08 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'MONTHLY', 'INTERVAL' => '2', 'BYMONTHDAY' => '8'),
|
||||||
|
'2017-08-31 11:00:00',
|
||||||
|
'2017-09-08 11:00:00', // ??????
|
||||||
|
),
|
||||||
|
// yearly
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '1'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '8'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYDAY' => '-1MO'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-12-25 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '8', 'BYDAY' => '-1MO'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-08-28 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '1', 'BYDAY' => '1MO'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2018-01-01 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '1,9', 'BYDAY' => '1MO'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-09-04 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '2'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '2', 'BYMONTH' => '8'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array('FREQ' => 'YEARLY', 'INTERVAL' => '2', 'BYDAY' => '-1MO'),
|
||||||
|
'2017-08-16 11:00:00',
|
||||||
|
'2017-12-25 11:00:00',
|
||||||
|
),
|
||||||
|
// on dates (FIXME: do we really expect the first occurrence to be on the start date?)
|
||||||
|
array(
|
||||||
|
array('RDATE' => array (new DateTime('2017-08-10 11:00:00 Europe/Warsaw'))),
|
||||||
|
'2017-08-01 11:00:00',
|
||||||
|
'2017-08-01 11:00:00',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue