Title: [commits] (jeffrey) [11171] - Implement bug 5953, add "subscribe to freebusy report" option
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)

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

Reply via email to