[CalendarServer-changes] [4447] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Thu Jul 9 19:24:27 PDT 2009


Revision: 4447
          http://trac.macosforge.org/projects/calendarserver/changeset/4447
Author:   cdaboo at apple.com
Date:     2009-07-09 19:24:26 -0700 (Thu, 09 Jul 2009)
Log Message:
-----------
Support getting freebusy entirely from the index. The index is upgraded on-the-fly to support the new
data needed for this.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/index.py
    CalendarServer/trunk/twistedcaldav/method/report_common.py
    CalendarServer/trunk/twistedcaldav/test/test_index.py

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2009-07-09 23:56:11 UTC (rev 4446)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2009-07-10 02:24:26 UTC (rev 4447)
@@ -747,6 +747,25 @@
         else:
             return None, None
 
+    def getFBType(self):
+        
+        # Only VEVENTs block time
+        if self.name() not in ("VEVENT", ):
+            return "FREE"
+        
+        # If it is TRANSPARENT we always ignore it
+        if self.propertyValue("TRANSP") == "TRANSPARENT":
+            return "FREE"
+        
+        # Handle status
+        status = self.propertyValue("STATUS")
+        if status == "CANCELLED":
+            return "FREE"
+        elif status == "TENTATIVE":
+            return "BUSY-TENTATIVE"
+        else:
+            return "BUSY"
+
     def addProperty(self, property):
         """
         Adds a property to this component.
@@ -1277,11 +1296,11 @@
                     got_override = True
                             
                 # Check that if an override is present then the master is recurring
-                # Leopard iCal sometimes does this for overridden instances that an Attendee receisves and
+                # Leopard iCal sometimes does this for overridden instances that an Attendee receives and
                 # it creates a "fake" (invalid) master. We are going to skip this test here. Instead implicit
                 # scheduling with verify the validity of the components and raise if they don't make sense.
                 # If no scheduling is happening then we allow this - that may cause other clients to choke.
-                # If it does we will have to re-instate this check but only after we have checked for implicit.
+                # If it does we will have to reinstate this check but only after we have checked for implicit.
 #                if got_override and got_master and not master_recurring:
 #                    msg = "Calendar resources must have a recurring master component if there is an overridden one (%s)" % (subcomponent.propertyValue("UID"),)
 #                    log.debug(msg)
@@ -1963,7 +1982,7 @@
         Also normalize the RRULE value parts.
         
         Strictly speaking we should not need to do this as clients should not be messing with
-        these properties - i.e. they should roundtrip them. Unfortunately some do...
+        these properties - i.e. they should round trip them. Unfortunately some do...
         """
         
         # TODO: what about VJOURNAL and VTODO?
@@ -2201,7 +2220,7 @@
 
 def tzexpand(tzdata, start, end):
     """
-    Expand a timezone to get onset/utc-offset observance tuples withinthe specified
+    Expand a timezone to get onset/utc-offset observance tuples within the specified
     time range.
 
     @param tzdata: the iCalendar data containing a VTIMEZONE.

Modified: CalendarServer/trunk/twistedcaldav/index.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/index.py	2009-07-09 23:56:11 UTC (rev 4446)
+++ CalendarServer/trunk/twistedcaldav/index.py	2009-07-10 02:24:26 UTC (rev 4447)
@@ -57,9 +57,17 @@
 log = Logger()
 
 db_basename = db_prefix + "sqlite"
-schema_version = "6"
+schema_version = "7"
 collection_types = {"Calendar": "Regular Calendar Collection", "iTIP": "iTIP Calendar Collection"}
 
+icalfbtype_to_indexfbtype = {
+    "FREE"            : 'F',
+    "BUSY"            : 'B',
+    "BUSY-UNAVAILABLE": 'U',
+    "BUSY-TENTATIVE"  : 'T',
+}
+indexfbtype_to_icalfbtype = dict([(v, k) for k,v in icalfbtype_to_indexfbtype.iteritems()])
+
 #
 # Duration into the future through which recurrences are expanded in the index
 # by default.  This is a caching parameter which affects the size of the index;
@@ -74,13 +82,13 @@
 # does not affect search results beyond this period, but it may affect
 # performance of such a search.
 #
-# When a search is performed on a timespan that goes beyond that which is
+# When a search is performed on a time span that goes beyond that which is
 # expanded in the index, we have to open each resource which may have data in
 # that time period.  In order to avoid doing that multiple times, we want to
 # cache those results.  However, we don't necessarily want to cache all
-# occurances into some obscenely far-in-the-future date, so we cap the caching
+# occurrences into some obscenely far-in-the-future date, so we cap the caching
 # period.  Searches beyond this period will always be relatively expensive for
-# resources with occurances beyond this period.
+# resources with occurrences beyond this period.
 #
 maximum_future_expansion_duration = datetime.timedelta(days=365*5)
 
@@ -266,11 +274,11 @@
             self.log_info("Search falls outside range of index for %s %s" % (name, minDate))
             self.reExpandResource(name, minDate)
 
-    def indexedSearch(self, filter):
+    def indexedSearch(self, filter, fbtype=False):
         """
         Finds resources matching the given qualifiers.
         @param filter: the L{Filter} for the calendar-query to execute.
