2 commits - plugins/calendar plugins/libcalendaring plugins/libkolab
Thomas Brüderli
bruederli at kolabsys.com
Tue Jul 8 12:39:49 CEST 2014
plugins/calendar/calendar.php | 15
plugins/calendar/calendar_ui.js | 21
plugins/calendar/config.inc.php.dist | 3
plugins/calendar/drivers/calendar_driver.php | 12
plugins/calendar/drivers/database/database_driver.php | 2
plugins/calendar/drivers/kolab/kolab_calendar.php | 28
plugins/calendar/drivers/kolab/kolab_driver.php | 84 ++
plugins/calendar/drivers/kolab/kolab_invitation_calendar.php | 324 +++++++++++
plugins/calendar/localization/en_US.inc | 2
plugins/calendar/skins/larry/calendar.css | 49 +
plugins/calendar/skins/larry/templates/calendar.html | 2
plugins/libcalendaring/lib/libcalendaring_itip.php | 12
plugins/libcalendaring/libcalendaring.php | 30 -
plugins/libcalendaring/localization/en_US.inc | 2
plugins/libkolab/lib/kolab_format_event.php | 6
plugins/libkolab/lib/kolab_format_task.php | 5
plugins/libkolab/lib/kolab_format_xcal.php | 23
17 files changed, 586 insertions(+), 34 deletions(-)
New commits:
commit 7affe524f1d9b3f9a301394091f9e632e0de82f2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Jul 8 12:36:34 2014 +0200
List virtual calendars showing pending/declined inivtations (#1796)
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index a370c22..ac7ee9f 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -234,6 +234,9 @@ class calendar extends rcube_plugin
if (!$this->itip) {
require_once($this->home . '/lib/calendar_itip.php');
$this->itip = new calendar_itip($this);
+
+ if ($this->rc->config->get('kolab_invitation_calendars'))
+ $this->itip->set_rsvp_actions(array('accepted','tentative','declined','needs-action'));
}
return $this->itip;
@@ -883,12 +886,13 @@ class calendar extends rcube_plugin
break;
case "rsvp":
+ $status = get_input_value('status', RCUBE_INPUT_GPC);
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$event = $ev;
- if ($success = $this->driver->edit_event($event)) {
- $status = get_input_value('status', RCUBE_INPUT_GPC);
+ if ($success = $this->driver->edit_rsvp($event, $status)) {
+ $reload = $event['calendar'] != $ev['calendar'] ? 2 : 1;
$organizer = null;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
@@ -947,7 +951,7 @@ class calendar extends rcube_plugin
if ($reload > 1)
$args['refetch'] = true;
else if ($success && $action != 'remove')
- $args['update'] = $this->_client_event($this->driver->get_event($event));
+ $args['update'] = $this->_client_event($this->driver->get_event($event), true);
$this->rc->output->command('plugin.refresh_calendar', $args);
}
}
@@ -1314,6 +1318,7 @@ class calendar extends rcube_plugin
$settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
$settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
+ $settings['invitation_calendars'] = (bool)$this->rc->config->get('kolab_invitation_calendars', false);
// get user identity to create default attendee
if ($this->ui->screen == 'calendar') {
@@ -2398,7 +2403,7 @@ class calendar extends rcube_plugin
else
$error_msg = $this->gettext('newerversionexists');
}
- else if (!$existing && $status != 'declined') {
+ else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) {
$success = $this->driver->new_event($event);
}
else if ($status == 'declined')
@@ -2609,7 +2614,7 @@ class calendar extends rcube_plugin
/**
* Get a list of email addresses of the current user (from login and identities)
*/
- private function get_user_emails()
+ public function get_user_emails()
{
return $this->lib->get_user_emails();
}
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 66525f2..b833918 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -384,6 +384,9 @@ function rcube_calendar_ui(settings)
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
me.selected_event = event;
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
// allow other plugins to do actions when event form is opened
rcmail.triggerEvent('calendar-event-init', {o: event});
@@ -408,7 +411,7 @@ function rcube_calendar_ui(settings)
$('#event-alarm').show().children('.event-text').html(Q(event.alarms_text));
if (calendar.name)
- $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).attr('class', 'event-text').addClass('cal-'+calendar.id);
+ $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).attr('class', 'event-text cal-'+calendar.id).css('color', calendar.textColor || calendar.color || '');
if (event.categories)
$('#event-category').show().children('.event-text').html(Q(event.categories)).attr('class', 'event-text cat-'+String(event.categories).toLowerCase().replace(rcmail.identifier_expr, ''));
if (event.free_busy)
@@ -1926,10 +1929,16 @@ function rcube_calendar_ui(settings)
data.status = response.toUpperCase();
}
event_show_dialog(me.selected_event);
-
+
// submit status change to server
- me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
- rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response });
+ var submit_data = $.extend({}, me.selected_event, { source:null });
+ if (settings.invitation_calendars) {
+ update_event('rsvp', submit_data, { status:response });
+ }
+ else {
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('event', { action:'rsvp', e:submit_data, status:response });
+ }
}
};
@@ -1962,10 +1971,10 @@ function rcube_calendar_ui(settings)
}
// post the given event data to server
- var update_event = function(action, data)
+ var update_event = function(action, data, add)
{
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
- rcmail.http_post('calendar/event', { action:action, e:data });
+ rcmail.http_post('calendar/event', $.extend({ action:action, e:data }, (add || {})));
// render event temporarily into the calendar
if ((data.start && data.end) || data.id) {
diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist
index f09d30f..5216642 100644
--- a/plugins/calendar/config.inc.php.dist
+++ b/plugins/calendar/config.inc.php.dist
@@ -127,6 +127,9 @@ $rcmail_config['calendar_itip_smtp_user'] = 'smtpauth';
// SMTP password used to send (anonymous) itip messages
$rcmail_config['calendar_itip_smtp_pass'] = '123456';
+// show virtual invitation calendars (Kolab driver only)
+$rcmail_config['kolab_invitation_calendars'] = true;
+
// Base URL to build fully qualified URIs to access calendars via CALDAV
// The following replacement variables are supported:
// %h - Current HTTP host
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 9fb6ffb..4c4f516 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -191,6 +191,18 @@ abstract class calendar_driver
abstract function edit_event($event);
/**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status)
+ {
+ return $this->edit_event($event);
+ }
+
+ /**
* Move a single event
*
* @param array Hash array with event properties:
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 3b833ab..158adc9 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -138,7 +138,7 @@ class database_driver extends calendar_driver
'color' => $prefs['color'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
'active' => !in_array($id, $hidden),
- 'group' => 'birthdays',
+ 'group' => 'x-birthdays',
'readonly' => true,
'default' => false,
'children' => false,
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 401b295..706c3cd 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -209,9 +209,10 @@ class kolab_calendar extends kolab_storage_folder_api
* @param string Search query (optional)
* @param boolean Include virtual events (optional)
* @param array Additional parameters to query storage
+ * @param array Additional query to filter events
* @return array A list of event records
*/
- public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = array(), $filter_query = null)
{
// convert to DateTime for comparisons
try {
@@ -227,10 +228,24 @@ class kolab_calendar extends kolab_storage_folder_api
$end = new DateTime('today +10 years');
}
+ // get email addresses of the current user
+ $user_emails = $this->cal->get_user_emails();
+
// query Kolab storage
$query[] = array('dtstart', '<=', $end);
$query[] = array('dtend', '>=', $start);
+ // add query to exclude pending/declined invitations
+ if (empty($filter_query)) {
+ foreach ($user_emails as $email) {
+ $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
+ $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
+ }
+ }
+ else if (is_array($filter_query)) {
+ $query = array_merge($query, $filter_query);
+ }
+
if (!empty($search)) {
$search = mb_strtolower($search);
foreach (rcube_utils::normalize_string($search, true) as $word) {
@@ -240,6 +255,15 @@ class kolab_calendar extends kolab_storage_folder_api
$events = array();
foreach ($this->storage->select($query) as $record) {
+ // post-filter events to skip pending and declined invitations
+ if (empty($filter_query) && is_array($record['attendees'])) {
+ foreach ($record['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], array('NEEDS-ACTION','DECLINED'))) {
+ continue 2;
+ }
+ }
+ }
+
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
@@ -671,7 +695,7 @@ class kolab_calendar extends kolab_storage_folder_api
}
// remove some internal properties which should not be saved
- unset($event['_savemode'], $event['_fromcalendar'], $event['_identity']);
+ unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'], $event['className']);
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index ca14d89..ba634cb 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -25,9 +25,14 @@
require_once(dirname(__FILE__) . '/kolab_calendar.php');
require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
+require_once(dirname(__FILE__) . '/kolab_invitation_calendar.php');
+
class kolab_driver extends calendar_driver
{
+ const INVITATIONS_CALENDAR_PENDING = '--invitation--pending';
+ const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined';
+
// features this backend supports
public $alarms = true;
public $attendees = true;
@@ -202,6 +207,35 @@ class kolab_driver extends calendar_driver
}
}
+ // list virtual calendars showing invitations
+ if ($this->rc->config->get('kolab_invitation_calendars')) {
+ foreach (array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED) as $id) {
+ $cal = new kolab_invitation_calendar($id, $this->cal);
+ $this->calendars[$cal->id] = $cal;
+ if (!$active || $cal->is_active()) {
+ $calendars[$id] = array(
+ 'id' => $cal->id,
+ 'name' => $cal->get_name(),
+ 'listname' => $cal->get_name(),
+ 'editname' => $cal->get_foldername(),
+ 'title' => $cal->get_title(),
+ 'color' => $cal->get_color(),
+ 'readonly' => $cal->readonly,
+ 'showalarms' => $cal->alarms,
+ 'group' => 'x-invitations',
+ 'default' => false,
+ 'active' => $cal->is_active(),
+ 'owner' => $cal->get_owner(),
+ 'children' => false,
+ );
+
+ if (is_object($tree)) {
+ $tree->children[] = $cal;
+ }
+ }
+ }
+ }
+
// append the virtual birthdays calendar
if ($this->rc->config->get('calendar_contact_birthdays', false)) {
$id = self::BIRTHDAY_CALENDAR_ID;
@@ -214,7 +248,7 @@ class kolab_driver extends calendar_driver
'color' => $prefs[$id]['color'],
'active' => $prefs[$id]['active'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
- 'group' => 'birthdays',
+ 'group' => 'x-birthdays',
'readonly' => true,
'default' => false,
'children' => false,
@@ -273,10 +307,13 @@ class kolab_driver extends calendar_driver
* @param string Calendar identifier (encoded imap folder name)
* @return object kolab_calendar Object nor null if calendar doesn't exist
*/
- protected function get_calendar($id)
+ public function get_calendar($id)
{
// create calendar object if necesary
- if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
+ if (!$this->calendars[$id] && in_array($id, array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
+ $this->calendars[$id] = new kolab_invitation_calendar($id, $this->cal);
+ }
+ else if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
$calendar = kolab_calendar::factory($id, $this->cal);
if ($calendar->ready)
$this->calendars[$calendar->id] = $calendar;
@@ -363,7 +400,7 @@ class kolab_driver extends calendar_driver
*/
public function subscribe_calendar($prop)
{
- if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
+ if ($prop['id'] && ($cal = $this->get_calendar($prop['id'])) && is_object($cal->storage)) {
$ret = false;
if (isset($prop['permanent']))
$ret |= $cal->storage->subscribe(intval($prop['permanent']));
@@ -452,6 +489,7 @@ class kolab_driver extends calendar_driver
// don't list the birthday calendar
$this->rc->config->set('calendar_contact_birthdays', false);
+ $this->rc->config->set('kolab_invitation_calendars', false);
return $this->list_calendars();
}
@@ -536,6 +574,29 @@ class kolab_driver extends calendar_driver
}
/**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status)
+ {
+ if (($ret = $this->update_event($event)) && $this->rc->config->get('kolab_invitation_calendars')) {
+ // re-assign to the according (virtual) calendar
+ if (strtoupper($status) == 'DECLINED')
+ $event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
+ else if (strtoupper($status) == 'NEEDS-ACTION')
+ $event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
+ else if ($event['_folder_id'])
+ $event['calendar'] = $event['_folder_id'];
+ }
+
+ return $ret;
+ }
+
+
+ /**
* Move a single event
*
* @see calendar_driver::move_event()
@@ -580,6 +641,7 @@ class kolab_driver extends calendar_driver
{
$success = false;
$savemode = $event['_savemode'];
+ $decline = $event['decline'];
if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
$event['_savemode'] = $savemode;
@@ -664,7 +726,15 @@ class kolab_driver extends calendar_driver
}
default: // 'all' is default
- $success = $storage->delete_event($master, $force);
+ if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
+ // don't delete but set PARTSTAT=DECLINED
+ if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
+ $success = $storage->update_event($master);
+ }
+ }
+
+ if (!$success)
+ $success = $storage->delete_event($master, $force);
break;
}
}
@@ -1227,7 +1297,9 @@ class kolab_driver extends calendar_driver
public function calendar_form($action, $calendar, $formfields)
{
// show default dialog for birthday calendar
- if ($calendar['id'] == self::BIRTHDAY_CALENDAR_ID) {
+ if (in_array($calendar['id'], array(self::BIRTHDAY_CALENDAR_ID, self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
+ if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID)
+ unset($formfields['showalarms']);
return parent::calendar_form($action, $calendar, $formfields);
}
diff --git a/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php b/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php
new file mode 100644
index 0000000..ab5fe4b
--- /dev/null
+++ b/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php
@@ -0,0 +1,324 @@
+<?php
+
+/**
+ * Kolab calendar storage class simulating a virtual calendar listing pedning/declined invitations
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_invitation_calendar
+{
+ public $id = '__invitation__';
+ public $ready = true;
+ public $alarms = false;
+ public $readonly = true;
+ public $attachments = false;
+ public $subscriptions = false;
+ public $partstats = array('unknown');
+ public $categories = array();
+ public $name = 'Invitations';
+
+ /**
+ * Default constructor
+ */
+ public function __construct($id, $calendar)
+ {
+ $this->cal = $calendar;
+ $this->id = $id;
+
+ switch ($this->id) {
+ case kolab_driver::INVITATIONS_CALENDAR_PENDING:
+ $this->partstats = array('NEEDS-ACTION');
+ $this->name = $this->cal->gettext('invitationspending');
+ if (!empty($_REQUEST['_quickview']))
+ $this->partstats[] = 'TENTATIVE';
+ break;
+
+ case kolab_driver::INVITATIONS_CALENDAR_DECLINED:
+ $this->partstats = array('DECLINED');
+ $this->name = $this->cal->gettext('invitationsdeclined');
+ break;
+ }
+
+ // user-specific alarms settings win
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+ if (isset($prefs[$this->id]['showalarms']))
+ $this->alarms = $prefs[$this->id]['showalarms'];
+ }
+
+
+ /**
+ * Getter for a nice and human readable name for this calendar
+ *
+ * @return string Name of this calendar
+ */
+ public function get_name()
+ {
+ return $this->name;
+ }
+
+
+ /**
+ * Getter for the IMAP folder owner
+ *
+ * @return string Name of the folder owner
+ */
+ public function get_owner()
+ {
+ return $this->cal->rc->get_user_name();
+ }
+
+
+ /**
+ *
+ */
+ public function get_title()
+ {
+ return $this->get_name();
+ }
+
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ return 'x-special';
+ }
+
+
+ /**
+ * Getter for the top-end calendar folder name (not the entire path)
+ *
+ * @return string Name of this calendar
+ */
+ public function get_foldername()
+ {
+ return $this->get_name();
+ }
+
+ /**
+ * Return color to display this calendar
+ */
+ public function get_color()
+ {
+ // calendar color is stored in local user prefs
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+
+ if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
+ return $prefs[$this->id]['color'];
+
+ return 'ffffff';
+ }
+
+ /**
+ * Compose an URL for CalDAV access to this calendar (if configured)
+ */
+ public function get_caldav_url()
+ {
+ return false;
+ }
+
+ /**
+ * Check activation status of this folder
+ *
+ * @return boolean True if enabled, false if not
+ */
+ public function is_active()
+ {
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array()); // read local prefs
+ return (bool)$prefs[$this->id]['active'];
+ }
+
+ /**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ // don't change anything.
+ // let kolab_driver save props in local prefs
+ return $prop['id'];
+ }
+
+
+ /**
+ * Getter for a single event object
+ */
+ public function get_event($id)
+ {
+ // redirect call to kolab_driver::get_event()
+ $event = $this->cal->driver->get_event($id, true);
+
+ if (is_array($event)) {
+ // add pointer to original calendar folder
+ $event['_folder_id'] = $event['calendar'];
+ $event = $this->_mod_event($event);
+ }
+
+ return $event;
+ }
+
+
+ /**
+ * @param integer Event's new start (unix timestamp)
+ * @param integer Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param boolean Include virtual events (optional)
+ * @param array Additional parameters to query storage
+ * @return array A list of event records
+ */
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+ {
+ // convert to DateTime for comparisons
+ try {
+ $start_dt = new DateTime('@'.$start);
+ }
+ catch (Exception $e) {
+ $start_dt = new DateTime('@0');
+ }
+ try {
+ $end_dt = new DateTime('@'.$end);
+ }
+ catch (Exception $e) {
+ $end_dt = new DateTime('today +10 years');
+ }
+
+ // get email addresses of the current user
+ $user_emails = $this->cal->get_user_emails();
+ $subquery = array();
+ foreach ($user_emails as $email) {
+ foreach ($this->partstats as $partstat) {
+ $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':' . strtolower($partstat));
+ }
+ }
+
+ // aggregate events from all calendar folders
+ $events = array();
+ foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ foreach ($cal->list_events($start, $end, $search, 1, $query, array(array($subquery, 'OR'))) as $event) {
+ $match = false;
+
+ // post-filter events to skip pending and declined invitations
+ if (is_array($event['attendees'])) {
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $this->partstats)) {
+ $match = true;
+ break;
+ }
+ }
+ }
+
+ if ($match) {
+ $events[$event['id']] = $this->_mod_event($event);
+ }
+ }
+
+ // merge list of event categories (really?)
+ $this->categories += $cal->categories;
+ }
+
+ return $events;
+ }
+
+ /**
+ * Helper method to modify some event properties
+ */
+ private function _mod_event($event)
+ {
+ // set classes according to PARTSTAT
+ if (is_array($event['attendees'])) {
+ $user_emails = $this->cal->get_user_emails();
+ $partstat = 'UNKNOWN';
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails)) {
+ $partstat = $attendee['status'];
+ break;
+ }
+ }
+
+ if (in_array($partstat, $this->partstats)) {
+ $event['className'] = 'fc-invitation-' . strtolower($partstat);
+ $event['calendar'] = $this->id;
+ }
+ }
+
+ return $event;
+ }
+
+
+ /**
+ * Create a new event record
+ *
+ * @see calendar_driver::new_event()
+ *
+ * @return mixed The created record ID on success, False on error
+ */
+ public function insert_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Update a specific event record
+ *
+ * @see calendar_driver::new_event()
+ * @return boolean True on success, False on error
+ */
+
+ public function update_event($event, $exception_id = null)
+ {
+ // forward call to the actual storage folder
+ if ($event['_folder_id']) {
+ $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+ if ($cal && $cal->ready) {
+ return $cal->update_event($event, $exception_id);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Delete an event record
+ *
+ * @see calendar_driver::remove_event()
+ * @return boolean True on success, False on error
+ */
+ public function delete_event($event, $force = true)
+ {
+ return false;
+ }
+
+ /**
+ * Restore deleted event record
+ *
+ * @see calendar_driver::undelete_event()
+ * @return boolean True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ return false;
+ }
+
+
+}
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 52da746..308364f 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -95,6 +95,8 @@ $labels['calendarsubscribe'] = 'List permanently';
$labels['nocalendarsfound'] = 'No calendars found';
$labels['nrcalendarsfound'] = '$nr calendars found';
$labels['quickview'] = 'View only this calendar';
+$labels['invitationspending'] = 'Pending invitations';
+$labels['invitationsdeclined'] = 'Declined invitations';
// agenda view
$labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index c79e334..e44a0eb 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -237,6 +237,11 @@ pre {
left: 20px;
}
+#calendars .treelist li.x-birthdays span.calname,
+#calendars .treelist li.x-invitations span.calname {
+ font-style: italic;
+}
+
#calendars .treelist.flat li span.calname {
left: 24px;
right: 42px;
@@ -1524,6 +1529,43 @@ a.dropdown-link:after {
top: -5000px;
}
+.fc-invitation-declined {
+
+}
+
+.fc-event-vert.fc-invitation-needs-action,
+.fc-event-hori.fc-invitation-needs-action {
+ border: 1px dashed #5757c7 !important;
+}
+
+.fc-event-vert.fc-invitation-tentative,
+.fc-event-hori.fc-invitation-tentative {
+ border: 1px dashed #eb8900 !important;
+}
+
+.fc-event-vert.fc-invitation-declined,
+.fc-event-hori.fc-invitation-declined {
+ border: 1px dashed #c00 !important;
+}
+
+.fc-event-vert.fc-invitation-tentative .fc-event-head,
+.fc-event-vert.fc-invitation-declined .fc-event-head,
+.fc-event-vert.fc-invitation-needs-action .fc-event-head {
+/* background-color: transparent !important; */
+}
+
+.fc-event-vert.fc-invitation-tentative .fc-event-bg {
+ background: url(data:image/gif;base64,R0lGODlhCAAIAPABAOuJAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
+.fc-event-vert.fc-invitation-needs-action .fc-event-bg {
+ background: url(data:image/gif;base64,R0lGODlhCAAIAPABAFdXx////yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
+.fc-event-vert.fc-invitation-declined .fc-event-bg {
+ background: url(data:image/gif;base64,R0lGODlhCAAIAPABAMwAAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
.calendarmain .fc-event:focus {
outline: 1px solid rgba(71,135,177, 0.4);
-webkit-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
@@ -1767,7 +1809,8 @@ div.calendar-invitebox .rsvp-status.hint {
div.calendar-invitebox .rsvp-status.declined,
div.calendar-invitebox .rsvp-status.tentative,
div.calendar-invitebox .rsvp-status.accepted,
-div.calendar-invitebox .rsvp-status.delegated {
+div.calendar-invitebox .rsvp-status.delegated,
+div.calendar-invitebox .rsvp-status.needs-action {
padding: 0 0 1px 22px;
background: url(images/attendee-status.png) 2px -20px no-repeat;
}
@@ -1784,6 +1827,10 @@ div.calendar-invitebox .rsvp-status.delegated {
background-position: 2px -180px;
}
+div.calendar-invitebox .rsvp-status.needs-action {
+ background-position: 2px 0;
+}
+
/* iTIP attend reply page */
.calendaritipattend .centerbox {
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 6e7ed49..d608157 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -130,7 +130,7 @@
<div class="event-text"></div>
</div>
- <roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" style="display:none" />
+ <roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" class="event-dialog-message" style="display:none" />
</div>
<roundcube:include file="/templates/eventedit.html" />
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index ec4eb1a..66b20eb 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -30,6 +30,8 @@ class libcalendaring_itip
protected $sender;
protected $domain;
protected $itip_send = false;
+ protected $rsvp_actions = array('accepted','tentative','declined');
+ protected $rsvp_status = array('accepted','tentative','declined','delegated');
function __construct($plugin, $domain = 'libcalendaring')
{
@@ -52,6 +54,12 @@ class libcalendaring_itip
$this->sender['email'] = $email;
}
+ public function set_rsvp_actions($actions)
+ {
+ $this->rsvp_actions = (array)$actions;
+ // $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
+ }
+
/**
* Wrapper for rcube_plugin::gettext()
* Checking for a label in different domains
@@ -276,7 +284,7 @@ class libcalendaring_itip
$html = html::div('rsvp-status', $this->gettext('notanattendee'));
$action = 'import';
}
- else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED','DELEGATED'))) {
+ else if (in_array(strtolower($status), $this->rsvp_status)) {
$html = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
if ($existing && ($existing['sequence'] > $event['sequence'] || (!$event['sequence'] && $existing['changed'] && $existing['changed'] > $event['changed']))) {
@@ -402,7 +410,7 @@ class libcalendaring_itip
$metadata['rsvp'] = true;
// 1. display RSVP buttons (if the user was invited)
- foreach (array('accepted','tentative','declined') as $method) {
+ foreach ($this->rsvp_actions as $method) {
$rsvp_buttons .= html::tag('input', array(
'type' => 'button',
'class' => "button $method",
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 5888ff0..5a1a8b0 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -329,6 +329,13 @@ class libcalendaring extends rcube_plugin
*/
public function get_user_emails()
{
+ static $emails;
+
+ // return cached result
+ if (is_array($emails)) {
+ return $emails;
+ }
+
$emails = array();
$plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
$emails = array_map('strtolower', $plugin['emails']);
@@ -342,7 +349,28 @@ class libcalendaring extends rcube_plugin
$emails[] = strtolower($identity['email']);
}
- return array_unique($emails);
+ $emails = array_unique($emails);
+ return $emails;
+ }
+
+ /**
+ * Set the given participant status to the attendee matching the current user's identities
+ *
+ * @param array Hash array with event struct
+ * @param string The PARTSTAT value to set
+ * @return mixed Email address of the updated attendee or False if none matching found
+ */
+ public function set_partstat(&$event, $status)
+ {
+ $emails = $this->get_user_emails();
+ foreach ((array)$event['attendees'] as $i => $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $event['attendees'][$i]['status'] = strtoupper($status);
+ return $attendee['email'];
+ }
+ }
+
+ return false;
}
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index d89833c..588185a 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -75,6 +75,7 @@ $labels['itipreply'] = 'Reply to';
$labels['itipaccepted'] = 'Accept';
$labels['itiptentative'] = 'Maybe';
$labels['itipdeclined'] = 'Decline';
+$labels['itipneeds-action'] = 'Postpone';
$labels['itipcomment'] = 'Your response';
$labels['itipeditresponse'] = 'Enter a response text';
$labels['itipsendercomment'] = 'Sender\'s comment: ';
@@ -96,6 +97,7 @@ $labels['youhaveaccepted'] = 'You have accepted this invitation';
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
$labels['youhavedeclined'] = 'You have declined this invitation';
$labels['youhavedelegated'] = 'You have delegated this invitation';
+$labels['youhaveneeds-action'] = 'You have copied this invitation into your calendar';
$labels['attendeeaccepted'] = 'Participant has accepted';
$labels['attendeetentative'] = 'Participant has tentatively accepted';
$labels['attendeedeclined'] = 'Participant has declined';
commit bdf2faafae42282e52694af90c3d21ceed69949c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Tue Jul 8 12:32:05 2014 +0200
Store tags to enable partstat queries (#1796)
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index c0bcef4..c9a1c9f 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -193,16 +193,12 @@ class kolab_format_event extends kolab_format_xcal
*/
public function get_tags()
{
- $tags = array();
+ $tags = parent::get_tags();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
- if (!empty($this->data['valarms'])) {
- $tags[] = 'x-has-alarms';
- }
-
return $tags;
}
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index b3c6a42..87c3df9 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -111,7 +111,7 @@ class kolab_format_task extends kolab_format_xcal
*/
public function get_tags()
{
- $tags = array();
+ $tags = parent::get_tags();
if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
$tags[] = 'x-complete';
@@ -119,9 +119,6 @@ class kolab_format_task extends kolab_format_xcal
if ($this->data['priority'] == 1)
$tags[] = 'x-flagged';
- if (!empty($this->data['valarms']))
- $tags[] = 'x-has-alarms';
-
if ($this->data['parent_id'])
$tags[] = 'x-parent:' . $this->data['parent_id'];
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 0742f2a..c9e06ac 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -560,4 +560,27 @@ abstract class kolab_format_xcal extends kolab_format
return array_unique(rcube_utils::normalize_string($data, true));
}
+ /**
+ * Callback for kolab_storage_cache to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags()
+ {
+ $tags = array();
+
+ if (!empty($this->data['valarms'])) {
+ $tags[] = 'x-has-alarms';
+ }
+
+ // create tags reflecting participant status
+ if (is_array($this->data['attendees'])) {
+ foreach ($this->data['attendees'] as $attendee) {
+ if (!empty($attendee['email']) && !empty($attendee['status']))
+ $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
+ }
+ }
+
+ return $tags;
+ }
}
\ No newline at end of file
More information about the commits
mailing list