0005072: UID can be duplicate
authorCornelius Weiss <c.weiss@metaways.de>
Wed, 8 Oct 2014 12:10:04 +0000 (14:10 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Mon, 27 Jul 2015 11:16:33 +0000 (13:16 +0200)
allow same event in different containers

* introduce base_container_id
* check existance by UID AND container

https://forge.tine20.org/view.php?id=5072

Change-Id: I73c4d4a0a6c46a67f388d612e18b71ab6671baae
Reviewed-on: http://gerrit.tine20.com/customers/2065
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
18 files changed:
tests/tine20/Calendar/Controller/EventTests.php
tests/tine20/Calendar/Controller/MSEventFacadeTest.php
tests/tine20/Calendar/Controller/RecurTest.php
tests/tine20/Calendar/Frontend/CalDAV/PluginManagedAttachmentsTest.php
tests/tine20/Calendar/Frontend/iMIPTest.php
tine20/Calendar/Controller/Event.php
tine20/Calendar/Controller/MSEventFacade.php
tine20/Calendar/Exception/ExdateContainer.php [new file with mode: 0644]
tine20/Calendar/Frontend/iMIP.php
tine20/Calendar/Model/Event.php
tine20/Calendar/Model/EventFilter.php
tine20/Calendar/Model/Rrule.php
tine20/Calendar/Model/iMIP.php
tine20/Calendar/Setup/Import/Egw14.php
tine20/Calendar/Setup/Update/Release8.php
tine20/Calendar/Setup/setup.xml
tine20/Calendar/js/MainScreenCenterPanel.js
tine20/Calendar/js/Model.js

index 8e31c14..a9254b7 100644 (file)
@@ -100,21 +100,6 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
     
         Tinebase_Core::set(Tinebase_Core::USERTIMEZONE, $currentTz);
     }
-
-    /**
-     * testUpdateEventWithRruleAndRecurId
-     * 
-     * @see 0008696: do not allow both rrule and recurId in event
-     */
-    public function testUpdateEventWithRruleAndRecurId()
-    {
-        $persistentRecurEvent = $this->testCreateRecurException();
-        $persistentRecurEvent->rrule = 'FREQ=DAILY;INTERVAL=1';
-        
-        $updatedEvent = $this->_controller->update($persistentRecurEvent);
-        
-        $this->assertEquals(NULL, $updatedEvent->rrule);
-    }
     
     public function testConcurrentUpdate()
     {
@@ -897,7 +882,7 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         $exception->setId(NULL);
         unset($exception->rrule);
         unset($exception->exdate);
-        $exception->recurid = $exception->uid . '-' . $exception->dtstart->get(Tinebase_Record_Abstract::ISO8601LONG);
+        $exception->setRecurId($event->getId());
         $persistentException = $this->_controller->create($exception);
         
         $persistentEvent->dtstart->addHour(5);
@@ -994,7 +979,7 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         $exception->setId(NULL);
         unset($exception->rrule);
         unset($exception->exdate);
-        $exception->recurid = $exception->uid . '-' . $exception->dtstart->get(Tinebase_Record_Abstract::ISO8601LONG);
+        $exception->setRecurId($persistentEvent->getId());
         $persistentException = $this->_controller->create($exception);
         
         unset($persistentEvent->rrule);
@@ -1038,7 +1023,7 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         $exception->setId(NULL);
         unset($exception->rrule);
         unset($exception->exdate);
-        $exception->recurid = $exception->uid . '-' . $exception->dtstart->get(Tinebase_Record_Abstract::ISO8601LONG);
+        $exception->setRecurId($persistentEvent->getId());
         $persistentException = $this->_controller->create($exception);
         
         $this->_controller->delete($persistentEvent->getId());
@@ -1046,30 +1031,6 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         $this->_controller->get($persistentException->getId());
     }
     
-    public function testCreateRecurException()
-    {
-        $event = $this->_getEvent();
-        $event->rrule = 'FREQ=DAILY;INTERVAL=1;UNTIL=2009-04-30 13:30:00';
-        $persistentEvent = $this->_controller->create($event);
-        
-        $exception = clone $persistentEvent;
-        $exception->dtstart->addDay(3);
-        $exception->dtend->addDay(3);
-        $exception->summary = 'Abendbrot';
-        $exception->recurid = $exception->uid . '-' . $exception->dtstart->get(Tinebase_Record_Abstract::ISO8601LONG);
-        $persistentException = $this->_controller->createRecurException($exception);
-        
-        $persistentEvent = $this->_controller->get($persistentEvent->getId());
-        $this->assertEquals(1, count($persistentEvent->exdate));
-        
-        $events = $this->_controller->search(new Calendar_Model_EventFilter(array(
-            array('field' => 'uid',     'operator' => 'equals', 'value' => $persistentEvent->uid),
-        )));
-        $this->assertEquals(2, count($events));
-        
-        return $persistentException;
-    }
-    
     public function testDeleteNonPersistentRecurException()
     {
         $event = $this->_getEvent();
@@ -1250,23 +1211,6 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         $this->assertEquals($year . '-10-24 23:00:00', $updatedAlarm->alarm_time->toString(), print_r($updatedAlarm->toArray(), true));
     }
     