-        @return: an interable iterable of tuples for each resource matching the
+        @return: an iterable of tuples for each resource matching the
             given C{qualifiers}. The tuples are C{(name, uid, type)}, where
             C{name} is the resource name, C{uid} is the resource UID, and
             C{type} is the resource iCalendar component type.x
@@ -302,7 +310,15 @@
         if qualifiers is None:
             rowiter = self._db_execute("select NAME, UID, TYPE from RESOURCE")
         else:
-            rowiter = self._db_execute("select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE" + qualifiers[0], *qualifiers[1])
+            if fbtype:
+                # For a free-busy time-range query we return all instances
+                rowiter = self._db_execute(
+                    "select RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE, RESOURCE.ORGANIZER, TIMESPAN.FLOAT, TIMESPAN.START, TIMESPAN.END, TIMESPAN.FBTYPE" + 
+                    qualifiers[0],
+                    *qualifiers[1]
+                )
+            else:
+                rowiter = self._db_execute("select DISTINCT RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE" + qualifiers[0], *qualifiers[1])
 
         # Check result for missing resources
 
@@ -385,44 +401,41 @@
         #   UID: iCalendar UID (may or may not be unique)
         #   TYPE: iCalendar component type
         #   RECURRANCE_MAX: Highest date of recurrence expansion
+        #   ORGANIZER: cu-address of the Organizer of the event
         #
