- Revision
- 11171
- Author
- jeffrey
- Date
- 2006-07-14 15:19:08 -0700 (Fri, 14 Jul 2006)
Log Message
- Implement bug 5953, add "subscribe to freebusy report" option
when subscribing to Cosmo collections (or other CalDAV servers)
when subscribing to Cosmo collections (or other CalDAV servers)
Modified Paths
Diff
Modified: trunk/chandler/application/dialogs/SubscribeCollection.py (11170 => 11171)
--- trunk/chandler/application/dialogs/SubscribeCollection.py 2006-07-14 21:17:45 UTC (rev 11170) +++ trunk/chandler/application/dialogs/SubscribeCollection.py 2006-07-14 22:19:08 UTC (rev 11171) @@ -71,6 +71,8 @@ self.textUsername = wx.xrc.XRCCTRL(self, "TEXT_USERNAME") self.textPassword = wx.xrc.XRCCTRL(self, "TEXT_PASSWORD") self.checkboxKeepOut = wx.xrc.XRCCTRL(self, "CHECKBOX_KEEPOUT") + self.forceFreeBusy = wx.xrc.XRCCTRL(self, "CHECKBOX_FORCEFREEBUSY") + self.subscribeButton = wx.xrc.XRCCTRL(self, "wxID_OK") self.Bind(wx.EVT_BUTTON, self.OnSubscribe, id=wx.ID_OK) @@ -154,6 +156,8 @@ username = None password = None + forceFreeBusy = self.forceFreeBusy.GetValue() + self.subscribeButton.Enable(False) self.gauge.SetValue(0) self.subscribing = True @@ -163,11 +167,12 @@ class ShareTask(task.Task): - def __init__(task, view, url, username, password): + def __init__(task, view, url, username, password, forceFreeBusy): super(ShareTask, task).__init__(view) task.url = "" task.username = username task.password = password + task.forceFreeBusy = forceFreeBusy def error(task, err): self._shareError(err) @@ -185,13 +190,15 @@ collection = sharing.subscribe(task.view, task.url, updateCallback=task._updateCallback, - username=task.username, password=task.password) + username=task.username, password=task.password, + forceFreeBusy=task.forceFreeBusy) return collection.itsUUID self.view.commit() self.taskView = viewpool.getView(self.view.repository) - self.currentTask = ShareTask(self.taskView, url, username, password) + self.currentTask = ShareTask(self.taskView, url, username, password, + forceFreeBusy) self.currentTask.start(inOwnThread=True)
Modified: trunk/chandler/application/dialogs/SubscribeCollection_wdr.xrc (11170 => 11171)
--- trunk/chandler/application/dialogs/SubscribeCollection_wdr.xrc 2006-07-14 21:17:45 UTC (rev 11170) +++ trunk/chandler/application/dialogs/SubscribeCollection_wdr.xrc 2006-07-14 22:19:08 UTC (rev 11171) @@ -36,6 +36,13 @@ </object> </object> <object class="sizeritem"> + <flag>wxALIGN_CENTER_VERTICAL|wxALL</flag> + <border>5</border> + <object class="wxCheckBox" name="CHECKBOX_FORCEFREEBUSY"> + <label>Subscribe as freebusy</label> + </object> + </object> + <object class="sizeritem"> <flag>wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL</flag> <border>5</border> <object class="wxBoxSizer">
Modified: trunk/chandler/parcels/osaf/framework/blocks/calendar/CalendarCanvas.py (11170 => 11171)
--- trunk/chandler/parcels/osaf/framework/blocks/calendar/CalendarCanvas.py 2006-07-14 21:17:45 UTC (rev 11170) +++ trunk/chandler/parcels/osaf/framework/blocks/calendar/CalendarCanvas.py 2006-07-14 22:19:08 UTC (rev 11171) @@ -32,7 +32,7 @@ from osaf.usercollections import UserCollection from application.dialogs import RecurrenceDialog, Util, TimeZoneList -from osaf.sharing import ChooseFormat +from osaf.sharing import ChooseFormat, Sharing from osaf.framework.blocks import ( DragAndDrop, Block, SplitterWindow, Styles, BoxContainer, BlockEvent @@ -860,7 +860,31 @@ Convenience method for changing between day and week mode. """ self.postEventByName ('DayMode', {'dayMode': dayMode, 'newDay' : newDay}) + + def getFreeBusyCollections(self): + """ + Convenience method, returns any selected or overlaid collections + whose conduit is a CalDAVFreeBusyConduit. + """ + hits = [] + try: + collections = getattr(self.contents, 'collectionList', + [self.contents]) + except AttributeError: + # sometimes self.contents hasn't been set yet. That's fine. + return hits + + for collection in collections: + shares = getattr(collection, 'shares', []) + for share in shares: + if isinstance(share.conduit, Sharing.CalDAVFreeBusyConduit): + hits.append(collection) + break + + return hits + + # Managing the date range def setRange(self, date): @@ -877,6 +901,7 @@ if self.dayMode: self.rangeStart = date + number = 1 else: calendar = GregorianCalendar() calendar.setTimeZone(ICUtzinfo.default.timezone) @@ -884,8 +909,18 @@ delta = timedelta(days=(calendar.get(calendar.DAY_OF_WEEK) - calendar.getFirstDayOfWeek())) self.rangeStart = date - delta + number = 7 + + # get an extra day on either side of the displayed range, because + # timezone displayed could be earlier or later than UTC + fb_date = self.rangeStart.date() - timedelta(1) + dates = [fb_date + n * timedelta(1) for n in range(number + 2)] + + for col in self.getFreeBusyCollections(): + annotation = Sharing.FreeBusyAnnotation(col) + for date in dates: + annotation.addDateNeeded(self.itsView, date) - def incrementRange(self): """ Increments the calendar's current range.
Modified: trunk/chandler/parcels/osaf/sharing/Sharing.py (11170 => 11171)
--- trunk/chandler/parcels/osaf/sharing/Sharing.py 2006-07-14 21:17:45 UTC (rev 11170) +++ trunk/chandler/parcels/osaf/sharing/Sharing.py 2006-07-14 22:19:08 UTC (rev 11171) @@ -29,6 +29,7 @@ from PyICU import ICUtzinfo import M2Crypto.BIO, WebDAV, twisted.web.http, zanshin.webdav, wx from cStringIO import StringIO +import bisect logger = logging.getLogger(__name__) @@ -36,6 +37,7 @@ 'AlreadyExists', 'AlreadySubscribed', 'CalDAVConduit', + 'CalDAVFreeBusyConduit', 'CloudXMLFormat', 'CouldNotConnect', 'FileSystemConduit', @@ -1929,8 +1931,174 @@ return result +MINIMUM_FREEBUSY_UPDATE_FREQUENCY = datetime.timedelta(hours=1) +MERGE_GAP_DAYS = 3 + +utc = ICUtzinfo.getInstance('UTC') +class FreeBusyAnnotation(schema.Annotation): + schema.kindInfo(annotates=pim.ContentCollection) + update_needed = schema.Sequence() + recently_updated = schema.Sequence() + + def addDateNeeded(self, view, needed_date, force_update = False): + """ + Check for recently updated dates, if it was updated more than + MINIMUM_FREEBUSY_UPDATE_FREQUENCY in the past (or force_update is True) + move it to update_needed. + + Next, check if that date has already been requested. If no existing + update is found, create a new one. + + Return True if an update is created or changed, False otherwise. + + """ + # need to think about what happens when bgsync changes get merged + # with the UI view when shuffling FreeBusyUpdates about + + # test if the date's in recently_updated, then check in update_needed + for update in getattr(self, 'recently_updated', []): + if update.date == needed_date: + if force_update or \ + update.last_update + MINIMUM_FREEBUSY_UPDATE_FREQUENCY < \ + datetime.datetime.now(utc): + update.needed_for = self + return True + else: + # nothing to do + return False + + for update in getattr(self, 'update_needed', []): + if update.date == needed_date: + return False + + # no existing update items for needed_date, create one + FreeBusyUpdate(itsView = view, date = needed_date, + needed_for = self.itsItem) + return True + + def dateUpdated(self, updated_date): + update_found = False + # this is inefficient when processing, say, 60 days have been updated, + # with difficulty I convinced myself to avoid premature optimization + for update in getattr(self, 'recently_updated', []): + if update.date == updated_date: + update.last_update = datetime.datetime.now(utc) + if getattr(update, 'needed_for', False): + del update.needed_for + update_found = True + break + for update in getattr(self, 'update_needed', []): + if update.date == updated_date: + if update_found: + # redundant update request created by a different view + update.delete() + else: + del update.needed_for + update.updated_for = self.itsItem + update.last_update = datetime.datetime.now(utc) + return + + def cleanUpdates(self): + for update in getattr(self, 'recently_updated', []): + if update.last_update + MINIMUM_FREEBUSY_UPDATE_FREQUENCY < \ + datetime.datetime.now(utc) and \ + getattr(update, 'needed_for', False): + update.delete() +class FreeBusyUpdate(schema.Item): + """ + A FreeBusyUpdate item can be a request to update a particular date, or a + record of a recent update received. Items are used instead of a simple + dictionary so the background sync view can merge changes from the UI view, + because changes to a repository Dictionary don't merge smoothly. + + """ + date = schema.One(schema.Date) + last_update = schema.One(schema.DateTime) + needed_for = schema.One(FreeBusyAnnotation, + inverse=FreeBusyAnnotation.update_needed) + updated_for = schema.One(FreeBusyAnnotation, + inverse=FreeBusyAnnotation.recently_updated) + + +class CalDAVFreeBusyConduit(CalDAVConduit): + """A read-only conduit, using the results of a free-busy report for get()""" + + def _getFreeBusy(self, resource, start, end): + serverHandle = self._getServerHandle() + response = serverHandle.blockUntil(resource.getFreebusy, start, end, depth=1) + # quick hack to temporarily handle Cosmo's multistatus response + return response.body + + def exists(self): + # this should probably do something nicer + return True + + def _get(self, contentView, *args, **kwargs): + + if self.share.contents is None: + self.share.contents = pim.SmartCollection(itsView=self.itsView) + updates = FreeBusyAnnotation(self.share.contents) + updates.cleanUpdates() + + + + yesterday = datetime.date.today() - oneday + for i in xrange(29): + updates.addDateNeeded(self.itsView, yesterday + i * oneday) + + needed_dates = [] + date_ranges = [] # a list of (date, number_of_days) tuples + for update in getattr(updates, 'update_needed', []): + bisect.insort(needed_dates, update.date) + + if len(needed_dates) > 0: + start_date = working_date = needed_dates[0] + for date in needed_dates: + if date - working_date > oneday * MERGE_GAP_DAYS: + days = (working_date - start_date).days + date_ranges.append( (start_date, days) ) + start_date = working_date = date + else: + working_date = date + + days = (working_date - start_date).days + date_ranges.append( (start_date, days) ) + + # prepare resource, add security context + resource = self._resourceFromPath(u"") + if getattr(self, 'ticketReadOnly', False): + resource.ticketId = self.ticketReadOnly + + zero_utc = datetime.time(0, tzinfo = utc) + for period_start, days in date_ranges: + start = datetime.datetime.combine(period_start, zero_utc) + end = datetime.datetime.combine(period_start + (days + 1) * oneday, + zero_utc) + + text = self._getFreeBusy(resource, start, end) + self.share.format.importProcess(contentView, text, item=self.share) + + for i in xrange(days + 1): + updates.dateUpdated(period_start + i * oneday) + + # a stats data structure appears to be required + stats = { + 'share' : self.share.itsUUID, + 'op' : 'get', + 'added' : [], + 'modified' : [], + 'removed' : [] + } + + return stats + + def get(self): + self._get() + + + class SimpleHTTPConduit(WebDAVConduit): """ Useful for get-only subscriptions of remote .ics files
Modified: trunk/chandler/parcels/osaf/sharing/__init__.py (11170 => 11171)
--- trunk/chandler/parcels/osaf/sharing/__init__.py 2006-07-14 21:17:45 UTC (rev 11170) +++ trunk/chandler/parcels/osaf/sharing/__init__.py 2006-07-14 22:19:08 UTC (rev 11171) @@ -31,6 +31,7 @@ from application.Utility import getDesktopDir from osaf import pim from i18n import OSAFMessageFactory as _ +import vobject from repository.item.Monitors import Monitors from repository.item.Item import Item @@ -690,7 +691,8 @@ deleteShare(share) -def subscribe(view, url, updateCallback=None, username=None, password=None): +def subscribe(view, url, updateCallback=None, username=None, password=None, + forceFreeBusy=False): if updateCallback: progressMonitor = ProgressMonitor(0, updateCallback) @@ -830,7 +832,42 @@ logger.exception("Failed to subscribe to %s", url) share.delete(True) raise - + + elif forceFreeBusy: + share = Share(itsView=view) + share.format = FreeBusyFileFormat(itsParent=share) + share.conduit = CalDAVFreeBusyConduit(itsParent=share, + host=host, + port=port, + useSSL=useSSL, + shareName=shareName, + sharePath=parentPath, + account=account) + if ticket: + share.conduit.ticketReadOnly = ticket + share.mode = "get" + share.filterClasses = \ + ["osaf.pim.calendar.Calendar.CalendarEventMixin"] + + if updateCallback: + updateCallback(msg=_(u"Subscribing to freebusy...")) + + try: + share.get(updateCallback=callback) + + try: + share.contents.shares.append(share, 'main') + except ValueError: + # There is already a 'main' share for this collection + share.contents.shares.append(share) + + return share.contents + + except Exception, err: + logger.exception("Failed to subscribe to %s", url) + share.delete(True) + raise + if updateCallback: updateCallback(msg=_(u"Detecting share settings..."))
_______________________________________________ Commits mailing list [email protected] http://lists.osafoundation.org/mailman/listinfo/commits
