def test_calendar_query_bogus_timezone_id(self): """ Partial retrieval of events by time range. (CalDAV-access-09, section 7.6.1) """ TimezoneCache.create() self.addCleanup(TimezoneCache.clear) calendar_properties = ( davxml.GETETag(), caldavxml.CalendarData(), ) query_timerange = caldavxml.TimeRange( start="%04d1001T000000Z" % (DateTime.getToday().getYear(),), end="%04d1101T000000Z" % (DateTime.getToday().getYear(),), ) query = caldavxml.CalendarQuery( davxml.PropertyContainer(*calendar_properties), caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( query_timerange, name="VEVENT", ), name="VCALENDAR", ), ), caldavxml.TimeZoneID.fromString("bogus"), ) result = yield self.calendar_query(query, got_xml=None, expected_code=responsecode.FORBIDDEN) self.assertTrue("valid-timezone" in result)
def _doRefresh(tzpath, xmlfile, tzdb, tzvers): """ Refresh data from IANA. """ print("Downloading latest data from IANA") if tzvers: path = "https://www.iana.org/time-zones/repository/releases/tzdata%s.tar.gz" % (tzvers,) else: path = "https://www.iana.org/time-zones/repository/tzdata-latest.tar.gz" data = urllib.urlretrieve(path) print("Extract data at: %s" % (data[0])) rootdir = tempfile.mkdtemp() zonedir = os.path.join(rootdir, "tzdata") os.mkdir(zonedir) with tarfile.open(data[0], "r:gz") as t: t.extractall(zonedir) # Get the version from the Makefile try: makefile = open(os.path.join(zonedir, "Makefile")).read() lines = makefile.splitlines() for line in lines: if line.startswith("VERSION="): tzvers = line[8:].strip() break except IOError: pass if not tzvers: tzvers = DateTime.getToday().getText() print("Converting data (version: %s) at: %s" % (tzvers, zonedir,)) startYear = 1800 endYear = DateTime.getToday().getYear() + 10 Calendar.sProdID = "-//calendarserver.org//Zonal//EN" zonefiles = "northamerica", "southamerica", "europe", "africa", "asia", "australasia", "antarctica", "etcetera", "backward" parser = tzconvert() for file in zonefiles: parser.parse(os.path.join(zonedir, file)) parser.generateZoneinfoFiles(os.path.join(rootdir, "zoneinfo"), startYear, endYear, filterzones=()) print("Copy new zoneinfo to destination: %s" % (tzpath,)) z = FilePath(os.path.join(rootdir, "zoneinfo")) tz = FilePath(tzpath) z.copyTo(tz) print("Updating XML file at: %s" % (xmlfile,)) tzdb.readDatabase() tzdb.updateDatabase() print("Current total: %d" % (len(tzdb.timezones),)) print("Total Changed: %d" % (tzdb.changeCount,)) if tzdb.changeCount: print("Changed:") for k in sorted(tzdb.changed): print(" %s" % (k,)) versfile = os.path.join(os.path.dirname(xmlfile), "version.txt") print("Updating version file at: %s" % (versfile,)) with open(versfile, "w") as f: f.write(TimezoneCache.IANA_VERSION_PREFIX + tzvers)
def setUpCalendarStore(test): test.root = FilePath(test.mktemp()) test.root.createDirectory() storeRootPath = test.storeRootPath = test.root.child("store") calendarPath = storeRootPath.child("calendars").child("__uids__") calendarPath.parent().makedirs() storePath.copyTo(calendarPath) # Set year values to current year nowYear = DateTime.getToday().getYear() for home in calendarPath.child("ho").child("me").children(): if not home.basename().startswith("."): for calendar in home.children(): if not calendar.basename().startswith("."): for resource in calendar.children(): if resource.basename().endswith(".ics"): resource.setContent(resource.getContent() % {"now": nowYear}) testID = test.id() test.calendarStore = CalendarStore( storeRootPath, {"push": test.notifierFactory} if test.notifierFactory else {}, buildDirectory(), quota=deriveQuota(test), ) test.txn = test.calendarStore.newTransaction(testID + "(old)") assert test.calendarStore is not None, "No calendar store?"
def updateToCurrentYear(data): """ Update the supplied iCalendar data so that all dates are updated to the current year. """ nowYear = DateTime.getToday().getYear() return data % {"now": nowYear}
def test_calendar_query_timezone(self): """ Partial retrieval of events by time range. (CalDAV-access-09, section 7.6.1) """ TimezoneCache.create() self.addCleanup(TimezoneCache.clear) tzid1 = "Etc/GMT+1" tz1 = Component(None, pycalendar=readVTZ(tzid1)) calendar_properties = ( davxml.GETETag(), caldavxml.CalendarData(), ) query_timerange = caldavxml.TimeRange( start="%04d1001T000000Z" % (DateTime.getToday().getYear(),), end="%04d1101T000000Z" % (DateTime.getToday().getYear(),), ) query = caldavxml.CalendarQuery( davxml.PropertyContainer(*calendar_properties), caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( query_timerange, name="VEVENT", ), name="VCALENDAR", ), ), caldavxml.TimeZone.fromCalendar(tz1), ) def got_xml(doc): if not isinstance(doc.root_element, davxml.MultiStatus): self.fail("REPORT response XML root element is not multistatus: %r" % (doc.root_element,)) return self.calendar_query(query, got_xml)
def updateToCurrentYear(data): """ Update the supplied iCalendar data so that all dates are updated to the current year. """ subs = {} nowYear = DateTime.getToday().getYear() subs["now"] = nowYear for i in range(1, 10): subs["now-{}".format(i)] = nowYear - 1 subs["now+{}".format(i)] = nowYear + 1 return data % subs
def test_calendar_query_wrong_timezone_elements(self): """ Partial retrieval of events by time range. (CalDAV-access-09, section 7.6.1) """ TimezoneCache.create() self.addCleanup(TimezoneCache.clear) tzid1 = "Etc/GMT+1" tz1 = Component(None, pycalendar=readVTZ(tzid1)) calendar_properties = ( davxml.GETETag(), caldavxml.CalendarData(), ) query_timerange = caldavxml.TimeRange( start="%04d1001T000000Z" % (DateTime.getToday().getYear(),), end="%04d1101T000000Z" % (DateTime.getToday().getYear(),), ) query = caldavxml.CalendarQuery( davxml.PropertyContainer(*calendar_properties), caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( query_timerange, name="VEVENT", ), name="VCALENDAR", ), ), caldavxml.TimeZone.fromCalendar(tz1), ) query.children += (caldavxml.TimeZoneID.fromString(tzid1),) result = yield self.calendar_query(query, got_xml=None, expected_code=responsecode.BAD_REQUEST) self.assertTrue("Only one of" in result)
def command_purgeOldEvents(self, command): """ Convert RetainDays from the command dictionary into a date, then purge events older than that date. @param command: the dictionary parsed from the plist read from stdin @type command: C{dict} """ retainDays = command.get("RetainDays", DEFAULT_RETAIN_DAYS) cutoff = DateTime.getToday() cutoff.setDateOnly(False) cutoff.offsetDay(-retainDays) eventCount = (yield PurgeOldEventsService.purgeOldEvents(self.store, cutoff, DEFAULT_BATCH_SIZE)) self.respond(command, {'EventsRemoved': eventCount, "RetainDays": retainDays})
def purgeAttachments(cls, store, uuid, days, limit, dryrun, verbose): service = cls(store) service.uuid = uuid if days > 0: cutoff = DateTime.getToday() cutoff.setDateOnly(False) cutoff.offsetDay(-days) service.cutoff = cutoff else: service.cutoff = None service.batchSize = limit service.dryrun = dryrun service.verbose = verbose result = (yield service.doWork()) returnValue(result)
def componentUpdate(data): """ Update the supplied iCalendar data so that all dates are updated to the current year. """ if len(relativeDateSubstitutions) == 0: now = DateTime.getToday() relativeDateSubstitutions["now"] = now for i in range(30): attrname = "now_back%s" % (i + 1,) dt = now.duplicate() dt.offsetDay(-(i + 1)) relativeDateSubstitutions[attrname] = dt for i in range(30): attrname = "now_fwd%s" % (i + 1,) dt = now.duplicate() dt.offsetDay(i + 1) relativeDateSubstitutions[attrname] = dt return Component.fromString(data.format(**relativeDateSubstitutions))
def setUpCalendarStore(test): test.root = FilePath(test.mktemp()) test.root.createDirectory() storeRootPath = test.storeRootPath = test.root.child("store") calendarPath = storeRootPath.child("calendars").child("__uids__") calendarPath.parent().makedirs() storePath.copyTo(calendarPath) # Set year values to current year subs = {} nowYear = DateTime.getToday().getYear() subs["now"] = nowYear for i in range(1, 10): subs["now-{}".format(i)] = nowYear - 1 subs["now+{}".format(i)] = nowYear + 1 for home in calendarPath.child("ho").child("me").children(): if not home.basename().startswith("."): for calendar in home.children(): if not calendar.basename().startswith("."): for resource in calendar.children(): if resource.basename().endswith(".ics"): resource.setContent(resource.getContent() % subs) testID = test.id() test.counter = 0 test.notifierFactory = StubNotifierFactory() test.calendarStore = CalendarStore( storeRootPath, {"push": test.notifierFactory} if test.notifierFactory else {}, None, # must create directory later quota=deriveQuota(test), ) test.directory = buildTestDirectory(test.calendarStore, test.mktemp()) test.txn = test.calendarStore.newTransaction(testID + "(old)") assert test.calendarStore is not None, "No calendar store?"
def test_purgeOldEvents_old_cutoff(self): # Dry run cutoff = DateTime.getToday() cutoff.setDateOnly(False) cutoff.offsetDay(-400) total = (yield PurgeOldEventsService.purgeOldEvents( self._sqlCalendarStore, "ho", cutoff, 2, dryrun=True, debug=True )) self.assertEquals(total, 12) # Actually remove total = (yield PurgeOldEventsService.purgeOldEvents( self._sqlCalendarStore, None, cutoff, 2, debug=True )) self.assertEquals(total, 12) total = (yield PurgeOldEventsService.purgeOldEvents( self._sqlCalendarStore, "ho", cutoff, 2, dryrun=True, debug=True )) self.assertEquals(total, 0)
class TestConduitAPI(MultiStoreConduitTest): """ Test that the conduit api works. """ nowYear = {"now": DateTime.getToday().getYear()} caldata1 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid1 DTSTART:{now:04d}0102T140000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:instance END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**nowYear) caldata1_changed = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid1 DTSTART:{now:04d}0102T140000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:instance changed END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**nowYear) caldata1_failed = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid1-failed DTSTART:{now:04d}0102T140000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:instance changed END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**nowYear) caldata2 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid2 DTSTART:{now:04d}0102T160000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:instance END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**nowYear) @inlineCallbacks def _remoteHome(self, txn, uid): """ Create a synthetic external home object that maps to the actual remote home. @param ownerUID: directory uid of the user's home @type ownerUID: L{str} """ from txdav.caldav.datastore.sql_external import CalendarHomeExternal recipient = yield txn.store().directoryService().recordWithUID(uid) resourceID = yield txn.store().conduit.send_home_resource_id(txn, recipient) home = CalendarHomeExternal.makeSyntheticExternalHome(txn, recipient.uid, resourceID) if resourceID is not None else None if home: home._childClass = home._childClass._externalClass returnValue(home) @inlineCallbacks def test_remote_home(self): """ Test that a remote home can be accessed. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) self.assertEqual(home.id(), home01.id()) yield self.commitTransaction(1) @inlineCallbacks def test_homechild_listobjects(self): """ Test that a remote home L{listChildren} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) children01 = yield home01.listChildren() yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) self.assertEqual(home.id(), home01.id()) children = yield home.listChildren() self.assertEqual(set(children), set(children01)) yield self.commitTransaction(1) @inlineCallbacks def test_homechild_loadallobjects(self): """ Test that a remote home L{loadChildren} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) children01 = yield home01.loadChildren() names01 = [child.name() for child in children01] ids01 = [child.id() for child in children01] yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) self.assertEqual(home.id(), home01.id()) children = yield home.loadChildren() names = [child.name() for child in children] ids = [child.id() for child in children] self.assertEqual(set(names), set(names01)) self.assertEqual(set(ids), set(ids01)) yield self.commitTransaction(1) @inlineCallbacks def test_homechild_objectwith(self): """ Test that a remote home L{loadChildren} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") self.assertEqual(calendar.id(), calendar01.id()) yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_loadallobjects(self): """ Test that a remote home child L{objectResources} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") objects = yield calendar.objectResources() self.assertEqual(len(objects), 1) self.assertEqual(objects[0].name(), "1.ics") yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_loadallobjectswithnames(self): """ Test that a remote home child L{objectResourcesWithNames} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") objects = yield calendar.objectResourcesWithNames(("2.ics",)) self.assertEqual(len(objects), 1) self.assertEqual(objects[0].name(), "2.ics") yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_listobjects(self): """ Test that a remote home child L{listObjectResources} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") names = yield calendar.listObjectResources() self.assertEqual(set(names), set(("1.ics", "2.ics",))) yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_countobjects(self): """ Test that a remote home child L{countObjectResources} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") count = yield calendar.countObjectResources() self.assertEqual(count, 2) yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_objectwith(self): """ Test that a remote home child L{objectResourceWithName} and L{objectResourceWithUID} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") resource01 = yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") resource = yield calendar.objectResourceWithName("2.ics") self.assertEqual(resource.name(), "2.ics") resource = yield calendar.objectResourceWithName("foo.ics") self.assertEqual(resource, None) resource = yield calendar.objectResourceWithUID("uid1") self.assertEqual(resource.name(), "1.ics") resource = yield calendar.objectResourceWithUID("foo") self.assertEqual(resource, None) resource = yield calendar.objectResourceWithID(resource01.id()) self.assertEqual(resource.name(), "1.ics") resource = yield calendar.objectResourceWithID(12345) self.assertEqual(resource, None) yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_resourcenameforuid(self): """ Test that a remote home child L{resourceNameForUID} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") name = yield calendar.resourceNameForUID("uid1") self.assertEqual(name, "1.ics") name = yield calendar.resourceNameForUID("uid2") self.assertEqual(name, "2.ics") name = yield calendar.resourceNameForUID("foo") self.assertEqual(name, None) yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_resourceuidforname(self): """ Test that a remote home child L{resourceUIDForName} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") uid = yield calendar.resourceUIDForName("1.ics") self.assertEqual(uid, "uid1") uid = yield calendar.resourceUIDForName("2.ics") self.assertEqual(uid, "uid2") uid = yield calendar.resourceUIDForName("foo.ics") self.assertEqual(uid, None) yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_create(self): """ Test that a remote object resource L{create} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) yield home01.childWithName("calendar") yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") resource = yield calendar.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(1) home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") resource01 = yield calendar01.objectResourceWithName("1.ics") self.assertEqual(resource01.id(), resource.id()) caldata = yield resource01.component() self.assertEqual(str(caldata), self.caldata1) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") resource = yield calendar.objectResourceWithName("1.ics") caldata = yield resource.component() self.assertEqual(str(caldata), self.caldata1) yield self.commitTransaction(1) # Recreate fails home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") self.assertFailure( calendar.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)), ObjectResourceNameAlreadyExistsError, ) yield self.abortTransaction(1) @inlineCallbacks def test_objectresource_setcomponent(self): """ Test that a remote object resource L{setComponent} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") resource = yield calendar.objectResourceWithName("1.ics") changed = yield resource.setComponent(Component.fromString(self.caldata1_changed)) self.assertFalse(changed) caldata = yield resource.component() self.assertEqual(normalize_iCalStr(str(caldata)), normalize_iCalStr(self.caldata1_changed)) yield self.commitTransaction(1) home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") resource01 = yield calendar01.objectResourceWithName("1.ics") caldata = yield resource01.component() self.assertEqual(normalize_iCalStr(str(caldata)), normalize_iCalStr(self.caldata1_changed)) yield self.commitTransaction(0) # Fail to set with different UID home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") resource = yield calendar.objectResourceWithName("1.ics") self.assertFailure( resource.setComponent(Component.fromString(self.caldata1_failed)), InvalidUIDError, ) yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_component(self): """ Test that a remote object resource L{component} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield calendar01.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") resource = yield calendar.objectResourceWithName("1.ics") caldata = yield resource.component() self.assertEqual(str(caldata), self.caldata1) resource = yield calendar.objectResourceWithName("2.ics") caldata = yield resource.component() self.assertEqual(str(caldata), self.caldata2) yield self.commitTransaction(1) @inlineCallbacks def test_objectresource_remove(self): """ Test that a remote object resource L{component} works. """ home01 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.assertTrue(home01 is not None) calendar01 = yield home01.childWithName("calendar") yield calendar01.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") resource = yield calendar.objectResourceWithName("1.ics") yield resource.remove() yield self.commitTransaction(1) resource01 = yield self.calendarObjectUnderTest( txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics", ) self.assertTrue(resource01 is None) yield self.commitTransaction(0) home = yield self._remoteHome(self.theTransactionUnderTest(1), "user01") self.assertTrue(home is not None) calendar = yield home.childWithName("calendar") resource = yield calendar.objectResourceWithName("1.ics") self.assertTrue(resource is None) yield self.commitTransaction(1)
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
elif opt in ("-n", "--dry-run"): dryrun = True elif opt in ("-f", "--config"): configFileName = arg else: raise NotImplementedError(opt) if args: cls.usage("Too many arguments: %s" % (args,)) if dryrun: verbose = True cutoff = DateTime.getToday() cutoff.setDateOnly(False) cutoff.offsetDay(-days) cls.cutoff = cutoff cls.batchSize = batchSize cls.dryrun = dryrun cls.verbose = verbose utilityMain( configFileName, cls, verbose=debug, ) @classmethod
def test_calendar_query_time_range(self): """ Partial retrieval of events by time range. (CalDAV-access-09, section 7.6.1) """ calendar_properties = ( davxml.GETETag(), caldavxml.CalendarData( caldavxml.CalendarComponent( caldavxml.AllProperties(), caldavxml.CalendarComponent( caldavxml.Property(name="X-ABC-GUID"), caldavxml.Property(name="UID"), caldavxml.Property(name="DTSTART"), caldavxml.Property(name="DTEND"), caldavxml.Property(name="DURATION"), caldavxml.Property(name="EXDATE"), caldavxml.Property(name="EXRULE"), caldavxml.Property(name="RDATE"), caldavxml.Property(name="RRULE"), caldavxml.Property(name="LOCATION"), caldavxml.Property(name="SUMMARY"), name="VEVENT", ), caldavxml.CalendarComponent( caldavxml.AllProperties(), caldavxml.AllComponents(), name="VTIMEZONE", ), name="VCALENDAR", ), ), ) query_timerange = caldavxml.TimeRange( start="%04d1001T000000Z" % (DateTime.getToday().getYear(),), end="%04d1101T000000Z" % (DateTime.getToday().getYear(),), ) query = caldavxml.CalendarQuery( davxml.PropertyContainer(*calendar_properties), caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( query_timerange, name="VEVENT", ), name="VCALENDAR", ), ), ) def got_xml(doc): if not isinstance(doc.root_element, davxml.MultiStatus): self.fail("REPORT response XML root element is not multistatus: %r" % (doc.root_element,)) for response in doc.root_element.childrenOfType(davxml.PropertyStatusResponse): properties_to_find = [p.qname() for p in calendar_properties] for propstat in response.childrenOfType(davxml.PropertyStatus): status = propstat.childOfType(davxml.Status) properties = propstat.childOfType(davxml.PropertyContainer).children if status.code != responsecode.OK: self.fail("REPORT failed (status %s) to locate properties: %r" % (status.code, properties)) for property in properties: qname = property.qname() if qname in properties_to_find: properties_to_find.remove(qname) else: self.fail("REPORT found property we didn't ask for: %r" % (property,)) if isinstance(property, caldavxml.CalendarData): cal = property.calendar() instances = cal.expandTimeRanges(query_timerange.end) vevents = [x for x in cal.subcomponents() if x.name() == "VEVENT"] if not TimeRange(query_timerange).matchinstance(vevents[0], instances): self.fail("REPORT property %r returned calendar %s outside of request time range %r" % (property, property.calendar, query_timerange)) return self.calendar_query(query, got_xml)
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)
def doExpand(self, request): """ Expand a timezone within specified start/end dates. """ tzids = request.args.get("tzid", ()) if len(tzids) != 1: raise HTTPError( JSONResponse( responsecode.BAD_REQUEST, {"error": "invalid-tzid", "description": "Invalid tzid query parameter"} ) ) try: start = request.args.get("start", ()) if len(start) > 1: raise ValueError() elif len(start) == 1: start = DateTime.parseText("{}0101".format(int(start[0]))) else: start = DateTime.getToday() start.setDay(1) start.setMonth(1) except ValueError: raise HTTPError( JSONResponse( responsecode.BAD_REQUEST, {"error": "invalid-start", "description": "Invalid start query parameter"} ) ) try: end = request.args.get("end", ()) if len(end) > 1: raise ValueError() elif len(end) == 1: end = DateTime.parseText("{}0101".format(int(end[0]))) else: end = DateTime.getToday() end.setDay(1) end.setMonth(1) end.offsetYear(10) if end <= start: raise ValueError() except ValueError: raise HTTPError( JSONResponse( responsecode.BAD_REQUEST, {"error": "invalid-end", "description": "Invalid end query parameter"} ) ) tzid = tzids[0] tzdata = self.timezones.getTimezone(tzid) if tzdata is None: raise HTTPError( JSONResponse( responsecode.NOT_FOUND, {"error": "tzid-not-found", "description": "Tzid could not be found"} ) ) # Now do the expansion (but use a cache to avoid re-calculating TZs) observances = self.expandcache.get((tzid, start, end), None) if observances is None: observances = tzexpandlocal(tzdata, start, end) self.expandcache[(tzid, start, end)] = observances # Turn into JSON result = { "dtstamp": self.timezones.dtstamp, "observances": [ { "name": name, "onset": onset.getXMLText(), "utc-offset-from": utc_offset_from, "utc-offset-to": utc_offset_to, } for onset, utc_offset_from, utc_offset_to, name in observances ], } return JSONResponse(responsecode.OK, result)
def _doRefresh(tzpath, xmlfile, tzdb, tzvers): """ Refresh data from IANA. """ print("Downloading latest data from IANA") if tzvers: path = "https://www.iana.org/time-zones/repository/releases/tzdata%s.tar.gz" % ( tzvers, ) else: path = "https://www.iana.org/time-zones/repository/tzdata-latest.tar.gz" data = urllib.urlretrieve(path) print("Extract data at: %s" % (data[0])) rootdir = tempfile.mkdtemp() zonedir = os.path.join(rootdir, "tzdata") os.mkdir(zonedir) with tarfile.open(data[0], "r:gz") as t: t.extractall(zonedir) # Get the version from the Makefile try: makefile = open(os.path.join(zonedir, "Makefile")).read() lines = makefile.splitlines() for line in lines: if line.startswith("VERSION="): tzvers = line[8:].strip() break except IOError: pass if not tzvers: tzvers = DateTime.getToday().getText() print("Converting data (version: %s) at: %s" % ( tzvers, zonedir, )) startYear = 1800 endYear = DateTime.getToday().getYear() + 10 Calendar.sProdID = "-//calendarserver.org//Zonal//EN" zonefiles = "northamerica", "southamerica", "europe", "africa", "asia", "australasia", "antarctica", "etcetera", "backward" parser = tzconvert() for file in zonefiles: parser.parse(os.path.join(zonedir, file)) # Check for windows aliases print("Downloading latest data from unicode.org") path = "http://unicode.org/repos/cldr/tags/latest/common/supplemental/windowsZones.xml" data = urllib.urlretrieve(path) wpath = data[0] # Generate the iCalendar data print("Generating iCalendar data") parser.generateZoneinfoFiles(os.path.join(rootdir, "zoneinfo"), startYear, endYear, windowsAliases=wpath, filterzones=()) print("Copy new zoneinfo to destination: %s" % (tzpath, )) z = FilePath(os.path.join(rootdir, "zoneinfo")) tz = FilePath(tzpath) z.copyTo(tz) print("Updating XML file at: %s" % (xmlfile, )) tzdb.readDatabase() tzdb.updateDatabase() print("Current total: %d" % (len(tzdb.timezones), )) print("Total Changed: %d" % (tzdb.changeCount, )) if tzdb.changeCount: print("Changed:") for k in sorted(tzdb.changed): print(" %s" % (k, )) versfile = os.path.join(os.path.dirname(xmlfile), "version.txt") print("Updating version file at: %s" % (versfile, )) with open(versfile, "w") as f: f.write(TimezoneCache.IANA_VERSION_PREFIX + tzvers)
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)
def test_calendar_query_time_range(self): """ Partial retrieval of events by time range. (CalDAV-access-09, section 7.6.1) """ calendar_properties = ( davxml.GETETag(), caldavxml.CalendarData( caldavxml.CalendarComponent( caldavxml.AllProperties(), caldavxml.CalendarComponent( caldavxml.Property(name="X-ABC-GUID"), caldavxml.Property(name="UID"), caldavxml.Property(name="DTSTART"), caldavxml.Property(name="DTEND"), caldavxml.Property(name="DURATION"), caldavxml.Property(name="EXDATE"), caldavxml.Property(name="EXRULE"), caldavxml.Property(name="RDATE"), caldavxml.Property(name="RRULE"), caldavxml.Property(name="LOCATION"), caldavxml.Property(name="SUMMARY"), name="VEVENT", ), caldavxml.CalendarComponent( caldavxml.AllProperties(), caldavxml.AllComponents(), name="VTIMEZONE", ), name="VCALENDAR", ), ), ) query_timerange = caldavxml.TimeRange( start="%04d1001T000000Z" % (DateTime.getToday().getYear(), ), end="%04d1101T000000Z" % (DateTime.getToday().getYear(), ), ) query = caldavxml.CalendarQuery( davxml.PropertyContainer(*calendar_properties), caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( query_timerange, name="VEVENT", ), name="VCALENDAR", ), ), ) def got_xml(doc): if not isinstance(doc.root_element, davxml.MultiStatus): self.fail( "REPORT response XML root element is not multistatus: %r" % (doc.root_element, )) for response in doc.root_element.childrenOfType( davxml.PropertyStatusResponse): properties_to_find = [p.qname() for p in calendar_properties] for propstat in response.childrenOfType(davxml.PropertyStatus): status = propstat.childOfType(davxml.Status) properties = propstat.childOfType( davxml.PropertyContainer).children if status.code != responsecode.OK: self.fail( "REPORT failed (status %s) to locate properties: %r" % (status.code, properties)) for property in properties: qname = property.qname() if qname in properties_to_find: properties_to_find.remove(qname) else: self.fail( "REPORT found property we didn't ask for: %r" % (property, )) if isinstance(property, caldavxml.CalendarData): cal = property.calendar() instances = cal.expandTimeRanges( query_timerange.end) vevents = [ x for x in cal.subcomponents() if x.name() == "VEVENT" ] if not TimeRange(query_timerange).matchinstance( vevents[0], instances): self.fail( "REPORT property %r returned calendar %s outside of request time range %r" % (property, property.calendar, query_timerange)) return self.calendar_query(query, got_xml)
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 doWork(self): # Delete all other work items for this event yield Delete( From=self.table, Where=self.group, ).on(self.transaction) # get db object calendarObject = yield CalendarStoreFeatures( self.transaction._store).calendarObjectWithID( self.transaction, self.resourceID) component = yield calendarObject.componentForUser() # Change a copy of the original, as we need the original cached on the resource # so we can do a diff to test implicit scheduling changes component = component.duplicate() # sync group attendees if (yield calendarObject.reconcileGroupAttendees(component)): # group attendees in event have changed if (component.masterComponent() is None or not component.isRecurring()): # skip non-recurring old events, no instances if (yield calendarObject.removeOldEventGroupLink( component, instances=None, inserting=False, txn=self.transaction)): returnValue(None) else: # skip recurring old events expand = (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandAheadDays)) if config.FreeBusyIndexLowerLimitDays: truncateLowerLimit = DateTime.getToday() truncateLowerLimit.offsetDay( -config.FreeBusyIndexLowerLimitDays) else: truncateLowerLimit = None instances = component.expandTimeRanges( expand, lowerLimit=truncateLowerLimit, ignoreInvalidInstances=True) if (yield calendarObject.removeOldEventGroupLink( component, instances=instances, inserting=False, txn=self.transaction)): returnValue(None) # split spanning events and only update present-future split result splitter = iCalSplitter(0, 1) break_point = DateTime.getToday() - Duration( seconds=config.GroupAttendees.UpdateOldEventLimitSeconds) rid = splitter.whereSplit(component, break_point=break_point) if rid is not None: yield calendarObject.split(onlyThis=True, rid=rid) # remove group link to ensure update (update to unknown hash would work too) # FIXME: its possible that more than one group id gets updated during this single work item, so we # need to make sure that ALL the group_id's are removed by this query. ga = schema.GROUP_ATTENDEE yield Delete(From=ga, Where=(ga.RESOURCE_ID == self.resourceID).And( ga.GROUP_ID == self.groupID)).on( self.transaction) # update group attendee in remaining component component = yield calendarObject.componentForUser() component = component.duplicate() change = yield calendarObject.reconcileGroupAttendees( component) assert change yield calendarObject._setComponentInternal( component, False, ComponentUpdateState.SPLIT_OWNER) returnValue(None) yield calendarObject.setComponent(component)
class TestConduitAPI(MultiStoreConduitTest): """ Test that the conduit api works. """ nowYear = {"now": DateTime.getToday().getYear()} caldata1 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid1 DTSTART:{now:04d}0102T140000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:instance END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**nowYear) caldata1_changed = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid1 DTSTART:{now:04d}0102T150000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:instance changed END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**nowYear) caldata2 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid2 DTSTART:{now:04d}0102T160000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:instance END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**nowYear) caldata3 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid3 DTSTART:{now:04d}0102T160000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:instance END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**nowYear) @inlineCallbacks def test_basic_share(self): """ Test that basic invite/uninvite works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") shared = yield calendar1.shareeView("puser01") self.assertEqual(shared.shareStatus(), _BIND_STATUS_ACCEPTED) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") self.assertTrue(shared is not None) self.assertTrue(shared.external()) yield self.commitTransaction(1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.uninviteUIDFromShare("puser01") yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") self.assertTrue(shared is None) yield self.commitTransaction(1) @inlineCallbacks def test_countobjects(self): """ Test that action=countobjects works. """ yield self.createShare("user01", "puser01") shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") count = yield shared.countObjectResources() self.assertEqual(count, 0) yield self.commitTransaction(1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) count = yield calendar1.countObjectResources() self.assertEqual(count, 1) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") count = yield shared.countObjectResources() self.assertEqual(count, 1) yield self.commitTransaction(1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.remove() count = yield calendar1.countObjectResources() self.assertEqual(count, 0) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") count = yield shared.countObjectResources() self.assertEqual(count, 0) yield self.commitTransaction(1) @inlineCallbacks def test_listobjects(self): """ Test that action=listobjects works. """ yield self.createShare("user01", "puser01") shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") objects = yield shared.listObjectResources() self.assertEqual(set(objects), set()) yield self.commitTransaction(1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) objects = yield calendar1.listObjectResources() self.assertEqual(set(objects), set(("1.ics", "2.ics",))) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") objects = yield shared.listObjectResources() self.assertEqual(set(objects), set(("1.ics", "2.ics",))) yield self.commitTransaction(1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.remove() objects = yield calendar1.listObjectResources() self.assertEqual(set(objects), set(("2.ics",))) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") objects = yield shared.listObjectResources() self.assertEqual(set(objects), set(("2.ics",))) yield self.commitTransaction(1) @inlineCallbacks def test_synctoken(self): """ Test that action=synctoken works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") token1_1 = yield calendar1.syncTokenRevision() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") token2_1 = yield shared.syncTokenRevision() yield self.commitTransaction(1) self.assertEqual(token1_1, token2_1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") token1_2 = yield calendar1.syncTokenRevision() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") token2_2 = yield shared.syncTokenRevision() yield self.commitTransaction(1) self.assertNotEqual(token1_1, token1_2) self.assertEqual(token1_2, token2_2) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.remove() count = yield calendar1.countObjectResources() self.assertEqual(count, 0) yield self.commitTransaction(0) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") token1_3 = yield calendar1.syncTokenRevision() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") token2_3 = yield shared.syncTokenRevision() yield self.commitTransaction(1) self.assertNotEqual(token1_1, token1_3) self.assertNotEqual(token1_2, token1_3) self.assertEqual(token1_3, token2_3) @inlineCallbacks def test_resourcenamessincerevision(self): """ Test that action=synctoken works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") token1_1 = yield calendar1.syncToken() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") token2_1 = yield shared.syncToken() yield self.commitTransaction(1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") token1_2 = yield calendar1.syncToken() names1 = yield calendar1.resourceNamesSinceToken(token1_1) self.assertEqual(names1, ([u"1.ics"], [], [],)) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") token2_2 = yield shared.syncToken() names2 = yield shared.resourceNamesSinceToken(token2_1) self.assertEqual(names2, ([u"1.ics"], [], [],)) yield self.commitTransaction(1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.remove() count = yield calendar1.countObjectResources() self.assertEqual(count, 0) yield self.commitTransaction(0) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") token1_3 = yield calendar1.syncToken() names1 = yield calendar1.resourceNamesSinceToken(token1_2) self.assertEqual(names1, ([], [u"1.ics"], [],)) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") token2_3 = yield shared.syncToken() names2 = yield shared.resourceNamesSinceToken(token2_2) self.assertEqual(names2, ([], [u"1.ics"], [],)) yield self.commitTransaction(1) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") names1 = yield calendar1.resourceNamesSinceToken(token1_3) self.assertEqual(names1, ([], [], [],)) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") names2 = yield shared.resourceNamesSinceToken(token2_3) self.assertEqual(names2, ([], [], [],)) yield self.commitTransaction(1) @inlineCallbacks def test_resourceuidforname(self): """ Test that action=resourceuidforname works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") uid = yield calendar1.resourceUIDForName("1.ics") self.assertEqual(uid, "uid1") uid = yield calendar1.resourceUIDForName("2.ics") self.assertTrue(uid is None) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") uid = yield shared.resourceUIDForName("1.ics") self.assertEqual(uid, "uid1") uid = yield shared.resourceUIDForName("2.ics") self.assertTrue(uid is None) yield self.commitTransaction(1) @inlineCallbacks def test_resourcenameforuid(self): """ Test that action=resourcenameforuid works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") name = yield calendar1.resourceNameForUID("uid1") self.assertEqual(name, "1.ics") name = yield calendar1.resourceNameForUID("uid2") self.assertTrue(name is None) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") name = yield shared.resourceNameForUID("uid1") self.assertEqual(name, "1.ics") name = yield shared.resourceNameForUID("uid2") self.assertTrue(name is None) yield self.commitTransaction(1) @inlineCallbacks def test_search(self): """ Test that action=resourcenameforuid works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) filter = caldavxml.Filter( caldavxml.ComponentFilter( *[caldavxml.ComponentFilter( **{"name": ("VEVENT", "VFREEBUSY", "VAVAILABILITY")} )], **{"name": "VCALENDAR"} ) ) filter = Filter(filter) calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") names = [item[0] for item in (yield calendar1.search(filter))] self.assertEqual(names, ["1.ics", ]) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") names = [item[0] for item in (yield shared.search(filter))] self.assertEqual(names, ["1.ics", ]) yield self.commitTransaction(1) @inlineCallbacks def test_loadallobjects(self): """ Test that action=loadallobjects works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) resource_id1 = resource1.id() resource2 = yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) resource_id2 = resource2.id() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resources = yield shared.objectResources() byname = dict([(obj.name(), obj) for obj in resources]) byuid = dict([(obj.uid(), obj) for obj in resources]) self.assertEqual(len(resources), 2) self.assertEqual(set([obj.name() for obj in resources]), set(("1.ics", "2.ics",))) self.assertEqual(set([obj.uid() for obj in resources]), set(("uid1", "uid2",))) self.assertEqual(set([obj.id() for obj in resources]), set((resource_id1, resource_id2,))) resource = yield shared.objectResourceWithName("1.ics") self.assertTrue(resource is byname["1.ics"]) resource = yield shared.objectResourceWithName("2.ics") self.assertTrue(resource is byname["2.ics"]) resource = yield shared.objectResourceWithName("Missing.ics") self.assertTrue(resource is None) resource = yield shared.objectResourceWithUID("uid1") self.assertTrue(resource is byuid["uid1"]) resource = yield shared.objectResourceWithUID("uid2") self.assertTrue(resource is byuid["uid2"]) resource = yield shared.objectResourceWithUID("uid-missing") self.assertTrue(resource is None) resource = yield shared.objectResourceWithID(resource_id1) self.assertTrue(resource is byname["1.ics"]) resource = yield shared.objectResourceWithID(resource_id2) self.assertTrue(resource is byname["2.ics"]) resource = yield shared.objectResourceWithID(0) self.assertTrue(resource is None) yield self.commitTransaction(1) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.remove() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resources = yield shared.objectResources() byname = dict([(obj.name(), obj) for obj in resources]) byuid = dict([(obj.uid(), obj) for obj in resources]) self.assertEqual(len(resources), 1) self.assertEqual(set([obj.name() for obj in resources]), set(("2.ics",))) self.assertEqual(set([obj.uid() for obj in resources]), set(("uid2",))) self.assertEqual(set([obj.id() for obj in resources]), set((resource_id2,))) resource = yield shared.objectResourceWithName("1.ics") self.assertTrue(resource is None) resource = yield shared.objectResourceWithName("2.ics") self.assertTrue(resource is byname["2.ics"]) resource = yield shared.objectResourceWithName("Missing.ics") self.assertTrue(resource is None) resource = yield shared.objectResourceWithUID("uid1") self.assertTrue(resource is None) resource = yield shared.objectResourceWithUID("uid2") self.assertTrue(resource is byuid["uid2"]) resource = yield shared.objectResourceWithUID("uid-missing") self.assertTrue(resource is None) resource = yield shared.objectResourceWithID(resource_id1) self.assertTrue(resource is None) resource = yield shared.objectResourceWithID(resource_id2) self.assertTrue(resource is byname["2.ics"]) resource = yield shared.objectResourceWithID(0) self.assertTrue(resource is None) yield self.commitTransaction(1) @inlineCallbacks def test_loadallobjectswithnames(self): """ Test that action=loadallobjectswithnames works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") resource1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) resource_id1 = resource1.id() yield calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2)) resource3 = yield calendar1.createCalendarObjectWithName("3.ics", Component.fromString(self.caldata3)) resource_id3 = resource3.id() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resources = yield shared.objectResources() self.assertEqual(len(resources), 3) yield self.commitTransaction(1) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",)) byname = dict([(obj.name(), obj) for obj in resources]) byuid = dict([(obj.uid(), obj) for obj in resources]) self.assertEqual(len(resources), 2) self.assertEqual(set([obj.name() for obj in resources]), set(("1.ics", "3.ics",))) self.assertEqual(set([obj.uid() for obj in resources]), set(("uid1", "uid3",))) self.assertEqual(set([obj.id() for obj in resources]), set((resource_id1, resource_id3,))) resource = yield shared.objectResourceWithName("1.ics") self.assertTrue(resource is byname["1.ics"]) resource = yield shared.objectResourceWithName("3.ics") self.assertTrue(resource is byname["3.ics"]) resource = yield shared.objectResourceWithName("Missing.ics") self.assertTrue(resource is None) resource = yield shared.objectResourceWithUID("uid1") self.assertTrue(resource is byuid["uid1"]) resource = yield shared.objectResourceWithUID("uid3") self.assertTrue(resource is byuid["uid3"]) resource = yield shared.objectResourceWithUID("uid-missing") self.assertTrue(resource is None) resource = yield shared.objectResourceWithID(resource_id1) self.assertTrue(resource is byname["1.ics"]) resource = yield shared.objectResourceWithID(resource_id3) self.assertTrue(resource is byname["3.ics"]) resource = yield shared.objectResourceWithID(0) self.assertTrue(resource is None) yield self.commitTransaction(1) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.remove() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resources = yield shared.objectResourcesWithNames(("1.ics", "3.ics",)) byname = dict([(obj.name(), obj) for obj in resources]) byuid = dict([(obj.uid(), obj) for obj in resources]) self.assertEqual(len(resources), 1) self.assertEqual(set([obj.name() for obj in resources]), set(("3.ics",))) self.assertEqual(set([obj.uid() for obj in resources]), set(("uid3",))) self.assertEqual(set([obj.id() for obj in resources]), set((resource_id3,))) resource = yield shared.objectResourceWithName("1.ics") self.assertTrue(resource is None) resource = yield shared.objectResourceWithName("3.ics") self.assertTrue(resource is byname["3.ics"]) resource = yield shared.objectResourceWithName("Missing.ics") self.assertTrue(resource is None) resource = yield shared.objectResourceWithUID("uid1") self.assertTrue(resource is None) resource = yield shared.objectResourceWithUID("uid3") self.assertTrue(resource is byuid["uid3"]) resource = yield shared.objectResourceWithUID("uid-missing") self.assertTrue(resource is None) resource = yield shared.objectResourceWithID(resource_id1) self.assertTrue(resource is None) resource = yield shared.objectResourceWithID(resource_id3) self.assertTrue(resource is byname["3.ics"]) resource = yield shared.objectResourceWithID(0) self.assertTrue(resource is None) yield self.commitTransaction(1) @inlineCallbacks def test_objectwith(self): """ Test that action=objectwith works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") resource = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) resource_id = resource.id() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resource = yield shared.objectResourceWithName("1.ics") self.assertTrue(resource is not None) self.assertEqual(resource.name(), "1.ics") self.assertEqual(resource.uid(), "uid1") resource = yield shared.objectResourceWithName("2.ics") self.assertTrue(resource is None) yield self.commitTransaction(1) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resource = yield shared.objectResourceWithUID("uid1") self.assertTrue(resource is not None) self.assertEqual(resource.name(), "1.ics") self.assertEqual(resource.uid(), "uid1") resource = yield shared.objectResourceWithUID("uid2") self.assertTrue(resource is None) yield self.commitTransaction(1) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resource = yield shared.objectResourceWithID(resource_id) self.assertTrue(resource is not None) self.assertEqual(resource.name(), "1.ics") self.assertEqual(resource.uid(), "uid1") resource = yield shared.objectResourceWithID(0) self.assertTrue(resource is None) yield self.commitTransaction(1) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.remove() yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resource = yield shared.objectResourceWithName("1.ics") self.assertTrue(resource is None) yield self.commitTransaction(1) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resource = yield shared.objectResourceWithUID("uid1") self.assertTrue(resource is None) yield self.commitTransaction(1) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resource = yield shared.objectResourceWithID(resource_id) self.assertTrue(resource is None) yield self.commitTransaction(1) @inlineCallbacks def test_create(self): """ Test that action=create works. """ yield self.createShare("user01", "puser01") shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resource = yield shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) resource_id = resource.id() self.assertTrue(resource is not None) self.assertEqual(resource.name(), "1.ics") self.assertEqual(resource.uid(), "uid1") self.assertFalse(resource._componentChanged) yield self.commitTransaction(1) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") resource = yield shared.objectResourceWithUID("uid1") self.assertTrue(resource is not None) self.assertEqual(resource.name(), "1.ics") self.assertEqual(resource.uid(), "uid1") self.assertEqual(resource.id(), resource_id) yield self.commitTransaction(1) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") self.assertTrue(object1 is not None) self.assertEqual(object1.name(), "1.ics") self.assertEqual(object1.uid(), "uid1") self.assertEqual(object1.id(), resource_id) yield self.commitTransaction(0) @inlineCallbacks def test_create_exception(self): """ Test that action=create fails when a duplicate name is used. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") yield self.failUnlessFailure(shared.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)), ObjectResourceNameAlreadyExistsError) yield self.abortTransaction(1) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") yield self.failUnlessFailure(shared.createCalendarObjectWithName(".2.ics", Component.fromString(self.caldata2)), ObjectResourceNameNotAllowedError) yield self.abortTransaction(1) @inlineCallbacks def test_setcomponent(self): """ Test that action=setcomponent works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") ical = yield shared_object.component() self.assertTrue(isinstance(ical, Component)) self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1)) yield self.commitTransaction(1) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") changed = yield shared_object.setComponent(Component.fromString(self.caldata1_changed)) self.assertFalse(changed) ical = yield shared_object.component() self.assertTrue(isinstance(ical, Component)) self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed)) yield self.commitTransaction(1) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") ical = yield object1.component() self.assertTrue(isinstance(ical, Component)) self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1_changed)) yield self.commitTransaction(0) @inlineCallbacks def test_component(self): """ Test that action=component works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") ical = yield shared_object.component() self.assertTrue(isinstance(ical, Component)) self.assertEqual(normalize_iCalStr(str(ical)), normalize_iCalStr(self.caldata1)) yield self.commitTransaction(1) @inlineCallbacks def test_remove(self): """ Test that action=remove works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") yield shared_object.remove() yield self.commitTransaction(1) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") self.assertTrue(shared_object is None) yield self.commitTransaction(1) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") self.assertTrue(object1 is None) yield self.commitTransaction(0) @inlineCallbacks def test_freebusy(self): """ Test that action=component works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) fbstart = "{now:04d}0102T000000Z".format(**self.nowYear) fbend = "{now:04d}0103T000000Z".format(**self.nowYear) shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar") fbinfo = FreebusyQuery.FBInfo([], [], []) timerange = Period(DateTime.parseText(fbstart), DateTime.parseText(fbend)) organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:[email protected]", self.theTransactionUnderTest(1))) freebusy = FreebusyQuery(organizer=organizer, recipient=recipient, timerange=timerange) matchtotal = (yield freebusy.generateFreeBusyInfo([shared, ], fbinfo)) self.assertEqual(matchtotal, 1) self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ]) self.assertEqual(len(fbinfo[1]), 0) self.assertEqual(len(fbinfo[2]), 0) yield self.commitTransaction(1) def attachmentToString(self, attachment): """ Convenience to convert an L{IAttachment} to a string. @param attachment: an L{IAttachment} provider to convert into a string. @return: a L{Deferred} that fires with the contents of the attachment. @rtype: L{Deferred} firing C{bytes} """ capture = CaptureProtocol() attachment.retrieve(capture) return capture.deferred @inlineCallbacks def test_add_attachment(self): """ Test that action=add-attachment works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") object1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) resourceID = object1.id() yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") data = "Here is some text." attachment, location = yield shared_object.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream(data)) managedID = attachment.managedID() from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal self.assertTrue(isinstance(attachment, ManagedAttachmentExternal)) self.assertEqual(attachment.size(), len(data)) self.assertTrue("user01/dropbox/" in location) yield self.commitTransaction(1) cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID) self.assertEqual(cobjs, set((resourceID,))) attachment = yield ManagedAttachment.load(self.theTransactionUnderTest(0), resourceID, managedID) self.assertEqual(attachment.name(), "test.txt") data = yield self.attachmentToString(attachment) self.assertEqual(data, "Here is some text.") yield self.commitTransaction(0) @inlineCallbacks def test_update_attachment(self): """ Test that action=update-attachment works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") resourceID = object1.id() attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) managedID = attachment.managedID() yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") data = "Here is some more text." attachment, location = yield shared_object.updateAttachment(managedID, MimeType.fromString("text/plain"), "test.txt", MemoryStream(data)) managedID = attachment.managedID() from txdav.caldav.datastore.sql_external import ManagedAttachmentExternal self.assertTrue(isinstance(attachment, ManagedAttachmentExternal)) self.assertEqual(attachment.size(), len(data)) self.assertTrue("user01/dropbox/" in location) yield self.commitTransaction(1) cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID) self.assertEqual(cobjs, set((resourceID,))) attachment = yield ManagedAttachment.load(self.transactionUnderTest(), resourceID, managedID) self.assertEqual(attachment.name(), "test.txt") data = yield self.attachmentToString(attachment) self.assertEqual(data, "Here is some more text.") yield self.commitTransaction(0) @inlineCallbacks def test_remove_attachment(self): """ Test that action=remove-attachment works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") resourceID = object1.id() attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) managedID = attachment.managedID() yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") yield shared_object.removeAttachment(None, managedID) yield self.commitTransaction(1) cobjs = yield ManagedAttachment.referencesTo(self.theTransactionUnderTest(0), managedID) self.assertEqual(cobjs, set()) attachment = yield ManagedAttachment.load(self.theTransactionUnderTest(0), resourceID, managedID) self.assertTrue(attachment is None) yield self.commitTransaction(0) @inlineCallbacks def test_get_all_attachments(self): """ Test that action=get-all-attachments works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") attachments = yield shared_object.ownerHome().getAllAttachments() self.assertEqual(len(attachments), 1) self.assertTrue(isinstance(attachments[0], ManagedAttachment)) self.assertEqual(attachments[0].contentType(), MimeType.fromString("text/plain")) self.assertEqual(attachments[0].name(), "test.txt") yield self.commitTransaction(1) @inlineCallbacks def test_get_attachment_data(self): """ Test that action=get-all-attachments works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) remote_id = attachment.id() yield self.commitTransaction(0) home1 = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name="puser01") shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") attachment = yield ManagedAttachment._create(self.theTransactionUnderTest(1), None, home1.id()) attachment._contentType = MimeType.fromString("text/plain") attachment._name = "test.txt" yield shared_object.ownerHome().readAttachmentData(remote_id, attachment) yield self.commitTransaction(1) @inlineCallbacks def test_get_attachment_links(self): """ Test that action=get-attachment-links works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(0), home="user01", name="calendar") cobj1 = yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) calobjID = cobj1.id() yield self.commitTransaction(0) object1 = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(0), home="user01", calendar_name="calendar", name="1.ics") attachment, _ignore_location = yield object1.addAttachment(None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text.")) attID = attachment.id() managedID = attachment.managedID() yield self.commitTransaction(0) shared_object = yield self.calendarObjectUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", calendar_name="shared-calendar", name="1.ics") links = yield shared_object.ownerHome().getAttachmentLinks() self.assertEqual(len(links), 1) self.assertTrue(isinstance(links[0], AttachmentLink)) self.assertEqual(links[0]._attachmentID, attID) self.assertEqual(links[0]._managedID, managedID) self.assertEqual(links[0]._calendarObjectID, calobjID) yield self.commitTransaction(1)
def getMonthTable(month, year, weekstart, table, today_index): from pycalendar.datetime import DateTime # Get today today = DateTime.getToday(None) today_index = [-1, -1] # Start with empty table table = [] # Determine first weekday in month temp = DateTime(year, month, 1, 0) row = -1 initial_col = temp.getDayOfWeek() - weekstart if initial_col < 0: initial_col += 7 col = initial_col # Counters max_day = daysInMonth(month, year) # Fill up each row for day in range(1, max_day + 1): # Insert new row if we are at the start of a row if (col == 0) or (day == 1): table.extend([0] * 7) row += 1 # Set the table item to the current day table[row][col] = packDate(temp.getYear(), temp.getMonth(), day) # Check on today if (temp.getYear() == today.getYear()) and ( temp.getMonth() == today.getMonth()) and (day == today.getDay()): today_index = [row, col] # Bump column (modulo 7) col += 1 if (col > 6): col = 0 # Add next month to remainder temp.offsetMonth(1) if col != 0: day = 1 while col < 7: table[row][col] = packDate(temp.getYear(), temp.getMonth(), -day) # Check on today if (temp.getYear() == today.getYear()) and ( temp.getMonth() == today.getMonth()) and (day == today.getDay()): today_index = [row, col] day += 1 col += 1 # Add previous month to start temp.offsetMonth(-2) if (initial_col != 0): day = daysInMonth(temp.getMonth(), temp.getYear()) back_col = initial_col - 1 while (back_col >= 0): table[row][back_col] = packDate(temp.getYear(), temp.getMonth(), -day) # Check on today if (temp.getYear() == today.getYear()) and ( temp.getMonth() == today.getMonth()) and (day == today.getDay()): today_index = [0, back_col] back_col -= 1 day -= 1 return table, today_index
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, )) 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_record = (yield calresource.directoryService( ).recordWithCalendarUserAddress(test_organizer) ) if test_organizer else None test_uid = test_record.uid if test_record 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 > 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() test_record = ( yield calresource.principalForCalendarUserAddress( test_organizer)) if test_organizer else None test_uid = test_record.principalUID( ) if test_record 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 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 getMonthTable(month, year, weekstart, table, today_index): from pycalendar.datetime import DateTime # Get today today = DateTime.getToday(None) today_index = [-1, -1] # Start with empty table table = [] # Determine first weekday in month temp = DateTime(year, month, 1, 0) row = -1 initial_col = temp.getDayOfWeek() - weekstart if initial_col < 0: initial_col += 7 col = initial_col # Counters max_day = daysInMonth(month, year) # Fill up each row for day in range(1, max_day + 1): # Insert new row if we are at the start of a row if (col == 0) or (day == 1): table.extend([0] * 7) row += 1 # Set the table item to the current day table[row][col] = packDate(temp.getYear(), temp.getMonth(), day) # Check on today if (temp.getYear() == today.getYear()) and (temp.getMonth() == today.getMonth()) and (day == today.getDay()): today_index = [row, col] # Bump column (modulo 7) col += 1 if (col > 6): col = 0 # Add next month to remainder temp.offsetMonth(1) if col != 0: day = 1 while col < 7: table[row][col] = packDate(temp.getYear(), temp.getMonth(), -day) # Check on today if (temp.getYear() == today.getYear()) and (temp.getMonth() == today.getMonth()) and (day == today.getDay()): today_index = [row, col] day += 1 col += 1 # Add previous month to start temp.offsetMonth(-2) if (initial_col != 0): day = daysInMonth(temp.getMonth(), temp.getYear()) back_col = initial_col - 1 while(back_col >= 0): table[row][back_col] = packDate(temp.getYear(), temp.getMonth(), -day) # Check on today if (temp.getYear() == today.getYear()) and (temp.getMonth() == today.getMonth()) and (day == today.getDay()): today_index = [0, back_col] back_col -= 1 day -= 1 return table, today_index
def _matchCalendarResources(self, calresource): # Get the timezone property from the collection. tz = calresource.getTimezone() # Try cache aggregated_resources = (yield FBCacheEntry.getCacheEntry(calresource, self.attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None if aggregated_resources is None: if self.accountingItems is not None: self.accountingItems["fb-uncached"] = self.accountingItems.get("fb-uncached", 0) + 1 caching = False if config.EnableFreeBusyCache: # Log extended item if self.logItems is not None: self.logItems["fb-uncached"] = self.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(self.timerange.getStart(), cache_start) >= 0 and compareDateTime(self.timerange.getEnd(), cache_end) <= 0: cache_timerange = Period(cache_start, cache_end) 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 tr = TimeRange( start=(cache_timerange if caching else self.timerange).getStart().getText(), end=(cache_timerange if caching else self.timerange).getEnd().getText(), ) filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( tr, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", ) ) filter = Filter(filter) tzinfo = filter.settimezone(tz) if self.accountingItems is not None: self.accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),) try: resources = yield calresource.search(filter, useruid=self.attendee_uid, fbtype=True) aggregated_resources = {} for name, uid, comptype, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault((name, uid, comptype, test_organizer,), []).append(( float, tupleFromDateTime(parseSQLTimestampToPyCalendar(start)), tupleFromDateTime(parseSQLTimestampToPyCalendar(end)), fbtype, )) if caching: yield FBCacheEntry.makeCacheEntry(calresource, self.attendee_uid, cache_timerange, aggregated_resources) except IndexedSearchException: raise InternalDataStoreError("Invalid indexedSearch query") else: if self.accountingItems is not None: self.accountingItems["fb-cached"] = self.accountingItems.get("fb-cached", 0) + 1 # Log extended item if self.logItems is not None: self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone() if tz is not None else Timezone.UTCTimezone filter = None returnValue((aggregated_resources, tzinfo, filter,))
def doWork(self): # Delete all other work items for this event yield Delete( From=self.table, Where=self.group, ).on(self.transaction) # get db object calendarObject = yield CalendarStoreFeatures( self.transaction._store ).calendarObjectWithID( self.transaction, self.resourceID ) component = yield calendarObject.componentForUser() # Change a copy of the original, as we need the original cached on the resource # so we can do a diff to test implicit scheduling changes component = component.duplicate() # sync group attendees if (yield calendarObject.reconcileGroupAttendees(component)): # group attendees in event have changed if (component.masterComponent() is None or not component.isRecurring()): # skip non-recurring old events, no instances if ( yield calendarObject.removeOldEventGroupLink( component, instances=None, inserting=False, txn=self.transaction ) ): returnValue(None) else: # skip recurring old events expand = (DateTime.getToday() + Duration(days=config.FreeBusyIndexExpandAheadDays)) if config.FreeBusyIndexLowerLimitDays: truncateLowerLimit = DateTime.getToday() truncateLowerLimit.offsetDay(-config.FreeBusyIndexLowerLimitDays) else: truncateLowerLimit = None instances = component.expandTimeRanges( expand, lowerLimit=truncateLowerLimit, ignoreInvalidInstances=True ) if ( yield calendarObject.removeOldEventGroupLink( component, instances=instances, inserting=False, txn=self.transaction ) ): returnValue(None) # split spanning events and only update present-future split result splitter = iCalSplitter(0, 1) break_point = DateTime.getToday() - Duration(seconds=config.GroupAttendees.UpdateOldEventLimitSeconds) rid = splitter.whereSplit(component, break_point=break_point) if rid is not None: yield calendarObject.split(onlyThis=True, rid=rid) # remove group link to ensure update (update to unknown hash would work too) # FIXME: its possible that more than one group id gets updated during this single work item, so we # need to make sure that ALL the group_id's are removed by this query. ga = schema.GROUP_ATTENDEE yield Delete( From=ga, Where=(ga.RESOURCE_ID == self.resourceID).And( ga.GROUP_ID == self.groupID ) ).on(self.transaction) # update group attendee in remaining component component = yield calendarObject.componentForUser() component = component.duplicate() change = yield calendarObject.reconcileGroupAttendees(component) assert change yield calendarObject._setComponentInternal(component, False, ComponentUpdateState.SPLIT_OWNER) returnValue(None) yield calendarObject.setComponent(component)
def checkAttendeeAutoReply(self, calendar, automode): """ Check whether a reply to the given iTIP message is needed and if so make the appropriate changes to the calendar data. Changes are only made for the case where the PARTSTAT of the attendee is NEEDS-ACTION - i.e., any existing state is left unchanged. This allows, e.g., proxies to decline events that would otherwise have been auto-accepted and those stay declined as non-schedule-change updates are received. @param calendar: the iTIP message to process @type calendar: L{Component} @param automode: the auto-schedule mode for the recipient @type automode: L{txdav.who.idirectory.AutoScheduleMode} @return: C{tuple} of C{bool}, C{bool}, C{str} indicating whether changes were made, whether the inbox item should be added, and the new PARTSTAT. """ if accountingEnabled("AutoScheduling", self.recipient.record): accounting = { "when": DateTime.getNowUTC().getText(), "automode": automode.name, "changed": False, } else: accounting = None # First ignore the none mode if automode == AutoScheduleMode.none: returnValue((False, True, "", accounting,)) elif not automode: automode = { "none": AutoScheduleMode.none, "accept-always": AutoScheduleMode.accept, "decline-always": AutoScheduleMode.decline, "accept-if-free": AutoScheduleMode.acceptIfFree, "decline-if-busy": AutoScheduleMode.declineIfBusy, "automatic": AutoScheduleMode.acceptIfFreeDeclineIfBusy, }.get( config.Scheduling.Options.AutoSchedule.DefaultMode, AutoScheduleMode.acceptIfFreeDeclineIfBusy ) log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply with mode: %s" % (self.recipient.cuaddr, self.uid, automode.name,)) cuas = self.recipient.record.calendarUserAddresses # First expand current one to get instances (only go 1 year into the future) default_future_expansion_duration = Duration(days=config.Scheduling.Options.AutoSchedule.FutureFreeBusyDays) expand_max = DateTime.getToday() + default_future_expansion_duration instances = calendar.expandTimeRanges(expand_max, ignoreInvalidInstances=True) if accounting is not None: accounting["expand-max"] = expand_max.getText() accounting["instances"] = len(instances.instances) # We are going to ignore auto-accept processing for anything more than a day old (actually use -2 days # to add some slop to account for possible timezone offsets) min_date = DateTime.getToday() min_date.offsetDay(-2) allOld = True # Cache the current attendee partstat on the instance object for later use, and # also mark whether the instance time slot would be free for instance in instances.instances.itervalues(): attendee = instance.component.getAttendeeProperty(cuas) instance.partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") if attendee else None instance.free = True instance.active = (instance.end > min_date) if instance.active: allOld = False instances = sorted(instances.instances.values(), key=lambda x: x.rid) # If every instance is in the past we punt right here so we don't waste time on freebusy lookups etc. # There will be no auto-accept and no inbox item stored (so as not to waste storage on items that will # never be processed). if allOld: if accounting is not None: accounting["status"] = "all instances are old" returnValue((False, False, "", accounting,)) # Extract UID from primary component as we want to ignore this one if we match it # in any calendars. uid = calendar.resourceUID() # Now compare each instance time-range with the index and see if there is an overlap fbset = (yield self.recipient.inbox.ownerHome().loadCalendars()) fbset = [fbcalendar for fbcalendar in fbset if fbcalendar.isUsedForFreeBusy()] if accounting is not None: accounting["fbset"] = [testcal.name() for testcal in fbset] accounting["tr"] = [] for testcal in fbset: # Get the timezone property from the collection, and store in the query filter # for use during the query itself. tz = testcal.getTimezone() tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True) # Now do search for overlapping time-range and set instance.free based # on whether there is an overlap or not. # NB Do this in reverse order so that the date farthest in the future is tested first - that will # ensure that freebusy that far into the future is determined and will trigger time-range caching # and indexing out that far - and that will happen only once through this loop. for instance in reversed(instances): if instance.partstat == "NEEDS-ACTION" and instance.free and instance.active: try: # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE fbinfo = ([], [], []) def makeTimedUTC(dt): dt = dt.duplicate() if dt.isDateOnly(): dt.setDateOnly(False) dt.setHHMMSS(0, 0, 0) if dt.floating(): dt.setTimezone(tzinfo) dt.adjustToUTC() return dt tr = caldavxml.TimeRange( start=str(makeTimedUTC(instance.start)), end=str(makeTimedUTC(instance.end)), ) yield generateFreeBusyInfo(testcal, fbinfo, tr, 0, uid, servertoserver=True, accountingItems=accounting if len(instances) == 1 else None) # If any fbinfo entries exist we have an overlap if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]): instance.free = False if accounting is not None: accounting["tr"].insert(0, (tr.attributes["start"], tr.attributes["end"], instance.free,)) except QueryMaxResources: instance.free[instance] = False log.info("Exceeded number of matches whilst trying to find free-time.") if accounting is not None: accounting["problem"] = "Exceeded number of matches" # If everything is declined we can exit now if not any([instance.free for instance in instances]): break if accounting is not None: accounting["tr"] = accounting["tr"][:30] # Now adjust the instance.partstat currently set to "NEEDS-ACTION" to the # value determined by auto-accept logic based on instance.free state. However, # ignore any instance in the past - leave them as NEEDS-ACTION. partstat_counts = collections.defaultdict(int) for instance in instances: if instance.partstat == "NEEDS-ACTION" and instance.active: if automode == AutoScheduleMode.accept: freePartstat = busyPartstat = "ACCEPTED" elif automode == AutoScheduleMode.decline: freePartstat = busyPartstat = "DECLINED" else: freePartstat = "ACCEPTED" if automode in ( AutoScheduleMode.acceptIfFree, AutoScheduleMode.acceptIfFreeDeclineIfBusy, ) else "NEEDS-ACTION" busyPartstat = "DECLINED" if automode in ( AutoScheduleMode.declineIfBusy, AutoScheduleMode.acceptIfFreeDeclineIfBusy, ) else "NEEDS-ACTION" instance.partstat = freePartstat if instance.free else busyPartstat partstat_counts[instance.partstat] += 1 if len(partstat_counts) == 0: # Nothing to do if accounting is not None: accounting["status"] = "no partstat changes" returnValue((False, False, "", accounting,)) elif len(partstat_counts) == 1: # Do the simple case of all PARTSTATs the same separately # Extract the ATTENDEE property matching current recipient from the calendar data attendeeProps = calendar.getAttendeeProperties(cuas) if not attendeeProps: if accounting is not None: accounting["status"] = "no attendee to change" returnValue((False, False, "", accounting,)) made_changes = False partstat = partstat_counts.keys()[0] for component in calendar.subcomponents(): made_changes |= self.resetAttendeePartstat(component, cuas, partstat) store_inbox = partstat == "NEEDS-ACTION" if accounting is not None: accounting["status"] = "setting all partstats to {}".format(partstat) if made_changes else "all partstats correct" accounting["changed"] = made_changes else: # Hard case: some accepted, some declined, some needs-action # What we will do is mark any master instance as accepted, then mark each existing # overridden instance as accepted or declined, and generate new overridden instances for # any other declines. made_changes = False store_inbox = False partstat = "MIXED RESPONSE" # Default state is whichever of free or busy has most instances defaultPartStat = max(sorted(partstat_counts.items()), key=lambda x: x[1])[0] # See if there is a master component first hadMasterRsvp = False master = calendar.masterComponent() if master: attendee = master.getAttendeeProperty(cuas) if attendee: hadMasterRsvp = attendee.parameterValue("RSVP", "FALSE") == "TRUE" if defaultPartStat == "NEEDS-ACTION": store_inbox = True made_changes |= self.resetAttendeePartstat(master, cuas, defaultPartStat) # Look at expanded instances and change partstat accordingly for instance in instances: overridden = calendar.overriddenComponent(instance.rid) if not overridden and instance.partstat == defaultPartStat: # Nothing to do as state matches the master continue if overridden: # Change ATTENDEE property to match new state if instance.partstat == "NEEDS-ACTION" and instance.active: store_inbox = True made_changes |= self.resetAttendeePartstat(overridden, cuas, instance.partstat) else: # Derive a new overridden component and change partstat. We also need to make sure we restore any RSVP # value that may have been overwritten by any change to the master itself. derived = calendar.deriveInstance(instance.rid) if derived is not None: attendee = derived.getAttendeeProperty(cuas) if attendee: if instance.partstat == "NEEDS-ACTION" and instance.active: store_inbox = True self.resetAttendeePartstat(derived, cuas, instance.partstat, hadMasterRsvp) made_changes = True calendar.addComponent(derived) if accounting is not None: accounting["status"] = "mixed partstat changes" if made_changes else "mixed partstats correct" accounting["changed"] = made_changes # Fake a SCHEDULE-STATUS on the ORGANIZER property if made_changes: calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", iTIPRequestStatus.MESSAGE_DELIVERED_CODE, "ORGANIZER", None) returnValue((made_changes, store_inbox, partstat, accounting,))
""" Tests for txdav.caldav.datastore.utils """ from pycalendar.datetime import DateTime from twisted.internet.defer import inlineCallbacks from twisted.trial import unittest from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord, \ extractEmailDomain, uidFromCalendarUserAddress from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests now = DateTime.getToday().getYear() ORGANIZER_ICS = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Apple Inc.//iCal 4.0.1//EN CALSCALE:GREGORIAN BEGIN:VEVENT CREATED:20100303T181216Z UID:685BC3A1-195A-49B3-926D-388DDACA78A6 TRANSP:OPAQUE SUMMARY:Ancient event DTSTART:%(year)s0307T111500Z DURATION:PT1H DTSTAMP:20100303T181220Z ORGANIZER:urn:uuid:user01 ATTENDEE;PARTSTAT=ACCEPTED:urn:uuid:user01
uuid = arg else: raise NotImplementedError(opt) if args: cls.usage("Too many arguments: %s" % (args,)) if uuid is None: cls.usage("uuid must be specified") cls.uuid = uuid if dryrun: verbose = True cutoff = DateTime.getToday() cutoff.setDateOnly(False) cutoff.offsetDay(-days) cls.cutoff = cutoff cls.batchSize = batchSize cls.dryrun = dryrun cls.debug = debug utilityMain( configFileName, cls, verbose=verbose, ) @classmethod @inlineCallbacks
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, )) 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_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer)) if test_organizer else None test_uid = test_record.uid if test_record 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 > 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() test_record = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None test_uid = test_record.principalUID() if test_record 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 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 _matchCalendarResources(self, calresource): # Get the timezone property from the collection. tz = calresource.getTimezone() # Try cache aggregated_resources = (yield FBCacheEntry.getCacheEntry( calresource, self.attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None if aggregated_resources is None: if self.accountingItems is not None: self.accountingItems["fb-uncached"] = self.accountingItems.get( "fb-uncached", 0) + 1 caching = False if config.EnableFreeBusyCache: # Log extended item if self.logItems is not None: self.logItems["fb-uncached"] = self.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(self.timerange.getStart(), cache_start) >= 0 and compareDateTime( self.timerange.getEnd(), cache_end) <= 0: cache_timerange = Period(cache_start, cache_end) 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 tr = TimeRange( start=(cache_timerange if caching else self.timerange).getStart().getText(), end=(cache_timerange if caching else self.timerange).getEnd().getText(), ) filter = caldavxml.Filter( caldavxml.ComponentFilter( caldavxml.ComponentFilter( tr, name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"), ), name="VCALENDAR", )) filter = Filter(filter) tzinfo = filter.settimezone(tz) if self.accountingItems is not None: self.accountingItems["fb-query-timerange"] = ( str(tr.start), str(tr.end), ) try: resources = yield calresource.search(filter, useruid=self.attendee_uid, fbtype=True) aggregated_resources = {} for name, uid, comptype, test_organizer, float, start, end, fbtype, transp in resources: if transp == 'T' and fbtype != '?': fbtype = 'F' aggregated_resources.setdefault(( name, uid, comptype, test_organizer, ), []).append(( float, tupleFromDateTime( parseSQLTimestampToPyCalendar(start)), tupleFromDateTime(parseSQLTimestampToPyCalendar(end)), fbtype, )) if caching: yield FBCacheEntry.makeCacheEntry(calresource, self.attendee_uid, cache_timerange, aggregated_resources) except IndexedSearchException: raise InternalDataStoreError("Invalid indexedSearch query") else: if self.accountingItems is not None: self.accountingItems["fb-cached"] = self.accountingItems.get( "fb-cached", 0) + 1 # Log extended item if self.logItems is not None: self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1 # Determine appropriate timezone (UTC is the default) tzinfo = tz.gettimezone( ) if tz is not None else Timezone.UTCTimezone filter = None returnValue(( aggregated_resources, tzinfo, filter, ))
class TestCompleteMigrationCycle(MultiStoreConduitTest): """ Test that a full migration cycle using L{CrossPodHomeSync} works. """ def __init__(self, methodName='runTest'): super(TestCompleteMigrationCycle, self).__init__(methodName) self.stash = {} @inlineCallbacks def setUp(self): @inlineCallbacks def _fakeSubmitRequest(iself, ssl, host, port, request): pod = (port - 8008) / 100 inbox = IScheduleInboxResource(self.site.resource, self.theStoreUnderTest(pod), podding=True) response = yield inbox.http_POST( SimpleRequest( self.site, "POST", "http://{host}:{port}/podding".format(host=host, port=port), request.headers, request.stream.mem, )) returnValue(response) self.patch(IScheduleRequest, "_submitRequest", _fakeSubmitRequest) self.accounts = FilePath(__file__).sibling("accounts").child( "groupAccounts.xml") self.augments = FilePath(__file__).sibling("accounts").child( "augments.xml") yield super(TestCompleteMigrationCycle, self).setUp() yield self.populate() # Speed up work self.patch(MigrationCleanupWork, "notBeforeDelay", 1) self.patch(HomeCleanupWork, "notBeforeDelay", 1) self.patch(MigratedHomeCleanupWork, "notBeforeDelay", 1) def configure(self): super(TestCompleteMigrationCycle, self).configure() config.GroupAttendees.Enabled = True config.GroupAttendees.ReconciliationDelaySeconds = 0 config.GroupAttendees.AutoUpdateSecondsFromNow = 0 config.AccountingCategories.migration = True config.AccountingPrincipals = ["*"] @inlineCallbacks def populate(self): yield populateCalendarsFrom(self.requirements0, self.theStoreUnderTest(0)) yield populateCalendarsFrom(self.requirements1, self.theStoreUnderTest(1)) requirements0 = { "user01": None, "user02": None, "user03": None, "user04": None, "user05": None, "user06": None, "user07": None, "user08": None, "user09": None, "user10": None, } requirements1 = { "puser01": None, "puser02": None, "puser03": None, "puser04": None, "puser05": None, "puser06": None, "puser07": None, "puser08": None, "puser09": None, "puser10": None, } @inlineCallbacks def _createShare(self, shareFrom, shareTo, accept=True): # Invite txnindex = 1 if shareFrom[0] == "p" else 0 home = yield self.homeUnderTest( txn=self.theTransactionUnderTest(txnindex), name=shareFrom, create=True) calendar = yield home.childWithName("calendar") shareeView = yield calendar.inviteUIDToShare(shareTo, _BIND_MODE_READ, "summary") yield self.commitTransaction(txnindex) # Accept if accept: inviteUID = shareeView.shareUID() txnindex = 1 if shareTo[0] == "p" else 0 shareeHome = yield self.homeUnderTest( txn=self.theTransactionUnderTest(txnindex), name=shareTo) shareeView = yield shareeHome.acceptShare(inviteUID) sharedName = shareeView.name() yield self.commitTransaction(txnindex) else: sharedName = None returnValue(sharedName) def attachmentToString(self, attachment): """ Convenience to convert an L{IAttachment} to a string. @param attachment: an L{IAttachment} provider to convert into a string. @return: a L{Deferred} that fires with the contents of the attachment. @rtype: L{Deferred} firing C{bytes} """ capture = CaptureProtocol() attachment.retrieve(capture) return capture.deferred now = { "now": DateTime.getToday().getYear(), "now1": DateTime.getToday().getYear() + 1, } data01_1 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_data01_1 DTSTART:{now1:04d}0102T140000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:data01_1 END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) data01_1_changed = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_data01_1 DTSTART:{now1:04d}0102T140000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:data01_1_changed END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) data01_2 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_data01_2 DTSTART:{now1:04d}0102T160000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z SUMMARY:data01_2 ORGANIZER:mailto:[email protected] ATTENDEE:mailto:[email protected] ATTENDEE:mailto:[email protected] ATTENDEE:mailto:[email protected] END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) data01_3 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_data01_3 DTSTART:{now1:04d}0102T180000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z SUMMARY:data01_3 ORGANIZER:mailto:[email protected] ATTENDEE:mailto:[email protected] ATTENDEE:mailto:[email protected] END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) data02_1 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_data02_1 DTSTART:{now1:04d}0103T140000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:data02_1 END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) data02_2 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_data02_2 DTSTART:{now1:04d}0103T160000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z SUMMARY:data02_2 ORGANIZER:mailto:[email protected] ATTENDEE:mailto:[email protected] ATTENDEE:mailto:[email protected] ATTENDEE:mailto:[email protected] END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) data02_3 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_data02_3 DTSTART:{now1:04d}0103T180000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z SUMMARY:data02_3 ORGANIZER:mailto:[email protected] ATTENDEE:mailto:[email protected] ATTENDEE:mailto:[email protected] END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) datap02_1 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_datap02_1 DTSTART:{now1:04d}0103T140000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z RRULE:FREQ=WEEKLY SUMMARY:datap02_1 END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) datap02_2 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_datap02_2 DTSTART:{now1:04d}0103T160000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z SUMMARY:datap02_2 ORGANIZER:mailto:[email protected] ATTENDEE:mailto:[email protected] ATTENDEE:mailto:[email protected] END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) datap02_3 = """BEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:uid_datap02_3 DTSTART:{now1:04d}0103T180000Z DURATION:PT1H CREATED:20060102T190000Z DTSTAMP:20051222T210507Z SUMMARY:datap02_3 ORGANIZER:mailto:[email protected] ATTENDEE:mailto:[email protected] ATTENDEE:mailto:[email protected] END:VEVENT END:VCALENDAR """.replace("\n", "\r\n").format(**now) @inlineCallbacks def preCheck(self): """ Checks prior to starting any tests """ for i in range(self.numberOfStores): txn = self.theTransactionUnderTest(i) record = yield txn.directoryService().recordWithUID(u"user01") self.assertEqual(record.serviceNodeUID, "A") self.assertEqual(record.thisServer(), i == 0) record = yield txn.directoryService().recordWithUID(u"user02") self.assertEqual(record.serviceNodeUID, "A") self.assertEqual(record.thisServer(), i == 0) record = yield txn.directoryService().recordWithUID(u"puser02") self.assertEqual(record.serviceNodeUID, "B") self.assertEqual(record.thisServer(), i == 1) yield self.commitTransaction(i) @inlineCallbacks def initialState(self): """ Setup the server with an initial set of data user01 - migrating user user02 - has a calendar shared with user01 user03 - shared to by user01 puser01 - user on other pod puser02 - has a calendar shared with user01 puser03 - shared to by user01 """ # Data for user01 home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user01", create=True) self.stash["user01_pod0_home_id"] = home.id() calendar = yield home.childWithName("calendar") yield calendar.createCalendarObjectWithName( "01_1.ics", Component.fromString(self.data01_1)) yield calendar.createCalendarObjectWithName( "01_2.ics", Component.fromString(self.data01_2)) obj3 = yield calendar.createCalendarObjectWithName( "01_3.ics", Component.fromString(self.data01_3)) attachment, _ignore_location = yield obj3.addAttachment( None, MimeType.fromString("text/plain"), "test.txt", MemoryStream("Here is some text #1.")) self.stash["user01_attachment_id"] = attachment.id() self.stash["user01_attachment_md5"] = attachment.md5() self.stash["user01_attachment_mid"] = attachment.managedID() yield self.commitTransaction(0) # Data for user02 home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(0), name="user02", create=True) calendar = yield home.childWithName("calendar") yield calendar.createCalendarObjectWithName( "02_1.ics", Component.fromString(self.data02_1)) yield calendar.createCalendarObjectWithName( "02_2.ics", Component.fromString(self.data02_2)) yield calendar.createCalendarObjectWithName( "02_3.ics", Component.fromString(self.data02_3)) yield self.commitTransaction(0) # Data for puser02 home = yield self.homeUnderTest(txn=self.theTransactionUnderTest(1), name="puser02", create=True) calendar = yield home.childWithName("calendar") yield calendar.createCalendarObjectWithName( "p02_1.ics", Component.fromString(self.datap02_1)) yield calendar.createCalendarObjectWithName( "p02_2.ics", Component.fromString(self.datap02_2)) yield calendar.createCalendarObjectWithName( "p02_3.ics", Component.fromString(self.datap02_3)) yield self.commitTransaction(1) # Share calendars self.stash["sharename_user01_to_user03"] = yield self._createShare( "user01", "user03") self.stash["sharename_user01_to_puser03"] = yield self._createShare( "user01", "puser03") self.stash["sharename_user02_to_user01"] = yield self._createShare( "user02", "user01") self.stash["sharename_puser02_to_user01"] = yield self._createShare( "puser02", "user01") # Add some delegates txn = self.theTransactionUnderTest(0) record01 = yield txn.directoryService().recordWithUID(u"user01") record02 = yield txn.directoryService().recordWithUID(u"user02") record03 = yield txn.directoryService().recordWithUID(u"user03") precord01 = yield txn.directoryService().recordWithUID(u"puser01") group02 = yield txn.directoryService().recordWithUID(u"group02") group03 = yield txn.directoryService().recordWithUID(u"group03") # Add user02 and user03 as individual delegates yield Delegates.addDelegate(txn, record01, record02, True) yield Delegates.addDelegate(txn, record01, record03, False) yield Delegates.addDelegate(txn, record01, precord01, False) # Add group delegates yield Delegates.addDelegate(txn, record01, group02, True) yield Delegates.addDelegate(txn, record01, group03, False) # Add external delegates yield txn.assignExternalDelegates(u"user01", None, None, u"external1", u"external2") yield self.commitTransaction(0) yield self.waitAllEmpty() @inlineCallbacks def secondState(self): """ Setup the server with data changes appearing after the first sync """ txn = self.theTransactionUnderTest(0) obj = yield self.calendarObjectUnderTest(txn, name="01_1.ics", calendar_name="calendar", home="user01") yield obj.setComponent(self.data01_1_changed) obj = yield self.calendarObjectUnderTest(txn, name="02_2.ics", calendar_name="calendar", home="user02") attachment, _ignore_location = yield obj.addAttachment( None, MimeType.fromString("text/plain"), "test_02.txt", MemoryStream("Here is some text #02.")) self.stash["user02_attachment_id"] = attachment.id() self.stash["user02_attachment_md5"] = attachment.md5() self.stash["user02_attachment_mid"] = attachment.managedID() yield self.commitTransaction(0) yield self.waitAllEmpty() @inlineCallbacks def finalState(self): """ Setup the server with data changes appearing before the final sync """ txn = self.theTransactionUnderTest(1) obj = yield self.calendarObjectUnderTest(txn, name="p02_2.ics", calendar_name="calendar", home="puser02") attachment, _ignore_location = yield obj.addAttachment( None, MimeType.fromString("text/plain"), "test_p02.txt", MemoryStream("Here is some text #p02.")) self.stash["puser02_attachment_id"] = attachment.id() self.stash["puser02_attachment_mid"] = attachment.managedID() self.stash["puser02_attachment_md5"] = attachment.md5() yield self.commitTransaction(1) yield self.waitAllEmpty() @inlineCallbacks def switchAccounts(self): """ Switch the migrated user accounts to point to the new pod """ for i in range(self.numberOfStores): txn = self.theTransactionUnderTest(i) record = yield txn.directoryService().recordWithUID(u"user01") yield self.changeRecord( record, txn.directoryService().fieldName.serviceNodeUID, u"B", directory=txn.directoryService()) yield self.commitTransaction(i) for i in range(self.numberOfStores): txn = self.theTransactionUnderTest(i) record = yield txn.directoryService().recordWithUID(u"user01") self.assertEqual(record.serviceNodeUID, "B") self.assertEqual(record.thisServer(), i == 1) record = yield txn.directoryService().recordWithUID(u"user02") self.assertEqual(record.serviceNodeUID, "A") self.assertEqual(record.thisServer(), i == 0) record = yield txn.directoryService().recordWithUID(u"puser02") self.assertEqual(record.serviceNodeUID, "B") self.assertEqual(record.thisServer(), i == 1) yield self.commitTransaction(i) @inlineCallbacks def postCheck(self): """ Checks after migration is done """ # Check that the home has been moved home = yield self.homeUnderTest(self.theTransactionUnderTest(0), name="user01") self.assertTrue(home.external()) home = yield self.homeUnderTest(self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_NORMAL) self.assertTrue(home is None) home = yield self.homeUnderTest(self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_EXTERNAL) self.assertTrue(home is not None) home = yield self.homeUnderTest(self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_DISABLED) self.assertTrue(home is not None) home = yield self.homeUnderTest(self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_MIGRATING) self.assertTrue(home is None) yield self.commitTransaction(0) home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name="user01") self.assertTrue(home.normal()) home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_NORMAL) self.assertTrue(home is not None) home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_EXTERNAL) self.assertTrue(home is None) home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_DISABLED) self.assertTrue(home is not None) home = yield self.homeUnderTest(self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_MIGRATING) self.assertTrue(home is None) yield self.commitTransaction(1) # Check that the notifications have been moved notifications = yield self.notificationCollectionUnderTest( self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_NORMAL) self.assertTrue(notifications is None) notifications = yield self.notificationCollectionUnderTest( self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_EXTERNAL) self.assertTrue(notifications is None) notifications = yield self.notificationCollectionUnderTest( self.theTransactionUnderTest(0), name="user01", status=_HOME_STATUS_DISABLED) self.assertTrue(notifications is not None) yield self.commitTransaction(0) notifications = yield self.notificationCollectionUnderTest( self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_NORMAL) self.assertTrue(notifications is not None) notifications = yield self.notificationCollectionUnderTest( self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_EXTERNAL) self.assertTrue(notifications is None) notifications = yield self.notificationCollectionUnderTest( self.theTransactionUnderTest(1), name="user01", status=_HOME_STATUS_DISABLED) self.assertTrue(notifications is not None) yield self.commitTransaction(1) # New pod data homes = {} homes["user01"] = yield self.homeUnderTest( self.theTransactionUnderTest(1), name="user01") homes["user02"] = yield self.homeUnderTest( self.theTransactionUnderTest(1), name="user02") self.assertTrue(homes["user02"].external()) homes["user03"] = yield self.homeUnderTest( self.theTransactionUnderTest(1), name="user03") self.assertTrue(homes["user03"].external()) homes["puser01"] = yield self.homeUnderTest( self.theTransactionUnderTest(1), name="puser01") self.assertTrue(homes["puser01"].normal()) homes["puser02"] = yield self.homeUnderTest( self.theTransactionUnderTest(1), name="puser02") self.assertTrue(homes["puser02"].normal()) homes["puser03"] = yield self.homeUnderTest( self.theTransactionUnderTest(1), name="puser03") self.assertTrue(homes["puser03"].normal()) # Check calendar data on new pod calendars = yield homes["user01"].loadChildren() calnames = dict([(calendar.name(), calendar) for calendar in calendars]) self.assertEqual( set(calnames.keys()), set(( "calendar", "tasks", "inbox", self.stash["sharename_user02_to_user01"], self.stash["sharename_puser02_to_user01"], ))) # Check shared-by user01 on new pod shared = calnames["calendar"] invitations = yield shared.sharingInvites() by_sharee = dict([(invitation.shareeUID, invitation) for invitation in invitations]) self.assertEqual(len(invitations), 2) self.assertEqual(set(by_sharee.keys()), set(( "user03", "puser03", ))) self.assertEqual(by_sharee["user03"].shareeHomeID, homes["user03"].id()) self.assertEqual(by_sharee["puser03"].shareeHomeID, homes["puser03"].id()) # Check shared-to user01 on new pod shared = calnames[self.stash["sharename_user02_to_user01"]] self.assertEqual(shared.ownerHome().uid(), "user02") self.assertEqual(shared.ownerHome().id(), homes["user02"].id()) shared = calnames[self.stash["sharename_puser02_to_user01"]] self.assertEqual(shared.ownerHome().uid(), "puser02") self.assertEqual(shared.ownerHome().id(), homes["puser02"].id()) shared = yield homes["puser02"].calendarWithName("calendar") invitations = yield shared.sharingInvites() self.assertEqual(len(invitations), 1) self.assertEqual(invitations[0].shareeHomeID, homes["user01"].id()) yield self.commitTransaction(1) # Old pod data homes = {} homes["user01"] = yield self.homeUnderTest( self.theTransactionUnderTest(0), name="user01") homes["user02"] = yield self.homeUnderTest( self.theTransactionUnderTest(0), name="user02") self.assertTrue(homes["user02"].normal()) homes["user03"] = yield self.homeUnderTest( self.theTransactionUnderTest(0), name="user03") self.assertTrue(homes["user03"].normal()) homes["puser01"] = yield self.homeUnderTest( self.theTransactionUnderTest(0), name="puser01") self.assertTrue(homes["puser01"] is None) homes["puser02"] = yield self.homeUnderTest( self.theTransactionUnderTest(0), name="puser02") self.assertTrue(homes["puser02"].external()) homes["puser03"] = yield self.homeUnderTest( self.theTransactionUnderTest(0), name="puser03") self.assertTrue(homes["puser03"].external()) # Check shared-by user01 on old pod shared = yield homes["user03"].calendarWithName( self.stash["sharename_user01_to_user03"]) self.assertEqual(shared.ownerHome().uid(), "user01") self.assertEqual(shared.ownerHome().id(), homes["user01"].id()) # Check shared-to user01 on old pod shared = yield homes["user02"].calendarWithName("calendar") invitations = yield shared.sharingInvites() self.assertEqual(len(invitations), 1) self.assertEqual(invitations[0].shareeHomeID, homes["user01"].id()) yield self.commitTransaction(0) # Delegates on each pod for pod in range(self.numberOfStores): txn = self.theTransactionUnderTest(pod) records = {} for ctr in range(10): uid = u"user{:02d}".format(ctr + 1) records[uid] = yield txn.directoryService().recordWithUID(uid) for ctr in range(10): uid = u"puser{:02d}".format(ctr + 1) records[uid] = yield txn.directoryService().recordWithUID(uid) for ctr in range(10): uid = u"group{:02d}".format(ctr + 1) records[uid] = yield txn.directoryService().recordWithUID(uid) delegates = yield Delegates.delegatesOf(txn, records["user01"], True, False) self.assertTrue(records["user02"] in delegates) self.assertTrue(records["group02"] in delegates) delegates = yield Delegates.delegatesOf(txn, records["user01"], True, True) self.assertTrue(records["user02"] in delegates) self.assertTrue(records["user06"] in delegates) self.assertTrue(records["user07"] in delegates) self.assertTrue(records["user08"] in delegates) delegates = yield Delegates.delegatesOf(txn, records["user01"], False, False) self.assertTrue(records["user03"] in delegates) self.assertTrue(records["group03"] in delegates) self.assertTrue(records["puser01"] in delegates) delegates = yield Delegates.delegatesOf(txn, records["user01"], False, True) self.assertTrue(records["user03"] in delegates) self.assertTrue(records["user07"] in delegates) self.assertTrue(records["user08"] in delegates) self.assertTrue(records["user09"] in delegates) self.assertTrue(records["puser01"] in delegates) # Attachments obj = yield self.calendarObjectUnderTest( txn=self.theTransactionUnderTest(1), name="01_3.ics", calendar_name="calendar", home="user01") attachment = yield obj.attachmentWithManagedID( self.stash["user01_attachment_mid"]) self.assertTrue(attachment is not None) self.assertEqual(attachment.md5(), self.stash["user01_attachment_md5"]) data = yield self.attachmentToString(attachment) self.assertEqual(data, "Here is some text #1.") # Check removal of data from new pod # Make sure all jobs are done yield JobItem.waitEmpty( self.theStoreUnderTest(1).newTransaction, reactor, 60) # No migration state data left txn = self.theTransactionUnderTest(1) for migrationType in ( CalendarMigrationRecord, CalendarObjectMigrationRecord, AttachmentMigrationRecord, ): records = yield migrationType.all(txn) self.assertEqual(len(records), 0, msg=migrationType.__name__) yield self.commitTransaction(1) # No homes txn = self.theTransactionUnderTest(1) oldhome = yield txn.calendarHomeWithUID("user01", status=_HOME_STATUS_DISABLED) self.assertTrue(oldhome is None) oldhome = yield txn.notificationsWithUID("user01", status=_HOME_STATUS_DISABLED) self.assertTrue(oldhome is None) # Check removal of data from old pod # Make sure all jobs are done yield JobItem.waitEmpty( self.theStoreUnderTest(0).newTransaction, reactor, 60) # No homes txn = self.theTransactionUnderTest(0) oldhome = yield txn.calendarHomeWithUID("user01", status=_HOME_STATUS_DISABLED) self.assertTrue(oldhome is None) oldhome = yield txn.notificationsWithUID("user01", status=_HOME_STATUS_DISABLED) self.assertTrue(oldhome is None) # No delegates for delegateType in (DelegateRecord, DelegateGroupsRecord, ExternalDelegateGroupsRecord): records = yield delegateType.query( txn, delegateType.delegator == "user01") self.assertEqual(len(records), 0, msg=delegateType.__name__) # No work items for workType in allScheduleWork: records = yield workType.query( txn, workType.homeResourceID == self.stash["user01_pod0_home_id"]) self.assertEqual(len(records), 0, msg=workType.__name__) @inlineCallbacks def test_migration(self): """ Full migration cycle. """ yield self.preCheck() # Step 1. Live full sync yield self.initialState() syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01") yield syncer.sync() # Step 2. Live incremental sync yield self.secondState() syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01") yield syncer.sync() # Step 3. Disable home after final changes yield self.finalState() syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01") yield syncer.disableRemoteHome() # Step 4. Final incremental sync syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01", final=True) yield syncer.sync() # Step 5. Final reconcile sync syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01", final=True) yield syncer.finalSync() # Step 6. Enable new home syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01", final=True) yield syncer.enableLocalHome() # Step 7. Remove old home syncer = CrossPodHomeSync(self.theStoreUnderTest(1), "user01", final=True) yield syncer.removeRemoteHome() yield self.switchAccounts() yield self.postCheck()