-        if uidunique:
-            q.execute(
-                """
-                create table RESOURCE (
-                    NAME           text unique,
-                    UID            text unique,
-                    TYPE           text,
-                    RECURRANCE_MAX date
-                )
-                """
+        q.execute(
+            """
+            create table RESOURCE (
+                NAME           text unique,
+                UID            text%s,
+                TYPE           text,
+                RECURRANCE_MAX date,
+                ORGANIZER      text
             )
-        else:
-            q.execute(
-                """
-                create table RESOURCE (
-                    NAME           text unique,
-                    UID            text,
-                    TYPE           text,
-                    RECURRANCE_MAX date
-                )
-                """
-            )
+            """ % (" unique" if uidunique else "",)
+        )
 
         #
-        # TIMESPAN table tracks (expanded) timespans for resources
+        # TIMESPAN table tracks (expanded) time spans for resources
         #   NAME: Related resource (RESOURCE foreign key)
         #   FLOAT: 'Y' if start/end are floating, 'N' otherwise
         #   START: Start date
         #   END: End date
+        #   FBTYPE: FBTYPE value:
+        #     '?' - unknown
+        #     'F' - free
+        #     'B' - busy
+        #     'U' - busy-unavailable
+        #     'T' - busy-tentative
         #
         q.execute(
             """
             create table TIMESPAN (
-                NAME  text,
-                FLOAT text(1),
-                START date,
-                END   date
+                NAME   text,
+                FLOAT  text(1),
+                START  date,
+                END    date,
+                FBTYPE text(1)
             )
             """
         )
@@ -442,6 +455,23 @@
                 """
             )
 
+    def _db_upgrade(self, old_version):
+        """
+        Upgrade the database tables.
+        """
+        
+        # When going to version 7 all we need to do is add a column to the time-range
+        if old_version < "7":
+            self._db_connection = sqlite.connect(self.dbpath, isolation_level=None)
+            q = self._db_connection.cursor()
+            q.execute("alter table RESOURCE add column ORGANIZER text default '?'")
+            q.execute("alter table TIMESPAN add column FBTYPE text(1) default '?'")
+            self._db_upgrade_schema(q)
+            self._db_close()
+            return self._db()
+        else:
+            return super(AbstractCalendarIndex, self)._db_upgrade(old_version)
+
     def notExpandedBeyond(self, minDate):
         """
         Gives all resources which have not been expanded beyond a given date
@@ -470,6 +500,9 @@
             contents.
         """
         uid = calendar.resourceUID()
+        organizer = calendar.getOrganizer()
+        if not organizer:
+            organizer = ""
 
         # Decide how far to expand based on the component
         master = calendar.masterComponent()
@@ -502,9 +535,9 @@
             float = 'Y' if instance.start.tzinfo is None else 'N'
             self._db_execute(
                 """
-                insert into TIMESPAN (NAME, FLOAT, START, END)
-                values (:1, :2, :3, :4)
-                """, name, float, start, end
+                insert into TIMESPAN (NAME, FLOAT, START, END, FBTYPE)
+                values (:1, :2, :3, :4, :5)
+                """, name, float, start, end, icalfbtype_to_indexfbtype.get(instance.component.getFBType(), 'F')
             )
 
         # Special - for unbounded recurrence we insert a value for "infinity"
@@ -515,16 +548,16 @@
             float = 'N'
             self._db_execute(
                 """
-                insert into TIMESPAN (NAME, FLOAT, START, END)
-                values (:1, :2, :3, :4)
-                """, name, float, start, end
+                insert into TIMESPAN (NAME, FLOAT, START, END, FBTYPE)
+                values (:1, :2, :3, :4, :5)
+                """, name, float, start, end, '?'
             )
              
         self._db_execute(
             """
-            insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX)
-            values (:1, :2, :3, :4)
-            """, name, uid, calendar.resourceType(), instances.limit
+            insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX, ORGANIZER)
+            values (:1, :2, :3, :4, :5)
+            """, name, uid, calendar.resourceType(), instances.limit, organizer
         )
 
     def _delete_from_db(self, name, uid):
@@ -702,7 +735,7 @@
     def __init__(self, resource):
         """
         @param resource: the L{twistedcaldav.static.CalDAVFile} resource to
-            index. C{resource} must be a calendar collection (ie.
+            index. C{resource} must be a calendar collection (i.e.
             C{resource.isPseudoCalendarCollection()} returns C{True}.)
         """
         assert resource.isCalendarCollection(), "non-calendar collection resource %s has no index." % (resource,)
@@ -804,7 +837,7 @@
     def __init__(self, resource):
         """
         @param resource: the L{twistedcaldav.static.CalDAVFile} resource to
-            index. C{resource} must be a calendar collection (ie.
+            index. C{resource} must be a calendar collection (i.e.
             C{resource.isPseudoCalendarCollection()} returns C{True}.)
         """
         assert resource.isPseudoCalendarCollection() and not resource.isCalendarCollection(), "non-calendar collection resource %s has no index." % (resource,)

Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py	2009-07-09 23:56:11 UTC (rev 4446)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py	2009-07-10 02:24:26 UTC (rev 4447)
@@ -285,6 +285,9 @@
     
     returnValue(properties_by_status)
 
+fbtype_mapper = {"BUSY": 0, "BUSY-TENTATIVE": 1, "BUSY-UNAVAILABLE": 2}
+fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
+
 @inlineCallbacks
 def generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchtotal,
                          excludeuid=None, organizer=None, same_calendar_user=False,