-    public function testGetRecurExceptions()
-    {
-        $persistentException = $this->testCreateRecurException();
-        
-        $baseEvent = $this->_controller->getRecurBaseEvent($persistentException);
-        
-        $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event');
-        $nextOccurance = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, $exceptions, $baseEvent->dtend);
-        $this->_controller->createRecurException($nextOccurance, TRUE);
-        
-        $exceptions = $this->_controller->getRecurExceptions($persistentException, TRUE);
-        $dtstarts = $exceptions->dtstart;
-
-        $this->assertTrue(in_array($nextOccurance->dtstart, $dtstarts), 'deleted instance missing');
-        $this->assertTrue(in_array($persistentException->dtstart, $dtstarts), 'exception instance missing');
-    }
-    
     public function testPeriodFilter()
     {
         $persistentEvent = $this->testCreateEvent();
index afecf0c..3183e4d 100644 (file)
@@ -366,6 +366,7 @@ class Calendar_Controller_MSEventFacadeTest extends Calendar_TestCase
     {
         $newException = clone $event;
         $newException->id = NULL;
+        $newException->base_event_id = $event->getId();
         $newException->recurid = clone $newException->dtstart;
         $newException->recurid->addDay(3);
         $newException->dtstart->addDay(3)->addHour(2);
index 0c2c86c..3f92702 100644 (file)
@@ -133,7 +133,7 @@ class Calendar_Controller_RecurTest extends Calendar_TestCase
         $firstInstanceException->location = $location;
     
         $result = $this->_controller->update($firstInstanceException, FALSE, Calendar_Model_Event::RANGE_THISANDFUTURE);
-        $this->assertEquals($result->location, $location);
+        $this->assertEquals($location, $result->location);
     }
 
     /**
@@ -664,7 +664,63 @@ class Calendar_Controller_RecurTest extends Calendar_TestCase
         $this->assertFalse($events[1]->rrule_until instanceof Tinebase_DateTime, 'rrule_until of second baseEvent must not be set');
         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, Calendar_Model_Attender::getAttendee($events[1]->attendee, $event->attendee[1])->status, 'second baseEvent status is not touched');
     }
-    
+
+    public function testCreateRecurException()
+    {
+        $event = $this->_getEvent();
+        $event->rrule = 'FREQ=DAILY;INTERVAL=1;UNTIL=2009-04-30 13:30:00';
+        $persistentEvent = $this->_controller->create($event);
+
+        $exception = clone $persistentEvent;
+        $exception->dtstart->addDay(3);
+        $exception->dtend->addDay(3);
+        $exception->summary = 'Abendbrot';
+        $exception->recurid = $exception->uid . '-' . $exception->dtstart->get(Tinebase_Record_Abstract::ISO8601LONG);
+        $persistentException = $this->_controller->createRecurException($exception);
+
+        $persistentEvent = $this->_controller->get($persistentEvent->getId());
+        $this->assertEquals(1, count($persistentEvent->exdate));
+
+        $events = $this->_controller->search(new Calendar_Model_EventFilter(array(
+            array('field' => 'uid',     'operator' => 'equals', 'value' => $persistentEvent->uid),
+        )));
+        $this->assertEquals(2, count($events));
+
+        return $persistentException;
+    }
+
+    public function testGetRecurExceptions()
+    {
+        $persistentException = $this->testCreateRecurException();
+
+        $baseEvent = $this->_controller->getRecurBaseEvent($persistentException);
+
+        $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event');
+        $nextOccurance = Calendar_Model_Rrule::computeNextOccurrence($baseEvent, $exceptions, $baseEvent->dtend);
+        $this->_controller->createRecurException($nextOccurance, TRUE);
+
+        $exceptions = $this->_controller->getRecurExceptions($persistentException, TRUE);
+        $dtstarts = $exceptions->dtstart;
+
+        $this->assertTrue(in_array($nextOccurance->dtstart, $dtstarts), 'deleted instance missing');
+        $this->assertTrue(in_array($persistentException->dtstart, $dtstarts), 'exception instance missing');
+    }
+
+    /**
+     * testUpdateEventWithRruleAndRecurId
+     *
+     * @see 0008696: do not allow both rrule and recurId in event
+     */
+    public function testUpdateEventWithRruleAndRecurId()
+    {
+        $persistentRecurEvent = $this->testCreateRecurException();
+        $persistentRecurEvent->rrule = 'FREQ=DAILY;INTERVAL=1';
+
+        $updatedEvent = $this->_controller->update($persistentRecurEvent);
+
+        $this->assertEquals(NULL, $updatedEvent->rrule);
+    }
+
    /**
     * @see {http://forge.tine20.org/mantisbt/view.php?id=5686}
     */
@@ -780,7 +836,26 @@ class Calendar_Controller_RecurTest extends Calendar_TestCase
         $this->assertEquals(2, count($weekviewEvents->filter('uid', $weekviewEvents[0]->uid)), 'shorten failed');
         $this->assertEquals(5, count($weekviewEvents), 'wrong total count');
     }
