[CalendarServer-changes] [13633] CalendarServer/trunk/txdav

source_changes at macosforge.org source_changes at macosforge.org
Thu Jun 12 03:06:36 PDT 2014


Revision: 13633
          http://trac.calendarserver.org//changeset/13633
Author:   gaya at apple.com
Date:     2014-06-12 03:06:36 -0700 (Thu, 12 Jun 2014)
Log Message:
-----------
Do not update past instance of event that spans past to future

Modified Paths:
--------------
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/who/groups.py
    CalendarServer/trunk/txdav/who/test/test_group_attendees.py

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-06-11 18:38:06 UTC (rev 13632)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-06-12 10:06:36 UTC (rev 13633)
@@ -1961,7 +1961,7 @@
 
         # sync group attendee members if inserting or group changed
         changed = False
-        if inserting or (yield self.updateGROUP_ATTENDEE(groupCUAToAttendeeMemberPropMap)):
+        if inserting or (yield self.updateEventGroupLink(groupCUAToAttendeeMemberPropMap)):
             changed = component.reconcileGroupAttendees(groupCUAToAttendeeMemberPropMap)
 
         # save for post processing when self._resourceID is non-zero
@@ -1971,7 +1971,7 @@
 
 
     @inlineCallbacks
-    def updateGROUP_ATTENDEE(self, groupCUAToAttendeeMemberPropMap=None):
+    def updateEventGroupLink(self, groupCUAToAttendeeMemberPropMap=None):
         """
         update schema.GROUP_ATTENDEE
         """
@@ -2037,8 +2037,10 @@
 
 
     @inlineCallbacks
-    def deleteOldGROUP_ATTENDEE(self, component, instances, inserting, txn):
+    def removeOldEventGroupLink(self, component, instances, inserting, txn):
 
+        isOldEventWithGroupAttendees = False
+
         # If this event is old, break possible tie to group update
         if hasattr(self, "_groupCUAToAttendeeMemberPropMap"):
 
@@ -2056,7 +2058,7 @@
                         tr.END_DATE > cutoffDate_datatime
                     ),
                 ).on(txn)
-                isOld = rows[0][0] == 0
+                isOldEventWithGroupAttendees = rows[0][0] == 0
 
             else:
                 if instances and len(instances.instances):
@@ -2065,13 +2067,13 @@
                         Duration(seconds=config.GroupAttendees.UpdateOldEventLimitSeconds)
                     )
                     maxInstanceKey = sorted(instance for instance in instances)[-1]
-                    isOld = cutoffDate_DateTime > instances[maxInstanceKey].end
+                    isOldEventWithGroupAttendees = cutoffDate_DateTime > instances[maxInstanceKey].end
                 else:
-                    isOld = True
+                    isOldEventWithGroupAttendees = True
 
-            if isOld:
+            if isOldEventWithGroupAttendees:
                 if inserting:
-                    # don't create GROUP_ATTENDEE rows in updateGROUP_ATTENDEE()
+                    # don't create GROUP_ATTENDEE rows in updateEventGroupLink()
                     del self._groupCUAToAttendeeMemberPropMap
                 else:
                     # delete existing group rows
@@ -2082,7 +2084,9 @@
                         #Return=[ga.GROUP_ID]
                     ).on(txn)
 
+        returnValue(isOldEventWithGroupAttendees)
 
+
     def validCalendarDataCheck(self, component, inserting):
         """
         Check that the calendar data is valid iCalendar.
@@ -2777,7 +2781,7 @@
 
         # update GROUP_ATTENDEE table rows
         if inserting:
-            yield self.updateGROUP_ATTENDEE()
+            yield self.updateEventGroupLink()
 
         # Post process managed attachments
         if internal_state in (
@@ -2915,7 +2919,7 @@
 
             # Now coerce indexing to off if needed
             if not doInstanceIndexing:
-                #instances = None # used by deleteOldGROUP_ATTENDEE() call at end
+                #instances = None # used by removeOldEventGroupLink() call at end
                 recurrenceLowerLimit = None
                 recurrenceLimit = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
 
@@ -3025,7 +3029,7 @@
         if instanceIndexingRequired and doInstanceIndexing:
             yield self._addInstances(component, instances, truncateLowerLimit, isInboxItem, txn)
 
-        yield self.deleteOldGROUP_ATTENDEE(component, instances, inserting, txn)
+        yield self.removeOldEventGroupLink(component, instances, inserting, txn)
 
 
     @inlineCallbacks
@@ -4048,7 +4052,7 @@
             in L{CalendarObject.splitAt}).
         @type rid: L{DateTime}
         @param olderUID: sets the iCalendar UID to be used in the new resource created during the split.
-            If set to L{None}, a UUID is generated and used.
+            If L{None} a UUID is generated and used.
         @type olderUID: L{str}
         """
 

