Refactored alarms in calendar and tasks to support multiple alarms. Moved redundant functions to libcalendaring

This commit is contained in:
Thomas Bruederli 2014-04-17 17:49:00 +02:00
parent e648bee7aa
commit 93d2b69bb9
26 changed files with 547 additions and 197 deletions

View file

@ -763,7 +763,7 @@ class calendar extends rcube_plugin
case "new":
// create UID for new event
$event['uid'] = $this->generate_uid();
$this->prepare_event($event, $action);
$this->write_preprocess($event, $action);
if ($success = $this->driver->new_event($event)) {
$event['id'] = $event['uid'];
$this->cleanup_event($event);
@ -772,20 +772,20 @@ class calendar extends rcube_plugin
break;
case "edit":
$this->prepare_event($event, $action);
$this->write_preprocess($event, $action);
if ($success = $this->driver->edit_event($event))
$this->cleanup_event($event);
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
break;
case "resize":
$this->prepare_event($event, $action);
$this->write_preprocess($event, $action);
$success = $this->driver->resize_event($event);
$reload = $event['_savemode'] ? 2 : 1;
break;
case "move":
$this->prepare_event($event, $action);
$this->write_preprocess($event, $action);
$success = $this->driver->move_event($event);
$reload = $success && $event['_savemode'] ? 2 : 1;
break;
@ -1327,8 +1327,10 @@ class calendar extends rcube_plugin
private function _client_event($event, $addcss = false)
{
// compose a human readable strings for alarms_text and recurrence_text
if ($event['alarms'])
$event['alarms_text'] = libcalendaring::alarms_text($event['alarms']);
if ($event['valarms']) {
$event['alarms_text'] = libcalendaring::alarms_text($event['valarms']);
$event['valarms'] = libcalendaring::to_client_alarms($event['valarms']);
}
if ($event['recurrence']) {
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
if ($event['recurrence']['UNTIL'])
@ -1555,7 +1557,7 @@ class calendar extends rcube_plugin
/**
* Prepares new/edited event properties before save
*/
private function prepare_event(&$event, $action)
private function write_preprocess(&$event, $action)
{
// convert dates into DateTime objects in user's current timezone
$event['start'] = new DateTime($event['start'], $this->timezone);
@ -1584,6 +1586,11 @@ class calendar extends rcube_plugin
}, $event['recurrence']['RDATE']);
}
// convert the submitted alarm values
if ($event['valarms']) {
$event['valarms'] = libcalendaring::from_client_alarms($event['valarms']);
}
$attachments = array();
$eventid = 'cal:'.$event['id'];
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {

View file

@ -81,7 +81,6 @@ function rcube_calendar_ui(settings)
var date2unixtime = this.date2unixtime;
var fromunixtime = this.fromunixtime;
var parseISO8601 = this.parseISO8601;
var init_alarms_edit = this.init_alarms_edit;
/*** private methods ***/
@ -311,7 +310,7 @@ function rcube_calendar_ui(settings)
if (event.recurrence && event.recurrence_text)
$('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text));
if (event.alarms && event.alarms_text)
if (event.valarms && event.alarms_text)
$('#event-alarm').show().children('.event-text').html(Q(event.alarms_text));
if (calendar.name)
@ -519,34 +518,10 @@ function rcube_calendar_ui(settings)
else {
allday.checked = false;
}
// set alarm(s)
// TODO: support multiple alarm entries
if (event.alarms || action != 'new') {
if (typeof event.alarms == 'string')
event.alarms = event.alarms.split(';');
var valarms = event.alarms || [''];
for (var alarm, i=0; i < valarms.length; i++) {
alarm = String(valarms[i]).split(':');
if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY';
$('#eventedit select.edit-alarm-type').val(alarm[1]);
if (alarm[0].match(/@(\d+)/)) {
var ondate = fromunixtime(parseInt(RegExp.$1));
$('#eventedit select.edit-alarm-offset').val('@');
$('#eventedit input.edit-alarm-date').val($.fullCalendar.formatDate(ondate, settings['date_format']));
$('#eventedit input.edit-alarm-time').val($.fullCalendar.formatDate(ondate, settings['time_format']));
}
else if (alarm[0].match(/([-+])(\d+)([MHD])/)) {
$('#eventedit input.edit-alarm-value').val(RegExp.$2);
$('#eventedit select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3);
}
}
}
// set correct visibility by triggering onchange handlers
$('#eventedit select.edit-alarm-type, #eventedit select.edit-alarm-offset').change();
me.set_alarms_edit('#edit-alarms', action != 'new' && event.valarms && calendar.alarms ? event.valarms : []);
// enable/disable alarm property according to backend support
$('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')]();
@ -690,23 +665,12 @@ function rcube_calendar_ui(settings)
sensitivity: sensitivity.val(),
status: eventstatus.val(),
recurrence: '',
alarms: '',
valarms: me.serialize_alarms('#edit-alarms'),
attendees: event_attendees,
deleted_attachments: rcmail.env.deleted_attachments,
attachments: []
};
// serialize alarm settings
// TODO: support multiple alarm entries
var alarm = $('#eventedit select.edit-alarm-type').val();
if (alarm) {
var val, offset = $('#eventedit select.edit-alarm-offset').val();
if (offset == '@')
data.alarms = '@' + date2unixtime(parse_datetime($('#eventedit input.edit-alarm-time').val(), $('#eventedit input.edit-alarm-date').val())) + ':' + alarm;
else if ((val = parseInt($('#eventedit input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
data.alarms = offset[0] + val + offset[1] + ':' + alarm;
}
// uploaded attachments list
for (var i in rcmail.env.attachments)
if (i.match(/^rcmfile(.+)/))
@ -3260,7 +3224,7 @@ function rcube_calendar_ui(settings)
});
// register events on alarm fields
init_alarms_edit('#eventedit');
me.init_alarms_edit('#edit-alarms');
// toggle recurrence frequency forms
$('#edit-recurrence-frequency').change(function(e){

View file

@ -3,12 +3,11 @@
*
* Plugin to add a calendar to Roundcube.
*
* @version @package_version@
* @author Lazlo Westerhof
* @author Thomas Bruederli
* @url http://rc-calendar.lazlo.me
* @licence GNU AGPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
* @copyright (c) 2014 Kolab Systems AG
*
**/

View file

@ -3,13 +3,12 @@
*
* Plugin to add a calendar to RoundCube.
*
* @version @package_version@
* @author Lazlo Westerhof
* @author Albert Lee
* @author Aleksander Machniak <machniak@kolabsys.com>
* @url http://rc-calendar.lazlo.me
* @licence GNU AGPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
* @copyright (c) 2014 Kolab Systems AG
*
**/

View file

@ -3,13 +3,12 @@
*
* Plugin to add a calendar to Roundcube.
*
* @version @package_version@
* @author Lazlo Westerhof
* @author Thomas Bruederli
* @author Albert Lee
* @url http://rc-calendar.lazlo.me
* @licence GNU AGPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
* @copyright (c) 2014 Kolab Systems AG
*
**/

View file

@ -459,10 +459,14 @@ class database_driver extends calendar_driver
if (isset($event['allday'])) {
$event['all_day'] = $event['allday'] ? 1 : 0;
}
// compute absolute time to notify the user
$event['notifyat'] = $this->_get_notification($event);
if (is_array($event['valarms'])) {
$event['alarms'] = $this->serialize_alarms($event['valarms']);
}
// process event attendees
$_attendees = '';
foreach ((array)$event['attendees'] as $attendee) {
@ -484,10 +488,10 @@ class database_driver extends calendar_driver
*/
private function _get_notification($event)
{
if ($event['alarms'] && $event['start'] > new DateTime()) {
if ($event['valarms'] && $event['start'] > new DateTime()) {
$alarm = libcalendaring::get_next_alarm($event);
if ($alarm['time'] && $alarm['action'] == 'DISPLAY')
if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types))
return date('Y-m-d H:i:s', $alarm['time']);
}
@ -877,7 +881,12 @@ class database_driver extends calendar_driver
else {
$event['attendees'] = array();
}
// decode serialized alarms
if ($event['alarms']) {
$event['valarms'] = $this->unserialize_alarms($event['alarms']);
}
unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['_attachments']);
return $event;
}
@ -1088,4 +1097,48 @@ class database_driver extends calendar_driver
return $this->rc->db->affected_rows($query);
}
/**
* Helper method to serialize the list of alarms into a string
*/
private function serialize_alarms($valarms)
{
foreach ((array)$valarms as $i => $alarm) {
if ($alarm['trigger'] instanceof DateTime) {
$valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c');
}
}
return $valarms ? json_encode($valarms) : null;
}
/**
* Helper method to decode a serialized list of alarms
*/
private function unserialize_alarms($alarms)
{
// decode json serialized alarms
if ($alarms && $alarms[0] == '[') {
$valarms = json_decode($alarms, true);
foreach ($valarms as $i => $alarm) {
if ($alarm['trigger'][0] == '@') {
try {
$valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1));
}
catch (Exception $e) {
unset($valarms[$i]);
}
}
}
}
// convert legacy alarms data
else if (strlen($alarms)) {
list($trigger, $action) = explode(':', $alarms, 2);
if ($trigger = libcalendaring::parse_alaram_value($trigger)) {
$valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0]));
}
}
return $valarms;
}
}

