Branch 'dev/recurring-invitations' - 3 commits - plugins/calendar plugins/libkolab

Thomas Brüderli bruederli at kolabsys.com
Mon Feb 16 12:02:34 CET 2015


 plugins/calendar/calendar.php                     |   43 ++++++++++-
 plugins/calendar/calendar_ui.js                   |    1 
 plugins/calendar/drivers/calendar_driver.php      |    7 +
 plugins/calendar/drivers/kolab/kolab_calendar.php |   82 ++++++++++------------
 plugins/calendar/drivers/kolab/kolab_driver.php   |    1 
 plugins/libkolab/lib/kolab_format_event.php       |    9 +-
 plugins/libkolab/lib/kolab_format_task.php        |   15 ++--
 plugins/libkolab/lib/kolab_format_xcal.php        |   18 +++-
 8 files changed, 112 insertions(+), 64 deletions(-)

New commits:
commit d7733e7879c7c16074d11ebf58515c50dddf53bb
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 16 11:06:42 2015 +0100

    Allow to RSVP reply on a single occurence when viewing the event in the calendar preview
    
    This will copy the main event from the iTip invitation with unchanged partstat into the
    user calendar and register a recurrence exception with the selected partsat and send a
    reply for this occurrence only.

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 7682ba0..786cd76 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2655,6 +2655,8 @@ class calendar extends rcube_plugin
     $delete  = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
     $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST));
     $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
+    $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+    $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
 
     $error_msg = $this->gettext('errorimportingevent');
     $success = false;
@@ -2747,7 +2749,7 @@ class calendar extends rcube_plugin
 
         if ($existing) {
           // forward savemode for correct updates of recurring events
-          $existing['_savemode'] = $event['_savemode'];
+          $existing['_savemode'] = $savemode ?: $event['_savemode'];
 
           // only update attendee status
           if ($event['_method'] == 'REPLY') {
@@ -2850,9 +2852,43 @@ class calendar extends rcube_plugin
           if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
             $event['free_busy'] = 'free';
           }
+
+          // if the RSVP reply only refers to a single instance:
+          // store unmodified master event with current instance as exception
+          if (!empty($instance) && $savemode != 'all') {
+            $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event');
+            if ($master['recurrence'] && !$master['_instance']) {
+              // compute recurring events until this instance's date
+              if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) {
+                $recurrence_date->setTime(23,59,59);
+
+                foreach ($this->driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) {
+                  if ($recurring['_instance'] == $instance) {
+                    // copy attendees block with my partstat to exception
+                    $recurring['attendees'] = $event['attendees'];
+                    $master['recurrence']['EXCEPTIONS'][] = $recurring;
+                    $event = $recurring;  // set reference for iTip reply
+                    break;
+                  }
+                }
+
+                $master['calendar'] = $event['calendar'] = $calendar['id'];
+                $success = $this->driver->new_event($master);
+              }
+              else {
+                $master = null;
+              }
+            }
+            else {
+              $master = null;
+            }
+          }
+
           // save to the selected/default calendar
-          $event['calendar'] = $calendar['id'];
-          $success = $this->driver->new_event($event);
+          if (!$master) {
+            $event['calendar'] = $calendar['id'];
+            $success = $this->driver->new_event($event);
+          }
         }
         else if ($status == 'declined')
           $error_msg = null;
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index f9e0d28..ec83d1b 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2434,6 +2434,7 @@ function rcube_calendar_ui(settings)
             _rsvp: (delegate && delegate.rsvp) ? 1 : 0,
             _noreply: noreply,
             _comment: submit_data.comment,
+            _instance: submit_data._instance,
             _savemode: submit_data._savemode
           });
         }


commit aaaa9c58185d96a120893f2f3e3c361d44309e8a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Feb 16 11:00:26 2015 +0100

    Take differing parstat values in recurrence exceptions into account when querying for pending/declined/regular events:
    
    - Colelct partstat tags from recurrence exceptions when caching
    - Querying for 'tags != x-partstat:<email>:needs-action' may miss some valid records
    - Do post-filtering on all events, including recurring instances

diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index e402db9..742830b 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -450,6 +450,7 @@ abstract class calendar_driver
 
       $rcmail = rcmail::get_instance();
       $recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
+      $recurrence_id_format = $event['allday'] ? 'Ymd' : 'Ymd\THis';
 
       // determine a reasonable end date if none given
       if (!$end) {
@@ -465,10 +466,10 @@ abstract class calendar_driver
 
       $i = 0;
       while ($next_event = $recurrence->next_instance()) {
-        $next_event['uid'] = $event['uid'] . '-' . ++$i;
         // add to output if in range
         if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
-          $next_event['id'] = $next_event['uid'];
+          $next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
+          $next_event['id'] = $next_event['uid'] . '-' . $exception['_instance'];
           $next_event['recurrence_id'] = $event['uid'];
           $events[] = $next_event;
         }
@@ -477,7 +478,7 @@ abstract class calendar_driver
         }
 
         // avoid endless recursion loops
-        if ($i > 1000) {
+        if (++$i > 1000) {
           break;
         }
       }
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 06a8244..b5b272f 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -236,35 +236,31 @@ class kolab_calendar extends kolab_storage_folder_api
     $query[] = array('dtstart', '<=', $end);
     $query[] = array('dtend',   '>=', $start);
 
-    // add query to exclude pending/declined invitations
-    if (empty($filter_query) && $this->get_namespace() != 'other') {
-      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)) {
+    if (is_array($filter_query)) {
       $query = array_merge($query, $filter_query);
     }
 
     if (!empty($search)) {
         $search = mb_strtolower($search);
+        $words = rcube_utils::tokenize_string($search, 1);
         foreach (rcube_utils::normalize_string($search, true) as $word) {
             $query[] = array('words', 'LIKE', $word);
         }
     }
+    else {
+      $words = array();
+    }
+
+    // set partstat filter to skip pending and declined invitations
+    if (empty($filter_query) && $this->get_namespace() != 'other') {
+      $partstat_exclude = array('NEEDS-ACTION','DECLINED');
+    }
+    else {
+      $partstat_exclude = array();
+    }
 
     $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']) && $this->get_namespace() != 'other') {
-        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;
 
@@ -272,18 +268,6 @@ class kolab_calendar extends kolab_storage_folder_api
       if ($event['categories'])
         $this->categories[$event['categories']]++;
 
-      // filter events by search query
-      if (!empty($search)) {
-        $hits = 0;
-        $words = rcube_utils::tokenize_string($search, 1);
-        foreach ($words as $word) {
-          $hits += $this->_fulltext_match($event, $word);
-        }
-
-        if ($hits < count($words))  // skip this event if not match with search term
-          continue;
-      }
-
       // list events in requested time window
       if ($event['start'] <= $end && $event['end'] >= $start) {
         unset($event['_attendees']);
@@ -312,24 +296,38 @@ class kolab_calendar extends kolab_storage_folder_api
         if ($add)
           $events[] = $event;
       }
-      
+
       // resolve recurring events
       if ($record['recurrence'] && $virtual == 1) {
         $events = array_merge($events, $this->get_recurring_events($record, $start, $end));
+      }
+    }
 
-        // when searching, only recurrence exceptions may match the query so post-filter the results again
-        if (!empty($search) && $record['recurrence']['EXCEPTIONS']) {
-          $me = $this;
-          $events = array_filter($events, function($event) use ($words, $me) {
-            $hits = 0;
-            foreach ($words as $word) {
-              $hits += $me->_fulltext_match($event, $word, false);
-            }
-            return $hits >= count($words);
-          });
+    // post-filter all events by fulltext search and partstat values
+    $me = $this;
+    $events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
+      // fulltext search
+      if (count($words)) {
+        $hits = 0;
+        foreach ($words as $word) {
+          $hits += $me->_fulltext_match($event, $word, false);
+        }
+        if ($hits < count($words)) {
+          return false;
         }
       }
-    }
+
+      // partstat filter
+      if (count($partstat_exclude) && is_array($event['attendees'])) {
+        foreach ($event['attendees'] as $attendee) {
+          if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $partstat_exclude)) {
+            return false;
+          }
+        }
+      }
+
+      return true;
+    });
 
     // avoid session race conditions that will loose temporary subscriptions
     $this->cal->rc->session->nowrite = true;
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index a5b0f73..0fda1e3 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -223,15 +223,16 @@ class kolab_format_event extends kolab_format_xcal
      *
      * @return array List of tags to save in cache
      */
