diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 5794e01e..d1093024 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -132,6 +132,7 @@ class calendar extends rcube_plugin $this->register_action('index', array($this, 'calendar_view')); $this->register_action('event', array($this, 'event_action')); $this->register_action('calendar', array($this, 'calendar_action')); + $this->register_action('count', array($this, 'count_events')); $this->register_action('load_events', array($this, 'load_events')); $this->register_action('export_events', array($this, 'export_events')); $this->register_action('import_events', array($this, 'import_events')); @@ -1138,6 +1139,27 @@ class calendar extends rcube_plugin exit; } + /** + * Handler for requests fetching event counts for calendars + */ + public function count_events() + { + // don't update session on these requests (avoiding race conditions) + $this->rc->session->nowrite = true; + + $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET); + if (!$start) + $start = (new DateTime('today 00:00:00', $this->timezone))->format('U'); + + $counts = $this->driver->count_events( + rcube_utils::get_input_value('source', rcube_utils::INPUT_GET), + $start, + rcube_utils::get_input_value('end', rcube_utils::INPUT_GET) + ); + + $this->rc->output->command('plugin.update_counts', array('counts' => $counts)); + } + /** * Load event data from an iTip message attachment */ @@ -1187,6 +1209,8 @@ class calendar extends rcube_plugin return; } + $counts = array(); + foreach ($this->driver->list_calendars(true) as $cal) { $events = $this->driver->load_events( rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC), @@ -1201,6 +1225,16 @@ class calendar extends rcube_plugin $this->rc->output->command('plugin.refresh_calendar', array('source' => $cal['id'], 'update' => $this->_client_event($event))); } + + // refresh count for this calendar + if ($cal['counts']) { + $today = new DateTime('today 00:00:00', $this->timezone); + $counts += $this->driver->count_events($cal['id'], $today->format('U')); + } + } + + if (!empty($counts)) { + $this->rc->output->command('plugin.update_counts', array('counts' => $counts)); } } diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index fe11420c..f2ece351 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -67,6 +67,7 @@ function rcube_calendar_ui(settings) var freebusy_ui = { workinhoursonly:false, needsupdate:false }; var freebusy_data = {}; var current_view = null; + var count_sources = []; var exec_deferred = bw.ie6 ? 5 : 1; var sensitivitylabels = { 'public':rcmail.gettext('public','calendar'), 'private':rcmail.gettext('private','calendar'), 'confidential':rcmail.gettext('confidential','calendar') }; var ui_loading = rcmail.set_busy(true, 'loading'); @@ -3123,6 +3124,8 @@ function rcube_calendar_ui(settings) } else fc.fullCalendar('refetchEvents', source); + + fetch_counts(); } // add/update single event object else if (source && p.update) { @@ -3145,6 +3148,7 @@ function rcube_calendar_ui(settings) // refetch all calendars else if (p.refetch) { fc.fullCalendar('refetchEvents'); + fetch_counts(); } // remove temp events @@ -3165,6 +3169,28 @@ function rcube_calendar_ui(settings) return query; }; + // callback from server providing event counts + this.update_counts = function(p) + { + $.each(p.counts, function(cal, count) { + var li = calendars_list.get_item(cal), + bubble = $(li).children('.calendar').find('span.count'); + + if (!bubble.length && count > 0) { + bubble = $('') + .addClass('count') + .appendTo($(li).children('.calendar').first()) + } + + if (count > 0) { + bubble.text(count).show(); + } + else { + bubble.text('').hide(); + } + }); + }; + // callback after an iTip message event was imported this.itip_message_processed = function(data) { @@ -3415,6 +3441,16 @@ function rcube_calendar_ui(settings) } } + // fetch counts for some calendars from the server + var fetch_counts = function() + { + if (count_sources.length) { + setTimeout(function() { + rcmail.http_request('calendar/count', { source:count_sources }); + }, 500); + } + }; + /*** startup code ***/ @@ -3431,6 +3467,9 @@ function rcube_calendar_ui(settings) if (active) { event_sources.push(this.calendars[id]); } + if (cal.counts) { + count_sources.push(id); + } if (!cal.readonly && !this.selected_calendar) { this.selected_calendar = id; @@ -4005,6 +4044,9 @@ function rcube_calendar_ui(settings) // initialize more UI elements (deferred) window.setTimeout(init_calendar_ui, exec_deferred); + // fetch counts for some calendars + fetch_counts(); + // add proprietary css styles if not IE if (!bw.ie) $('div.fc-content').addClass('rcube-fc-content'); @@ -4055,6 +4097,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); }); rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); }); rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); }); + rcmail.addEventListener('plugin.update_counts', function(p){ cal.update_counts(p); }); rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); }); rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); }); rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); }); diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index 702dd22c..b6624cef 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -268,8 +268,8 @@ abstract class calendar_driver /** * Get events from source. * - * @param integer Event's new start (unix timestamp) - * @param integer Event's new end (unix timestamp) + * @param integer Date range start (unix timestamp) + * @param integer Date range end (unix timestamp) * @param string Search query (optional) * @param mixed List of calendar IDs to load events from (either as array or comma-separated string) * @param boolean Include virtual/recurring events (optional) @@ -278,6 +278,16 @@ abstract class calendar_driver */ abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null); + /** + * Get number of events in the given calendar + * + * @param mixed List of calendar IDs to count events (either as array or comma-separated string) + * @param integer Date range start (unix timestamp) + * @param integer Date range end (unix timestamp) + * @return array Hash array with counts grouped by calendar ID + */ + abstract function count_events($calendars, $start, $end = null); + /** * Get a list of pending alarms to be displayed to the user * diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index 781f71e2..c1e37da8 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -843,6 +843,20 @@ class database_driver extends calendar_driver return $events; } + /** + * Get number of events in the given calendar + * + * @param mixed List of calendar IDs to count events (either as array or comma-separated string) + * @param integer Date range start (unix timestamp) + * @param integer Date range end (unix timestamp) + * @return array Hash array with counts grouped by calendar ID + */ + public function count_events($calendars, $start, $end = null) + { + // not implemented + return array(); + } + /** * Convert sql record into a rcube style event object */ diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index c99ca61f..85a3b632 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -334,6 +334,51 @@ class kolab_calendar extends kolab_storage_folder_api return $events; } + /** + * + * @param integer Date range start (unix timestamp) + * @param integer Date range end (unix timestamp) + * @param array Additional query to filter events + * @return integer Count + */ + public function count_events($start, $end = null, $filter_query = null) + { + // convert to DateTime for comparisons + try { + $start = new DateTime('@'.$start); + } + catch (Exception $e) { + $start = new DateTime('@0'); + } + if ($end) { + try { + $end = new DateTime('@'.$end); + } + catch (Exception $e) { + $end = null; + } + } + + // query Kolab storage + $query[] = array('dtend', '>=', $start); + + if ($end) + $query[] = array('dtstart', '<=', $end); + + // add query to exclude pending/declined invitations + if (empty($filter_query)) { + foreach ($this->cal->get_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); + } + + // we rely the Kolab storage query (no post-filtering) + return $this->storage->count($query); + } /** * Create a new event record diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index fb2a1de5..f33267a8 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -243,6 +243,10 @@ class kolab_driver extends calendar_driver 'children' => false, ); + if ($id == self::INVITATIONS_CALENDAR_PENDING) { + $calendars[$id]['counts'] = true; + } + if (is_object($tree)) { $tree->children[] = $cal; } @@ -1029,6 +1033,32 @@ class kolab_driver extends calendar_driver return $events; } + /** + * Get number of events in the given calendar + * + * @param mixed List of calendar IDs to count events (either as array or comma-separated string) + * @param integer Date range start (unix timestamp) + * @param integer Date range end (unix timestamp) + * @return array Hash array with counts grouped by calendar ID + */ + public function count_events($calendars, $start, $end = null) + { + $counts = array(); + + if ($calendars && is_string($calendars)) + $calendars = explode(',', $calendars); + else if (!$calendars) + $calendars = array_keys($this->calendars); + + foreach ($calendars as $cid) { + if ($storage = $this->get_calendar($cid)) { + $counts[$cid] = $storage->count_events($start, $end); + } + } + + return $counts; + } + /** * Get a list of pending alarms to be displayed to the user * diff --git a/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php b/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php index 24fad592..a2e9cf2e 100644 --- a/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_invitation_calendar.php @@ -199,20 +199,6 @@ class kolab_invitation_calendar */ 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(); @@ -232,7 +218,7 @@ class kolab_invitation_calendar 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 + // post-filter events to match out partstats if (is_array($event['attendees'])) { foreach ($event['attendees'] as $attendee) { if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $this->partstats)) { @@ -254,6 +240,36 @@ class kolab_invitation_calendar return $events; } + /** + * + * @param integer Date range start (unix timestamp) + * @param integer Date range end (unix timestamp) + * @return integer Count + */ + public function count_events($start, $end = null) + { + // 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 counts from all calendar folders + $count = 0; + foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) { + $cal = new kolab_calendar($foldername, $this->cal); + if ($cal->get_namespace() == 'other') + continue; + + $count += $cal->count_events($start, $end, array(array($subquery, 'OR'))); + } + + return $count; + } + /** * Helper method to modify some event properties */ diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php index 93bcc0b6..7687e3e5 100644 --- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php @@ -239,6 +239,18 @@ class kolab_user_calendar extends kolab_calendar return $events; } + /** + * + * @param integer Date range start (unix timestamp) + * @param integer Date range end (unix timestamp) + * @return integer Count + */ + public function count_events($start, $end = null) + { + // not implemented + return 0; + } + /** * Helper method to fetch free/busy data for the user and turn it into calendar data */ diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css index 36b374bb..1caff489 100644 --- a/plugins/calendar/skins/larry/calendar.css +++ b/plugins/calendar/skins/larry/calendar.css @@ -221,7 +221,7 @@ pre { right: 45px; cursor: default; background: url(images/calendars.png) right 20px no-repeat; - overflow: hidden; + overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; color: #004458; @@ -275,9 +275,10 @@ pre { top: 2px; right: 22px; padding: 5px 20px 0 6px; - min-width: 40px; +/* min-width: 40px; */ height: 19px; text-align: right; + z-index: 4; } #calendars .treelist div:hover span.actions { @@ -425,6 +426,30 @@ pre { } */ +#calendars .treelist .calendar .count { + position: absolute; + display: inline-block; + top: 5px; + right: 68px; + min-width: 1.3em; + padding: 2px 4px; + background: #005d76; + background: -moz-linear-gradient(top, #005d76 0%, #004558 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558)); + background: -o-linear-gradient(top, #005d76 0%, #004558 100%); + background: -ms-linear-gradient(top, #005d76 0%, #004558 100%); + background: linear-gradient(to bottom, #005d76 0%, #004558 100%); + -webkit-box-shadow: inset 0 1px 1px 0 #002635; + box-shadow: inset 0 1px 1px 0 #002635; + border-radius: 10px; + color: #fff; + text-align: center; + font-style: normal; + font-weight: bold; + text-shadow: none; + z-index: 3; +} + #calendars .searchresults { background: #b0ccd7; margin-top: 8px;