@@ -349,12 +352,19 @@
     filteredaces = (yield calresource.inheritedACEsforChildren(request))
 
     try:
-        resources = calresource.index().indexedSearch(filter)
+        resources = calresource.index().indexedSearch(filter, fbtype=True)
     except IndexedSearchException:
         resources = calresource.index().bruteForceSearch()
 
-    for name, uid, type in resources: #@UnusedVariable
+    # We care about separate instances for VEVENTs only
+    aggregated_resources = {}
+    for name, uid, type, test_organizer, float, start, end, fbtype in resources:
+        aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
 
+    for key in aggregated_resources.iterkeys():
+
+        name, uid, type, test_organizer = key
+
         # Check privileges - must have at least CalDAV:read-free-busy
         child = (yield request.locateChildResource(calresource, name))
 
@@ -365,45 +375,92 @@
             except AccessDeniedError:
                 continue
 
-        calendar = calresource.iCalendar(name)
-        
-        # The calendar may come back as None if the resource is being changed, or was deleted
-        # between our initial index query and getting here. For now we will ignore this error, but in
-        # the longer term we need to implement some form of locking, perhaps.
-        if calendar is None:
-            log.err("Calendar %s is missing from calendar collection %r" % (name, calresource))
-            continue
-        
-        # Ignore ones of this UID
-        if excludeuid:
-            # See if we have a UID match
-            if (excludeuid == uid):
-                test_organizer = calendar.getOrganizer()
-                test_principal = calresource.principalForCalendarUserAddress(test_organizer) if test_organizer else None
-                test_uid = test_principal.principalUID() if test_principal else ""
-
-                # Check that ORGANIZER's match (security requirement)
-                if (organizer is None) or (organizer_uid == test_uid):
+        # Short-cut - if an fbtype exists we can use that
+        if type == "VEVENT" and aggregated_resources[key][0][3] != '?':
+            
+            # Look at each instance
+            for float, start, end, fbtype in aggregated_resources[key]:
+                # Ignore free time or unknown
+                if fbtype in ('F', '?'):
                     continue
-                # Check for no ORGANIZER and check by same calendar user
-                elif (test_organizer is None) and same_calendar_user:
-                    continue
+                
+                # Ignore ones of this UID
+                if excludeuid:
+                    # See if we have a UID match
+                    if (excludeuid == uid):
+                        test_principal = calresource.principalForCalendarUserAddress(test_organizer) if test_organizer else None
+                        test_uid = test_principal.principalUID() if test_principal else ""
 
-        if filter.match(calendar, None):
-            # Check size of results is within limit
-            matchtotal += 1
-            if matchtotal > max_number_of_matches:
-                raise NumberOfMatchesWithinLimits(max_number_of_matches)
+                        # Check that ORGANIZER's match (security requirement)
+                        if (organizer is None) or (organizer_uid == test_uid):
+                            continue
+                        # Check for no ORGANIZER and check by same calendar user
+                        elif (test_uid == "") and same_calendar_user:
+                            continue
+                        
+                # Apply a timezone to any floating times
+                fbstart = datetime.datetime.strptime(start[:-6], "%Y-%m-%d %H:%M:%S")
+                if float == 'Y':
+                    fbstart = fbstart.replace(tzinfo=tzinfo)
+                else:
+                    fbstart = fbstart.replace(tzinfo=utc)
+                fbend =datetime.datetime.strptime(end[:-6], "%Y-%m-%d %H:%M:%S")
+                if float == 'Y':
+                    fbend = fbend.replace(tzinfo=tzinfo)
+                else:
+                    fbend = fbend.replace(tzinfo=utc)
+                
+                # Click instance to time range
+                clipped = clipPeriod((fbstart, fbend - fbstart), (timerange.start, timerange.end))
 