-    public function get_tags()
+    public function get_tags($obj = null)
     {
-        $tags = parent::get_tags();
+        $tags = parent::get_tags($obj);
+        $object = $obj ?: $this->data;
 
-        foreach ((array)$this->data['categories'] as $cat) {
+        foreach ((array)$object['categories'] as $cat) {
             $tags[] = rcube_utils::normalize_string($cat);
         }
 
-        return $tags;
+        return array_unique($tags);
     }
 
     /**
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index d3ddfe9..4640875 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -121,20 +121,21 @@ class kolab_format_task extends kolab_format_xcal
      *
      * @return array List of tags to save in cache
      */
-    public function get_tags()
+    public function get_tags($obj = null)
     {
-        $tags = parent::get_tags();
+        $tags = parent::get_tags($obj);
+        $object = $obj ?: $this->data;
 
-        if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
+        if ($object['status'] == 'COMPLETED' || ($object['complete'] == 100 && empty($object['status'])))
             $tags[] = 'x-complete';
 
-        if ($this->data['priority'] == 1)
+        if ($object['priority'] == 1)
             $tags[] = 'x-flagged';
 
-        if ($this->data['parent_id'])
-            $tags[] = 'x-parent:' . $this->data['parent_id'];
+        if ($object['parent_id'])
+            $tags[] = 'x-parent:' . $object['parent_id'];
 
-        return $tags;
+        return array_unique($tags);
     }
 
 }
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 6d49ad1..3d7bc27 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -611,23 +611,31 @@ abstract class kolab_format_xcal extends kolab_format
      *
      * @return array List of tags to save in cache
      */
-    public function get_tags()
+    public function get_tags($obj = null)
     {
         $tags = array();
+        $object = $obj ?: $this->data;
 
-        if (!empty($this->data['valarms'])) {
+        if (!empty($object['valarms'])) {
             $tags[] = 'x-has-alarms';
         }
 
         // create tags reflecting participant status
-        if (is_array($this->data['attendees'])) {
-            foreach ($this->data['attendees'] as $attendee) {
+        if (is_array($object['attendees'])) {
+            foreach ($object['attendees'] as $attendee) {
                 if (!empty($attendee['email']) && !empty($attendee['status']))
                     $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
             }
         }
 
-        return $tags;
+        // collect tags from recurrence exceptions
+        if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
+            foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
+                $tags = array_merge($tags, $this->get_tags($exception));
+            }
+        }
+
+        return array_unique($tags);
     }
 
     /**


commit 8a74dc2d28aba4af89d123228fb638e4b4999dbd
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Feb 15 19:09:10 2015 +0100

    Don't copy recurrence_date to future occurrences

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index f61b681..7682ba0 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1676,6 +1676,7 @@ class calendar extends rcube_plugin
     if ($event['recurrence']) {
       $event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
       $event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
+      unset($event['recurrence_date']);
     }
 
     foreach ((array)$event['attachments'] as $k => $attachment) {
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 15f030d..06a8244 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -661,7 +661,7 @@ class kolab_calendar extends kolab_storage_folder_api
    */
   private function _merge_event_data(&$event, $overlay)
   {
-    static $forbidden = array('id','uid','recurrence','organizer','_attachments');
+    static $forbidden = array('id','uid','recurrence','recurrence_date','organizer','_attachments');
 
     foreach ($overlay as $prop => $value) {
       // adjust time of the recurring event instance
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index e414b87..0cd962d 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -962,6 +962,7 @@ class kolab_driver extends calendar_driver
         // save as new exception to master event
         if ($add_exception) {
           $event['_instance'] = $old['_instance'];
+          $event['recurrence_date'] = $old['recurrence_date'];
           $master['recurrence']['EXCEPTIONS'][] = $event;
         }
         $success = $storage->update_event($master);




More information about the commits mailing list