4 commits - plugins/calendar plugins/libcalendaring plugins/libkolab plugins/tasklist
Thomas Brüderli
bruederli at kolabsys.com
Thu Apr 17 17:49:13 CEST 2014
plugins/calendar/calendar.php | 21 -
plugins/calendar/calendar_ui.js | 48 --
plugins/calendar/drivers/database/SQL/mysql.initial.sql | 3
plugins/calendar/drivers/database/SQL/postgres.initial.sql | 3
plugins/calendar/drivers/database/SQL/sqlite.initial.sql | 3
plugins/calendar/drivers/database/database_driver.php | 63 +++
plugins/calendar/drivers/kolab/SQL/mysql.initial.sql | 4
plugins/calendar/drivers/kolab/SQL/mysql/2014041700.sql | 1
plugins/calendar/drivers/kolab/SQL/postgres.initial.sql | 2
plugins/calendar/drivers/kolab/kolab_driver.php | 47 +-
plugins/calendar/skins/classic/calendar.css | 38 ++
plugins/calendar/skins/classic/images/delete.png |binary
plugins/calendar/skins/classic/images/plus.png |binary
plugins/calendar/skins/classic/templates/eventedit.html | 10
plugins/calendar/skins/larry/templates/eventedit.html | 10
plugins/libcalendaring/libcalendaring.js | 80 ++++
plugins/libcalendaring/libcalendaring.php | 181 +++++++---
plugins/libcalendaring/libvcalendar.php | 7
plugins/libcalendaring/localization/en_US.inc | 3
plugins/libcalendaring/skins/larry/libcal.css | 39 ++
plugins/libcalendaring/tests/libvcalendar.php | 3
plugins/libkolab/SQL/mysql/2014040900.sql | 8
plugins/libkolab/lib/kolab_format_event.php | 2
plugins/libkolab/lib/kolab_format_task.php | 2
plugins/libkolab/lib/kolab_format_xcal.php | 2
plugins/tasklist/drivers/database/tasklist_database_driver.php | 62 +++
plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php | 41 +-
plugins/tasklist/skins/larry/templates/taskedit.html | 10
plugins/tasklist/tasklist.js | 40 --
plugins/tasklist/tasklist.php | 19 -
30 files changed, 555 insertions(+), 197 deletions(-)
New commits:
commit 93d2b69bb9205ae0ef06848569b29498fbaaa254
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Apr 17 17:49:00 2014 +0200
Refactored alarms in calendar and tasks to support multiple alarms. Moved redundant functions to libcalendaring
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 2666d3c..ef39665 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -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) {
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 04ec392..44681ba 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -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){
diff --git a/plugins/calendar/drivers/database/SQL/mysql.initial.sql b/plugins/calendar/drivers/database/SQL/mysql.initial.sql
index c45b3f2..ed989be 100644
--- a/plugins/calendar/drivers/database/SQL/mysql.initial.sql
+++ b/plugins/calendar/drivers/database/SQL/mysql.initial.sql
@@ -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
*
**/
diff --git a/plugins/calendar/drivers/database/SQL/postgres.initial.sql b/plugins/calendar/drivers/database/SQL/postgres.initial.sql
index 007bbf2..21239c6 100644
--- a/plugins/calendar/drivers/database/SQL/postgres.initial.sql
+++ b/plugins/calendar/drivers/database/SQL/postgres.initial.sql
@@ -3,13 +3,12 @@
*
* Plugin to add a calendar to RoundCube.
*
- * @version @package_version@
* @author Lazlo Westerhof
* @author Albert Lee
* @author Aleksander Machniak <machniak at kolabsys.com>
- * @url http://rc-calendar.lazlo.me
* @licence GNU AGPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
+ * @copyright (c) 2014 Kolab Systems AG
*
**/
diff --git a/plugins/calendar/drivers/database/SQL/sqlite.initial.sql b/plugins/calendar/drivers/database/SQL/sqlite.initial.sql
index 3d35907..078007d 100644
--- a/plugins/calendar/drivers/database/SQL/sqlite.initial.sql
+++ b/plugins/calendar/drivers/database/SQL/sqlite.initial.sql
@@ -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
*
**/
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 77e4951..b4de23b 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -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;
+ }
+
}
diff --git a/plugins/calendar/drivers/kolab/SQL/mysql.initial.sql b/plugins/calendar/drivers/kolab/SQL/mysql.initial.sql
index f10d902..88df960 100644
--- a/plugins/calendar/drivers/kolab/SQL/mysql.initial.sql
+++ b/plugins/calendar/drivers/kolab/SQL/mysql.initial.sql
@@ -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 */;
diff --git a/plugins/calendar/drivers/kolab/SQL/mysql/2014041700.sql b/plugins/calendar/drivers/kolab/SQL/mysql/2014041700.sql
new file mode 100644
index 0000000..9175b55
--- /dev/null
+++ b/plugins/calendar/drivers/kolab/SQL/mysql/2014041700.sql
@@ -0,0 +1 @@
+ALTER TABLE `kolab_alarms` CHANGE `event_id` `alarm_id` VARCHAR(255) NOT NULL;
\ No newline at end of file
diff --git a/plugins/calendar/drivers/kolab/SQL/postgres.initial.sql b/plugins/calendar/drivers/kolab/SQL/postgres.initial.sql
index b869240..e3ef9aa 100644
--- a/plugins/calendar/drivers/kolab/SQL/postgres.initial.sql
+++ b/plugins/calendar/drivers/kolab/SQL/postgres.initial.sql
@@ -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,
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index caa4043..6058dfb 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -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
diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 6c7ca14..40350fa 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -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;
diff --git a/plugins/calendar/skins/classic/images/delete.png b/plugins/calendar/skins/classic/images/delete.png
new file mode 100644
index 0000000..553ae43
Binary files /dev/null and b/plugins/calendar/skins/classic/images/delete.png differ
diff --git a/plugins/calendar/skins/classic/images/plus.png b/plugins/calendar/skins/classic/images/plus.png
new file mode 100644
index 0000000..1a35013
Binary files /dev/null and b/plugins/calendar/skins/classic/images/plus.png differ
diff --git a/plugins/calendar/skins/classic/templates/eventedit.html b/plugins/calendar/skins/classic/templates/eventedit.html
index 7e7170d..7678d0c 100644
--- a/plugins/calendar/skins/classic/templates/eventedit.html
+++ b/plugins/calendar/skins/classic/templates/eventedit.html
@@ -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>
diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html
index 85b3a77..208579f 100644
--- a/plugins/calendar/skins/larry/templates/eventedit.html
+++ b/plugins/calendar/skins/larry/templates/eventedit.html
@@ -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>
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 3808de1..aa9d227 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -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 *****/
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index edc0dde..e71509c 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -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'];
+ // 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]));
+ }
+ }
+
+ $expires = new DateTime('now - 12 hours');
+ $alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility
- // 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;
+ // handle multiple alarms
+ $notify_at = null;
+ foreach ($rec['valarms'] as $alarm) {
+ $notify_time = null;
+
+ if ($alarm['trigger'] instanceof DateTime) {
+ $notify_time = $alarm['trigger'];
}
- $offset = $notify[0] * $mult;
- $refdate = $mult > 0 ? $rec['end'] : $rec['start'];
+ else if (is_string($alarm['trigger'])) {
+ $refdate = $alarm['trigger'][0] == '+' ? $rec['end'] : $rec['start'];
- // abort of no reference date is available to compute notification time
- if (!is_a($refdate, 'DateTime'))
- return null;
+ // abort if no reference date is available to compute notification time
+ if (!is_a($refdate, 'DateTime'))
+ continue;
- $notify_at = $refdate->format('U') + $offset;
- }
- else { // absolute timestamp
- $notify_at = $notify[0];
+ // 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 array('time' => $notify_at, 'action' => $action ? strtoupper($action) : 'DISPLAY');
+ return !$notify_at ? null : array(
+ 'time' => $notify_at->format('U'),
+ 'action' => $action ? strtoupper($action) : 'DISPLAY',
+ 'id' => $alarm_id,
+ 'prop' => $alarm_prop,
+ );
}
/**
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index f7d7ccc..1dda548 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -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);
}
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 5eecd29..c3159ff 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -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';
diff --git a/plugins/libcalendaring/skins/larry/libcal.css b/plugins/libcalendaring/skins/larry/libcal.css
index 62d2947..04fb2f1 100644
--- a/plugins/libcalendaring/skins/larry/libcal.css
+++ b/plugins/libcalendaring/skins/larry/libcal.css
@@ -1,7 +1,7 @@
/**
* Roundcube libcalendaring plugin styles for skin "Larry"
*
- * Copyright (c) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (c) 2012-2014, Kolab Systems AG <contact at 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%;
-}
\ No newline at end of file
+}
+
+.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;
+}
+
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index a20844e..1ca81bb 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -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");
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
index 7d4c0f1..f40d504 100644
--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -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;
+ }
+
}
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index b2d3d56..56dc955 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -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'];
}
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 1c9aa4e..7dc0b40 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -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>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index cba9078..6fb3eb3 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -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(.+)/))
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index d53b0a8..aae960a 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -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']);
commit e648bee7aac2be5f151618c197757dadf2f05d35
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Apr 17 12:36:09 2014 +0200
Force cache-rebulding for events and tasks with alarms
diff --git a/plugins/libkolab/SQL/mysql/2014040900.sql b/plugins/libkolab/SQL/mysql/2014040900.sql
index 61649c1..cfcaa9d 100644
--- a/plugins/libkolab/SQL/mysql/2014040900.sql
+++ b/plugins/libkolab/SQL/mysql/2014040900.sql
@@ -6,3 +6,11 @@ ALTER TABLE `kolab_cache_note` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_file` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_configuration` CHANGE `data` `data` LONGTEXT NOT NULL;
ALTER TABLE `kolab_cache_freebusy` CHANGE `data` `data` LONGTEXT NOT NULL;
+
+-- rebuild cache entries for xcal objects with alarms
+DELETE FROM `kolab_cache_event` WHERE tags LIKE '% x-has-alarms %';
+DELETE FROM `kolab_cache_task` WHERE tags LIKE '% x-has-alarms %';
+
+-- force cache synchronization
+UPDATE `kolab_folders` SET ctag='' WHERE `type` IN ('event','task');
+
commit 2f87e09c3a95e22e9fc669c5bbda9a2c32950a18
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Apr 17 12:10:44 2014 +0200
Check for new valarms property when writing cache tags
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index b6745e7..c0bcef4 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -199,7 +199,7 @@ class kolab_format_event extends kolab_format_xcal
$tags[] = rcube_utils::normalize_string($cat);
}
- if (!empty($this->data['alarms'])) {
+ if (!empty($this->data['valarms'])) {
$tags[] = 'x-has-alarms';
}
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index b60145a..f1311d2 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -121,7 +121,7 @@ class kolab_format_task extends kolab_format_xcal
if ($this->data['priority'] == 1)
$tags[] = 'x-flagged';
- if (!empty($this->data['alarms']))
+ if (!empty($this->data['valarms']))
$tags[] = 'x-has-alarms';
if ($this->data['parent_id'])
commit 2b6706c5ffadc1897e25f2b4f2f9c2351cc3ee5a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Apr 17 12:08:52 2014 +0200
Make status map property visible to derived classes
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 8dbff5d..0742f2a 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -76,7 +76,7 @@ abstract class kolab_format_xcal extends kolab_format
'AUDIO' => Alarm::AudioAlarm,
);
- private $status_map = array(
+ protected $status_map = array(
'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
'IN-PROCESS' => kolabformat::StatusInProcess,
'COMPLETED' => kolabformat::StatusCompleted,
More information about the commits
mailing list