diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js index 931943dd..cf33f66d 100644 --- a/plugins/libcalendaring/libcalendaring.js +++ b/plugins/libcalendaring/libcalendaring.js @@ -337,9 +337,10 @@ function rcube_libcalendaring(settings) $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')](); }); $(prefix+' select.edit-alarm-offset').change(function(){ - var val = $(this).val(); - $(this).parent().find('.edit-alarm-date, .edit-alarm-time')[val == '@' ? 'show' : 'hide'](); - $(this).parent().find('.edit-alarm-value').prop('disabled', val === '@' || val === '0'); + var val = $(this).val(), parent = $(this).parent(); + parent.find('.edit-alarm-date, .edit-alarm-time')[val == '@' ? 'show' : 'hide'](); + parent.find('.edit-alarm-value').prop('disabled', val === '@' || val === '0'); + parent.find('.edit-alarm-related')[val == '@' ? 'hide' : 'show'](); }); $(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings); @@ -389,6 +390,7 @@ function rcube_libcalendaring(settings) } $('select.edit-alarm-type', domnode).val(alarm.action); + $('select.edit-alarm-related', domnode).val(/END/i.test(alarm.related) ? 'end' : 'start'); if (String(alarm.trigger).match(/@(\d+)/)) { var ondate = this.fromunixtime(parseInt(RegExp.$1)); @@ -417,7 +419,11 @@ function rcube_libcalendaring(settings) var valarms = []; $(prefix + ' .edit-alarm-item').each(function(i, elem) { - var val, offset, alarm = { action: $('select.edit-alarm-type', elem).val() }; + var val, offset, alarm = { + action: $('select.edit-alarm-type', elem).val(), + related: $('select.edit-alarm-related', elem).val() + }; + if (alarm.action) { offset = $('select.edit-alarm-offset', elem).val(); if (offset == '@') { diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php index 4246fa7f..ddbebe43 100644 --- a/plugins/libcalendaring/libcalendaring.php +++ b/plugins/libcalendaring/libcalendaring.php @@ -328,8 +328,10 @@ class libcalendaring extends rcube_plugin $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3)); $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10)); $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6)); - $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id'])); - $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset')); + $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id'])); + $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset')); + $select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related')); + $object_type = $attrib['_type'] ?: 'event'; $select_type->add($this->gettext('none'), ''); foreach ($alarm_types as $type) @@ -342,6 +344,9 @@ class libcalendaring extends rcube_plugin if ($absolute_time) $select_offset->add($this->gettext('trigger@'), '@'); + $select_related->add($this->gettext('relatedstart'), 'start'); + $select_related->add($this->gettext('relatedend' . $object_type), 'end'); + // pre-set with default values from user settings $preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); $hidden = array('style' => 'display:none'); @@ -350,6 +355,7 @@ class libcalendaring extends rcube_plugin html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'), $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]) . ' ' . + $select_related->show() . ' ' . $input_date->show('', $hidden) . ' ' . $input_time->show('', $hidden) ) @@ -527,10 +533,11 @@ class libcalendaring extends rcube_plugin } else { $trigger = $alarm['trigger']; - $action = $alarm['action']; + $action = $alarm['action']; + $related = $alarm['related']; } - $text = ''; + $text = ''; $rcube = rcube::get_instance(); switch ($action) { @@ -558,11 +565,15 @@ class libcalendaring extends rcube_plugin )); } else if ($val = self::parse_alarm_value($trigger)) { + $r = strtoupper($related ?: 'start') == 'END' ? 'end' : ''; // TODO: for all-day events say 'on date of event at XX' ? - if ($val[0] == 0) - $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime'); - else - $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext('libcalendaring.trigger' . $val[1]); + if ($val[0] == 0) { + $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r); + } + else { + $label = 'libcalendaring.trigger' . $r . $val[1]; + $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label); + } } else { return false; @@ -601,7 +612,7 @@ class libcalendaring extends rcube_plugin } } - $expires = new DateTime('now - 12 hours'); + $expires = new DateTime('now - 12 hours'); $alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility // handle multiple alarms @@ -613,8 +624,7 @@ class libcalendaring extends rcube_plugin $notify_time = $alarm['trigger']; } else if (is_string($alarm['trigger'])) { -// $refdate = $alarm['trigger'][0] == '+' ? $rec['end'] : $rec['start']; - $refdate = $rec['start']; + $refdate = $alarm['related'] == 'END' ? $rec['end'] : $rec['start']; // abort if no reference date is available to compute notification time if (!is_a($refdate, 'DateTime')) @@ -624,7 +634,7 @@ class libcalendaring extends rcube_plugin try { $interval = new DateInterval(trim($alarm['trigger'], '+-')); - $interval->invert = $alarm['trigger'][0] != '+'; + $interval->invert = $alarm['trigger'][0] == '-'; $notify_time = clone $refdate; $notify_time->add($interval); } diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php index 743ec242..f96f3ca6 100644 --- a/plugins/libcalendaring/libvcalendar.php +++ b/plugins/libcalendaring/libvcalendar.php @@ -611,18 +611,23 @@ class libvcalendar implements Iterator // find alarms foreach ($ve->select('VALARM') as $valarm) { - $action = 'DISPLAY'; + $action = 'DISPLAY'; $trigger = null; - $alarm = array(); + $alarm = array(); foreach ($valarm->children as $prop) { $value = strval($prop); switch ($prop->name) { case 'TRIGGER': - if ($prop['VALUE'] == 'DATE-TIME') { - $trigger = '@' . $prop->getDateTime()->format('U'); - $alarm['trigger'] = $prop->getDateTime(); + foreach ($prop->parameters as $param) { + if ($param->name == 'VALUE' && $param->value == 'DATE-TIME') { + $trigger = '@' . $prop->getDateTime()->format('U'); + $alarm['trigger'] = $prop->getDateTime(); + } + else if ($param->name == 'RELATED') { + $alarm['related'] = $param->value; + } } if (!$trigger && ($values = libcalendaring::parse_alarm_value($value))) { $trigger = $values[2]; @@ -1111,7 +1116,11 @@ class libvcalendar implements Iterator $va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true, null, true)); } else { - $va->add('TRIGGER', $alarm['trigger']); + $alarm_props = array(); + if (strtoupper($alarm['related']) == 'END') { + $alarm_props['RELATED'] = 'END'; + } + $va->add('TRIGGER', $alarm['trigger'], $alarm_props); } if ($alarm['action'] == 'EMAIL') { @@ -1388,5 +1397,4 @@ class vobject_location_property extends VObject\Property\Text // iCalendar 'REQUEST-STATUS', ); - } diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc index 00a01f5b..b0a3585e 100644 --- a/plugins/libcalendaring/localization/en_US.inc +++ b/plugins/libcalendaring/localization/en_US.inc @@ -29,8 +29,18 @@ $labels['trigger-D'] = 'days before'; $labels['trigger+M'] = 'minutes after'; $labels['trigger+H'] = 'hours after'; $labels['trigger+D'] = 'days after'; +$labels['triggerend-M'] = 'minutes before end'; +$labels['triggerend-H'] = 'hours before end'; +$labels['triggerend-D'] = 'days before end'; +$labels['triggerend+M'] = 'minutes after end'; +$labels['triggerend+H'] = 'hours after end'; +$labels['triggerend+D'] = 'days after end'; $labels['trigger0'] = 'on time'; -$labels['triggerattime'] = 'at time'; +$labels['triggerattime'] = 'at start time'; +$labels['triggerattimeend'] = 'at end time'; +$labels['relatedstart'] = 'start'; +$labels['relatedendevent'] = 'end'; +$labels['relatedendtask'] = 'due time'; $labels['addalarm'] = 'Add alarm'; $labels['removealarm'] = 'Remove alarm'; diff --git a/plugins/libcalendaring/tests/libcalendaring.php b/plugins/libcalendaring/tests/libcalendaring.php index b4373ad4..311d25e2 100644 --- a/plugins/libcalendaring/tests/libcalendaring.php +++ b/plugins/libcalendaring/tests/libcalendaring.php @@ -64,7 +64,7 @@ class libcalendaring_test extends PHPUnit_Framework_TestCase $date = date('Ymd', strtotime('today + 2 days')); $event = array( 'start' => new DateTime($date . 'T160000Z'), - 'end' => new DateTime($date . 'T180000Z'), + 'end' => new DateTime($date . 'T200000Z'), 'valarms' => array( array( 'trigger' => '-PT10M', @@ -76,7 +76,7 @@ class libcalendaring_test extends PHPUnit_Framework_TestCase $this->assertEquals($event['valarms'][0]['action'], $alarm['action']); $this->assertEquals(strtotime($date . 'T155000Z'), $alarm['time']); - // alarm 1 hour after before event + // alarm 1 hour after event start $event['valarms'] = array( array( 'trigger' => '+PT1H', @@ -84,8 +84,30 @@ class libcalendaring_test extends PHPUnit_Framework_TestCase ); $alarm = libcalendaring::get_next_alarm($event); $this->assertEquals('DISPLAY', $alarm['action']); + $this->assertEquals(strtotime($date . 'T170000Z'), $alarm['time']); + + // alarm 1 hour before event end + $event['valarms'] = array( + array( + 'trigger' => '-PT1H', + 'related' => 'END', + ), + ); + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals('DISPLAY', $alarm['action']); $this->assertEquals(strtotime($date . 'T190000Z'), $alarm['time']); + // alarm 1 hour after event end + $event['valarms'] = array( + array( + 'trigger' => 'PT1H', + 'related' => 'END', + ), + ); + $alarm = libcalendaring::get_next_alarm($event); + $this->assertEquals('DISPLAY', $alarm['action']); + $this->assertEquals(strtotime($date . 'T210000Z'), $alarm['time']); + // ignore past alarms $event['start'] = new DateTime('today 22:00:00'); $event['end'] = new DateTime('today 23:00:00'); @@ -159,6 +181,4 @@ class libcalendaring_test extends PHPUnit_Framework_TestCase $this->assertRegExp('/BYDAY='.$rrule['BYDAY'].'/', $s, "Recurrence BYDAY"); $this->assertRegExp('/UNTIL=20250501T160000Z/', $s, "Recurrence End date (in UTC)"); } - } - diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php index 4a105e0d..d8794acf 100644 --- a/plugins/libcalendaring/tests/libvcalendar.php +++ b/plugins/libcalendaring/tests/libvcalendar.php @@ -203,6 +203,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "Full alarm item (action)"); $this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)"); + $this->assertEquals('END', $event['valarms'][0]['related'], "Full alarm item (related)"); // alarm trigger with 0 values $events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8'); @@ -216,6 +217,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('-PT30M', $alarm[3], "Unified alarm string (stripped zero-values)"); $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "First alarm action"); + $this->assertEquals('', $event['valarms'][0]['related'], "First alarm related property"); $this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text"); $this->assertEquals(3, count($event['valarms']), "List all VALARM blocks"); @@ -354,7 +356,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals(100, $completed['complete'], "Task percent complete value"); $ics = $ical->export(array($completed)); - $this->assertRegExp('/COMPLETED:[0-9TZ]+/', $ics, "Export COMPLETED property"); + $this->assertRegExp('/COMPLETED(;VALUE=DATE-TIME)?:[0-9TZ]+/', $ics, "Export COMPLETED property"); } /** @@ -410,7 +412,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertRegExp('/EXDATE.*:20131218/', $ics, "Export Recurrence EXDATE"); $this->assertContains('BEGIN:VALARM', $ics, "Export VALARM"); - $this->assertContains('TRIGGER:-PT12H', $ics, "Export Alarm trigger"); + $this->assertContains('TRIGGER;RELATED=END:-PT12H', $ics, "Export Alarm trigger"); $this->assertRegExp('/ATTACH.*;VALUE=BINARY/', $ics, "Embed attachment"); $this->assertRegExp('/ATTACH.*;ENCODING=BASE64/', $ics, "Attachment B64 encoding"); diff --git a/plugins/libcalendaring/tests/resources/recurring.ics b/plugins/libcalendaring/tests/resources/recurring.ics index 85860be2..b04d434f 100644 --- a/plugins/libcalendaring/tests/resources/recurring.ics +++ b/plugins/libcalendaring/tests/resources/recurring.ics @@ -36,7 +36,7 @@ RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=20140718T215959Z;BYDAY=3WE EXDATE;TZID=Europe/Zurich:20131218T080000 EXDATE;TZID=Europe/Zurich:20140415T080000 BEGIN:VALARM -TRIGGER:-PT12H +TRIGGER;RELATED=END:-PT12H ACTION:DISPLAY END:VALARM END:VEVENT diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php index e7d1122a..a9dd70c8 100644 --- a/plugins/libkolab/lib/kolab_format_xcal.php +++ b/plugins/libkolab/lib/kolab_format_xcal.php @@ -231,12 +231,12 @@ abstract class kolab_format_xcal extends kolab_format $object['valarms'] = array(); for ($i=0; $i < $valarms->size(); $i++) { $alarm = $valarms->get($i); - $type = $alarm_types[$alarm->type()]; + $type = $alarm_types[$alarm->type()]; if ($type == 'DISPLAY' || $type == 'EMAIL' || $type == 'AUDIO') { // only some alarms are supported $valarm = array( - 'action' => $type, - 'summary' => $alarm->summary(), + 'action' => $type, + 'summary' => $alarm->summary(), 'description' => $alarm->description(), ); @@ -254,12 +254,14 @@ abstract class kolab_format_xcal extends kolab_format } if ($start = self::php_datetime($alarm->start())) { - $object['alarms'] = '@' . $start->format('U'); + $object['alarms'] = '@' . $start->format('U'); $valarm['trigger'] = $start; } else if ($offset = $alarm->relativeStart()) { - $prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-'; - $value = $time = ''; + $prefix = $offset->isNegative() ? '-' : '+'; + $value = ''; + $time = ''; + if ($w = $offset->weeks()) $value .= $w . 'W'; else if ($d = $offset->days()) $value .= $d . 'D'; else if ($h = $offset->hours()) $time .= $h . 'H'; @@ -269,23 +271,29 @@ abstract class kolab_format_xcal extends kolab_format // assume 'at event time' if (empty($value) && empty($time)) { $prefix = ''; - $time = '0S'; + $time = '0S'; } - $object['alarms'] = $prefix . $value . $time; + $object['alarms'] = $prefix . $value . $time; $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : ''); + + if ($alarm->relativeTo() == kolabformat::End) { + $valarm['related'] == 'END'; + } } // read alarm duration and repeat properties if (($duration = $alarm->duration()) && $duration->isValid()) { $value = $time = ''; + if ($w = $duration->weeks()) $value .= $w . 'W'; else if ($d = $duration->days()) $value .= $d . 'D'; else if ($h = $duration->hours()) $time .= $h . 'H'; else if ($m = $duration->minutes()) $time .= $m . 'M'; else if ($s = $duration->seconds()) $time .= $s . 'S'; + $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : ''); - $valarm['repeat'] = $alarm->numrepeat(); + $valarm['repeat'] = $alarm->numrepeat(); } $object['alarms'] .= ':' . $type; // legacy property @@ -508,9 +516,8 @@ abstract class kolab_format_xcal extends kolab_format } else { try { - $prefix = $valarm['trigger'][0]; - $period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger'])); - $duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-'); + $period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger'])); + $duration = new Duration($period->d, $period->h, $period->i, $period->s, $valarm['trigger'][0] == '-'); } catch (Exception $e) { // skip alarm with invalid trigger values @@ -518,7 +525,8 @@ abstract class kolab_format_xcal extends kolab_format continue; } - $alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End); + $related = strtoupper($valarm['related']) == 'END' ? kolabformat::End : kolabformat::Start; + $alarm->setRelativeStart($duration, $related); } if ($valarm['duration']) { diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index 307d4683..ff920ec9 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -355,6 +355,7 @@ class tasklist_ui */ function alarm_select($attrib = array()) { + $attrib['_type'] = 'task'; return $this->plugin->lib->alarm_select($attrib, $this->plugin->driver->alarm_types, $this->plugin->driver->alarm_absolute); }