Modified: CalendarServer/trunk/txdav/who/groups.py
===================================================================
--- CalendarServer/trunk/txdav/who/groups.py	2014-06-11 18:38:06 UTC (rev 13632)
+++ CalendarServer/trunk/txdav/who/groups.py	2014-06-12 10:06:36 UTC (rev 13633)
@@ -19,13 +19,16 @@
 Group membership caching
 """
 
+from pycalendar.datetime import DateTime
+from pycalendar.duration import Duration
 from twext.enterprise.dal.record import fromTable
 from twext.enterprise.dal.syntax import Delete, Select
 from twext.enterprise.jobqueue import WorkItem, RegeneratingWorkItem
 from twext.python.log import Logger
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twistedcaldav.config import config
-from txdav.caldav.datastore.sql import CalendarStoreFeatures
+from txdav.caldav.datastore.scheduling.icalsplitter import iCalSplitter
+from txdav.caldav.datastore.sql import CalendarStoreFeatures, ComponentUpdateState
 from txdav.common.datastore.sql_tables import schema
 import datetime
 import hashlib
@@ -135,7 +138,11 @@
         ).on(self.transaction)
 
         # get db object
-        calendarObject = (yield CalendarStoreFeatures(self.transaction._store).calendarObjectWithID(self.transaction, self.resourceID))
+        calendarObject = yield CalendarStoreFeatures(
+            self.transaction._store
+        ).calendarObjectWithID(
+            self.transaction, self.resourceID
+        )
         component = yield calendarObject.componentForUser()
 
         # Change a copy of the original, as we need the original cached on the resource
@@ -144,6 +151,66 @@
 
         # sync group attendees
         if (yield calendarObject.reconcileGroupAttendees(component)):
+
+            # group attendees in event have changed
+            if (component.masterComponent() is None or not component.isRecurring()):
+
+                # skip non-recurring old events, no instances
+                if (yield calendarObject.removeOldEventGroupLink(
+                    component,
+                    instances=None,
+                    inserting=False,
+                    txn=self.transaction
+                )):
+                    returnValue(None)
+            else:
+                # skip recurring old events
+                expand = (DateTime.getToday() +
+                          Duration(days=config.FreeBusyIndexExpandAheadDays))
+
+                if config.FreeBusyIndexLowerLimitDays:
+                    truncateLowerLimit = DateTime.getToday()
+                    truncateLowerLimit.offsetDay(-config.FreeBusyIndexLowerLimitDays)
+                else:
+                    truncateLowerLimit = None
+
+                instances = component.expandTimeRanges(
+                    expand,
+                    lowerLimit=truncateLowerLimit,
+                    ignoreInvalidInstances=True
+                )
+                if (yield calendarObject.removeOldEventGroupLink(
+                    component,
+                    instances=instances,
+                    inserting=False,
+                    txn=self.transaction
+                )):
+                    returnValue(None)
+
+                # split spanning events and only update present-future split result
+                splitter = iCalSplitter(0, 1)
+                break_point = DateTime.getToday() - Duration(seconds=config.GroupAttendees.UpdateOldEventLimitSeconds)
+                rid = splitter.whereSplit(component, break_point=break_point)
+                if rid is not None:
+                    yield calendarObject.split(onlyThis=True, rid=rid)
+
+                    # remove group link to ensure update (update to unknown hash would work too)
+                    ga = schema.GROUP_ATTENDEE
+                    yield Delete(
+                        From=ga,
+                        Where=(ga.RESOURCE_ID == self.resourceID).And(
+                            ga.GROUP_ID == self.groupID
+                        )
+                    ).on(self.transaction)
+
+                    # update group attendee in remaining component
+                    component = yield calendarObject.componentForUser()
+                    component = component.duplicate()
+                    change = yield calendarObject.reconcileGroupAttendees(component)
+                    assert change
+                    yield calendarObject._setComponentInternal(component, False, ComponentUpdateState.SPLIT_OWNER)
+                    returnValue(None)
+
             yield calendarObject.setComponent(component)
 
 

Modified: CalendarServer/trunk/txdav/who/test/test_group_attendees.py
===================================================================
--- CalendarServer/trunk/txdav/who/test/test_group_attendees.py	2014-06-11 18:38:06 UTC (rev 13632)
+++ CalendarServer/trunk/txdav/who/test/test_group_attendees.py	2014-06-12 10:06:36 UTC (rev 13633)
@@ -18,8 +18,7 @@
     group attendee tests
 """
 