-    
+
+    public function testCreateRecurExceptionAllFollowingContainerMove()
+    {
+        $this->markTestSkipped('exdate container move not yet forbidden');
+        $exception = $this->testCreateRecurException();
+        $baseEvent = $this->_controller->getRecurBaseEvent($exception);
+
+        $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event');
+        $from = $baseEvent->dtstart;
+        $until = $baseEvent->dtstart->getClone()->addDay(1);
+        $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($baseEvent, $exceptions, $from, $until);
+
+        $recurSet->getFirstRecord()->container_id = $this->_getTestContainer('Calendar')->getId();
+        $newSeries = $this->_controller->createRecurException($recurSet->getFirstRecord(), false, true);
+        $newExceptions = $this->_controller->getRecurExceptions($newSeries);
+
+//        print_r($newSeries->toArray());
+//        print_r($newExceptions->toArray());
+    }
+
     /**
      * testMoveRecurException
      * 
@@ -809,7 +884,48 @@ class Calendar_Controller_RecurTest extends Calendar_TestCase
         $this->setExpectedException('Tinebase_Timemachine_Exception_ConcurrencyConflict');
         $updatedPersistentEvent = $this->_controller->createRecurException($updatedPersistentEvent);
     }
-    
+
+    public function testExdateContainerMoveCreateException()
+    {
+        $this->markTestSkipped('exdate container move not yet forbidden');
+        $event = $this->_getDailyEvent(new Tinebase_DateTime('2014-02-03 09:00:00'));
+
+        $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event');
+        
+        $from = new Tinebase_DateTime('2014-02-01 00:00:00');
+        $until = new Tinebase_DateTime('2014-02-29 23:59:59');
+        
+        $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($event, $exceptions, $from, $until);
+
+        $this->setExpectedException('Calendar_Exception_ExdateContainer');
+
+        $recurSet[2]->container_id = $this->_getTestContainer('Calendar')->getId();
+        $this->_controller->createRecurException($recurSet[2]);
+    }
+
+    public function testExdateContainerMoveUpdateException()
+    {
+        $this->markTestSkipped('exdate container move not yet forbidden');
+        $event = $this->_getDailyEvent(new Tinebase_DateTime('2014-02-03 09:00:00'));
+
+        $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event');
+
+        $from = new Tinebase_DateTime('2014-02-01 00:00:00');
+        $until = new Tinebase_DateTime('2014-02-29 23:59:59');
+
+        $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($event, $exceptions, $from, $until);
+
+        $recurSet[2]->summary = 'exdate';
+
+        $updatedPersistentEvent = $this->_controller->createRecurException($recurSet[2]);
+
+        $this->setExpectedException('Calendar_Exception_ExdateContainer');
+
+        $updatedPersistentEvent->container_id = $this->_getTestContainer('Calendar')->getId();
+        $this->_controller->update($updatedPersistentEvent);
+
+    }
+
     /**
      * returns a simple recure event
      *
index 3755328..4662b18 100644 (file)
@@ -265,7 +265,7 @@ class Calendar_Frontend_CalDAV_PluginManagedAttachmentsTest extends TestCase
         $exception->rrule = NULL;
         $exception->dtstart->addDay(5)->addHour(1);
         $exception->dtend->addDay(5)->addHour(1);
-        $exception->setRecurId();
+        $exception->setRecurId($event->getRecord()->getId());
 
         $event->put($eventWithExdate->get());
         
index 84f9bd3..5fc7007 100644 (file)
@@ -241,8 +241,8 @@ class Calendar_Frontend_iMIPTest extends TestCase
         $event->dtstart->addHour(2);
         $event->dtend->addHour(2);
         Calendar_Controller_Event::getInstance()->update($event, false);
-        
-        $iMIP->getExistingEvent(true);
+
+        $this->_iMIPFrontend->getExistingEvent($iMIP, true);
         $iMIP->preconditionsChecked = false;
         $prepared = $this->_iMIPFrontend->prepareComponent($iMIP);
         
@@ -427,7 +427,7 @@ class Calendar_Frontend_iMIPTest extends TestCase
         $iMIP = $this->_getiMIP('REQUEST');
         $result = $this->_iMIPFrontendMock->process($iMIP, Calendar_Model_Attender::STATUS_TENTATIVE);
         
-        $event = Calendar_Controller_MSEventFacade::getInstance()->lookupExistingEvent($iMIP->getEvent());
+        $event = $this->_iMIPFrontend->getExistingEvent($iMIP, true);
         
         $attender = Calendar_Model_Attender::getOwnAttender($event->attendee);
         $this->assertEquals(Calendar_Model_Attender::STATUS_TENTATIVE, $attender->status);
@@ -557,7 +557,7 @@ class Calendar_Frontend_iMIPTest extends TestCase
         $this->assertEquals(Calendar_Model_Attender::STATUS_ACCEPTED, $updatedExternalAttendee->status, 'status not updated');
         
         // check if attendee are resolved
-        $existingEvent = $iMIP->getExistingEvent();
+        $existingEvent = $this->_iMIPFrontend->getExistingEvent($iMIP);
         $this->assertTrue($iMIP->existing_event->attendee instanceof Tinebase_Record_RecordSet);
         $this->assertEquals(3, count($iMIP->existing_event->attendee));
         
index 58153c2..c89403e 100644 (file)
@@ -501,11 +501,11 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
             
             $sendNotifications = $this->sendNotifications(FALSE);
-            
+
             $event = $this->get($_record->getId());
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
                     .' Going to update the following event. rawdata: ' . print_r($event->toArray(), true));
-            
+
             //NOTE we check via get(full rights) here whereas _updateACLCheck later checks limited rights from search
             if ($this->_doContainerACLChecks === FALSE || $event->hasGrant(Tinebase_Model_Grants::GRANT_EDIT)) {
                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " updating event: {$_record->id} (range: {$range})");
@@ -538,11 +538,11 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                     }
                 }
             }
-            
+
             if ($_record->isRecurException() && in_array($range, array(Calendar_Model_Event::RANGE_ALL, Calendar_Model_Event::RANGE_THISANDFUTURE))) {
                 $this->_updateExdateRange($_record, $range, $event);
             }
-            
+
             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
         } catch (Exception $e) {
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Rolling back because: ' . $e);
@@ -635,6 +635,9 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
      */
     protected function _applyExdateDiffToRecordSet($exdate, $diff, $events)
     {
+        // make sure baseEvent gets updated first to circumvent concurrency conflicts
+        $events->sort('recurdid', 'ASC');
+
         foreach ($events as $event) {
             if ($event->getId() === $exdate->getId()) {
                 // skip the exdate
@@ -920,14 +923,20 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                 . " It is not allowed to create recur instance if it is clone of base event");
             throw new Tinebase_Timemachine_Exception_ConcurrencyConflict('concurrency conflict!');
         }
-        
+
+//        // Maybe Later
+//        // exdates needs to stay in baseEvents container
+//        if ($_event->container_id != $baseEvent->container_id) {
+//            throw new Calendar_Exception_ExdateContainer();
+//        }
+
         // check if this is an exception to the first occurence
         if ($baseEvent->getId() == $_event->getId()) {
             if ($_allFollowing) {
                 throw new Exception('please edit or delete complete series!');
             }
             // NOTE: if the baseEvent gets a time change, we can't compute the recurdid w.o. knowing the original dtstart
-            $recurid = $baseEvent->setRecurId();
+            $recurid = $baseEvent->setRecurId($baseEvent->getId());
             unset($baseEvent->recurid);
             $_event->recurid = $recurid;
         }
@@ -986,7 +995,8 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                     . " Creating persistent exception for: '{$_event->recurid}'");
                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
                     . " Recur exception: " . print_r($_event->toArray(), TRUE));
-                
+
+                $_event->base_event_id = $baseEvent->getId();
                 $_event->setId(NULL);
                 unset($_event->rrule);
                 unset($_event->exdate);
@@ -1064,7 +1074,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
             }
             $baseEvent->rrule = (string) $rrule;
             $baseEvent->exdate = $pastExdates;
-            
+
             // NOTE: we don't want implicit attendee updates
             //$updatedBaseEvent = $this->update($baseEvent, FALSE);
             $this->_inspectEvent($baseEvent);
@@ -1095,18 +1105,19 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                     $_event->rrule = (string) $rrule;
                 }
                 
-                $_event->setId(NULL);
+                $_event->setId(Tinebase_Record_Abstract::generateUID());
                 $_event->uid = $futurePersistentExceptionEvents->uid = Tinebase_Record_Abstract::generateUID();
-                $futurePersistentExceptionEvents->setRecurId();
+                $_event->setId(Tinebase_Record_Abstract::generateUID());
+                $futurePersistentExceptionEvents->setRecurId($_event->getId());
                 unset($_event->recurid);
+                unset($_event->base_event_id);
                 foreach(array('attendee', 'notes', 'alarms') as $prop) {
                     if ($_event->{$prop} instanceof Tinebase_Record_RecordSet) {
                         $_event->{$prop}->setId(NULL);
                     }
                 }
                 $_event->exdate = $futureExdates;
-                $futurePersistentExceptionEvents->setRecurId();
-                
+
                 $attendees = $_event->attendee; unset($_event->attendee);
                 $note = $_event->notes; unset($_event->notes);
                 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts && $dtStartHasDiff);
@@ -1189,47 +1200,16 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
      */
     public function getRecurBaseEvent($_event)
     {
-        $possibleBaseEventIds = $this->_backend->search(new Calendar_Model_EventFilter(array(
-            array('field' => 'uid',     'operator' => 'equals', 'value' => $_event->uid),
-            array('field' => 'recurid', 'operator' => 'isnull', 'value' => NULL)
-        )), NULL, TRUE);
-        
-        if (count($possibleBaseEventIds) > 1) {
-            if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
-                ' Got multiple possible base events: ' . print_r($possibleBaseEventIds, true));
-        }
-        $baseEventId = Tinebase_Helper::array_value(0, $possibleBaseEventIds);
-        
+        $baseEventId = $_event->base_event_id ?: $_event->id;
+
         if (! $baseEventId) {
             throw new Tinebase_Exception_NotFound('base event of a recurring series not found');
         }
