def test_workdistribution(self): tzname = "US/Eastern" dist = WorkDistribution(["mon", "wed", "thu", "sat"], 10, 20, tzname) dist._helperDistribution = UniformDiscreteDistribution( [35 * 60 * 60 + 30 * 60]) dist.now = lambda tzname=None: DateTime( 2011, 5, 29, 18, 5, 36, tzid=tzname) value = dist.sample() self.assertEqual( # Move past three workdays - monday, wednesday, thursday - using 30 # of the hours, and then five and a half hours into the fourth # workday, saturday. Workday starts at 10am, so the sample value # is 3:30pm, ie 1530 hours. DateTime(2011, 6, 4, 15, 30, 0, tzid=Timezone(tzid=tzname)), value) dist = WorkDistribution(["mon", "tue", "wed", "thu", "fri"], 10, 20, tzname) dist._helperDistribution = UniformDiscreteDistribution( [35 * 60 * 60 + 30 * 60]) value = dist.sample() self.assertTrue(isinstance(value, DateTime))
def test_query_timerange(self): """ Basic query test - with time range """ filter = caldavxml.Filter( caldavxml.ComponentFilter( *[ caldavxml.ComponentFilter( *[ caldavxml.TimeRange( **{ "start": "20060605T160000Z", "end": "20060605T170000Z" }) ], **{"name": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")}) ], **{"name": "VCALENDAR"})) filter = Filter(filter) filter.child.settzinfo(Timezone(tzid="America/New_York")) expression = buildExpression(filter, self._queryFields) sql = CalDAVSQLQueryGenerator(expression, self, 1234) select, args, usedtimerange = sql.generate() self.assertEqual( select.toSQL(), SQLFragment( "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where ICALENDAR_TYPE in (?, ?, ?) and (FLOATING = ? and START_DATE < ? and END_DATE > ? or FLOATING = ? and START_DATE < ? and END_DATE > ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?", [ Parameter('arg1', 3), False, datetime.datetime(2006, 6, 5, 17, 0), datetime.datetime(2006, 6, 5, 16, 0), True, datetime.datetime(2006, 6, 5, 13, 0), datetime.datetime(2006, 6, 5, 12, 0), 1234 ])) self.assertEqual(args, {"arg1": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")}) self.assertEqual(usedtimerange, True)
def test_query_extended(self): """ Extended query test - two terms with anyof """ filter = caldavxml.Filter( caldavxml.ComponentFilter( *[ caldavxml.ComponentFilter( *[ caldavxml.TimeRange(**{ "start": "20060605T160000Z", }) ], **{"name": ("VEVENT")}), caldavxml.ComponentFilter(**{"name": ("VTODO")}), ], **{ "name": "VCALENDAR", "test": "anyof" })) filter = Filter(filter) filter.child.settzinfo(Timezone(tzid="America/New_York")) expression = buildExpression(filter, self._queryFields) sql = CalDAVSQLQueryGenerator(expression, self, 1234) select, args, usedtimerange = sql.generate() self.assertEqual( select.toSQL(), SQLFragment( "select distinct RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT, TIME_RANGE where (ICALENDAR_TYPE = ? and (FLOATING = ? and END_DATE > ? or FLOATING = ? and END_DATE > ?) or ICALENDAR_TYPE = ?) and CALENDAR_OBJECT_RESOURCE_ID = RESOURCE_ID and TIME_RANGE.CALENDAR_RESOURCE_ID = ?", [ 'VEVENT', False, datetime.datetime(2006, 6, 5, 16, 0, tzinfo=tzutc()), True, datetime.datetime(2006, 6, 5, 12, 0, tzinfo=tzutc()), 'VTODO', 1234 ])) self.assertEqual(args, {}) self.assertEqual(usedtimerange, True)
def validate(self, doFix=False): """ Validate the data in this component and optionally fix any problems. Return a tuple containing two lists: the first describes problems that were fixed, the second problems that were not fixed. Caller can then decide what to do with unfixed issues. """ # Do normal checks fixed, unfixed = super(ComponentRecur, self).validate(doFix) # Check that any UNTIL value matches that for DTSTART if self.mHasStart and self.mRecurrences: dtutc = self.mStart.duplicateAsUTC() for rrule in self.mRecurrences.getRules(): if rrule.getUseUntil(): if rrule.getUntil().isDateOnly() ^ self.mStart.isDateOnly( ): logProblem = "[%s] Value types must match: %s, %s" % ( self.getType(), definitions.cICalProperty_DTSTART, definitions.cICalValue_RECUR_UNTIL, ) if doFix: rrule.getUntil().setDateOnly( self.mStart.isDateOnly()) if not self.mStart.isDateOnly(): rrule.getUntil().setHHMMSS( dtutc.getHours(), dtutc.getMinutes(), dtutc.getSeconds()) rrule.getUntil().setTimezone( Timezone(utc=True)) self.mRecurrences.changed() fixed.append(logProblem) else: unfixed.append(logProblem) return fixed, unfixed
def test_normalizeToUTC(self): """ Test that dateops.normalizeToUTC works correctly on all four types of date/time: date only, floating, UTC and local time. """ data = ( (DateTime(2012, 1, 1), DateTime(2012, 1, 1, 0, 0, 0, tzid=Timezone.UTCTimezone)), (DateTime(2012, 1, 1, 10, 0, 0), DateTime(2012, 1, 1, 10, 0, 0, tzid=Timezone.UTCTimezone)), (DateTime(2012, 1, 1, 11, 0, 0, tzid=Timezone.UTCTimezone), DateTime(2012, 1, 1, 11, 0, 0, tzid=Timezone.UTCTimezone)), (DateTime(2012, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2012, 1, 1, 17, 0, 0, tzid=Timezone.UTCTimezone)), ) for value, result in data: self.assertEqual(normalizeToUTC(value), result)
def testDeriveComponent(self): data = ( ( "1.1 Recurring no VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;VALUE=DATE:20110601 DURATION:P1D DTSTAMP:20020101T000000Z RRULE:FREQ=DAILY SUMMARY:New Year's Day END:VEVENT BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;VALUE=DATE:20110602 DTSTART;VALUE=DATE:20110602 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), DateTime(2011, 6, 3), """BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;VALUE=DATE:20110603 DTSTART;VALUE=DATE:20110603 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT """.replace("\n", "\r\n"), ), ( "2.2 Recurring with VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VTIMEZONE TZID:Etc/GMT+1 X-LIC-LOCATION:Etc/GMT+1 BEGIN:STANDARD DTSTART:18000101T000000 RDATE:18000101T000000 TZNAME:GMT+1 TZOFFSETFROM:-0100 TZOFFSETTO:-0100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;TZID=Etc/GMT+1:20110601T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day RRULE:FREQ=DAILY END:VEVENT BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;TZID=Etc/GMT+1:20110602T000000 DTSTART;TZID=Etc/GMT+1:20110602T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), DateTime(2011, 6, 3, 1, 0, 0, Timezone(utc=True)), """BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;TZID=Etc/GMT+1:20110603T000000 DTSTART;TZID=Etc/GMT+1:20110603T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT """.replace("\n", "\r\n"), ), ( "2.3 Recurring with VTIMEZONE, DTEND", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VTIMEZONE TZID:Etc/GMT+1 X-LIC-LOCATION:Etc/GMT+1 BEGIN:STANDARD DTSTART:18000101T000000 RDATE:18000101T000000 TZNAME:GMT+1 TZOFFSETFROM:-0100 TZOFFSETTO:-0100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 DTSTART;TZID=Etc/GMT+1:20110601T000000 DTEND;TZID=Etc/GMT+1:20110601T020000 DTSTAMP:20020101T000000Z SUMMARY:New Year's Day RRULE:FREQ=DAILY END:VEVENT BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;TZID=Etc/GMT+1:20110602T000000 DTSTART;TZID=Etc/GMT+1:20110602T000000 DTEND;TZID=Etc/GMT+1:20110602T020000 DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), DateTime(2011, 6, 3, 1, 0, 0, Timezone(utc=True)), """BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;TZID=Etc/GMT+1:20110603T000000 DTSTART;TZID=Etc/GMT+1:20110603T000000 DTEND;TZID=Etc/GMT+1:20110603T020000 DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT """.replace("\n", "\r\n"), ), ( "2.1 Recurring no master, no VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;VALUE=DATE:20110602 DTSTART;VALUE=DATE:20110602 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), DateTime(2011, 6, 3), "", ), ( "2.2 Recurring no master, with VTIMEZONE", """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//mulberrymail.com//Mulberry v4.0//EN BEGIN:VTIMEZONE TZID:Etc/GMT+1 X-LIC-LOCATION:Etc/GMT+1 BEGIN:STANDARD DTSTART:18000101T000000 RDATE:18000101T000000 TZNAME:GMT+1 TZOFFSETFROM:-0100 TZOFFSETTO:-0100 END:STANDARD END:VTIMEZONE BEGIN:VEVENT UID:C3184A66-1ED0-11D9-A5E0-000A958A3252 RECURRENCE-ID;TZID=Etc/GMT+1:20110602T000000 DTSTART;TZID=Etc/GMT+1:20110602T000000 DURATION:P1D DTSTAMP:20020101T000000Z SUMMARY:New Year's Day END:VEVENT END:VCALENDAR """.replace("\n", "\r\n"), DateTime(2011, 6, 3, 1, 0, 0, Timezone(utc=True)), "", ), ) for title, caldata, rid, result in data: calendar = Calendar.parseText(caldata) master = calendar.deriveComponent(rid) if master is None: master = "" self.assertEqual( str(master), result, "Failed in %s: got %s, expected %s" % (title, master, result))
def doCapabilities(self, request): """ Return a list of all timezones known to the server. """ # Determine min/max date-time for iSchedule now = DateTime.getNowUTC() minDateTime = DateTime(now.getYear(), 1, 1, 0, 0, 0, Timezone(utc=True)) minDateTime.offsetYear(-1) maxDateTime = DateTime(now.getYear(), 1, 1, 0, 0, 0, Timezone(utc=True)) maxDateTime.offsetYear(10) dataTypes = [] dataTypes.append( ischedulexml.CalendarDataType(**{ "content-type": "text/calendar", "version": "2.0", }) ) if config.EnableJSONData: dataTypes.append( ischedulexml.CalendarDataType(**{ "content-type": "application/calendar+json", "version": "2.0", }) ) componentTypes = [] from twistedcaldav.ical import allowedSchedulingComponents for name in allowedSchedulingComponents: if name == "VFREEBUSY": componentTypes.append( ischedulexml.Component( ischedulexml.Method(name="REQUEST"), name=name ) ) else: componentTypes.append( ischedulexml.Component( ischedulexml.Method(name="REQUEST"), ischedulexml.Method(name="CANCEL"), ischedulexml.Method(name="REPLY"), name=name ) ) result = ischedulexml.QueryResult( ischedulexml.Capabilities( ischedulexml.Version.fromString(config.Scheduling.iSchedule.SerialNumber), ischedulexml.Versions( ischedulexml.Version.fromString("1.0"), ), ischedulexml.SchedulingMessages(*componentTypes), ischedulexml.CalendarDataTypes(*dataTypes), ischedulexml.Attachments( ischedulexml.External(), ), ischedulexml.MaxContentLength.fromString(config.MaxResourceSize), ischedulexml.MinDateTime.fromString(minDateTime.getText()), ischedulexml.MaxDateTime.fromString(maxDateTime.getText()), ischedulexml.MaxInstances.fromString(config.MaxAllowedInstances), ischedulexml.MaxRecipients.fromString(config.MaxAttendeesPerInstance), ischedulexml.Administrator.fromString(request.unparseURL(params="", querystring="", fragment="")), ), ) response = XMLResponse(responsecode.OK, result) response.headers.addRawHeader(ISCHEDULE_CAPABILITIES, str(config.Scheduling.iSchedule.SerialNumber)) return response
def testWeeklyTwice(self): recur = Recurrence() recur.parse("FREQ=WEEKLY") start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True)) end = DateTime(2014, 2, 1, 0, 0, 0, tzid=Timezone(utc=True)) items = [] range = Period(start, end) recur.expand( DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items) self.assertEqual( items, [ DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 1, 8, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 1, 15, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 1, 22, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 1, 29, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), ], ) start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True)) end = DateTime(2014, 3, 1, 0, 0, 0, tzid=Timezone(utc=True)) items = [] range = Period(start, end) recur.expand( DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items) self.assertEqual( items, [ DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 1, 8, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 1, 15, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 1, 22, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 1, 29, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 2, 5, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 2, 12, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 2, 19, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 2, 26, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), ], )
def initManager(self): # TODO: - read in timezones from vtimezones.ics file # Eventually we need to read these from prefs - for now they are # hard-coded to my personal prefs! self.setDefaultTimezone(Timezone(utc=False, tzid="US/Eastern"))
def test_timeRangesOverlap(self): data = ( # Timed ( "Start within, end within - overlap", DateTime(2012, 1, 1, 11, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 1, 12, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), True, ), ( "Start before, end before - no overlap", DateTime(2012, 1, 1, 11, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 1, 12, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 3, 0, 0, 0, tzid=Timezone(utc=True)), False, ), ( "Start before, end right before - no overlap", DateTime(2012, 1, 1, 23, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 3, 0, 0, 0, tzid=Timezone(utc=True)), False, ), ( "Start before, end within - overlap", DateTime(2012, 1, 1, 11, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 11, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 3, 0, 0, 0, tzid=Timezone(utc=True)), True, ), ( "Start after, end after - no overlap", DateTime(2012, 1, 2, 11, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 12, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), False, ), ( "Start right after, end after - no overlap", DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 1, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), False, ), ( "Start within, end after - overlap", DateTime(2012, 1, 1, 12, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 12, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), True, ), ( "Start before, end after - overlap", DateTime(2012, 1, 1, 11, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 3, 11, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 2, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 3, 0, 0, 0, tzid=Timezone(utc=True)), True, ), # All day ( "All day: Start within, end within - overlap", DateTime(2012, 1, 9), DateTime(2012, 1, 10), DateTime(2012, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 15, 0, 0, 0, tzid=Timezone(utc=True)), True, ), ( "All day: Start before, end before - no overlap", DateTime(2012, 1, 1), DateTime(2012, 1, 2), DateTime(2012, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 15, 0, 0, 0, tzid=Timezone(utc=True)), False, ), ( "All day: Start before, end right before - no overlap", DateTime(2012, 1, 7), DateTime(2012, 1, 8), DateTime(2012, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 15, 0, 0, 0, tzid=Timezone(utc=True)), False, ), ( "All day: Start before, end within - overlap", DateTime(2012, 1, 7), DateTime(2012, 1, 9), DateTime(2012, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 15, 0, 0, 0, tzid=Timezone(utc=True)), True, ), ( "All day: Start after, end after - no overlap", DateTime(2012, 1, 16), DateTime(2012, 1, 17), DateTime(2012, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 15, 0, 0, 0, tzid=Timezone(utc=True)), False, ), ( "All day: Start right after, end after - no overlap", DateTime(2012, 1, 15), DateTime(2012, 1, 16), DateTime(2012, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 15, 0, 0, 0, tzid=Timezone(utc=True)), False, ), ( "All day: Start within, end after - overlap", DateTime(2012, 1, 14), DateTime(2012, 1, 16), DateTime(2012, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 15, 0, 0, 0, tzid=Timezone(utc=True)), True, ), ( "All day: Start before, end after - overlap", DateTime(2012, 1, 7), DateTime(2012, 1, 16), DateTime(2012, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)), DateTime(2012, 1, 15, 0, 0, 0, tzid=Timezone(utc=True)), True, ), ) for title, start1, end1, start2, end2, result in data: self.assertEqual(timeRangesOverlap(start1, end1, start2, end2), result, msg="Failed: %s" % (title,))
def getNow(tzid): utc = DateTime.getNowUTC() utc.adjustTimezone(tzid if tzid is not None else Timezone()) return utc
def setNow(self): tz = Timezone(utc=self.mTZUTC, tzid=self.mTZID) self.copy_ICalendarDateTime(self.getNow(tz))
def getTimezone(self): return Timezone(utc=self.mTZUTC, tzid=self.mTZID)
def timeZoneDescriptor(self): tz = Timezone(utc=self.mTZUTC, tzid=self.mTZID) return tz.timeZoneDescriptor(self)
def generateFreeBusyInfo( request, calresource, fbinfo, timerange, matchtotal, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=None, ): """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param request: the L{IRequest} for the current request. @param calresource: the L{CalDAVResource} for a calendar collection. @param fbinfo: the array of busy periods to update. @param timerange: the L{TimeRange} for the query. @param matchtotal: the running total for the number of matches. @param excludeuid: a C{str} containing a UID value to exclude any components with that UID from contributing to free-busy. @param organizer: a C{str} containing the value of the ORGANIZER property in the VFREEBUSY request. This is used in conjunction with the UID value to process exclusions. @param same_calendar_user: a C{bool} indicating whether the calendar user requesting the free-busy information is the same as the calendar user being targeted. @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request. @param event_details: a C{list} into which to store extended VEVENT details if not C{None} """ # First check the privilege on this collection # TODO: for server-to-server we bypass this right now as we have no way to authorize external users. if not servertoserver: try: yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(), ), principal=organizerPrincipal) except AccessDeniedError: returnValue(matchtotal) # May need organizer principal organizer_principal = (yield calresource.principalForCalendarUserAddress( organizer)) if organizer else None organizer_uid = organizer_principal.principalUID( ) if organizer_principal else "" # Free busy is per-user userPrincipal = (yield calresource.resourceOwnerPrincipal(request)) if userPrincipal: useruid = userPrincipal.principalUID() else: useruid = "" # Get the timezone property from the collection. has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request)) if has_prop: tz = (yield calresource.readProperty(CalendarTimeZone(), request)) else: tz = None # Look for possible extended free busy information rich_options = { "organizer": False, "delegate": False, "resource": False, } do_event_details = False if event_details is not None and organizer_principal is not None and userPrincipal is not None: # Check if organizer is attendee if organizer_principal == userPrincipal: do_event_details = True rich_options["organizer"] = True # Check if organizer is a delegate of attendee proxy = (yield organizer_principal.isProxyFor(userPrincipal)) if config.Scheduling.Options.DelegeteRichFreeBusy and proxy: do_event_details = True rich_options["delegate"] = True # Check if attendee is room or resource if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.getCUType( ) in ( "RESOURCE", "ROOM", ): do_event_details = True rich_options["resource"] = True # Try cache resources = (yield FBCacheEntry.getCacheEntry( calresource, useruid, timerange)) if config.EnableFreeBusyCache else None if resources is None: caching = False if config.EnableFreeBusyCache: # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems[ "fb-uncached"] = request.extendedLogItems.get( "fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration( days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration( days=config.FreeBusyCacheDaysForward)) # If the requested timerange would fit in our allowed cache range, trigger the cache creation if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime( timerange.end, cache_end) <= 0: cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText()) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( cache_timerange if caching else timerange, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", )) filter = Filter(filter) tzinfo = filter.settimezone(tz) try: resources = yield calresource.search(filter, useruid=useruid, fbtype=True) if caching: yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources) except IndexedSearchException: raise HTTPError( StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "Failed freebusy query")) else: # Log extended item if not hasattr(request, "extendedLogItems"): request.extendedLogItems = {} request.extendedLogItems["fb-cached"] = request.extendedLogItems.get( "fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # We care about separate instances for VEVENTs only aggregated_resources = {} for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' 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 # Short-cut - if an fbtype exists we can use that if type == "VEVENT" and aggregated_resources[key][0][3] != '?': matchedResource = False # Look at each instance for float, start, end, fbtype in aggregated_resources[key]: # Ignore free time or unknown if fbtype in ('F', '?'): continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): test_principal = ( yield 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_uid == "") and same_calendar_user: continue # Apply a timezone to any floating times fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) # Clip instance to time range clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end)) # Double check for overlap if clipped: matchedResource = True fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped) if matchedResource: # Check size of results is within limit matchtotal += 1 if matchtotal > max_number_of_matches: raise NumberOfMatchesWithinLimits(max_number_of_matches) # Add extended details if do_event_details: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) else: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) # 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.error( "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 = ( yield 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) 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, ) # Add extended details if calendar.mainType() == "VEVENT" and do_event_details: child = (yield request.locateChildResource(calresource, name)) calendar = (yield child.iCalendarForUser(request)) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) returnValue(matchtotal)
class CalendarIndex(AbstractCalendarIndex): """ Calendar index - abstract class for indexer that indexes calendar objects in a collection. """ def __init__(self, resource): """ @param resource: the L{CalDAVResource} resource to index. """ super(CalendarIndex, self).__init__(resource) 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 # ORGANIZER: cu-address of the Organizer of the event # q.execute(""" create table RESOURCE ( RESOURCEID integer primary key autoincrement, NAME text unique, UID text%s, TYPE text, RECURRANCE_MAX date, ORGANIZER text ) """ % (" unique" if uidunique else "", )) # # 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 # TRANSPARENT: Y if transparent, N if opaque (default non-per-user value) # q.execute(""" create table TIMESPAN ( INSTANCEID integer primary key autoincrement, RESOURCEID integer, FLOAT text(1), START date, END date, FBTYPE text(1), TRANSPARENT text(1) ) """) q.execute(""" create index STARTENDFLOAT on TIMESPAN (START, END, FLOAT) """) # # PERUSER table tracks per-user ids # PERUSERID: autoincrement primary key # UID: User ID used in calendar data # q.execute(""" create table PERUSER ( PERUSERID integer primary key autoincrement, USERUID text ) """) q.execute(""" create index PERUSER_UID on PERUSER (USERUID) """) # # TRANSPARENCY table tracks per-user per-instance transparency # PERUSERID: user id key # INSTANCEID: instance id key # TRANSPARENT: Y if transparent, N if opaque # q.execute(""" create table TRANSPARENCY ( PERUSERID integer, INSTANCEID integer, TRANSPARENT text(1) ) """) # # REVISIONS table tracks changes # NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key) # REVISION: revision number # WASDELETED: Y if revision deleted, N if added or changed # q.execute(""" create table REVISION_SEQUENCE ( REVISION integer ) """) q.execute(""" insert into REVISION_SEQUENCE (REVISION) values (0) """) q.execute(""" create table REVISIONS ( NAME text unique, REVISION integer, DELETED text(1) ) """) q.execute(""" create index REVISION on REVISIONS (REVISION) """) 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 ) """) # Cascading triggers to help on delete q.execute(""" create trigger resourceDelete after delete on RESOURCE for each row begin delete from TIMESPAN where TIMESPAN.RESOURCEID = OLD.RESOURCEID; end """) q.execute(""" create trigger timespanDelete after delete on TIMESPAN for each row begin delete from TRANSPARENCY where INSTANCEID = OLD.INSTANCEID; end """) def _db_can_upgrade(self, old_version): """ Can we do an in-place upgrade """ # v10 is a big change - no upgrade possible return False def _db_upgrade_data_tables(self, q, old_version): """ Upgrade the data from an older version of the DB. """ # v10 is a big change - no upgrade possible pass def notExpandedBeyond(self, minDate): """ Gives all resources which have not been expanded beyond a given date in the index """ return self._db_values_for_sql( "select NAME from RESOURCE where RECURRANCE_MAX < :1", pyCalendarTodatetime(minDate)) def reExpandResource(self, name, expand_until): """ Given a resource name, remove it from the database and re-add it with a longer expansion. """ calendar = self.resource.getChild(name).iCalendar() self._add_to_db(name, calendar, expand_until=expand_until, reCreate=True) self._db_commit() 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() organizer = calendar.getOrganizer() if not organizer: organizer = "" # Decide how far to expand based on the component doInstanceIndexing = False master = calendar.masterComponent() if master is None or not calendar.isRecurring(): # When there is no master we have a set of overridden components - index them all. # When there is one instance - index it. expand = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) doInstanceIndexing = True else: # If migrating or re-creating or config option for delayed indexing is off, always index if reCreate or not config.FreeBusyIndexDelayedExpand: doInstanceIndexing = True # 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; # it does not affect search results beyond this period, but it may affect # performance of such a search. expand = (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandAheadDays)) if expand_until and expand_until > expand: expand = expand_until # Maximum duration into the future through which recurrences are expanded in the # index. This is a caching parameter which affects the size of the index; it # does not affect search results beyond this period, but it may affect # performance of such a search. # # 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 # 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 occurrences beyond this period. if expand > (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandMaxDays)): raise IndexedSearchException() # Always do recurrence expansion even if we do not intend to index - we need this to double-check the # validity of the iCalendar recurrence data. try: instances = calendar.expandTimeRanges( expand, ignoreInvalidInstances=reCreate) recurrenceLimit = instances.limit except InvalidOverriddenInstanceError, e: log.error("Invalid instance %s when indexing %s in %s" % ( e.rid, name, self.resource, )) raise # Now coerce indexing to off if needed if not doInstanceIndexing: instances = None recurrenceLimit = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) self._delete_from_db(name, uid, False) # Add RESOURCE item self._db_execute( """ insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX, ORGANIZER) values (:1, :2, :3, :4, :5) """, name, uid, calendar.resourceType(), pyCalendarTodatetime(recurrenceLimit) if recurrenceLimit else None, organizer) resourceid = self.lastrowid # Get a set of all referenced per-user UIDs and map those to entries already # in the DB and add new ones as needed useruids = calendar.allPerUserUIDs() useruids.add("") useruidmap = {} for useruid in useruids: peruserid = self._db_value_for_sql( "select PERUSERID from PERUSER where USERUID = :1", useruid) if peruserid is None: self._db_execute( """ insert into PERUSER (USERUID) values (:1) """, useruid) peruserid = self.lastrowid useruidmap[useruid] = peruserid if doInstanceIndexing: for key in instances: instance = instances[key] start = instance.start end = instance.end float = 'Y' if instance.start.floating() else 'N' transp = 'T' if instance.component.propertyValue( "TRANSP") == "TRANSPARENT" else 'F' self._db_execute( """ insert into TIMESPAN (RESOURCEID, FLOAT, START, END, FBTYPE, TRANSPARENT) values (:1, :2, :3, :4, :5, :6) """, resourceid, float, pyCalendarTodatetime(start), pyCalendarTodatetime(end), icalfbtype_to_indexfbtype.get( instance.component.getFBType(), 'F'), transp) instanceid = self.lastrowid peruserdata = calendar.perUserData(instance.rid) for useruid, (transp, _ignore_adjusted_start, _ignore_adjusted_end) in peruserdata: peruserid = useruidmap[useruid] self._db_execute( """ insert into TRANSPARENCY (PERUSERID, INSTANCEID, TRANSPARENT) values (:1, :2, :3) """, peruserid, instanceid, 'T' if transp else 'F') # 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(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) end = DateTime(2100, 1, 1, 1, 0, 0, tzid=Timezone(utc=True)) float = 'N' self._db_execute( """ insert into TIMESPAN (RESOURCEID, FLOAT, START, END, FBTYPE, TRANSPARENT) values (:1, :2, :3, :4, :5, :6) """, resourceid, float, pyCalendarTodatetime(start), pyCalendarTodatetime(end), '?', '?') instanceid = self.lastrowid peruserdata = calendar.perUserData(None) for useruid, (transp, _ignore_adjusted_start, _ignore_adjusted_end) in peruserdata: peruserid = useruidmap[useruid] self._db_execute( """ insert into TRANSPARENCY (PERUSERID, INSTANCEID, TRANSPARENT) values (:1, :2, :3) """, peruserid, instanceid, 'T' if transp else 'F') self._db_execute( """ insert or replace into REVISIONS (NAME, REVISION, DELETED) values (:1, :2, :3) """, name, self.bumpRevision(fast=True), 'N', )
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() organizer = calendar.getOrganizer() if not organizer: organizer = "" # Decide how far to expand based on the component doInstanceIndexing = False master = calendar.masterComponent() if master is None or not calendar.isRecurring(): # When there is no master we have a set of overridden components - index them all. # When there is one instance - index it. expand = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) doInstanceIndexing = True else: # If migrating or re-creating or config option for delayed indexing is off, always index if reCreate or not config.FreeBusyIndexDelayedExpand: doInstanceIndexing = True # 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; # it does not affect search results beyond this period, but it may affect # performance of such a search. expand = (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandAheadDays)) if expand_until and expand_until > expand: expand = expand_until # Maximum duration into the future through which recurrences are expanded in the # index. This is a caching parameter which affects the size of the index; it # does not affect search results beyond this period, but it may affect # performance of such a search. # # 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 # 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 occurrences beyond this period. if expand > (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandMaxDays)): raise IndexedSearchException() # Always do recurrence expansion even if we do not intend to index - we need this to double-check the # validity of the iCalendar recurrence data. try: instances = calendar.expandTimeRanges( expand, ignoreInvalidInstances=reCreate) recurrenceLimit = instances.limit except InvalidOverriddenInstanceError, e: log.error("Invalid instance %s when indexing %s in %s" % ( e.rid, name, self.resource, )) raise
def __init__(self): Timezone.sDefaultTimezone = Timezone()
def testCachePreserveOnAdjustment(self): # UTC first dt = DateTime(2012, 6, 7, 12, 0, 0, Timezone(tzid="utc")) dt.getPosixTime() # check existing cache is complete self.assertTrue(dt.mPosixTimeCached) self.assertNotEqual(dt.mPosixTime, 0) self.assertEqual(dt.mTZOffset, None) # duplicate preserves cache details dt2 = dt.duplicate() self.assertTrue(dt2.mPosixTimeCached) self.assertEqual(dt2.mPosixTime, dt.mPosixTime) self.assertEqual(dt2.mTZOffset, dt.mTZOffset) # adjust preserves cache details dt2.adjustToUTC() self.assertTrue(dt2.mPosixTimeCached) self.assertEqual(dt2.mPosixTime, dt.mPosixTime) self.assertEqual(dt2.mTZOffset, dt.mTZOffset) # Now timezone tzdata = """BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//calendarserver.org//Zonal//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:America/Pittsburgh BEGIN:STANDARD DTSTART:18831118T120358 RDATE:18831118T120358 TZNAME:EST TZOFFSETFROM:-045602 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19180331T020000 RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19181027T020000 RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19200101T000000 RDATE:19200101T000000 RDATE:19420101T000000 RDATE:19460101T000000 RDATE:19670101T000000 TZNAME:EST TZOFFSETFROM:-0500 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19200328T020000 RDATE:19200328T020000 RDATE:19740106T020000 RDATE:19750223T020000 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19201031T020000 RDATE:19201031T020000 RDATE:19450930T020000 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19210424T020000 RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19210925T020000 RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19420209T020000 RDATE:19420209T020000 TZNAME:EWT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450814T190000 RDATE:19450814T190000 TZNAME:EPT TZOFFSETFROM:-0400 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19460428T020000 RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19460929T020000 RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19551030T020000 RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE END:VCALENDAR """.replace("\n", "\r\n") Calendar.parseText(tzdata) dt = DateTime(2012, 6, 7, 12, 0, 0, Timezone(tzid="America/Pittsburgh")) dt.getPosixTime() # check existing cache is complete self.assertTrue(dt.mPosixTimeCached) self.assertNotEqual(dt.mPosixTime, 0) self.assertEqual(dt.mTZOffset, -14400) # duplicate preserves cache details dt2 = dt.duplicate() self.assertTrue(dt2.mPosixTimeCached) self.assertEqual(dt2.mPosixTime, dt.mPosixTime) self.assertEqual(dt2.mTZOffset, dt.mTZOffset) # adjust preserves cache details dt2.adjustToUTC() self.assertTrue(dt2.mPosixTimeCached) self.assertEqual(dt2.mPosixTime, dt.mPosixTime) self.assertEqual(dt2.mTZOffset, 0)
def _internalGenerateFreeBusyInfo( calresource, fbinfo, timerange, matchtotal, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=None, logItems=None, accountingItems=None, ): """ Run a free busy report on the specified calendar collection accumulating the free busy info for later processing. @param calresource: the L{Calendar} for a calendar collection. @param fbinfo: the array of busy periods to update. @param timerange: the L{TimeRange} for the query. @param matchtotal: the running total for the number of matches. @param excludeuid: a C{str} containing a UID value to exclude any components with that UID from contributing to free-busy. @param organizer: a C{str} containing the value of the ORGANIZER property in the VFREEBUSY request. This is used in conjunction with the UID value to process exclusions. @param same_calendar_user: a C{bool} indicating whether the calendar user requesting the free-busy information is the same as the calendar user being targeted. @param servertoserver: a C{bool} indicating whether we are doing a local or remote lookup request. @param event_details: a C{list} into which to store extended VEVENT details if not C{None} @param logItems: a C{dict} to store logging info to @param accountingItems: a C{dict} to store accounting info to """ # First check the privilege on this collection # TODO: for server-to-server we bypass this right now as we have no way to authorize external users. # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy # May need organizer principal organizer_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress(organizer)) if organizer else None organizer_uid = organizer_record.uid if organizer_record else "" # Free busy is per-user attendee_uid = calresource.viewerHome().uid() attendee_record = yield calresource.directoryService().recordWithUID( attendee_uid.decode("utf-8")) # Get the timezone property from the collection. tz = calresource.getTimezone() # Look for possible extended free busy information rich_options = { "organizer": False, "delegate": False, "resource": False, } do_event_details = False if event_details is not None and organizer_record is not None and attendee_record is not None: # Get the principal of the authorized user which may be different from the organizer if a delegate of # the organizer is making the request authz_uid = organizer_uid authz_record = organizer_record if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid: authz_uid = calresource._txn._authz_uid authz_record = yield calresource.directoryService().recordWithUID( authz_uid.decode("utf-8")) # Check if attendee is also the organizer or the delegate doing the request if attendee_uid in (organizer_uid, authz_uid): do_event_details = True rich_options["organizer"] = True # Check if authorized user is a delegate of attendee proxy = (yield authz_record.isProxyFor(attendee_record)) if config.Scheduling.Options.DelegeteRichFreeBusy and proxy: do_event_details = True rich_options["delegate"] = True # Check if attendee is room or resource if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType( ) in ( "RESOURCE", "ROOM", ): do_event_details = True rich_options["resource"] = True # Try cache resources = (yield FBCacheEntry.getCacheEntry( calresource, attendee_uid, timerange)) if config.EnableFreeBusyCache else None if resources is None: if accountingItems is not None: accountingItems["fb-uncached"] = accountingItems.get( "fb-uncached", 0) + 1 caching = False if config.EnableFreeBusyCache: # Log extended item if logItems is not None: logItems["fb-uncached"] = logItems.get("fb-uncached", 0) + 1 # We want to cache a large range of time based on the current date cache_start = normalizeToUTC(DateTime.getToday() + Duration( days=0 - config.FreeBusyCacheDaysBack)) cache_end = normalizeToUTC(DateTime.getToday() + Duration( days=config.FreeBusyCacheDaysForward)) # If the requested time range would fit in our allowed cache range, trigger the cache creation if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime( timerange.end, cache_end) <= 0: cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText()) caching = True # # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range. # We then take those results and merge them into one VFREEBUSY component # with appropriate FREEBUSY properties, and return that single item as iCal data. # # Create fake filter element to match time-range filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( cache_timerange if caching else timerange, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", )) filter = Filter(filter) tzinfo = filter.settimezone(tz) if accountingItems is not None: tr = cache_timerange if caching else timerange accountingItems["fb-query-timerange"] = ( str(tr.start), str(tr.end), ) try: resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True) if caching: yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources) except IndexedSearchException: raise InternalDataStoreError("Invalid indexedSearch query") else: if accountingItems is not None: accountingItems["fb-cached"] = accountingItems.get("fb-cached", 0) + 1 # Log extended item if logItems is not None: logItems["fb-cached"] = logItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # We care about separate instances for VEVENTs only aggregated_resources = {} for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault(( name, uid, type, test_organizer, ), []).append(( float, start, end, fbtype, )) if accountingItems is not None: accountingItems["fb-resources"] = {} for k, v in aggregated_resources.items(): name, uid, type, test_organizer = k accountingItems["fb-resources"][uid] = [] for float, start, end, fbtype in v: fbstart = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) accountingItems["fb-resources"][uid].append(( float, str(fbstart), str(fbend), fbtype, )) # Cache directory record lookup outside this loop as it is expensive and will likely # always end up being called with the same organizer address. recordUIDCache = {} for key in aggregated_resources.iterkeys(): name, uid, type, test_organizer = key # Short-cut - if an fbtype exists we can use that if type == "VEVENT" and aggregated_resources[key][0][3] != '?': matchedResource = False # Look at each instance for float, start, end, fbtype in aggregated_resources[key]: # Ignore free time or unknown if fbtype in ('F', '?'): continue # Ignore ones of this UID if excludeuid: # See if we have a UID match if (excludeuid == uid): if test_organizer: test_uid = recordUIDCache.get(test_organizer) if test_uid is None: test_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress( test_organizer)) test_uid = test_record.uid if test_record else "" recordUIDCache[test_organizer] = test_uid else: test_uid = "" # 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 = parseSQLTimestampToPyCalendar(start) if float == 'Y': fbstart.setTimezone(tzinfo) else: fbstart.setTimezone(Timezone(utc=True)) fbend = parseSQLTimestampToPyCalendar(end) if float == 'Y': fbend.setTimezone(tzinfo) else: fbend.setTimezone(Timezone(utc=True)) # Clip instance to time range clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end)) # Double check for overlap if clipped: matchedResource = True fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped) if matchedResource: # Check size of results is within limit matchtotal += 1 if matchtotal > config.MaxQueryWithDataResults: raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal) # Add extended details if do_event_details: child = (yield calresource.calendarObjectWithName(name)) # Only add fully public events if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC: calendar = (yield child.componentForUser()) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) else: child = (yield calresource.calendarObjectWithName(name)) calendar = (yield child.componentForUser()) # 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.error( "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() if test_organizer: test_uid = recordUIDCache.get(test_organizer) if test_uid is None: test_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress(test_organizer)) test_uid = test_record.uid if test_record else "" recordUIDCache[test_organizer] = test_uid else: test_uid = "" # 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 accountingItems is not None: accountingItems.setdefault("fb-filter-match", []).append(uid) if filter.match(calendar, None): if accountingItems is not None: accountingItems.setdefault("fb-filter-matched", []).append(uid) # Check size of results is within limit matchtotal += 1 if matchtotal > config.MaxQueryWithDataResults: raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal) 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, ) # Add extended details if calendar.mainType() == "VEVENT" and do_event_details: child = (yield calresource.calendarObjectWithName(name)) # Only add fully public events if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC: calendar = (yield child.componentForUser()) _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo) returnValue(matchtotal)
def testSetWeekNo(self): dt = DateTime(2013, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) self.assertEqual(dt.getWeekNo(), 1) dt.setWeekNo(1) self.assertEqual( dt, DateTime(2013, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))) self.assertEqual(dt.getWeekNo(), 1) dt = DateTime(2013, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) self.assertEqual(dt.getWeekNo(), 1) dt.setWeekNo(2) self.assertEqual( dt, DateTime(2013, 1, 8, 0, 0, 0, tzid=Timezone(utc=True))) self.assertEqual(dt.getWeekNo(), 2) dt = DateTime(2013, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)) self.assertEqual(dt.getWeekNo(), 2) dt.setWeekNo(1) self.assertEqual( dt, DateTime(2013, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))) self.assertEqual(dt.getWeekNo(), 1) dt = DateTime(2014, 1, 7, 0, 0, 0, tzid=Timezone(utc=True)) self.assertEqual(dt.getWeekNo(), 2) dt.setWeekNo(1) self.assertEqual( dt, DateTime(2013, 12, 31, 0, 0, 0, tzid=Timezone(utc=True))) self.assertEqual(dt.getWeekNo(), 1) dt = DateTime(2012, 12, 31, 0, 0, 0, tzid=Timezone(utc=True)) self.assertEqual(dt.getWeekNo(), 1) dt.setWeekNo(1) self.assertEqual( dt, DateTime(2012, 12, 31, 0, 0, 0, tzid=Timezone(utc=True))) self.assertEqual(dt.getWeekNo(), 1) dt = DateTime(2016, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) self.assertEqual(dt.getWeekNo(), 53) dt.setWeekNo(1) self.assertEqual( dt, DateTime(2016, 1, 8, 0, 0, 0, tzid=Timezone(utc=True))) self.assertEqual(dt.getWeekNo(), 1) dt.setWeekNo(2) self.assertEqual( dt, DateTime(2016, 1, 15, 0, 0, 0, tzid=Timezone(utc=True))) self.assertEqual(dt.getWeekNo(), 2) dt = DateTime(2016, 1, 8, 0, 0, 0, tzid=Timezone(utc=True)) self.assertEqual(dt.getWeekNo(), 1) dt.setWeekNo(1) self.assertEqual( dt, DateTime(2016, 1, 8, 0, 0, 0, tzid=Timezone(utc=True))) self.assertEqual(dt.getWeekNo(), 1)
def testMonthlyInUTC(self): recur = Recurrence() recur.parse("FREQ=MONTHLY") start = DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(utc=True)) end = DateTime(2015, 1, 1, 0, 0, 0, tzid=Timezone(utc=True)) items = [] range = Period(start, end) recur.expand( DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), range, items) self.assertEqual( items, [ DateTime(2014, 1, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 2, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 3, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 4, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 5, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 6, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 7, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 8, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 9, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 10, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 11, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), DateTime(2014, 12, 1, 12, 0, 0, tzid=Timezone(tzid="America/New_York")), ], )
def testConversions(self): tzdata = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//calendarserver.org//Zonal//EN BEGIN:VTIMEZONE TZID:America/New_York X-LIC-LOCATION:America/New_York BEGIN:STANDARD DTSTART:18831118T120358 RDATE:18831118T120358 TZNAME:EST TZOFFSETFROM:-045602 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19180331T020000 RRULE:FREQ=YEARLY;UNTIL=19190330T070000Z;BYDAY=-1SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19181027T020000 RRULE:FREQ=YEARLY;UNTIL=19191026T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19200101T000000 RDATE:19200101T000000 RDATE:19420101T000000 RDATE:19460101T000000 RDATE:19670101T000000 TZNAME:EST TZOFFSETFROM:-0500 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19200328T020000 RDATE:19200328T020000 RDATE:19740106T020000 RDATE:19750223T020000 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19201031T020000 RDATE:19201031T020000 RDATE:19450930T020000 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19210424T020000 RRULE:FREQ=YEARLY;UNTIL=19410427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19210925T020000 RRULE:FREQ=YEARLY;UNTIL=19410928T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19420209T020000 RDATE:19420209T020000 TZNAME:EWT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450814T190000 RDATE:19450814T190000 TZNAME:EPT TZOFFSETFROM:-0400 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19460428T020000 RRULE:FREQ=YEARLY;UNTIL=19660424T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19460929T020000 RRULE:FREQ=YEARLY;UNTIL=19540926T060000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:STANDARD DTSTART:19551030T020000 RRULE:FREQ=YEARLY;UNTIL=19661030T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;UNTIL=19730429T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;UNTIL=20061029T060000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;UNTIL=19860427T070000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;UNTIL=20060402T070000Z;BYDAY=1SU;BYMONTH=4 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE BEGIN:VTIMEZONE TZID:America/Los_Angeles X-LIC-LOCATION:America/Los_Angeles BEGIN:STANDARD DTSTART:18831118T120702 RDATE:18831118T120702 TZNAME:PST TZOFFSETFROM:-075258 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19180331T020000 RRULE:FREQ=YEARLY;UNTIL=19190330T100000Z;BYDAY=-1SU;BYMONTH=3 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19181027T020000 RRULE:FREQ=YEARLY;UNTIL=19191026T090000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19420209T020000 RDATE:19420209T020000 TZNAME:PWT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450814T160000 RDATE:19450814T160000 TZNAME:PPT TZOFFSETFROM:-0700 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19450930T020000 RDATE:19450930T020000 RDATE:19490101T020000 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:STANDARD DTSTART:19460101T000000 RDATE:19460101T000000 RDATE:19670101T000000 TZNAME:PST TZOFFSETFROM:-0800 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19480314T020000 RDATE:19480314T020000 RDATE:19740106T020000 RDATE:19750223T020000 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19500430T020000 RRULE:FREQ=YEARLY;UNTIL=19660424T100000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19500924T020000 RRULE:FREQ=YEARLY;UNTIL=19610924T090000Z;BYDAY=-1SU;BYMONTH=9 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:STANDARD DTSTART:19621028T020000 RRULE:FREQ=YEARLY;UNTIL=19661030T090000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;UNTIL=19730429T100000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYDAY=-1SU;BYMONTH=10 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;UNTIL=19860427T100000Z;BYDAY=-1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYDAY=1SU;BYMONTH=4 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE END:VCALENDAR """ data = ( ( DateTime(2014, 3, 8, 23, 0, 0, Timezone(tzid="America/New_York")), DateTime(2014, 3, 8, 20, 0, 0, Timezone(tzid="America/Los_Angeles")), ), ( DateTime(2014, 3, 9, 3, 0, 0, Timezone(utc=True)), DateTime(2014, 3, 8, 19, 0, 0, Timezone(tzid="America/Los_Angeles")), ), ( DateTime(2014, 3, 9, 13, 0, 0, Timezone(utc=True)), DateTime(2014, 3, 9, 6, 0, 0, Timezone(tzid="America/Los_Angeles")), ), ) Calendar.parseText(tzdata.replace("\n", "\r\n")) for dtfrom, dtto in data: self.assertEqual(dtfrom, dtto) newdtfrom = dtfrom.duplicate() newdtfrom.adjustTimezone(dtto.getTimezone()) self.assertEqual(newdtfrom, dtto) self.assertEqual(newdtfrom.getHours(), dtto.getHours())