-import os
-
+from twext.enterprise.dal.syntax import Insert
 from twext.enterprise.jobqueue import JobItem
 from twext.python.filepath import CachingFilePath as FilePath
 from twext.who.directory import DirectoryService
@@ -29,8 +28,10 @@
 from twistedcaldav.config import config
 from twistedcaldav.ical import Component, normalize_iCalStr, ignoredComponents
 from txdav.caldav.datastore.test.util import populateCalendarsFrom, CommonCommonTests
+from txdav.common.datastore.sql_tables import schema
 from txdav.who.directory import CalendarDirectoryRecordMixin
 from txdav.who.groups import GroupCacher
+import os
 
 
 class GroupAttendeeReconciliation(CommonCommonTests, unittest.TestCase):
@@ -508,6 +509,7 @@
 
         groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
         self.assertEqual(len(groupsToRefresh), 1)
+        self.assertEqual(list(groupsToRefresh)[0], "20000000-0000-0000-0000-000000000001")
 
         wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "20000000-0000-0000-0000-000000000001")
         yield self.commit()
@@ -649,6 +651,7 @@
 
         groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
         self.assertEqual(len(groupsToRefresh), 1)
+        self.assertEqual(list(groupsToRefresh)[0], "20000000-0000-0000-0000-000000000001")
 
         wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "20000000-0000-0000-0000-000000000001")
         yield self.commit()
@@ -824,6 +827,7 @@
             yield None
             returnValue(set())
 
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="10000000-0000-0000-0000-000000000002")
@@ -833,6 +837,7 @@
 
         groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
         self.assertEqual(len(groupsToRefresh), 1)
+        self.assertEqual(list(groupsToRefresh)[0], "20000000-0000-0000-0000-000000000001")
 
         wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "20000000-0000-0000-0000-000000000001")
         yield self.commit()
@@ -856,12 +861,31 @@
         self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
 
         wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "20000000-0000-0000-0000-000000000001")
-        if len(wps): # This is needed because the test currently fails and does actually create job items we have to wait for
-            yield self.commit()
-            yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
         self.assertEqual(len(wps), 0)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
 
+        #finally, simulate an event that has become old
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", unpatchedExpandedMembers)
 