-        
+
         // make sure we have a 'fully featured' event
         return $this->get($baseEventId);
     }
 
-   /**
-    * lookup existing event by uid
-    *
-    * @param  Calendar_Model_Event $_event
-    * @return Calendar_Model_Event|NULL
-    * 
-    * @todo also add more criteria for lookup (recurid, ...)
-    * @todo sophisticated reccurring event handling
-    */
-    public function lookupExistingEvent($_event)
-    {
-        $events = $this->_backend->search(new Calendar_Model_EventFilter(array(
-            array('field' => 'uid',     'operator' => 'equals', 'value' => $_event->uid),
-            //array('field' => 'recurid', 'operator' => 'isnull', 'value' => NULL)
-        )));
-        
-        $event = $events->filter(Tinebase_Model_Grants::GRANT_READ, TRUE)->getFirstRecord();
-    
-        // make sure we have a 'fully featured' event
-        return ($event !== NULL) ? $this->get($event->getId()) : NULL;
-    }
-    
     /**
      * returns all persistent recur exceptions of recur series identified by uid of given event
      * 
@@ -1243,9 +1223,10 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
      */
     public function getRecurExceptions($_event, $_fakeDeletedInstances = FALSE, $_eventFilter = NULL)
     {
+        $baseEventId = $_event->base_event_id ?: $_event->id;
+
         $exceptionFilter = new Calendar_Model_EventFilter(array(
-            array('field' => 'uid',     'operator' => 'equals',  'value' => $_event->uid),
-            array('field' => 'recurid', 'operator' => 'notnull', 'value' => NULL)
+            array('field' => 'base_event_id', 'operator' => 'equals',  'value' => $baseEventId),
         ));
         
         if ($_eventFilter instanceof Calendar_Model_EventFilter) {
@@ -1290,7 +1271,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
             $fakeEvent->dtend = clone $deletedInstanceDtStart;
             $fakeEvent->dtend->add($eventLength);
             $fakeEvent->is_deleted = TRUE;
-            $fakeEvent->setRecurId();
+            $fakeEvent->setRecurId($baseEvent->getId());
             $fakeEvent->rrule = null;
 
             $exceptions->addRecord($fakeEvent);
@@ -1452,7 +1433,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
             Calendar_Model_Rrule::addUTCDateDstFix($exception->recurid, $diff, $_record->originator_tz);
             $exdates[] = $exception->recurid;
             
-            $exception->setRecurId();
+            $exception->setRecurId($_record->getId());
             $this->_backend->update($exception);
         }
         
@@ -1519,11 +1500,22 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
         if ($_record->rrule instanceof Calendar_Model_Rrule) {
             $_record->rrule->normalize($_record);
         }
-        
-        if ($_record->isRecurException() && $_record->rrule !== NULL) {
-            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
-                . ' Removing invalid rrule from recur exception: ' . $_record->rrule);
-            $_record->rrule = NULL;
+
+        if ($_record->isRecurException()) {
+            $baseEvent = $this->getRecurBaseEvent($_record);
+
+            // remove invalid rrules
+            if ($_record->rrule !== NULL) {
+                if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+                    . ' Removing invalid rrule from recur exception: ' . $_record->rrule);
+                $_record->rrule = NULL;
+            }
+
+//            // Maybe Later
+//            // exdates needs to stay in baseEvents container
+//            if($_record->container_id != $baseEvent->container_id) {
+//                throw new Calendar_Exception_ExdateContainer();
+//            }
         }
     }
 
@@ -1736,7 +1728,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
             
             if ($baseEvent->getId() == $_recurInstance->getId()) {
                 // exception to the first occurence
-                $_recurInstance->setRecurId();
+                $_recurInstance->setRecurId($baseEvent->getId());
             }
             
             // NOTE: recurid is computed by rrule recur computations and therefore is already part of the event.
@@ -1790,10 +1782,11 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                 $baseEvent->dtstart = new Tinebase_DateTime(substr($_recurInstance->recurid, -19), 'UTC');
                 $baseEvent->dtend   = clone $baseEvent->dtstart;
                 $baseEvent->dtend->add($diff);
-                
+
+                $baseEvent->base_event_id = $baseEvent->id;
                 $baseEvent->id = $_recurInstance->id;
                 $baseEvent->recurid = $_recurInstance->recurid;
-                
+
                 $attendee = $baseEvent->attendee;
                 unset($baseEvent->attendee);
                 
@@ -2215,13 +2208,18 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " About to send alarm " . print_r($_alarm->toArray(), TRUE));
         
         $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
-        
-        $event = $this->get($_alarm->record_id);
-        $event->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm', array($_alarm));
-        $this->_inspectAlarmGet($event);
-        
+
+        try {
+            $event = $this->get($_alarm->record_id);
+            $event->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm', array($_alarm));
+            $this->_inspectAlarmGet($event);
+        } catch (Exception $e) {
+            $this->doContainerACLChecks($doContainerACLChecks);
+            throw($e);
+        }
+
         $this->doContainerACLChecks($doContainerACLChecks);
