def test_one_event(self): """ Test when the calendar is empty. """ data = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:1234-5678 DTSTAMP:20080601T000000Z DTSTART:%s DTEND:%s END:VEVENT END:VCALENDAR """ % (self.now_12H.getText(), self.now_13H.getText(),) yield self._createCalendarObject(data, "user01", "test.ics") calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1")) fbinfo = [[], [], [], ] matchtotal = 0 timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText()) result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal)) self.assertEqual(result, 1) self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ]) self.assertEqual(len(fbinfo[1]), 0) self.assertEqual(len(fbinfo[2]), 0)
def recv_freebusy(self, txn, request): """ Process a freebusy cross-pod request. Message arguments as per L{send_freebusy}. @param request: request arguments @type request: C{dict} """ # Operate on the L{CommonHomeChild} calresource, _ignore = yield self._getStoreObjectForRequest(txn, request) fbinfo = [[], [], []] matchtotal = yield generateFreeBusyInfo( calresource, fbinfo, TimeRange(start=request["timerange"][0], end=request["timerange"][1]), request["matchtotal"], request["excludeuid"], request["organizer"], request["organizerPrincipal"], request["same_calendar_user"], request["servertoserver"], request["event_details"], logItems=None ) # Convert L{DateTime} objects to text for JSON response for i in range(3): for j in range(len(fbinfo[i])): fbinfo[i][j] = fbinfo[i][j].getText() returnValue({ "fbresults": fbinfo, "matchtotal": matchtotal, })
def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None): # Find the current recipients calendars that are not transparent fbset = (yield recipient.inbox.ownerHome().loadCalendars()) fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()] # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE fbinfo = ([], [], []) # Process the availability property from the Inbox. availability = recipient.inbox.ownerHome().getAvailability() if availability is not None: processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange) # Check to see if the recipient is the same calendar user as the organizer. # Needed for masked UID stuff. if isinstance(self.scheduler.organizer, LocalCalendarUser): same_calendar_user = self.scheduler.organizer.record.uid == recipient.record.uid else: same_calendar_user = False # Now process free-busy set calendars matchtotal = 0 for calendar in fbset: matchtotal = (yield generateFreeBusyInfo( calendar, fbinfo, self.scheduler.timeRange, matchtotal, excludeuid=self.scheduler.excludeUID, organizer=self.scheduler.organizer.cuaddr, organizerPrincipal=organizerPrincipal, same_calendar_user=same_calendar_user, servertoserver=remote, event_details=event_details, logItems=self.scheduler.logItems, )) # Build VFREEBUSY iTIP reply for this recipient fbresult = buildFreeBusyResult( fbinfo, self.scheduler.timeRange, organizer=organizerProp, attendee=attendeeProp, uid=uid, method="REPLY", event_details=event_details, ) returnValue(fbresult)
def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None): # Find the current recipients calendars that are not transparent fbset = (yield recipient.inbox.ownerHome().loadCalendars()) fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()] # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE fbinfo = ([], [], []) # Process the availability property from the Inbox. availability = recipient.inbox.ownerHome().getAvailability() if availability is not None: processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange) # Check to see if the recipient is the same calendar user as the organizer. # Needed for masked UID stuff. if isinstance(self.scheduler.organizer, LocalCalendarUser): same_calendar_user = self.scheduler.organizer.principal.uid == recipient.principal.uid else: same_calendar_user = False # Now process free-busy set calendars matchtotal = 0 for calendar in fbset: matchtotal = (yield generateFreeBusyInfo( calendar, fbinfo, self.scheduler.timeRange, matchtotal, excludeuid=self.scheduler.excludeUID, organizer=self.scheduler.organizer.cuaddr, organizerPrincipal=organizerPrincipal, same_calendar_user=same_calendar_user, servertoserver=remote, event_details=event_details, logItems=self.scheduler.logItems, )) # Build VFREEBUSY iTIP reply for this recipient fbresult = buildFreeBusyResult( fbinfo, self.scheduler.timeRange, organizer=organizerProp, attendee=attendeeProp, uid=uid, method="REPLY", event_details=event_details, ) returnValue(fbresult)
def test_no_events(self): """ Test when the calendar is empty. """ calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1")) fbinfo = [[], [], [], ] matchtotal = 0 timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText()) result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal)) self.assertEqual(result, 0) self.assertEqual(len(fbinfo[0]), 0) self.assertEqual(len(fbinfo[1]), 0) self.assertEqual(len(fbinfo[2]), 0)
def recv_freebusy(self, txn, message): """ Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}. @param message: message arguments @type message: C{dict} """ shareeView, _ignore_objectResource = yield self._recv( txn, message, "freebusy") try: # Operate on the L{CommonHomeChild} fbinfo = [[], [], []] matchtotal = yield generateFreeBusyInfo( shareeView, fbinfo, TimeRange(start=message["timerange"][0], end=message["timerange"][1]), message["matchtotal"], message["excludeuid"], message["organizer"], message["organizerPrincipal"], message["same_calendar_user"], message["servertoserver"], message["event_details"], logItems=None) except Exception as e: returnValue({ "result": "exception", "class": ".".join(( e.__class__.__module__, e.__class__.__name__, )), "message": str(e), }) for i in range(3): for j in range(len(fbinfo[i])): fbinfo[i][j] = fbinfo[i][j].getText() returnValue({ "result": "ok", "fbresults": fbinfo, "matchtotal": matchtotal, })
def recv_freebusy(self, txn, message): """ Process a freebusy cross-pod message. Message arguments as per L{send_freebusy}. @param message: message arguments @type message: C{dict} """ shareeView, _ignore_objectResource = yield self._recv(txn, message, "freebusy") try: # Operate on the L{CommonHomeChild} fbinfo = [[], [], []] matchtotal = yield generateFreeBusyInfo( shareeView, fbinfo, TimeRange(start=message["timerange"][0], end=message["timerange"][1]), message["matchtotal"], message["excludeuid"], message["organizer"], message["organizerPrincipal"], message["same_calendar_user"], message["servertoserver"], message["event_details"], logItems=None ) except Exception as e: returnValue({ "result": "exception", "class": ".".join((e.__class__.__module__, e.__class__.__name__,)), "message": str(e), }) for i in range(3): for j in range(len(fbinfo[i])): fbinfo[i][j] = fbinfo[i][j].getText() returnValue({ "result": "ok", "fbresults": fbinfo, "matchtotal": matchtotal, })
def test_one_event_event_details(self): """ Test when the calendar is empty. """ data = """BEGIN:VCALENDAR VERSION:2.0 PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN BEGIN:VEVENT UID:1234-5678 DTSTAMP:20080601T000000Z DTSTART:%s DTEND:%s END:VEVENT END:VCALENDAR """ % (self.now_12H.getText(), self.now_13H.getText(),) yield self._createCalendarObject(data, "user01", "test.ics") calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1")) fbinfo = [[], [], [], ] matchtotal = 0 timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText()) event_details = [] result = (yield generateFreeBusyInfo( calendar, fbinfo, timerange, matchtotal, organizer="mailto:[email protected]", event_details=event_details )) self.assertEqual(result, 1) self.assertEqual(fbinfo[0], [PyCalendarPeriod.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ]) self.assertEqual(len(fbinfo[1]), 0) self.assertEqual(len(fbinfo[2]), 0) self.assertEqual(len(event_details), 1) self.assertEqual(str(event_details[0]), str(tuple(Component.fromString(data).subcomponents())[0]))
def test_freebusy(self): """ Test that action=component works. """ yield self.createShare("user01", "puser01") calendar1 = yield self.calendarUnderTest(home="user01", name="calendar") yield calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1)) yield self.commit() fbstart = "{now:04d}0102T000000Z".format(**self.nowYear) fbend = "{now:04d}0103T000000Z".format(**self.nowYear) shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar") fbinfo = [[], [], []] matchtotal = yield generateFreeBusyInfo( shared, fbinfo, TimeRange(start=fbstart, end=fbend), 0, excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False, servertoserver=False, event_details=False, logItems=None ) 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.otherCommit()
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,))