[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