-        
+
         if ($event->rrule) {
             $recurid = $_alarm->getOption('recurid');
             
index 446a88c..3cea2c7 100644 (file)
@@ -234,62 +234,48 @@ class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_In
         // if an id filter is set, we need to fetch exceptions in a second query
         if ($_filter->getFilter('id', true, true)) {
             $events->merge($this->_eventController->search(new Calendar_Model_EventFilter(array(
-                array('field' => 'uid',     'operator' => 'in',      'value' => $events->uid),
-                array('field' => 'id',      'operator' => 'notin',   'value' => $events->id),
-                array('field' => 'recurid', 'operator' => 'notnull', 'value' => null)
+                array('field' => 'base_event_id', 'operator' => 'in',      'value' => $events->id),
+                array('field' => 'id',            'operator' => 'notin',   'value' => $events->id),
+                array('field' => 'recurid',       'operator' => 'notnull', 'value' => null),
             )), NULL, FALSE, FALSE, $_action));
         }
 
         $this->_eventController->getAlarms($events);
         Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($events);
 
-        $baseEventMap = array(); // uid => baseEvent
-        $exceptionSets = array(); // uid => exceptions
+        $baseEventMap = array(); // id => baseEvent
+        $exceptionSets = array(); // id => exceptions
         $exceptionMap = array(); // idx => event
 
         foreach($events as $event) {
             if ($event->rrule) {
-                $eventUid = $event->uid;
-                $baseEventMap[$eventUid] = $event;
-                $exceptionSets[$eventUid] = new Tinebase_Record_RecordSet('Calendar_Model_Event');
+                $eventId = $event->id;
+                $baseEventMap[$eventId] = $event;
+                $exceptionSets[$eventId] = new Tinebase_Record_RecordSet('Calendar_Model_Event');
             } else if ($event->recurid) {
                 $exceptionMap[] = $event;
             }
         }
 
         foreach($exceptionMap as $exception) {
-            $exceptionUid = $exception->uid;
-            $baseEvent = array_key_exists($exceptionUid, $baseEventMap) ? $baseEventMap[$exceptionUid] : false;
+            $baseEventId = $exception->base_event_id;
+            $baseEvent = array_key_exists($baseEventId, $baseEventMap) ? $baseEventMap[$baseEventId] : false;
             if ($baseEvent) {
-                $exceptionSet = $exceptionSets[$exceptionUid];
+                $exceptionSet = $exceptionSets[$baseEventId];
                 $exceptionSet->addRecord($exception);
                 $events->removeRecord($exception);
             }
         }
 
-        foreach($baseEventMap as $uid => $baseEvent) {
-            $exceptionSet = $exceptionSets[$uid];
+        foreach($baseEventMap as $id => $baseEvent) {
+            $exceptionSet = $exceptionSets[$id];
             $this->_eventController->fakeDeletedExceptions($baseEvent, $exceptionSet);
             $baseEvent->exdate = $exceptionSet;
         }
 
         return $events;
     }
-    
-   /**
-     * (non-PHPdoc)
-     * @see Calendar_Controller_Event::lookupExistingEvent()
-     */
-    public function lookupExistingEvent($_event)
-    {
-        $event = $this->_eventController->lookupExistingEvent($_event);
 
-        if ($event) {
-            $this->_resolveData($event);
-            return $this->_toiTIP($event);
-        }
-    }
-    
     /*************** add / update / delete *****************/    
 
     /**
@@ -325,8 +311,8 @@ class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_In
             }
         }
 
-        $this->_resolveData($savedEvent);
-        return $this->_toiTIP($savedEvent);
+        // NOTE: exdate creation changes baseEvent, so we need to refetch it here
+        return $this->get($savedEvent->getId());
     }
     
     /**
@@ -358,7 +344,7 @@ class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_In
         $newPersistentExceptions = $exceptions->filter('is_deleted', 0);
         
         $migration = $this->_getExceptionsMigration($currentPersistentExceptions, $newPersistentExceptions);
-        
+
         $this->_eventController->delete($migration['toDelete']->getId());
         
         // NOTE: we need to exclude the toCreate exdates here to not confuse computations in createRecurException!
@@ -980,6 +966,7 @@ class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_In
             $_exception->container_id = $_baseEvent->container_id;
         }
         $_exception->uid = $_baseEvent->uid;
+        $_exception->base_event_id = $_baseEvent->getId();
         $_exception->recurid = $_baseEvent->uid . '-' . $_exception->getOriginalDtStart()->format(Tinebase_Record_Abstract::ISO8601LONG);
         
         // NOTE: we always refetch the base event as it might be touched in the meantime
diff --git a/tine20/Calendar/Exception/ExdateContainer.php b/tine20/Calendar/Exception/ExdateContainer.php
new file mode 100644 (file)
index 0000000..fdeac05
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Calendar
+ * @subpackage  Exception
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Alexander Stintzing <a.stintzing@metaways.de>
+ *
+ */
+
+/**
+ * Container
+ *
+ * @package     Calendar
+ * @subpackage  Exception
+ */
+class Calendar_Exception_ExdateContainer extends Calendar_Exception
+{
+    /**
+     * the title of the Exception (may be shown in a dialog)
+     *
+     * @var string
+     */
+    protected $_title = 'Different Calendar'; // _('Different Calendar')
+
+    /**
+     * @see SPL Exception
+     */
+    protected $message = 'Exdate container move are not allowed'; //_('Exdate container move are not allowed')
+
+    /**
+     * @see SPL Exception
+     */
+    protected $code = 912;
+}
index b9995db..1a3e7eb 100644 (file)
@@ -31,7 +31,7 @@ class Calendar_Frontend_iMIP
             return;
         }
         
-        if (! $_iMIP->getExistingEvent(TRUE)) {
+        if (! $this->getExistingEvent($_iMIP, TRUE)) {
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . " skip auto processing of iMIP component whose event is not in our db yet");
             return;
         }
@@ -67,7 +67,7 @@ class Calendar_Frontend_iMIP
         
         Calendar_Convert_Event_Json::resolveRelatedData($_iMIP->event);
         Tinebase_Model_Container::resolveContainerOfRecord($_iMIP->event);
-        Tinebase_Model_Container::resolveContainerOfRecord($_iMIP->getExistingEvent());
+        Tinebase_Model_Container::resolveContainerOfRecord($this->getExistingEvent($_iMIP));
         
         return $_iMIP;
     }
@@ -189,7 +189,7 @@ class Calendar_Frontend_iMIP
         $result  = $this->_assertOwnAttender($_iMIP, TRUE, FALSE);
         $result &= $this->_assertOrganizer($_iMIP, TRUE, TRUE);
         