+        groupID, _ignore_name, _ignore_membershipHash, _ignore_modDate = yield self.transactionUnderTest().groupByUID("20000000-0000-0000-0000-000000000001")
+        ga = schema.GROUP_ATTENDEE
+        yield Insert({
+                ga.RESOURCE_ID: cobj._resourceID,
+                ga.GROUP_ID: groupID,
+                ga.MEMBERSHIP_HASH: (-1),
+            }
+        ).on(self.transactionUnderTest())
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "20000000-0000-0000-0000-000000000001")
+        self.assertEqual(len(wps), 1)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000002")
+        vcalendar = yield cobj.component()
+        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+
+
     @inlineCallbacks
     def test_groupChangeOldNoMasterEvent(self):
         """
@@ -957,6 +981,7 @@
             yield None
             returnValue(set())
 
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
         groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
 
         calendar = yield self.calendarUnderTest(name="calendar", home="10000000-0000-0000-0000-000000000002")
@@ -966,6 +991,7 @@
 
         groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
         self.assertEqual(len(groupsToRefresh), 1)
+        self.assertEqual(list(groupsToRefresh)[0], "20000000-0000-0000-0000-000000000001")
 
         wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "20000000-0000-0000-0000-000000000001")
         yield self.commit()
@@ -994,8 +1020,129 @@
             yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
         self.assertEqual(len(wps), 0)
 
+        #finally, simulate an event that has become old
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", unpatchedExpandedMembers)
 
+        groupID, _ignore_name, _ignore_membershipHash, _ignore_modDate = yield self.transactionUnderTest().groupByUID("20000000-0000-0000-0000-000000000001")
+        ga = schema.GROUP_ATTENDEE
+        yield Insert({
+                ga.RESOURCE_ID: cobj._resourceID,
+                ga.GROUP_ID: groupID,
+                ga.MEMBERSHIP_HASH: (-1),
+            }
+        ).on(self.transactionUnderTest())
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "20000000-0000-0000-0000-000000000001")
+        self.assertEqual(len(wps), 1)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000002")
+        vcalendar = yield cobj.component()
+        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_2))
+
+
     @inlineCallbacks
+    def test_groupChangeSpanningEvent(self):
+        """
+        Test that group attendee changes not applied to old recurring events
+        """
+
+        data_put_1 = """BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+VERSION:2.0
+BEGIN:VEVENT
+DTSTAMP:20051222T205953Z
+CREATED:20060101T150000Z
+DTSTART:20120101T100000Z
+DURATION:PT1H
+RRULE:FREQ=DAILY;UNTIL=20240101T100000
+SUMMARY:event 1
+UID:event1 at ninevah.local
+ORGANIZER:MAILTO:user02 at example.com
+ATTENDEE:mailto:user02 at example.com
+ATTENDEE:MAILTO:group01 at example.com
+END:VEVENT
+END:VCALENDAR"""
+
+        data_get_1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:event1 at ninevah.local
+DTSTART:20120101T100000Z
+DURATION:PT1H
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:10000000-0000-0000-0000-000000000002
+ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;RSVP=TRUE:urn:x-uid:20000000-0000-0000-0000-000000000001
+ATTENDEE;CN=User 01;EMAIL=user01 at example.com;MEMBER="urn:x-uid:20000000-0000-0000-0000-000000000001";PARTSTAT=NEEDS-ACTION;RSVP=TRUE;SCHEDULE-STATUS=1.2:urn:x-uid:10000000-0000-0000-0000-000000000001
+CREATED:20060101T150000Z
+ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:10000000-0000-0000-0000-000000000002
+RRULE:FREQ=DAILY;UNTIL=20240101T100000
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        data_get_2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//Example Inc.//Example Calendar//EN
+BEGIN:VEVENT
+UID:event1 at ninevah.local
+{start}DURATION:PT1H
+ATTENDEE;CN=User 02;EMAIL=user02 at example.com;RSVP=TRUE:urn:x-uid:10000000-0000-0000-0000-000000000002
+ATTENDEE;CN=Group 01;CUTYPE=X-SERVER-GROUP;EMAIL=group01 at example.com;RSVP=TRUE:urn:x-uid:20000000-0000-0000-0000-000000000001
+CREATED:20060101T150000Z
+ORGANIZER;CN=User 02;EMAIL=user02 at example.com:urn:x-uid:10000000-0000-0000-0000-000000000002
+{relatedTo}RRULE:FREQ=DAILY;UNTIL=20240101T100000
+SEQUENCE:2
+SUMMARY:event 1
+END:VEVENT
+END:VCALENDAR
+"""
+
+        @inlineCallbacks
+        def expandedMembers(self, records=None):
+            yield None
+            returnValue(set())
+
+        groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
+
+        calendar = yield self.calendarUnderTest(name="calendar", home="10000000-0000-0000-0000-000000000002")
+        vcalendar = Component.fromString(data_put_1)
+        yield calendar.createCalendarObjectWithName("data1.ics", vcalendar)
+        yield self.commit()
+
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000002")
+        vcalendar = yield cobj.component()
+        self.assertEqual(normalize_iCalStr(vcalendar), normalize_iCalStr(data_get_1))
+
+        yield self._verifyObjectResourceCount("10000000-0000-0000-0000-000000000001", 1)
+
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
+
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "20000000-0000-0000-0000-000000000001")
+        yield self.commit()
+        self.assertEqual(len(wps), 1)
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+        cobj = yield self.calendarObjectUnderTest(name="data1.ics", calendar_name="calendar", home="10000000-0000-0000-0000-000000000002")
+        vcalendar = yield cobj.component()
+
+        for component in vcalendar.subcomponents():
+            if component.name() in ignoredComponents:
+                continue
+            relatedTo = component.getProperty("RELATED-TO")
+            start = component.getProperty("DTSTART")
+            break
+
+        self.assertEqual(
+            normalize_iCalStr(vcalendar),
+            normalize_iCalStr(data_get_2.format(start=start, relatedTo=relatedTo)))
+
+
+    @inlineCallbacks
     def test_groupRemovalFromDirectory(self):
         """
         Test that removing a group from the directory also removes the expanded attendees.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140612/78382fe7/attachment-0001.html>


More information about the calendarserver-changes mailing list