View file

@ -7,11 +7,11 @@
**/
CREATE TABLE IF NOT EXISTS `kolab_alarms` (
`event_id` VARCHAR(255) NOT NULL,
`alarm_id` VARCHAR(255) NOT NULL,
`user_id` int(10) UNSIGNED NOT NULL,
`notifyat` DATETIME DEFAULT NULL,
`dismissed` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY(`event_id`),
PRIMARY KEY(`alarm_id`),
CONSTRAINT `fk_kolab_alarms_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */;

View file

@ -0,0 +1 @@
ALTER TABLE `kolab_alarms` CHANGE `event_id` `alarm_id` VARCHAR(255) NOT NULL;

View file

@ -6,7 +6,7 @@
**/
CREATE TABLE IF NOT EXISTS kolab_alarms (
event_id character varying(255) NOT NULL,
alarm_id character varying(255) NOT NULL,
user_id integer NOT NULL
REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
notifyat timestamp without time zone DEFAULT NULL,

View file

@ -33,7 +33,7 @@ class kolab_driver extends calendar_driver
public $freebusy = true;
public $attachments = true;
public $undelete = true;
public $alarm_types = array('DISPLAY');
public $alarm_types = array('DISPLAY','AUDIO');
public $categoriesimmutable = true;
private $rc;
@ -834,7 +834,7 @@ class kolab_driver extends calendar_driver
$time = $slot + $interval;
$events = array();
$candidates = array();
$query = array(array('tags', '=', 'x-has-alarms'));
foreach ($this->calendars as $cid => $calendar) {
// skip calendars with alarms disabled
@ -844,41 +844,48 @@ class kolab_driver extends calendar_driver
foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
// add to list if alarm is set
$alarm = libcalendaring::get_next_alarm($e);
if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') {
$id = $e['id'];
$events[$id] = $e;
$events[$id]['notifyat'] = $alarm['time'];
if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) {
$id = $alarm['id']; // use alarm-id as primary identifier
$candidates[$id] = array(
'id' => $id,
'title' => $e['title'],
'location' => $e['location'],
'start' => $e['start'],
'end' => $e['end'],
'notifyat' => $alarm['time'],
'action' => $alarm['action'],
);
}
}
}
// get alarm information stored in local database
if (!empty($events)) {
$event_ids = array_map(array($this->rc->db, 'quote'), array_keys($events));
if (!empty($candidates)) {
$alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
$result = $this->rc->db->query(sprintf(
"SELECT * FROM kolab_alarms
WHERE event_id IN (%s) AND user_id=?",
join(',', $event_ids),
WHERE alarm_id IN (%s) AND user_id=?",
join(',', $alarm_ids),
$this->rc->db->now()
),
$this->rc->user->ID
);
while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
$dbdata[$e['event_id']] = $e;
$dbdata[$e['alarm_id']] = $e;
}
}
$alarms = array();
foreach ($events as $id => $e) {
// skip dismissed
foreach ($candidates as $id => $alarm) {
// skip dismissed alarms
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $e['notifyat'];
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat'];
if ($notifyat <= $time)
$alarms[] = $e;
$alarms[] = $alarm;
}
return $alarms;
@ -889,13 +896,13 @@ class kolab_driver extends calendar_driver
*
* @see calendar_driver::dismiss_alarm()
*/
public function dismiss_alarm($event_id, $snooze = 0)
public function dismiss_alarm($alarm_id, $snooze = 0)
{
// delete old alarm entry
$this->rc->db->query(
"DELETE FROM kolab_alarms
WHERE event_id=? AND user_id=?",
$event_id,
WHERE alarm_id=? AND user_id=?",
$alarm_id,
$this->rc->user->ID
);
@ -904,9 +911,9 @@ class kolab_driver extends calendar_driver
$query = $this->rc->db->query(
"INSERT INTO kolab_alarms
(event_id, user_id, dismissed, notifyat)
(alarm_id, user_id, dismissed, notifyat)
VALUES(?, ?, ?, ?)",
$event_id,
$alarm_id,
$this->rc->user->ID,
$snooze > 0 ? 0 : 1,
$notifyat

View file

@ -535,6 +535,44 @@ td.topalign {
vertical-align: top;
}
#eventedit .edit-alarm-item {
position: relative;
padding-right: 30px;
margin-bottom: 2px;
}
#eventedit .edit-alarm-buttons {
position: absolute;
top: 2px;
right: 0;
}
#eventedit .edit-alarm-buttons a.iconlink {
display: none;
width: 18px;
height: 17px;
padding: 1px;
text-indent: -5000px;
overflow: hidden;
}
#eventedit .edit-alarm-buttons a.add-alarm {
background: url(images/plus.png) 1px 1px no-repeat;
}
#eventedit .edit-alarm-buttons a.delete-alarm {
background: url(images/delete.png) 1px 1px no-repeat;
}
#eventedit .edit-alarm-buttons a.delete-alarm,
#eventedit .first .edit-alarm-buttons a.add-alarm {
display: inline-block;
}
#eventedit .first .edit-alarm-buttons a.delete-alarm {
display: none;
}
#eventedit label.weekday,
#eventedit label.monthday {
min-width: 3em;

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -41,8 +41,14 @@
<input type="text" name="endtime" size="6" id="edit-endtime" />
</div>
<div class="event-section" id="edit-alarms">
<label for="edit-alarm"><roundcube:label name="calendar.alarms" /></label>
<roundcube:object name="plugin.alarm_select" />
<div class="edit-alarm-item first">
<label for="edit-alarm"><roundcube:label name="calendar.alarms" /></label>
<roundcube:object name="plugin.alarm_select" />
<span class="edit-alarm-buttons">
<a href="#add" class="iconlink add add-alarm">+</a>
<a href="#delete" class="iconlink delete delete-alarm">-</a>
</span>
</div>
</div>
<div class="event-section" id="calendar-select">
<label for="edit-calendar"><roundcube:label name="calendar.calendar" /></label>

View file

@ -37,8 +37,14 @@
<input type="text" name="endtime" size="6" id="edit-endtime" />
</div>
<div class="event-section" id="edit-alarms">
<label for="edit-alarm"><roundcube:label name="calendar.alarms" /></label>
<roundcube:object name="plugin.alarm_select" />
<div class="edit-alarm-item first">
<label><roundcube:label name="calendar.alarms" /></label>
<roundcube:object name="plugin.alarm_select" />
<span class="edit-alarm-buttons">
<a href="#add" class="iconlink add add-alarm">+</a>
<a href="#delete" class="iconlink delete delete-alarm">-</a>
</span>
</div>
</div>
<div class="event-section" id="calendar-select">
<label for="edit-calendar"><roundcube:label name="calendar.calendar" /></label>

View file

@ -311,9 +311,87 @@ function rcube_libcalendaring(settings)
$(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
});
$(prefix+' .edit-alarm-date').datepicker(datepicker_settings);
$(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings);
$(prefix).on('click', 'a.delete-alarm', function(e){
if ($(this).closest('.edit-alarm-item').siblings().length > 0) {
$(this).closest('.edit-alarm-item').remove();
}
return false;
});
$(prefix).on('click', 'a.add-alarm', function(e){
var i = $(this).closest('.edit-alarm-item').siblings().length + 1;
var item = $(this).closest('.edit-alarm-item').clone(false)
.removeClass('first')
.appendTo(prefix);
me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')');
$('select.edit-alarm-type, select.edit-alarm-offset', item).change();
return false;
});
}
this.set_alarms_edit = function(prefix, valarms)
{
$(prefix + ' .edit-alarm-item:gt(0)').remove();
var i, alarm, domnode, val, offset;
for (i=0; i < valarms.length; i++) {
alarm = valarms[i];
if (!alarm.action)
alarm.action = 'DISPLAY';
if (i == 0) {
domnode = $(prefix + ' .edit-alarm-item').eq(0);
}
else {
domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix);
this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')');
}
$('select.edit-alarm-type', domnode).val(alarm.action);
if (String(alarm.trigger).match(/@(\d+)/)) {
var ondate = this.fromunixtime(parseInt(RegExp.$1));
$('select.edit-alarm-offset', domnode).val('@');
$('input.edit-alarm-value', domnode).val('');
$('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1));
$('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2));
}
else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) {
val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3;
$('input.edit-alarm-value', domnode).val(val);
$('select.edit-alarm-offset', domnode).val(offset);
}
}
// set correct visibility by triggering onchange handlers
$(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change();
};
this.serialize_alarms = function(prefix)
{
var valarms = [];
$(prefix + ':visible .edit-alarm-item').each(function(i, elem){
var val, offset, alarm = { action: $('select.edit-alarm-type', elem).val() };
if (alarm.action) {
offset = $('select.edit-alarm-offset', elem).val();
if (offset == '@') {
alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val()));
}
else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) {
alarm.trigger = offset[0] + val + offset[1];
}
valarms.push(alarm);
}
});
return valarms;
};
/***** Alarms handling *****/

View file

@ -345,25 +345,89 @@ class libcalendaring extends rcube_plugin
public static function parse_alaram_value($val)
{
if ($val[0] == '@') {
return array(substr($val, 1));
return array(new DateTime($val));
}
else if (preg_match('/([+-])P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
if ($m[1] == '')
$m[1] = '+';
foreach ($m2 as $seg) {
$prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT';
if ($seg[1] > 0) { // ignore zero values
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2]);
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
}
// return zero value nevertheless
return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
}
return false;
}
/**
* Convert the alarms list items to be processed on the client
*/
public static function to_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'] instanceof DateTime) {
$alarm['trigger'] = '@' . $alarm['trigger']->format('U');
}
else if ($trigger = self::parse_alaram_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[2];
}
return $alarm;
}, (array)$valarms);
}
/**
* Process the alarms values submitted by the client
*/
public static function from_client_alarms($valarms)
{
return array_map(function($alarm){
if ($alarm['trigger'][0] == '@') {
try { $alarm['trigger'] = new DateTime($alarm['trigger']); }
catch (Exception $e) { /* handle this ? */ }
}
else if ($trigger = libcalendaring::parse_alaram_value($alarm['trigger'])) {
$alarm['trigger'] = $trigger[3];
}
return $alarm;
}, (array)$valarms);
}
/**
* Render localized text for alarm settings
*/
public static function alarms_text($alarm)
public static function alarms_text($alarms)
{
list($trigger, $action) = explode(':', $alarm);
if (is_array($alarms) && is_array($alarms[0])) {
$texts = array();
foreach ($alarms as $alarm) {
if ($text = self::alarm_text($alarm))
$texts[] = $text;
}
return join(', ', $texts);
}
else {
return self::alarm_text($alarms);
}
}
/**
* Render localized text for a single alarm property
*/
public static function alarm_text($alarm)
{
if (is_string($alarm)) {
list($trigger, $action) = explode(':', $alarm);
}
else {
$trigger = $alarm['trigger'];
$action = $alarm['action'];
}
$text = '';
$rcube = rcube::get_instance();
@ -375,19 +439,33 @@ class libcalendaring extends rcube_plugin
case 'DISPLAY':
$text = $rcube->gettext('libcalendaring.alarmdisplay');
break;
case 'AUDIO':
$text = $rcube->gettext('libcalendaring.alarmaudio');
break;
}
if (preg_match('/@(\d+)/', $trigger, $m)) {
if ($trigger instanceof DateTime) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($trigger))
));
}
else if (preg_match('/@(\d+)/', $trigger, $m)) {
$text .= ' ' . $rcube->gettext(array(
'name' => 'libcalendaring.alarmat',
'vars' => array('datetime' => $rcube->format_date($m[1]))
));
}
else if ($val = self::parse_alaram_value($trigger)) {
$text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext('libcalendaring.trigger' . $val[1]);
// 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]);
}
else
else {
return false;
}
return $text;
}
@ -400,53 +478,78 @@ class libcalendaring extends rcube_plugin
*/
public static function get_next_alarm($rec, $type = 'event')
{
if (!$rec['alarms'] || $rec['cancelled'] || $rec['status'] == 'CANCELLED')
if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED')
return null;
if ($type == 'task') {
$timezone = self::get_instance()->timezone;
if ($rec['date'])
$rec['start'] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
if ($rec['startdate'])
$rec['end'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
$rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
if ($rec['date'])
$rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
}
if (!$rec['end'])
$rec['end'] = $rec['start'];
// TODO: handle multiple alarms (currently not supported)
list($trigger, $action) = explode(':', $rec['alarms'], 2);
$notify = self::parse_alaram_value($trigger);
if (!empty($notify[1])){ // offset
$mult = 1;
switch ($notify[1]) {
case '-S': $mult = -1; break;
case '+S': $mult = 1; break;
case '-M': $mult = -60; break;
case '+M': $mult = 60; break;
case '-H': $mult = -3600; break;
case '+H': $mult = 3600; break;
case '-D': $mult = -86400; break;
case '+D': $mult = 86400; break;
case '-W': $mult = -604800; break;
case '+W': $mult = 604800; break;
// support legacy format
if (!$rec['valarms']) {
list($trigger, $action) = explode(':', $rec['alarms'], 2);
if ($alarm = self::parse_alaram_value($trigger)) {
$rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0]));
}
$offset = $notify[0] * $mult;
$refdate = $mult > 0 ? $rec['end'] : $rec['start'];
// abort of no reference date is available to compute notification time
if (!is_a($refdate, 'DateTime'))
return null;
$notify_at = $refdate->format('U') + $offset;
}
else { // absolute timestamp
$notify_at = $notify[0];
}
return array('time' => $notify_at, 'action' => $action ? strtoupper($action) : 'DISPLAY');
$expires = new DateTime('now - 12 hours');
$alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility
// handle multiple alarms
$notify_at = null;
foreach ($rec['valarms'] as $alarm) {
$notify_time = null;
if ($alarm['trigger'] instanceof DateTime) {
$notify_time = $alarm['trigger'];
}
else if (is_string($alarm['trigger'])) {
$refdate = $alarm['trigger'][0] == '+' ? $rec['end'] : $rec['start'];
// abort if no reference date is available to compute notification time
if (!is_a($refdate, 'DateTime'))
continue;
// TODO: for all-day events, take start @ 00:00 as reference date ?
try {
$interval = new DateInterval(trim($alarm['trigger'], '+-'));
$interval->invert = $alarm['trigger'][0] != '+';
$notify_time = clone $refdate;
$notify_time->add($interval);
}
catch (Exception $e) {
rcube::raise_error($e, true);
continue;
}
}
if ($notify_time && (!$notify_at || ($notify_time < $notify_at && $notify_time > $expires))) {
$notify_at = $notify_time;
$action = $alarm['action'];
$alarm_prop = $alarm;
// generate a unique alarm ID if multiple alarms are set
if (count($rec['valarms']) > 1) {
$alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis');
}
}
}
return !$notify_at ? null : array(
'time' => $notify_at->format('U'),
'action' => $action ? strtoupper($action) : 'DISPLAY',
'id' => $alarm_id,
'prop' => $alarm_prop,
);
}
/**

View file

@ -1025,9 +1025,10 @@ class libvcalendar implements Iterator
$va = VObject\Component::create('VALARM');
list($trigger, $va->action) = explode(':', $event['alarms']);
$val = libcalendaring::parse_alaram_value($trigger);
$period = $val[1] && preg_match('/[HMS]$/', $val[1]) ? 'PT' : 'P';
if ($val[1]) $va->add('TRIGGER', preg_replace('/^([-+])P?T?(.+)/', "\\1$period\\2", $trigger));
else $va->add('TRIGGER', gmdate('Ymd\THis\Z', $val[0]), array('VALUE' => 'DATE-TIME'));
if ($val[3])
$va->add('TRIGGER', $val[3]);
else if ($val[0] instanceof DateTime)
$va->add(self::datetime_prop('TRIGGER', $val[0]));
$ve->add($va);
}

View file

@ -4,8 +4,10 @@ $labels = array();
$labels['alarmemail'] = 'Send Email';
$labels['alarmdisplay'] = 'Show message';
$labels['alarmaudio'] = 'Play sound';
$labels['alarmdisplayoption'] = 'Message';
$labels['alarmemailoption'] = 'Email';
$labels['alarmaudiooption'] = 'Sound';
$labels['alarmat'] = 'at $datetime';
$labels['trigger@'] = 'on date';
$labels['trigger-M'] = 'minutes before';
@ -14,6 +16,7 @@ $labels['trigger-D'] = 'days before';
$labels['trigger+M'] = 'minutes after';
$labels['trigger+H'] = 'hours after';
$labels['trigger+D'] = 'days after';
$labels['triggerattime'] = 'at time';
$labels['addalarm'] = 'add alarm';
$labels['alarmtitle'] = 'Upcoming events';

View file

@ -1,7 +1,7 @@
/**
* Roundcube libcalendaring plugin styles for skin "Larry"
*
* Copyright (c) 2012, Kolab Systems AG <contact@kolabsys.com>
* Copyright (c) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
@ -75,4 +75,39 @@ a.reply-comment-toggle {
.popup textarea.itip-comment {
width: 98%;
}
}
.edit-alarm-item {
position: relative;
padding-right: 30px;
margin-bottom: 0.2em;
}
.edit-alarm-buttons {
position: absolute;
top: 1px;
right: 0;
}
.edit-alarm-buttons a.iconlink {
display: none;
width: 18px;
height: 17px;
padding: 1px;
text-indent: -5000px;
overflow: hidden;
}
.edit-alarm-buttons a.delete-alarm {
background-position: -7px -377px;
}
.edit-alarm-buttons a.delete-alarm,
.edit-alarm-item.first .edit-alarm-buttons a.add-alarm {
display: inline-block;
}
.edit-alarm-item.first .edit-alarm-buttons a.delete-alarm {
display: none;
}

View file

@ -182,7 +182,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
$this->assertEquals('-H', $alarm[1], "Alarm unit");
$this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "Full alarm item (action)");
$this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
$this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
// alarm trigger with 0 values
$events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8');
@ -193,6 +193,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
$this->assertEquals('30', $alarm[0], "Alarm value");
$this->assertEquals('-M', $alarm[1], "Alarm unit");
$this->assertEquals('-30M', $alarm[2], "Alarm string");
$this->assertEquals('-PT30M', $alarm[3], "Unified alarm string (stripped zero-values)");
$this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "First alarm action");
$this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text");

View file

@ -482,6 +482,12 @@ class tasklist_database_driver extends tasklist_driver
if (!$rec['parent_id'])
unset($rec['parent_id']);
// decode serialized alarms
if ($rec['alarms']) {
$rec['valarms'] = $this->unserialize_alarms($rec['alarms']);
unset($rec['alarms']);
}
unset($rec['task_id'], $rec['tasklist_id'], $rec['created']);
return $rec;
}
@ -500,6 +506,10 @@ class tasklist_database_driver extends tasklist_driver
if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly'])
return false;
if (is_array($prop['valarms'])) {
$prop['alarms'] = $this->serialize_alarms($prop['valarms']);
}
foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) {
if (empty($prop[$col]))
$prop[$col] = null;
@ -542,6 +552,10 @@ class tasklist_database_driver extends tasklist_driver
*/
public function edit_task($prop)
{
if (is_array($prop['valarms'])) {
$prop['alarms'] = $this->serialize_alarms($prop['valarms']);
}
$sql_set = array();
foreach (array('title', 'description', 'flagged', 'complete') as $col) {
if (isset($prop[$col]))
@ -655,14 +669,58 @@ class tasklist_database_driver extends tasklist_driver
*/
private function _get_notification($task)
{
if ($task['alarms'] && $task['complete'] < 1 || strpos($task['alarms'], '@') !== false) {
if ($task['valarms'] && $task['complete'] < 1) {
$alarm = libcalendaring::get_next_alarm($task, 'task');
if ($alarm['time'] && $alarm['action'] == 'DISPLAY')
if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types))
return date('Y-m-d H:i:s', $alarm['time']);
}
return null;
}
/**
* Helper method to serialize the list of alarms into a string
*/
private function serialize_alarms($valarms)
{
foreach ((array)$valarms as $i => $alarm) {
if ($alarm['trigger'] instanceof DateTime) {
$valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c');
}
}
return $valarms ? json_encode($valarms) : null;
}
/**
* Helper method to decode a serialized list of alarms
*/
private function unserialize_alarms($alarms)
{
// decode json serialized alarms
if ($alarms && $alarms[0] == '[') {
$valarms = json_decode($alarms, true);
foreach ($valarms as $i => $alarm) {
if ($alarm['trigger'][0] == '@') {
try {
$valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1));
}
catch (Exception $e) {
unset($valarms[$i]);
}
}
}
}
// convert legacy alarms data
else if (strlen($alarms)) {
list($trigger, $action) = explode(':', $alarms, 2);
if ($trigger = libcalendaring::parse_alaram_value($trigger)) {
$valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0]));
}
}
return $valarms;
}
}

View file

@ -28,7 +28,7 @@ class tasklist_kolab_driver extends tasklist_driver
public $alarms = false;
public $attachments = true;
public $undelete = false; // task undelete action
public $alarm_types = array('DISPLAY');
public $alarm_types = array('DISPLAY','AUDIO');
private $rc;
private $plugin;
@ -477,7 +477,7 @@ class tasklist_kolab_driver extends tasklist_driver
$time = $slot + $interval;
$tasks = array();
$candidates = array();
$query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete'));
foreach ($this->lists as $lid => $list) {
// skip lists with alarms disabled
@ -486,40 +486,46 @@ class tasklist_kolab_driver extends tasklist_driver
$folder = $this->folders[$lid];
foreach ($folder->select($query) as $record) {
if (!$record['alarms'] || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-)
if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-)
continue;
$task = $this->_to_rcube_task($record);
// add to list if alarm is set
$alarm = libcalendaring::get_next_alarm($task, 'task');
if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') {
$id = $task['id'];
$tasks[$id] = $task;
$tasks[$id]['notifyat'] = $alarm['time'];
if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) {
$id = $alarm['id']; // use alarm-id as primary identifier
$candidates[$id] = array(
'id' => $id,
'title' => $task['title'],
'date' => $task['date'],
'time' => $task['time'],
'notifyat' => $alarm['time'],
'action' => $alarm['action'],
);
}
}
}
// get alarm information stored in local database
if (!empty($tasks)) {
$task_ids = array_map(array($this->rc->db, 'quote'), array_keys($tasks));
if (!empty($candidates)) {
$alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
$result = $this->rc->db->query(sprintf(
"SELECT * FROM kolab_alarms
WHERE event_id IN (%s) AND user_id=?",
join(',', $task_ids),
WHERE alarm_id IN (%s) AND user_id=?",
join(',', $alarm_ids),
$this->rc->db->now()
),
$this->rc->user->ID
);
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$dbdata[$rec['event_id']] = $rec;
$dbdata[$rec['alarm_id']] = $rec;
}
}
$alarms = array();
foreach ($tasks as $id => $task) {
foreach ($candidates as $id => $task) {
// skip dismissed
if ($dbdata[$id]['dismissed'])
continue;
@ -545,7 +551,7 @@ class tasklist_kolab_driver extends tasklist_driver
// delete old alarm entry
$this->rc->db->query(
"DELETE FROM kolab_alarms
WHERE event_id=? AND user_id=?",
WHERE alarm_id=? AND user_id=?",
$id,
$this->rc->user->ID
);
@ -555,7 +561,7 @@ class tasklist_kolab_driver extends tasklist_driver
$query = $this->rc->db->query(
"INSERT INTO kolab_alarms
(event_id, user_id, dismissed, notifyat)
(alarm_id, user_id, dismissed, notifyat)
VALUES(?, ?, ?, ?)",
$id,
$this->rc->user->ID,
@ -599,7 +605,10 @@ class tasklist_kolab_driver extends tasklist_driver
$task['changed'] = $record['dtstamp'];
}
if ($record['alarms']) {
if ($record['valarms']) {
$task['valarms'] = $record['valarms'];
}
else if ($record['alarms']) {
$task['alarms'] = $record['alarms'];
}

View file

@ -32,8 +32,14 @@
<a href="#nodate" style="margin-left:1em" class="edit-nodate" rel="#taskedit-date,#taskedit-time"><roundcube:label name="tasklist.nodate" /></a>
</div>
<div class="form-section" id="taskedit-alarms">
<label for="taskedit-alarm"><roundcube:label name="tasklist.alarms" /></label>
<roundcube:object name="plugin.alarm_select" />
<div class="edit-alarm-item first">
<label><roundcube:label name="tasklist.alarms" /></label>
<roundcube:object name="plugin.alarm_select" />
<span class="edit-alarm-buttons">
<a href="#add" class="iconlink add add-alarm">+</a>
<a href="#delete" class="iconlink delete delete-alarm">-</a>
</span>
</div>
</div>
<div class="form-section">
<label for="taskedit-completeness"><roundcube:label name="tasklist.complete" /></label>

View file

@ -107,7 +107,6 @@ function rcube_tasklist_ui(settings)
var parse_datetime = this.parse_datetime;
var date2unixtime = this.date2unixtime;
var fromunixtime = this.fromunixtime;
var init_alarms_edit = this.init_alarms_edit;
/**
* initialize the tasks UI
@ -380,7 +379,7 @@ function rcube_tasklist_ui(settings)
});
// register events on alarm fields
init_alarms_edit('#taskedit');
me.init_alarms_edit('#taskedit-alarms');
$('#taskedit-date, #taskedit-startdate').datepicker(datepicker_settings);
@ -1169,7 +1168,7 @@ function rcube_tasklist_ui(settings)
if (rcmail.busy || !list.editable || (action == 'edit' && (!rec || rec.readonly)))
return false;
me.selected_task = $.extend({ alarms:'' }, rec); // clone task object
me.selected_task = $.extend({ valarms:[] }, rec); // clone task object
rec = me.selected_task;
// assign temporary id
@ -1210,29 +1209,7 @@ function rcube_tasklist_ui(settings)
});
// set alarm(s)
if (rec.alarms || action != 'new') {
var valarms = (typeof rec.alarms == 'string' ? rec.alarms.split(';') : rec.alarms) || [''];
for (var alarm, i=0; i < valarms.length; i++) {
alarm = String(valarms[i]).split(':');
if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY';
$('#taskedit select.edit-alarm-type').val(alarm[1]);
if (alarm[0].match(/@(\d+)/)) {
var ondate = fromunixtime(parseInt(RegExp.$1));
$('#taskedit select.edit-alarm-offset').val('@');
$('#taskedit input.edit-alarm-date').val(me.format_datetime(ondate, 1));
$('#taskedit input.edit-alarm-time').val(me.format_datetime(ondate, 2));
}
else if (alarm[0].match(/([-+])(\d+)([MHD])/)) {
$('#taskedit input.edit-alarm-value').val(RegExp.$2);
$('#taskedit select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3);
}
break; // only one alarm is currently supported
}
}
// set correct visibility by triggering onchange handlers
$('#taskedit select.edit-alarm-type, #taskedit select.edit-alarm-offset').change();
me.set_alarms_edit('#taskedit-alarms', action != 'new' && rec.valarms ? rec.valarms : []);
// attachments
rcmail.enable_command('remove-attachment', list.editable);
@ -1263,6 +1240,7 @@ function rcube_tasklist_ui(settings)
});
me.selected_task.tags = [];
me.selected_task.attachments = [];
me.selected_task.valarms = me.serialize_alarms('#taskedit-alarms');
// do some basic input validation
if (!me.selected_task.title || !me.selected_task.title.length) {
@ -1289,16 +1267,6 @@ function rcube_tasklist_ui(settings)
me.selected_task.tags.push(newtag);
}
// serialize alarm settings
var alarm = $('#taskedit select.edit-alarm-type').val();
if (alarm) {
var val, offset = $('#taskedit select.edit-alarm-offset').val();
if (offset == '@')
me.selected_task.alarms = '@' + date2unixtime(parse_datetime($('#taskedit input.edit-alarm-time').val(), $('#taskedit input.edit-alarm-date').val())) + ':' + alarm;
else if ((val = parseInt($('#taskedit input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
me.selected_task.alarms = offset[0] + val + offset[1] + ':' + alarm;
}
// uploaded attachments list
for (var i in rcmail.env.attachments) {
if (i.match(/^rcmfile(.+)/))

View file

@ -406,9 +406,16 @@ class tasklist extends rcube_plugin
$rec['tags'] = array_filter((array)$rec['tags']);
}
// alarms cannot work without a date
if ($rec['alarms'] && !$rec['date'] && !$rec['startdate'] && strpos($rec['alarms'], '@') === false)
$rec['alarms'] = '';
// convert the submitted alarm values
if ($rec['valarms']) {
$valarms = array();
foreach (libcalendaring::from_client_alarms($rec['valarms']) as $alarm) {
// alarms can only work with a date (either task start, due or absolute alarm date)
if (is_a($alarm['trigger'], 'DateTime') || $rec['date'] || $rec['startdate'])
$valarms[] = $alarm;
}
$rec['valarms'] = $valarms;
}
$attachments = array();
$taskid = $rec['id'];
@ -663,8 +670,10 @@ class tasklist extends rcube_plugin
}
}
if ($rec['alarms'])
$rec['alarms_text'] = libcalendaring::alarms_text($rec['alarms']);
if ($rec['valarms']) {
$rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']);
$rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']);
}
foreach ((array)$rec['attachments'] as $k => $attachment) {
$rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);