-        $existingEvent = $_iMIP->getExistingEvent();
+        $existingEvent = $this->getExistingEvent($_iMIP);
         if ($existingEvent) {
             $iMIPEvent = $_iMIP->getEvent();
             $isObsoleted = false;
@@ -224,7 +224,7 @@ class Calendar_Frontend_iMIP
     {
         $result = TRUE;
         
-        $existingEvent = $_iMIP->getExistingEvent();
+        $existingEvent = $this->getExistingEvent($_iMIP);
         $ownAttender = Calendar_Model_Attender::getOwnAttender($existingEvent ? $existingEvent->attendee : $_iMIP->getEvent()->attendee);
         if ($_assertExistence && ! $ownAttender) {
             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_ATTENDEE, "processing {$_iMIP->method} for non attendee is not supported");
@@ -281,7 +281,7 @@ class Calendar_Frontend_iMIP
     {
         $result = TRUE;
         
-        $existingEvent = $_iMIP->getExistingEvent();
+        $existingEvent = $this->getExistingEvent($_iMIP);
         $organizer = $existingEvent ? $existingEvent->resolveOrganizer() : $_iMIP->getEvent()->resolveOrganizer();
         
         if ($_assertExistence && ! $organizer) {
@@ -311,7 +311,34 @@ class Calendar_Frontend_iMIP
         
         return $result;
     }
-    
+
+    /**
+     * find existing event by uid
+     *
+     * @param $_iMIP
+     * @param bool $_refetch
+     * @return NULL|Tinebase_Record_Abstract
+     * @throws Exception
+     */
+    public function getExistingEvent($_iMIP, $_refetch = FALSE)
+    {
+        if ($_refetch || ! $_iMIP->existing_event instanceof Calendar_Model_Event) {
+
+            $iMIPEvent = $_iMIP->getEvent();
+
+            $events = Calendar_Controller_MSEventFacade::getInstance()->search(new Calendar_Model_EventFilter(array(
+                array('field' => 'uid',          'operator' => 'equals', 'value' => $iMIPEvent->uid),
+            )));
+
+            $event = $events->filter(Tinebase_Model_Grants::GRANT_READ, TRUE)->getFirstRecord();
+            Calendar_Model_Attender::resolveAttendee($event['attendee']);
+
+            $_iMIP->existing_event = $event;
+        }
+
+        return $_iMIP->existing_event;
+    }
+
     /**
      * process request
      * 
@@ -320,7 +347,7 @@ class Calendar_Frontend_iMIP
      */
     protected function _processRequest($_iMIP, $_status)
     {
-        $existingEvent = $_iMIP->getExistingEvent();
+        $existingEvent = $this->getExistingEvent($_iMIP);
         $ownAttender = Calendar_Model_Attender::getOwnAttender($existingEvent ? $existingEvent->attendee : $_iMIP->getEvent()->attendee);
         $organizer = $existingEvent ? $existingEvent->resolveOrganizer() : $_iMIP->getEvent()->resolveOrganizer();
         
@@ -385,7 +412,7 @@ class Calendar_Frontend_iMIP
     {
         $result = TRUE;
         
-        $existingEvent = $_iMIP->getExistingEvent();
+        $existingEvent = $this->getExistingEvent($_iMIP);
         if (! $existingEvent) {
             $_iMIP->addFailedPrecondition(Calendar_Model_iMIP::PRECONDITION_EVENTEXISTS, "cannot process REPLY to non existent/invisible event");
             $result = FALSE;
@@ -443,7 +470,7 @@ class Calendar_Frontend_iMIP
     protected function _processReply(Calendar_Model_iMIP $_iMIP)
     {
         // merge ics into existing event
-        $existingEvent = $_iMIP->getExistingEvent();
+        $existingEvent = $this->getExistingEvent($_iMIP);
         $event = $_iMIP->mergeEvent($existingEvent);
         $attendee = $event->attendee[array_search($_iMIP->originator, $existingEvent->attendee->getEmail())];
         
index d50d9f4..a43b4d8 100644 (file)
@@ -123,6 +123,7 @@ class Calendar_Model_Event extends Tinebase_Record_Abstract
         // ical scheduleable interface fields
         'dtstart'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         'recurid'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'base_event_id'         => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         // ical scheduleable interface fields with multiple appearance
         'exdate'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), //  array of Tinebase_DateTimeTinebase_DateTime's
         //'exrule'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
@@ -466,7 +467,7 @@ class Calendar_Model_Event extends Tinebase_Record_Abstract
      * 
      * @return string recurid which was set
      */
-    public function setRecurId()
+    public function setRecurId($baseEventId)
     {
         if (! ($this->uid && $this->dtstart)) {
             throw new Exception ('uid _and_ dtstart must be set to generate recurid');
@@ -477,7 +478,8 @@ class Calendar_Model_Event extends Tinebase_Record_Abstract
         $dtstart->setTimezone('UTC');
         
         $this->recurid = $this->uid . '-' . $dtstart->get(Tinebase_Record_Abstract::ISO8601LONG);
-        
+        $this->base_event_id = $baseEventId;
+
         return $this->recurid;
     }
     
index 8b10d17..1d09349 100644 (file)
@@ -61,6 +61,7 @@ class Calendar_Model_EventFilter extends Tinebase_Model_Filter_FilterGroup
         'transp'                => array('filter' => 'Tinebase_Model_Filter_Text'),
         'rrule'                 => array('filter' => 'Tinebase_Model_Filter_Text'),
         'recurid'               => array('filter' => 'Tinebase_Model_Filter_Text'),
+        'base_event_id'         => array('filter' => 'Tinebase_Model_Filter_Text'),
         'rrule_until'           => array('filter' => 'Tinebase_Model_Filter_DateTime'),
         'summary'               => array('filter' => 'Tinebase_Model_Filter_Text'),
         'location'              => array('filter' => 'Tinebase_Model_Filter_Text'),
index 5f4d4f3..2154b67 100644 (file)
@@ -592,7 +592,7 @@ class Calendar_Model_Rrule extends Tinebase_Record_Abstract
         
         // we don't want to compute ourself
         $ownEvent = clone $_event;
-        $ownEvent->setRecurId();
+        $ownEvent->setRecurId($_event->getId());
         $exceptions = clone $_exceptions;
         $exceptions->addRecord($ownEvent);
         $recurSet = new Tinebase_Record_RecordSet('Calendar_Model_Event');
@@ -725,7 +725,7 @@ class Calendar_Model_Rrule extends Tinebase_Record_Abstract
                     
                     // check if base event (recur instance) needs to be added to the set
                     if ($baseEvent->dtstart > $_event->dtstart && $baseEvent->dtstart >= $_from && $baseEvent->dtstart < $_until) {
-                        if (! in_array($baseEvent->setRecurId(), $exceptionRecurIds)) {
+                        if (! in_array($baseEvent->setRecurId($baseEvent->getId()), $exceptionRecurIds)) {
                             self::addRecurrence($baseEvent, $recurSet);
                         }
                     }
@@ -764,7 +764,7 @@ class Calendar_Model_Rrule extends Tinebase_Record_Abstract
                     
                     // check if base event (recur instance) needs to be added to the set
                     if ($baseEvent->dtstart->isLater($_from) && $baseEvent->dtstart->isEarlier($_until)) {
-                        if (! in_array($baseEvent->setRecurId(), $exceptionRecurIds)) {
+                        if (! in_array($baseEvent->setRecurId($baseEvent->getId()), $exceptionRecurIds)) {
                             self::addRecurrence($baseEvent, $recurSet);
                         }
                     }
@@ -866,7 +866,7 @@ class Calendar_Model_Rrule extends Tinebase_Record_Abstract
             $recurEvent->dtend = clone $recurEvent->dtstart;
             $recurEvent->dtend->add($eventLength);
             
-            $recurEvent->setRecurId();
+            $recurEvent->setRecurId($_event->getId());
             
             if ($_from->compare($recurEvent->dtend) >= 0) {
                 continue;
@@ -952,7 +952,7 @@ class Calendar_Model_Rrule extends Tinebase_Record_Abstract
                 continue;
             }
             
-            $recurEvent->setRecurId();
+            $recurEvent->setRecurId($_event->getId());
             
             
             if (! in_array($recurEvent->recurid, $_exceptionRecurIds)) {
@@ -1052,7 +1052,7 @@ class Calendar_Model_Rrule extends Tinebase_Record_Abstract
                 continue;
             }
             
-            $recurEvent->setRecurId();
+            $recurEvent->setRecurId($_event->getId());
             
             if (! in_array($recurEvent->recurid, $_exceptionRecurIds)) {
                 self::addRecurrence($recurEvent, $_recurSet);
index b808eae..7e5697a 100644 (file)
@@ -187,22 +187,6 @@ class Calendar_Model_iMIP extends Tinebase_Record_Abstract
     }
 
     /**
-    * get existing event record
-    *
-    * @param boolean $_refetch the event
-    * @return Calendar_Model_Event
-    */
-    public function getExistingEvent($_refetch = FALSE)
-    {
-        if ($_refetch || ! $this->existing_event instanceof Calendar_Model_Event) {
-            $this->existing_event = Calendar_Controller_MSEventFacade::getInstance()->lookupExistingEvent($this->getEvent());
-            Calendar_Model_Attender::resolveAttendee($this->existing_event['attendee']);
-        }
-        
-        return $this->existing_event;
-    }
-    
-    /**
      * merge ics data into given event
      * 
      * @param Calendar_Model_Event $_event
index cc71115..a1ba90a 100644 (file)
@@ -146,7 +146,7 @@ class Calendar_Setup_Import_Egw14 extends Tinebase_Setup_Import_Egw14_Abstract
                         $exception['rrule'] = NULL;
                         
                         $exception->uid = $event->uid;
-                        $exception->setRecurId();
+                        $exception->setRecurId($event->getId());
                         
                         $exdateKey = array_search($exception->dtstart, $event->exdate);
                         if ($exdateKey !== FALSE) {
index d1ce406..46b9f9f 100644 (file)
@@ -255,7 +255,7 @@ class Calendar_Setup_Update_Release8 extends Setup_Update_Abstract
     public function update_7()
     {
         $allUser = $this->_db->query(
-            "SELECT " . $this->_db->quoteIdentifier('id') . "," .  $this->_db->quoteIdentifier('contact_id') .
+            "SELECT " . $this->_db->quoteIdentifier('id') . "," . $this->_db->quoteIdentifier('contact_id') .
             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "accounts") .
             " WHERE " . $this->_db->quoteIdentifier("contact_id") . " IS NOT NULL"
         )->fetchAll(Zend_Db::FETCH_ASSOC);
@@ -270,8 +270,8 @@ class Calendar_Setup_Update_Release8 extends Setup_Update_Abstract
             "SELECT DISTINCT" . $this->_db->quoteIdentifier('user_type') . "," . $this->_db->quoteIdentifier('user_id') .
             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
             " WHERE " . $this->_db->quoteIdentifier("displaycontainer_id") . " IS  NULL" .
-            "  AND " . $this->_db->quoteIdentifier("user_type")  . $this->_db->quoteInto(" IN (?)", array('user', 'groupmemeber')) .
-            "  AND " . $this->_db->quoteIdentifier("user_id")  . $this->_db->quoteInto(" IN (?)", array_keys($contactUserMap))
+            "  AND " . $this->_db->quoteIdentifier("user_type") . $this->_db->quoteInto(" IN (?)", array('user', 'groupmemeber')) .
+            "  AND " . $this->_db->quoteIdentifier("user_id") . $this->_db->quoteInto(" IN (?)", array_keys($contactUserMap))
         )->fetchAll(Zend_Db::FETCH_ASSOC);
 
         // find all user/groupmember attendees with missing displaycontainer
@@ -279,7 +279,7 @@ class Calendar_Setup_Update_Release8 extends Setup_Update_Abstract
             "SELECT DISTINCT" . $this->_db->quoteIdentifier('user_type') . "," . $this->_db->quoteIdentifier('user_id') .
             " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
             " WHERE " . $this->_db->quoteIdentifier("displaycontainer_id") . " IS  NULL" .
-            "  AND " . $this->_db->quoteIdentifier("user_type")  . $this->_db->quoteInto(" IN (?)", array('resource'))
+            "  AND " . $this->_db->quoteIdentifier("user_type") . $this->_db->quoteInto(" IN (?)", array('resource'))
         )->fetchAll(Zend_Db::FETCH_ASSOC));
 
         $resources = $this->_db->query(
@@ -288,7 +288,7 @@ class Calendar_Setup_Update_Release8 extends Setup_Update_Abstract
         )->fetchAll(Zend_Db::FETCH_ASSOC);
 
         $resourceContainerMap = array();
-        foreach($resources as $resource) {
+        foreach ($resources as $resource) {
             $resourceContainerMap[$resource['id']] = $resource['container_id'];
         }
 
@@ -312,4 +312,110 @@ class Calendar_Setup_Update_Release8 extends Setup_Update_Abstract
 
         $this->setApplicationVersion('Calendar', '8.8');
     }
+
+    /**
+     * identify base event via new base_event_id field instead of UID
+     */
+    public function update_8()
+    {
+        /* find possibly broken events
+         SELECT group_concat(id), uid, count(id) as cnt from tine20_cal_events
+             WHERE rrule IS NOT NULL
+             GROUP BY uid
+             HAVING cnt > 1;
+         */
+
+        $declaration = new Setup_Backend_Schema_Field_Xml('
+            <field>
+                <name>base_event_id</name>
+                <type>text</type>
+                <length>40</length>
+            </field>');
+        $this->_backend->addCol('cal_events', $declaration);
+
+        $declaration = new Setup_Backend_Schema_Index_Xml('
+            <index>
+                <name>base_event_id</name>
+                <field>
+                    <name>base_event_id</name>
+                </field>
+            </index>');
+        $this->_backend->addIndex('cal_events', $declaration);
+
+        // find all events with rrule
+        $events = $this->_db->query(
+            "SELECT " . $this->_db->quoteIdentifier('id') .
+                 ', ' . $this->_db->quoteIdentifier('uid') .
+                 ', ' . $this->_db->quoteIdentifier('container_id') .
+                 ', ' . $this->_db->quoteIdentifier('created_by') .
+            " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
+            " WHERE " . $this->_db->quoteIdentifier("rrule") . " IS NOT NULL" .
+              " AND " . $this->_db->quoteIdentifier("is_deleted") . " = " . $this->_db->quote(0, Zend_Db::INT_TYPE)
+        )->fetchAll(Zend_Db::FETCH_ASSOC);
+
+        // update all exdates in same container
+        foreach($events as $event) {
+            $this->_db->query(
+                "UPDATE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
+                  " SET " . $this->_db->quoteIdentifier('base_event_id') . ' = ' . $this->_db->quote($event['id']) .
+                " WHERE " . $this->_db->quoteIdentifier('uid') . ' = ' . $this->_db->quote($event['uid']) .
+                  " AND " . $this->_db->quoteIdentifier("container_id") . ' = ' . $this->_db->quote($event['container_id']) .
+                  " AND " . $this->_db->quoteIdentifier("recurid") . " IS NOT NULL" .
+                  " AND " . $this->_db->quoteIdentifier("is_deleted") . " = " . $this->_db->quote(0, Zend_Db::INT_TYPE)
+            );
+        }
+
+        // find all container move exdates
+        $danglingExdates = $this->_db->query(
+            "SELECT " . $this->_db->quoteIdentifier('uid') .
+                ', ' . $this->_db->quoteIdentifier('id') .
+                ', ' . $this->_db->quoteIdentifier('created_by') .
+            " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
+            " WHERE " . $this->_db->quoteIdentifier("recurid") . " IS NOT NULL" .
+              " AND " . $this->_db->quoteIdentifier("base_event_id") . " IS NULL" .
+              " AND " . $this->_db->quoteIdentifier("is_deleted") . " = " . $this->_db->quote(0, Zend_Db::INT_TYPE)
+        )->fetchAll(Zend_Db::FETCH_ASSOC);
+
+        // try to match by creator
+        foreach ($danglingExdates as $exdate) {
+            $possibleBaseEvents = array();
+            $matches = array_filter($events, function ($event) use ($exdate, $possibleBaseEvents) {
+                if ($event['uid'] == $exdate['uid']) {
+                    $possibleBaseEvents[] = $event;
+                    return $event['created_by'] == $exdate['created_by'];
+                }
+                return false;
+            });
+
+            switch(count($matches)) {
+                case 0:
+                    // no match :-(
+                    if (count($possibleBaseEvents) == 0) {
+                        // garbage? exdate without any base event
+                        Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " dangling exdate with id {$exdate['id']}");
+                        continue 2;
+                    }
+                    Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " no match for exdate with id {$exdate['id']}");
+                    $baseEvent = current($possibleBaseEvents);
+                    break;
+                case 1:
+                    // exact match :-)
+                    $baseEvent = current($matches);
+                    break;
+                default:
+                    // to much matches :-(
+                    Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " multiple matches for exdate with id {$exdate['id']}");
+                    $baseEvent = current($matches);
+            }
+
+            $this->_db->query(
+                "UPDATE " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_events") .
+                " SET " . $this->_db->quoteIdentifier('base_event_id') . ' = ' . $this->_db->quote($baseEvent['id']) .
+                " WHERE " . $this->_db->quoteIdentifier('id') . ' = ' . $this->_db->quote($exdate['id'])
+            );
+        }
+
+        $this->setTableVersion('cal_events', '10');
+        $this->setApplicationVersion('Calendar', '8.9');
+    }
 }
index bc5fea1..0fa8eed 100644 (file)
@@ -2,14 +2,14 @@
 <application>
     <name>Calendar</name>
     <!-- gettext('Calendar') -->   
-    <version>8.8</version>
+    <version>8.9</version>
     <order>15</order>
     <status>enabled</status>
     <tables>
         <!-- events -->
         <table>
             <name>cal_events</name>
-            <version>9</version>
+            <version>10</version>
             <declaration>
                 <field>
                     <name>id</name>
                     <length>255</length>
                 </field>
                 <field>
+                    <name>base_event_id</name>
+                    <type>text</type>
+                    <length>40</length>
+                </field>
+                <field>
                     <name>rrule</name>
                     <type>text</type>
                     <length>255</length>
                     </field>
                 </index>
                 <index>
+                    <name>base_event_id</name>
+                    <field>
+                        <name>base_event_id</name>
+                    </field>
+                </index>
+                <index>
                     <name>etag</name>
                     <field>
                         <name>etag</name>
index 4e87774..7cefcdd 100644 (file)
@@ -694,7 +694,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         return copyAction
     },
     
-    checkPastEvent: function(event, checkBusyConflicts, actionType) {
+    checkPastEvent: function(event, checkBusyConflicts, actionType, oldEvent) {
         var start = event.get('dtstart').getTime();
         var morning = new Date().clearTime().getTime();
 
@@ -725,7 +725,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
                     try {
                         switch (option) {
                             case 'yes':
-                                if (actionType == 'update') this.onUpdateEvent(event, true);
+                                if (actionType == 'update') this.onUpdateEvent(event, true, oldEvent);
                                 else this.onAddEvent(event, checkBusyConflicts, true);
                                 break;
                             case 'no':
@@ -767,7 +767,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
                 }             
             });
         } else {
-            if (actionType == 'update') this.onUpdateEvent(event, true);
+            if (actionType == 'update') this.onUpdateEvent(event, true, oldEvent);
             else this.onAddEvent(event, checkBusyConflicts, true);
         }
     },
@@ -821,11 +821,11 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         });
     },
     
-    onUpdateEvent: function(event, pastChecked) {
+    onUpdateEvent: function(event, pastChecked, oldEvent) {
         this.setLoading(true);
         
         if(!pastChecked) {
-            this.checkPastEvent(event, null, 'update');
+            this.checkPastEvent(event, null, 'update', oldEvent);
             return;
         }
         
@@ -834,17 +834,23 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         }
         
         if (event.id && (event.isRecurInstance() || event.isRecurException() || (event.isRecurBase() && ! event.get('rrule').newrule))) {
+            
+            var options = [];
+            
+            // the container has changed - force update whole series
+//            if (! oldEvent || event.get('container_id') == oldEvent.get('container_id')) {
+                options.push({text: this.app.i18n._('Update this event only'), name: 'this'});
+                options.push({text: this.app.i18n._('Update this and all future events'), name: (event.isRecurBase() && ! event.get('rrule').newrule) ? 'series' : 'future'});
+//            }
+            
+            options.push({text: this.app.i18n._('Update whole series'), name: 'series'});
+            options.push({text: this.app.i18n._('Update nothing'), name: 'cancel'});
+            
             Tine.widgets.dialog.MultiOptionsDialog.openWindow({
                 title: this.app.i18n._('Update Event'),
                 height: 170,
                 scope: this,
-                options: [
-                    {text: this.app.i18n._('Update this event only'), name: 'this'},
-                    {text: this.app.i18n._('Update this and all future events'), name: (event.isRecurBase() && ! event.get('rrule').newrule) ? 'series' : 'future'},
-                    {text: this.app.i18n._('Update whole series'), name: 'series'},
-                    {text: this.app.i18n._('Update nothing'), name: 'cancel'}
-                    
-                ],
+                options: options,
                 handler: function(option) {
                     var store = event.store;
 
@@ -1265,7 +1271,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
                         store.add(updatedEvent);
                     }
                     
-                    this.onUpdateEvent(updatedEvent, false);
+                    this.onUpdateEvent(updatedEvent, false, event);
                 }
             }
         });
index 8c39eab..d539319 100644 (file)
@@ -43,6 +43,7 @@ Tine.Calendar.Model.Event = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model
     // scheduleable interface fields
     { name: 'dtstart', type: 'date', dateFormat: Date.patterns.ISO8601Long },
     { name: 'recurid' },
+    { name: 'base_event_id' },
     // scheduleable interface fields with multiple appearance
     { name: 'exdate' },
     //{ name: 'exrule' },