-            if calendar.mainType() == "VEVENT":
-                processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
-            elif calendar.mainType() == "VFREEBUSY":
-                processFreeBusyFreeBusy(calendar, fbinfo, timerange)
-            elif calendar.mainType() == "VAVAILABILITY":
-                processAvailabilityFreeBusy(calendar, fbinfo, timerange)
-            else:
-                assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
+                # Double check for overlap
+                if clipped:
+                    fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)
+                
+        else:
+            calendar = calresource.iCalendar(name)
+            
+            # The calendar may come back as None if the resource is being changed, or was deleted
+            # between our initial index query and getting here. For now we will ignore this error, but in
+            # the longer term we need to implement some form of locking, perhaps.
+            if calendar is None:
+                log.err("Calendar %s is missing from calendar collection %r" % (name, calresource))
+                continue
+            
+            # Ignore ones of this UID
+            if excludeuid:
+                # See if we have a UID match
+                if (excludeuid == uid):
+                    test_organizer = calendar.getOrganizer()
+                    test_principal = calresource.principalForCalendarUserAddress(test_organizer) if test_organizer else None
+                    test_uid = test_principal.principalUID() if test_principal else ""
     
+                    # Check that ORGANIZER's match (security requirement)
+                    if (organizer is None) or (organizer_uid == test_uid):
+                        continue
+                    # Check for no ORGANIZER and check by same calendar user
+                    elif (test_organizer is None) and same_calendar_user:
+                        continue
+    
+            if filter.match(calendar, None):
+                # Check size of results is within limit
+                matchtotal += 1
+                if matchtotal > max_number_of_matches:
+                    raise NumberOfMatchesWithinLimits(max_number_of_matches)
+    
+                if calendar.mainType() == "VEVENT":
+                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
+                    
+                    # Lets also force an index rebuild for this resource so that next time we have the fbtype set
+                    calresource.index().addResource(name, calendar, reCreate=True)
+
+                elif calendar.mainType() == "VFREEBUSY":
+                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
+                elif calendar.mainType() == "VAVAILABILITY":
+                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
+                else:
+                    assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
+    
     returnValue(matchtotal)
 
 def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
@@ -501,8 +558,7 @@
                 # Clip period for this instance
                 clipped = clipPeriod(period, (timerange.start, timerange.end))
                 if clipped:
-                    mapper = {"BUSY": 0, "BUSY-TENTATIVE": 1, "BUSY-UNAVAILABLE": 2}
-                    fbinfo[mapper.get(fbtype, 0)].append(clipped)
+                    fbinfo[fbtype_mapper.get(fbtype, 0)].append(clipped)
 
 def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
     """
@@ -544,8 +600,7 @@
         if fbtype is None:
             fbtype = "BUSY-UNAVAILABLE"
 
-        mapper = {"BUSY": 0, "BUSY-TENTATIVE": 1, "BUSY-UNAVAILABLE": 2}
-        fbinfo[mapper.get(fbtype, 2)].extend(busyperiods)
+        fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
             
 
 def processAvailablePeriods(calendar, timerange):

Modified: CalendarServer/trunk/twistedcaldav/test/test_index.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_index.py	2009-07-09 23:56:11 UTC (rev 4446)
+++ CalendarServer/trunk/twistedcaldav/test/test_index.py	2009-07-10 02:24:26 UTC (rev 4447)
@@ -18,11 +18,17 @@
 from twisted.internet.task import deferLater
 
 from twistedcaldav.ical import Component
-from twistedcaldav.index import Index
+from twistedcaldav.index import Index, default_future_expansion_duration,\
+    maximum_future_expansion_duration, IndexedSearchException,\
+    AbstractCalendarIndex
 from twistedcaldav.index import ReservationError, MemcachedUIDReserver
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.test.util import InMemoryMemcacheProtocol
 import twistedcaldav.test.util
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import TimeRange
+from vobject.icalendar import utc
+import sqlite3
 
 import datetime
 import os
@@ -251,6 +257,424 @@
             else:
                 self.assertFalse(self.db.resourceExists(name), msg=description)
 
+    def test_index_timespan(self):
+        data = (
+            (
+                "#1.1 Simple component - busy",
+                "1.1",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.1
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "20080601T000000Z", "20080602T000000Z",
+                "mailto:user1 at example.com",
+                (('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B'),),
+            ),
+            (
+                "#1.2 Simple component - transparent",
+                "1.2",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.2
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+                "20080602T000000Z", "20080603T000000Z",
+                "mailto:user1 at example.com",
+                (('N', "2008-06-02 12:00:00+00:00", "2008-06-02 13:00:00+00:00", 'F'),),
+            ),
+            (
+                "#1.3 Simple component - canceled",
+                "1.3",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.3
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+STATUS:CANCELLED
+END:VEVENT
+END:VCALENDAR
+""",
+                "20080603T000000Z", "20080604T000000Z",
+                "mailto:user1 at example.com",
+                (('N', "2008-06-03 12:00:00+00:00", "2008-06-03 13:00:00+00:00", 'F'),),
+            ),
+            (
+                "#1.4 Simple component - tentative",
+                "1.4",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.4
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+STATUS:TENTATIVE
+END:VEVENT
+END:VCALENDAR
+""",
+                "20080604T000000Z", "20080605T000000Z",
+                "mailto:user1 at example.com",
+                (('N', "2008-06-04 12:00:00+00:00", "2008-06-04 13:00:00+00:00", 'T'),),
+            ),
+            (
+                "#2.1 Recurring component - busy",
+                "2.1",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2.1
+DTSTART:20080605T120000Z
+DTEND:20080605T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY;COUNT=2
+END:VEVENT
+END:VCALENDAR
+""",
+                "20080605T000000Z", "20080607T000000Z",
+                "mailto:user1 at example.com",
+                (
+                    ('N', "2008-06-05 12:00:00+00:00", "2008-06-05 13:00:00+00:00", 'B'),
+                    ('N', "2008-06-06 12:00:00+00:00", "2008-06-06 13:00:00+00:00", 'B'),
+                ),
+            ),
+            (
+                "#2.2 Recurring component - busy",
+                "2.2",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2.2
+DTSTART:20080607T120000Z
+DTEND:20080607T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY;COUNT=2
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-2.2
+RECURRENCE-ID:20080608T120000Z
+DTSTART:20080608T140000Z
+DTEND:20080608T150000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""",
+                "20080607T000000Z", "20080609T000000Z",
+                "mailto:user1 at example.com",
+                (
+                    ('N', "2008-06-07 12:00:00+00:00", "2008-06-07 13:00:00+00:00", 'B'),
+                    ('N', "2008-06-08 14:00:00+00:00", "2008-06-08 15:00:00+00:00", 'F'),
+                ),
+            ),
+        )
+
+        for description, name, calendar_txt, trstart, trend, organizer, instances in data:
+            calendar = Component.fromString(calendar_txt)
+
+            f = open(os.path.join(self.site.resource.fp.path, name), "w")
+            f.write(calendar_txt)
+            del f
+
+            self.db.addResource(name, calendar)
+            self.assertTrue(self.db.resourceExists(name), msg=description)
+
+            # Create fake filter element to match time-range
+            filter =  caldavxml.Filter(
+                  caldavxml.ComponentFilter(
+                      caldavxml.ComponentFilter(
+                          TimeRange(
+                              start=trstart,
+                              end=trend,
+                          ),
+                          name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+                      ),
+                      name="VCALENDAR",
+                   )
+              )
+
+            resources = self.db.indexedSearch(filter, fbtype=True)
+            index_results = set()
+            for _ignore_name, _ignore_uid, type, test_organizer, float, start, end, fbtype in resources:
+                self.assertEqual(test_organizer, organizer, msg=description)
+                index_results.add((float, start, end, fbtype,))
+
+            self.assertEqual(set(instances), index_results, msg=description)
+
+class SQLIndexUpgradeTests (twistedcaldav.test.util.TestCase):
+    """
+    Test abstract SQL DB class
+    """
+
+    class OldIndexv6(Index):
+
+        def _db_version(self):
+            """
+            @return: the schema version assigned to this index.
+            """
+            return "6"
+
+        def _db_init_data_tables_base(self, q, uidunique):
+            """
+            Initialise the underlying database tables.
+            @param q:           a database cursor to use.
+            """
+            #
+            # RESOURCE table is the primary index table
+            #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
+            #   UID: iCalendar UID (may or may not be unique)
+            #   TYPE: iCalendar component type
+            #   RECURRANCE_MAX: Highest date of recurrence expansion
+            #
+            if uidunique:
+                q.execute(
+                    """
+                    create table RESOURCE (
+                        NAME           text unique,
+                        UID            text unique,
+                        TYPE           text,
+                        RECURRANCE_MAX date
+                    )
+                    """
+                )
+            else:
+                q.execute(
+                    """
+                    create table RESOURCE (
+                        NAME           text unique,
+                        UID            text,
+                        TYPE           text,
+                        RECURRANCE_MAX date
+                    )
+                    """
+                )
+    
+            #
+            # TIMESPAN table tracks (expanded) timespans for resources
+            #   NAME: Related resource (RESOURCE foreign key)
+            #   FLOAT: 'Y' if start/end are floating, 'N' otherwise
+            #   START: Start date
+            #   END: End date
+            #
+            q.execute(
+                """
+                create table TIMESPAN (
+                    NAME  text,
+                    FLOAT text(1),
+                    START date,
+                    END   date
+                )
+                """
+            )
+    
+            if uidunique:
+                #
+                # RESERVED table tracks reserved UIDs
+                #   UID: The UID being reserved
+                #   TIME: When the reservation was made
+                #
+                q.execute(
+                    """
+                    create table RESERVED (
+                        UID  text unique,
+                        TIME date
+                    )
+                    """
+                )
+
+        def _db_upgrade(self, old_version):
+            """
+            Upgrade the database tables.
+            """
+            
+            return super(AbstractCalendarIndex, self)._db_upgrade(old_version)
+
+        def _add_to_db(self, name, calendar, cursor = None, expand_until=None, reCreate=False):
+            """
+            Records the given calendar resource in the index with the given name.
+            Resource names and UIDs must both be unique; only one resource name may
+            be associated with any given UID and vice versa.
+            NB This method does not commit the changes to the db - the caller
+            MUST take care of that
+            @param name: the name of the resource to add.
+            @param calendar: a L{Calendar} object representing the resource
+                contents.
+            """
+            uid = calendar.resourceUID()
+    
+            # Decide how far to expand based on the component
+            master = calendar.masterComponent()
+            if master is None or not calendar.isRecurring() and not calendar.isRecurringUnbounded():
+                # When there is no master we have a set of overridden components - index them all.
+                # When there is one instance - index it.
+                # When bounded - index all.
+                expand = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
+            else:
+                if expand_until:
+                    expand = expand_until
+                else:
+                    expand = datetime.date.today() + default_future_expansion_duration
+        
+                if expand > (datetime.date.today() + maximum_future_expansion_duration):
+                    raise IndexedSearchException
+    
+            try:
+                instances = calendar.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)
+            except InvalidOverriddenInstanceError:
+                raise
+    
+            self._delete_from_db(name, uid)
+    
+            for key in instances:
+                instance = instances[key]
+                start = instance.start.replace(tzinfo=utc)
+                end = instance.end.replace(tzinfo=utc)
+                float = 'Y' if instance.start.tzinfo is None else 'N'
+                self._db_execute(
+                    """
+                    insert into TIMESPAN (NAME, FLOAT, START, END)
+                    values (:1, :2, :3, :4)
+                    """, name, float, start, end
+                )
+    
+            # Special - for unbounded recurrence we insert a value for "infinity"
+            # that will allow an open-ended time-range to always match it.
+            if calendar.isRecurringUnbounded():
+                start = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
+                end = datetime.datetime(2100, 1, 1, 1, 0, 0, tzinfo=utc)
+                float = 'N'
+                self._db_execute(
+                    """
+                    insert into TIMESPAN (NAME, FLOAT, START, END)
+                    values (:1, :2, :3, :4)
+                    """, name, float, start, end
+                )
+                 
+            self._db_execute(
+                """
+                insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX)
+                values (:1, :2, :3, :4)
+                """, name, uid, calendar.resourceType(), instances.limit
+            )
+
+    def setUp(self):
+        super(SQLIndexUpgradeTests, self).setUp()
+        self.site.resource.isCalendarCollection = lambda: True
+        self.db = Index(self.site.resource)
+        self.olddb = SQLIndexUpgradeTests.OldIndexv6(self.site.resource)
+
+    def prepareOldDB(self):
+        if os.path.exists(self.olddb.dbpath):
+            os.remove(self.olddb.dbpath)
+
+    def test_old_schema(self):
+        
+        self.prepareOldDB()
+
+        schema = self.olddb._db_value_for_sql(
+            """
+            select VALUE from CALDAV
+             where KEY = 'SCHEMA_VERSION'
+            """)
+        self.assertEqual(schema, self.olddb._db_version())
+
+    def test_empty_upgrade(self):
+        
+        self.prepareOldDB()
+
+        schema = self.olddb._db_value_for_sql(
+            """
+            select VALUE from CALDAV
+             where KEY = 'SCHEMA_VERSION'
+            """)
+        self.assertEqual(schema, self.olddb._db_version())
+
+        self.assertRaises(sqlite3.OperationalError, self.olddb._db_value_for_sql, "select ORGANIZER from RESOURCE")
+        self.assertRaises(sqlite3.OperationalError, self.olddb._db_value_for_sql, "select FBTYPE from TIMESPAN")
+
+        schema = self.db._db_value_for_sql(
+            """
+            select VALUE from CALDAV
+             where KEY = 'SCHEMA_VERSION'
+            """)
+        self.assertEqual(schema, self.db._db_version())
+
+        value = self.db._db_value_for_sql("select ORGANIZER from RESOURCE")
+        self.assertEqual(value, None)
+
+    def test_basic_upgrade(self):
+        
+        self.prepareOldDB()
+
+        calendar_name = "1.ics"
+        calendar_data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.1
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+"""
+
+        self.olddb.addResource(calendar_name, Component.fromString(calendar_data))
+        self.assertTrue(self.olddb.resourceExists(calendar_name))
+
+        self.assertRaises(sqlite3.OperationalError, self.olddb._db_value_for_sql, "select ORGANIZER from RESOURCE")
+        self.assertRaises(sqlite3.OperationalError, self.olddb._db_value_for_sql, "select FBTYPE from TIMESPAN")
+
+        value = self.db._db_value_for_sql("select ORGANIZER from RESOURCE where NAME = :1", calendar_name)
+        self.assertEqual(value, "?")
+
+        value = self.db._db_value_for_sql("select FBTYPE from TIMESPAN where NAME = :1", calendar_name)
+        self.assertEqual(value, "?")
+
+        self.db.addResource(calendar_name, Component.fromString(calendar_data))
+        self.assertTrue(self.olddb.resourceExists(calendar_name))
+
+        value = self.db._db_value_for_sql("select ORGANIZER from RESOURCE where NAME = :1", calendar_name)
+        self.assertEqual(value, "mailto:user1 at example.com")
+
+        value = self.db._db_value_for_sql("select FBTYPE from TIMESPAN where NAME = :1", calendar_name)
+        self.assertEqual(value, "B")
+
 class MemcacheTests(SQLIndexTests):
     def setUp(self):
         super(MemcacheTests, self).setUp()
@@ -259,6 +683,6 @@
 
 
     def tearDown(self):
-        for k, v in self.memcache._timeouts.iteritems():
+        for _ignore_k, v in self.memcache._timeouts.iteritems():
             if v.active():
                 v.cancel()
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090709/63758611/attachment-0001.html>


More information about the calendarserver-changes mailing list