Basic support for recurring events; virtual events are yet not editable
This commit is contained in:
parent
bca398a61c
commit
46b68799ca
7 changed files with 6868 additions and 28 deletions
|
@ -104,7 +104,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
|
|||
}
|
||||
|
||||
var buttons = {};
|
||||
if (calendar.editable) {
|
||||
if (calendar.editable && event.editable !== false) {
|
||||
buttons[rcmail.gettext('edit', 'calendar')] = function() {
|
||||
event_edit_dialog('edit', event);
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ class calendar extends rcube_plugin
|
|||
public $task = '?(?!login|logout).*';
|
||||
public $rc;
|
||||
public $driver;
|
||||
public $home; // declare public to be used in other classes
|
||||
|
||||
public $ical;
|
||||
public $ui;
|
||||
|
@ -383,7 +384,11 @@ class calendar extends rcube_plugin
|
|||
*/
|
||||
function load_events()
|
||||
{
|
||||
$events = $this->driver->load_events(get_input_value('start', RCUBE_INPUT_GET), get_input_value('end', RCUBE_INPUT_GET), get_input_value('source', RCUBE_INPUT_GET));
|
||||
$events = $this->driver->load_events(
|
||||
get_input_value('start', RCUBE_INPUT_GET),
|
||||
get_input_value('end', RCUBE_INPUT_GET),
|
||||
get_input_value('source', RCUBE_INPUT_GET)
|
||||
);
|
||||
echo $this->encode($events);
|
||||
exit;
|
||||
}
|
||||
|
@ -511,6 +516,10 @@ class calendar extends rcube_plugin
|
|||
if ($event['recurrence'])
|
||||
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
|
||||
|
||||
// TEMPORARY: recurring instances are immutable
|
||||
if ($event['recurrence_id'])
|
||||
$event['editable'] = false;
|
||||
|
||||
$json[] = array(
|
||||
'start' => date('c', $event['start']), // ISO 8601 date (added in PHP 5)
|
||||
'end' => date('c', $event['end']), // ISO 8601 date (added in PHP 5)
|
||||
|
@ -579,7 +588,24 @@ class calendar extends rcube_plugin
|
|||
*/
|
||||
private function _recurrence_text($rrule)
|
||||
{
|
||||
// TODO: implement this
|
||||
// TODO: finish this
|
||||
$text = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
|
||||
switch ($rrule['FREQ']) {
|
||||
case 'DAILY':
|
||||
$text .= $this->gettext('days');
|
||||
break;
|
||||
case 'WEEKLY':
|
||||
$text .= $this->gettext('weeks');
|
||||
break;
|
||||
case 'MONTHLY':
|
||||
$text .= $this->gettext('months');
|
||||
break;
|
||||
case 'YEARY':
|
||||
$text .= $this->gettext('years');
|
||||
break;
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -595,5 +621,27 @@ class calendar extends rcube_plugin
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the internal structured data into a vcalendar rrule 2.0 string
|
||||
*/
|
||||
public static function to_rrule($recurrence)
|
||||
{
|
||||
if (is_string($recurrence))
|
||||
return $recurrence;
|
||||
|
||||
$rrule = '';
|
||||
foreach ((array)$recurrence as $k => $val) {
|
||||
$k = strtoupper($k);
|
||||
switch ($k) {
|
||||
case 'UNTIL':
|
||||
$val = gmdate('Ymd\THis', $val);
|
||||
break;
|
||||
}
|
||||
$rrule .= $k . '=' . $val . ';';
|
||||
}
|
||||
|
||||
return $rrule;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -53,6 +53,9 @@ class database_driver extends calendar_driver
|
|||
$this->cal = $cal;
|
||||
$this->rc = $cal->rc;
|
||||
|
||||
// load library classes
|
||||
require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php');
|
||||
|
||||
// read database config
|
||||
$this->db_events = $this->rc->config->get('db_table_events', $this->db_events);
|
||||
$this->db_calendars = $this->rc->config->get('db_table_calendars', $this->db_calendars);
|
||||
|
@ -161,7 +164,11 @@ class database_driver extends calendar_driver
|
|||
$event['alarms'],
|
||||
$event['notifyat']
|
||||
);
|
||||
return $this->rc->db->insert_id($this->sequence_events);
|
||||
|
||||
if ($success = $this->rc->db->insert_id($this->sequence_events))
|
||||
$this->_update_recurring($event);
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -199,7 +206,11 @@ class database_driver extends calendar_driver
|
|||
$event['notifyat'],
|
||||
$event['id']
|
||||
);
|
||||
return $this->rc->db->affected_rows($query);
|
||||
|
||||
if ($success = $this->rc->db->affected_rows($query))
|
||||
$this->_update_recurring($event);
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -211,26 +222,22 @@ class database_driver extends calendar_driver
|
|||
private function _save_preprocess($event)
|
||||
{
|
||||
// compose vcalendar-style recurrencue rule from structured data
|
||||
$rrule = '';
|
||||
if (is_array($event['recurrence'])) {
|
||||
foreach ($event['recurrence'] as $k => $val) {
|
||||
$k = strtoupper($k);
|
||||
switch ($k) {
|
||||
case 'UNTIL':
|
||||
$val = gmdate('Ymd\THis', $val);
|
||||
break;
|
||||
}
|
||||
$rrule .= $k . '=' . $val . ';';
|
||||
}
|
||||
}
|
||||
else if (is_string($event['recurrence']))
|
||||
$rrule = $event['recurrence'];
|
||||
|
||||
$rrule = $event['recurrence'] ? calendar::to_rrule($event['recurrence']) : '';
|
||||
$event['recurrence'] = rtrim($rrule, ';');
|
||||
$event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
|
||||
$event['allday'] = $event['allday'] ? 1 : 0;
|
||||
|
||||
// compute absolute time to notify the user
|
||||
$event['notifyat'] = $this->_get_notification($event);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute absolute time to notify the user
|
||||
*/
|
||||
private function _get_notification($event)
|
||||
{
|
||||
if ($event['alarms']) {
|
||||
list($trigger, $action) = explode(':', $event['alarms']);
|
||||
$notify = calendar::parse_alaram_value($trigger);
|
||||
|
@ -253,12 +260,63 @@ class database_driver extends calendar_driver
|
|||
}
|
||||
|
||||
if ($notify_at > time())
|
||||
$event['notifyat'] = date('Y-m-d H:i:s', $notify_at);
|
||||
return date('Y-m-d H:i:s', $notify_at);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert "fake" entries for recurring occurences of this event
|
||||
*/
|
||||
private function _update_recurring($event)
|
||||
{
|
||||
if (empty($this->calendars))
|
||||
return;
|
||||
|
||||
// clear existing recurrence copies
|
||||
$this->rc->db->query(
|
||||
"DELETE FROM " . $this->db_events . "
|
||||
WHERE recurrence_id=?
|
||||
AND calendar_id IN (" . $this->calendar_ids . ")",
|
||||
$event['id']
|
||||
);
|
||||
|
||||
// create new fake entries
|
||||
if ($event['recurrence']) {
|
||||
// TODO: replace Horde classes with something that has less than 6'000 lines of code
|
||||
$recurrence = new Horde_Date_Recurrence($event['start']);
|
||||
$recurrence->fromRRule20($event['recurrence']);
|
||||
|
||||
$duration = $event['end'] - $event['start'];
|
||||
$next = new Horde_Date($event['start']);
|
||||
while ($next = $recurrence->nextActiveRecurrence(array('year' => $next->year, 'month' => $next->month, 'mday' => $next->mday + 1, 'hour' => $next->hour, 'min' => $next->min, 'sec' => $next->sec))) {
|
||||
$next_ts = $next->timestamp();
|
||||
$notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_ts, 'end' => $next_ts + $duration));
|
||||
$query = $this->rc->db->query(sprintf(
|
||||
"INSERT INTO " . $this->db_events . "
|
||||
(calendar_id, recurrence_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, notifyat)
|
||||
SELECT calendar_id, ?, %s, %s, uid, %s, %s, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, ?
|
||||
FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")",
|
||||
$this->rc->db->now(),
|
||||
$this->rc->db->now(),
|
||||
$this->rc->db->fromunixtime($next_ts),
|
||||
$this->rc->db->fromunixtime($next_ts + $duration)
|
||||
),
|
||||
$event['id'],
|
||||
$notify_at,
|
||||
$event['id']
|
||||
);
|
||||
|
||||
if (!$this->rc->db->affected_rows($query))
|
||||
break;
|
||||
|
||||
// stop adding events for inifinite recurrence after 20 years
|
||||
if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next->year > date('Y') + 20))
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
$event['notifyat'] = null;
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -284,7 +342,11 @@ class database_driver extends calendar_driver
|
|||
$event['notifyat'],
|
||||
$event['id']
|
||||
);
|
||||
return $this->rc->db->affected_rows($query);
|
||||
|
||||
if ($success = $this->rc->db->affected_rows($query))
|
||||
$this->_update_recurring($event);
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -312,7 +374,11 @@ class database_driver extends calendar_driver
|
|||
$event['notifyat'],
|
||||
$event['id']
|
||||
);
|
||||
return $this->rc->db->affected_rows($query);
|
||||
|
||||
if ($success = $this->rc->db->affected_rows($query))
|
||||
$this->_update_recurring($event);
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -329,8 +395,9 @@ class database_driver extends calendar_driver
|
|||
if (!empty($this->calendars)) {
|
||||
$query = $this->rc->db->query(
|
||||
"DELETE FROM " . $this->db_events . "
|
||||
WHERE event_id=?
|
||||
WHERE (event_id=? OR recurrence_id=?)
|
||||
AND calendar_id IN (" . $this->calendar_ids . ")",
|
||||
$event['id'],
|
||||
$event['id']
|
||||
);
|
||||
return $this->rc->db->affected_rows($query);
|
||||
|
|
|
@ -44,6 +44,7 @@ CREATE TABLE `events` (
|
|||
`attendees` text DEFAULT NULL,
|
||||
`notifyat` datetime DEFAULT NULL,
|
||||
PRIMARY KEY(`event_id`),
|
||||
INDEX `recurrence_idx` (`recurrence_id`),
|
||||
CONSTRAINT `fk_events_calendar_id` FOREIGN KEY (`calendar_id`)
|
||||
REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
|
||||
|
|
|
@ -84,14 +84,41 @@ class kolab_calendar
|
|||
*/
|
||||
public function list_events($start, $end)
|
||||
{
|
||||
// use Horde classes to compute recurring instances
|
||||
require_once 'Horde/Date/Recurrence.php';
|
||||
|
||||
$this->_fetch_events();
|
||||
|
||||
$events = array();
|
||||
foreach ($this->events as $id => $event) {
|
||||
// TODO: also list recurring events
|
||||
// list events in requested time window
|
||||
if ($event['start'] <= $end && $event['end'] >= $start) {
|
||||
$events[] = $event;
|
||||
}
|
||||
|
||||
// resolve recurring events (maybe move to _fetch_events() for general use?)
|
||||
if ($event['recurrence']) {
|
||||
$recurrence = new Horde_Date_Recurrence($event['start']);
|
||||
$recurrence->fromRRule20(calendar::to_rrule($event['recurrence']));
|
||||
|
||||
$duration = $event['end'] - $event['start'];
|
||||
$next = new Horde_Date($event['start']);
|
||||
while ($next = $recurrence->nextActiveRecurrence(array('year' => $next->year, 'month' => $next->month, 'mday' => $next->mday + 1, 'hour' => $next->hour, 'min' => $next->min, 'sec' => $next->sec))) {
|
||||
$rec_start = $next->timestamp();
|
||||
$rec_end = $rec_start + $duration;
|
||||
|
||||
// add to output if in range
|
||||
if ($rec_start <= $end && $rec_end >= $start) {
|
||||
$rec_event = $event;
|
||||
$rec_event['recurrence_id'] = $event['id'];
|
||||
$rec_event['start'] = $rec_start;
|
||||
$rec_event['end'] = $rec_end;
|
||||
$events[] = $rec_event;
|
||||
}
|
||||
else if ($start_ts > $end) // stop loop if out of range
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
|
@ -147,6 +174,54 @@ class kolab_calendar
|
|||
$allday = $start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']);
|
||||
if ($allday) // in Roundcube all-day events only go until 23:59:59 of the last day
|
||||
$rec['end-date']--;
|
||||
|
||||
// convert alarm time into internal format
|
||||
if ($rec['alarm']) {
|
||||
$alarm_value = $rec['alarm'];
|
||||
$alarm_unit = 'M';
|
||||
if ($rec['alarm'] % 1440 == 0) {
|
||||
$alarm_value /= 1440;
|
||||
$alarm_unit = 'D';
|
||||
}
|
||||
else if ($rec['alarm'] % 60 == 0) {
|
||||
$alarm_value /= 60;
|
||||
$alarm_unit = 'H';
|
||||
}
|
||||
$alarm_value *= -1;
|
||||
}
|
||||
|
||||
// convert recurrence rules into internal pseudo-vcalendar format
|
||||
if ($recurrence = $rec['recurrence']) {
|
||||
$rrule = array(
|
||||
'FREQ' => strtoupper($recurrence['cycle']),
|
||||
'INTERVAL' => intval($recurrence['interval']),
|
||||
);
|
||||
|
||||
if ($recurrence['range-type'] == 'number')
|
||||
$rrule['COUNT'] = intval($recurrence['range']);
|
||||
else if ($recurrence['range-type'] == 'date')
|
||||
$rrule['UNTIL'] = strtotime($recurrence['range']);
|
||||
|
||||
if ($recurrence['day']) {
|
||||
$byday = array();
|
||||
$prefix = ($rrule['FREQ'] == 'MONTHLY' || $rrule['FREQ'] == 'YEARLY') ? intval($recurrence['daynumber'] ? $recurrence['daynumber'] : 1) : '';
|
||||
foreach ($recurrence['day'] as $day)
|
||||
$byday[] = $prefix . substr(strtoupper($day), 0, 2);
|
||||
$rrule['BYDAY'] = join(',', $byday);
|
||||
}
|
||||
if ($recurrence['daynumber']) {
|
||||
if ($recurrence['type'] == 'monthday')
|
||||
$rrule['BYMONTHDAY'] = $recurrence['daynumber'];
|
||||
else if ($recurrence['type'] == 'yearday')
|
||||
$rrule['BYYEARDAY'] = $recurrence['daynumber'];
|
||||
}
|
||||
if ($rec['month']) {
|
||||
$monthmap = array('january' => 1, 'february' => 2, 'march' => 3, 'april' => 4, 'may' => 5, 'june' => 6, 'july' => 7, 'august' => 8, 'september' => 9, 'october' => 10, 'november' => 11, 'december' => 12);
|
||||
$rrule['BYMONTH'] = strtolower($monthmap[$recurrence['month']]);
|
||||
}
|
||||
|
||||
// TODO: handle exclusions (not yet supported by the internal format)
|
||||
}
|
||||
|
||||
$sensitivity_map = array_flip($this->sensitivity_map);
|
||||
|
||||
|
@ -159,6 +234,8 @@ class kolab_calendar
|
|||
'start' => $rec['start-date'],
|
||||
'end' => $rec['end-date'],
|
||||
'all_day' => $allday,
|
||||
'recurrence' => $rrule,
|
||||
'alarms' => $alarm_value . $alarm_unit,
|
||||
'categories' => $rec['categories'],
|
||||
'free_busy' => $rec['show-time-as'],
|
||||
'priority' => 1, // normal
|
||||
|
|
6646
plugins/calendar/lib/Horde_Date_Recurrence.php
Normal file
6646
plugins/calendar/lib/Horde_Date_Recurrence.php
Normal file
File diff suppressed because it is too large
Load diff
|
@ -91,6 +91,7 @@ $labels['errorsaving'] = "Failed to save changes";
|
|||
$labels['operationfailed'] = "The requested operation failed";
|
||||
|
||||
// recurrence form
|
||||
$labels['repeat'] = 'Repeat';
|
||||
$labels['frequency'] = 'Repeat';
|
||||
$labels['never'] = 'never';
|
||||
$labels['daily'] = 'daily';
|
||||
|
|
Loading…
Add table
Reference in a new issue