conduit r1534 - in trunk: . conduit conduit/dataproviders conduit/gtkui conduit/modules conduit/modules/FileModule test/python-tests



Author: johncarr
Date: Wed Jul  9 16:07:11 2008
New Revision: 1534
URL: http://svn.gnome.org/viewvc/conduit?rev=1534&view=rev

Log:
Merge in branch from nzjrs

Added:
   trunk/   (props changed)
      - copied from r1532, /trunk/
   trunk/test/python-tests/TestCoreConflict.py
Modified:
   trunk/ChangeLog
   trunk/conduit/Conduit.py
   trunk/conduit/Conflict.py
   trunk/conduit/Synchronization.py
   trunk/conduit/dataproviders/DataProvider.py
   trunk/conduit/dataproviders/SimpleFactory.py
   trunk/conduit/dataproviders/VolumeFactory.py
   trunk/conduit/gtkui/ConflictResolver.py
   trunk/conduit/gtkui/UI.py
   trunk/conduit/modules/FileModule/FileConfiguration.py
   trunk/conduit/modules/FileModule/FileModule.py
   trunk/conduit/modules/FileModule/config.glade
   trunk/conduit/modules/TestModule.py

Modified: trunk/conduit/Conduit.py
==============================================================================
--- /trunk/conduit/Conduit.py	(original)
+++ trunk/conduit/Conduit.py	Wed Jul  9 16:07:11 2008
@@ -85,12 +85,14 @@
         self.autoSyncEnabled = False
         self.conflictPolicy = ""
         self.deletedPolicy = ""
-        
+
         #set conduits to have the default conflict/deleted policy
         for policyName in CONFLICT_POLICY_NAMES:
             policyValue = conduit.GLOBALS.settings.get("default_policy_%s" % policyName)
             self.set_policy(policyName,policyValue)
 
+        self._conflicts = {}
+
     def _parameters_changed(self):
         self.emit("parameters-changed")
         
@@ -297,7 +299,7 @@
                     )
         if newDpw.module != None:
             newDpw.module.connect("change-detected", self._change_detected)
-        
+
         self.emit("dataprovider-changed", oldDpw, newDpw) 
 
     def refresh_dataprovider(self, dp, block=False):
@@ -324,4 +326,15 @@
         else:
             log.info("Conduit must have a datasource and a datasink")
 
-
+    def emit_conflict(self, conflict):
+        hc = hash(conflict)
+        if hc not in self._conflicts:
+            self._conflicts[hc] = conflict
+            self.emit("sync-conflict", conflict)
+
+    def resolved_conflict(self, conflict):
+        try:
+            hc = hash(conflict)
+            del(self._conflicts[hc])
+        except KeyError:
+            log.warn("Unknown conflict")

Modified: trunk/conduit/Conflict.py
==============================================================================
--- /trunk/conduit/Conflict.py	(original)
+++ trunk/conduit/Conflict.py	Wed Jul  9 16:07:11 2008
@@ -4,6 +4,11 @@
 Copyright: John Stowers, 2006
 License: GPLv2
 """
+import logging
+log = logging.getLogger("Conflict")
+
+import conduit
+
 #ENUM of directions when resolving a conflict
 CONFLICT_ASK = 0                    
 CONFLICT_SKIP = 1
@@ -15,7 +20,8 @@
     """
     Represents a conflict
     """
-    def __init__(self, sourceWrapper, sourceData, sourceDataRid, sinkWrapper, sinkData, sinkDataRid, validResolveChoices, isDeletion):
+    def __init__(self, cond, sourceWrapper, sourceData, sourceDataRid, sinkWrapper, sinkData, sinkDataRid, validResolveChoices, isDeletion):
+        self.cond = cond
         self.sourceWrapper = sourceWrapper
         self.sourceData = sourceData
         self.sourceDataRid = sourceDataRid
@@ -25,4 +31,85 @@
         self.choices = validResolveChoices
         self.isDeletion = isDeletion
 
+        self._gen_hash()
+
+    def _gen_hash(self):
+        if self.sourceWrapper and self.sourceDataRid and self.sinkWrapper:
+            mapping = conduit.GLOBALS.mappingDB.get_mapping(
+                                sourceUID=self.sourceWrapper.get_UID(),
+                                dataLUID=self.sourceDataRid.get_UID(),
+                                sinkUID=self.sinkWrapper.get_UID())
+
+            if mapping.oid:
+                self._hash = hash(mapping.oid)
+                return
+
+        #approximate a stable hash for the relationship that is invariant
+        #based upon the order of source and sink
+        uids = [hash(self.sourceWrapper.get_UID()),
+                hash(self.sinkWrapper.get_UID()),
+                hash(self.sourceDataRid),
+                hash(self.sinkDataRid)]
+        uids.sort()
+        self._hash = hash(tuple(uids))
+
+    def __hash__(self):
+        return self._hash
+
+    def get_partnership(self):
+        return self.sourceWrapper,self.sinkWrapper
+
+    def get_snippet(self, is_source):
+        if is_source:
+            return self.sourceData.get_snippet()
+        else:
+            return self.sinkData.get_snippet()
+
+    def get_icon(self, is_source):
+        return None
+
+    def resolve(self, direction):
+        resolve = True
+        delete = False
+
+        if direction == CONFLICT_ASK:
+            log.debug("Not resolving")
+            resolve = False
+        elif direction == CONFLICT_SKIP:
+            log.debug("Skipping conflict")
+            resolve = False
+        elif direction == CONFLICT_COPY_SOURCE_TO_SINK:
+            log.debug("Resolving source data --> sink")
+            data = self.sourceData
+            dataRid = self.sourceDataRid
+            source = self.sourceWrapper
+            sink = self.sinkWrapper
+        elif direction == CONFLICT_COPY_SINK_TO_SOURCE:
+            log.debug("Resolving source <-- sink data")
+            data = self.sinkData
+            dataRid = self.sinkDataRid
+            source = self.sinkWrapper
+            sink = self.sourceWrapper
+        elif direction == CONFLICT_DELETE:
+            log.debug("Resolving deletion  --->")
+            data = self.sinkData
+            dataRid = self.sinkDataRid
+            source = self.sourceWrapper
+            sink = self.sinkWrapper
+            delete = True
+        else:
+            log.warn("Unknown resolution")
+            resolve = False
+
+        if resolve:
+            if delete:
+                log.debug("Resolving self. Deleting %s from %s" % (data, sink))
+                conduit.Synchronization.delete_data(source, sink, data.get_UID())
+            else:
+                log.debug("Resolving self. Putting %s --> %s" % (data, sink))
+                conduit.Synchronization.put_data(source, sink, data, dataRid, True)
+
+            self.cond.resolved_conflict(self)
+
+        return resolve
 

Modified: trunk/conduit/Synchronization.py
==============================================================================
--- /trunk/conduit/Synchronization.py	(original)
+++ trunk/conduit/Synchronization.py	Wed Jul  9 16:07:11 2008
@@ -391,6 +391,7 @@
             sourceData = DeletedData(sourceDataLUID)
             sinkData = DeletedData(sinkDataLUID)
             c = Conflict(
+                    self.cond,                  #the conduit this conflict belongs to
                     sourceWrapper,              #datasource wrapper
                     sourceData,                 #from data
                     sourceData.get_rid(),       #from data rid
@@ -400,7 +401,7 @@
                     validResolveChoices,        #valid resolve choices
                     True                        #This conflict is a deletion
                     )
-            self.cond.emit("sync-conflict", c)
+            self.cond.emit_conflict(c)
 
         elif self.cond.get_policy("deleted") == "replace":
             log.debug("Deleted Policy: Delete")
@@ -426,6 +427,7 @@
                 avail = (CONFLICT_SKIP,CONFLICT_COPY_SOURCE_TO_SINK)
 
             c = Conflict(
+                    self.cond,
                     sourceWrapper, 
                     fromData,
                     fromDataRid, 
@@ -435,7 +437,7 @@
                     avail,
                     False
                     )
-            self.cond.emit("sync-conflict", c)
+            self.cond.emit_conflict(c)
 
         elif self.cond.get_policy("conflict") == "replace":
             log.debug("Conflict Policy: Replace")

Modified: trunk/conduit/dataproviders/DataProvider.py
==============================================================================
--- /trunk/conduit/dataproviders/DataProvider.py	(original)
+++ trunk/conduit/dataproviders/DataProvider.py	Wed Jul  9 16:07:11 2008
@@ -459,9 +459,10 @@
                     category=category
                     )
         dpw.set_dnd_key(customKey)
-        log.debug("DataProviderFactory %s: Emitting dataprovider-added for %s" % (self, dpw.get_dnd_key()))
+        key = dpw.get_dnd_key()
+        log.debug("DataProviderFactory %s: Emitting dataprovider-added for %s" % (self, key))
         self.emit("dataprovider-added", dpw, klass)
-        return dpw.get_dnd_key()
+        return key
 
     def emit_removed(self, key):
         log.debug("DataProviderFactory %s: Emitting dataprovider-removed for %s" % (self, key))
@@ -476,18 +477,22 @@
         """
         pass
 
-    def get_configuration_widget(self):
+    def setup_configuration_widget(self):
         """
         If the factory needs to offer configuration options then
-        it should return a gtk.widget here.
+        it should return a gtk.widget here. This widget is then packed
+        into the configuration notebook.
         """
         return None
 
-    def save_configuration(self):
+    def save_configuration(self, ok):
         """
-        If the user closes the configuration panel with RESPONSE_OK (e.g.
-        doesnt click cancel) then this will be called on all derived classes
+        @param ok: True if the user closed the prefs panel with OK, false if 
+        they cancelled it.
         """
         pass
+
+    def get_name(self):
+        return self.__class__.__name__
         
 

Modified: trunk/conduit/dataproviders/SimpleFactory.py
==============================================================================
--- /trunk/conduit/dataproviders/SimpleFactory.py	(original)
+++ trunk/conduit/dataproviders/SimpleFactory.py	Wed Jul  9 16:07:11 2008
@@ -15,6 +15,7 @@
         self.items = {}
 
     def item_added(self, key, **kwargs):
+        log.info("Item Added: %s" % key)
         cat = self.get_category(key, **kwargs)
         idxs = []
         for klass in self.get_dataproviders(key, **kwargs):
@@ -27,7 +28,7 @@
         if key in self.items:
             for idx in self.items[key]:
                 self.emit_removed(idx)
-            del self.items[key]
+            del(self.items[key])
 
     def get_category(self, key, **kwargs):
         """ Return a category to contain these dataproviders """
@@ -40,3 +41,6 @@
     def get_args(self, key, **kwargs):
         raise NotImplementedError
 
+    def is_interesting(self, udi, properties):
+        raise NotImplementedError
+

Modified: trunk/conduit/dataproviders/VolumeFactory.py
==============================================================================
--- /trunk/conduit/dataproviders/VolumeFactory.py	(original)
+++ trunk/conduit/dataproviders/VolumeFactory.py	Wed Jul  9 16:07:11 2008
@@ -38,10 +38,11 @@
         return True
 
     def _volume_unmounted_cb(self, monitor, volume):
-        log.info("Volume Umounted")
+        log.info("Volume Umounted: %s" % volume.get_hal_udi())
         device_udi = volume.get_hal_udi()
         if device_udi :
             if self.is_interesting(device_udi, self._get_properties(device_udi)):
+                log.info("Removing Volume")
                 self.item_removed(device_udi)
         return False
 
@@ -84,5 +85,4 @@
         """ VolumeFactory passes mount point and udi to dataproviders """
         return (kwargs['mount'], udi,)
 
-    def is_interesting(self, udi, properties):
-        raise NotImplementedError
+

Modified: trunk/conduit/gtkui/ConflictResolver.py
==============================================================================
--- /trunk/conduit/gtkui/ConflictResolver.py	(original)
+++ trunk/conduit/gtkui/ConflictResolver.py	Wed Jul  9 16:07:11 2008
@@ -5,7 +5,6 @@
 License: GPLv2
 """
 import traceback
-import threading
 import time
 import gobject
 import gtk, gtk.gdk
@@ -24,9 +23,22 @@
 CONFLICT_IDX = 0            #The conflict object
 DIRECTION_IDX = 1           #The current user decision re: the conflict (-->, <-- or -x-)
 
-class ConflictHeader(Conflict.Conflict):
+class ConflictHeader:
     def __init__(self, sourceWrapper, sinkWrapper):
-        Conflict.Conflict.__init__(self, sourceWrapper, None, None, sinkWrapper, None, None, (), False)
+        self.sourceWrapper = sourceWrapper
+        self.sinkWrapper = sinkWrapper
+
+    def get_snippet(self, is_source):
+        if is_source:
+            return self.sourceWrapper.name
+        else:
+            return self.sinkWrapper.name
+
+    def get_icon(self, is_source):
+        if is_source:
+            return self.sourceWrapper.get_icon()
+        else:
+            return self.sinkWrapper.get_icon()
 
 class ConflictResolver:
     """
@@ -41,9 +53,6 @@
         self.partnerships = {}
         self.numConflicts = 0
 
-        #resolve conflicts in a background thread
-        self.resolveThreadManager = ConflictResolveThreadManager(3)
-
         self.view = gtk.TreeView( self.model )
         self._build_view()
 
@@ -118,37 +127,13 @@
         self.view.get_selection().connect("changed", self.on_selection_changed)
 
     def _name_data_func(self, column, cell_renderer, tree_model, rowref, is_source):
-        """
-        The format for displaying the data is
-        uri (modified)
-        snippet
-        """
         conflict = tree_model.get_value(rowref, CONFLICT_IDX)
-        #render the headers different to the data
-        if tree_model.iter_depth(rowref) == 0:
-            if is_source:
-                text = conflict.sourceWrapper.name
-            else:
-                text = conflict.sinkWrapper.name
-        else:
-            if is_source:
-                text = conflict.sourceData.get_snippet()
-            else:
-                text = conflict.sinkData.get_snippet()
-
+        text = conflict.get_snippet(is_source)
         cell_renderer.set_property("text", text)
 
     def _icon_data_func(self, column, cell_renderer, tree_model, rowref, is_source):
         conflict = tree_model.get_value(rowref, CONFLICT_IDX)
-        #Only the headers have icons
-        if tree_model.iter_depth(rowref) == 0:
-            if is_source:
-                icon = conflict.sourceWrapper.get_icon()
-            else:
-                icon = conflict.sinkWrapper.get_icon()
-        else:
-            icon = None
-
+        icon = conflict.get_icon(is_source)
         cell_renderer.set_property("pixbuf", icon)
 
     def _direction_data_func(self, column, cell_renderer, tree_model, rowref, user_data):
@@ -164,54 +149,22 @@
     def _set_conflict_titles(self):
         self.expander.set_label(_("Conflicts (%s)") % self.numConflicts)
         self.standalone.set_title(_("Conflicts (%s)") % self.numConflicts)
-        
-    def _conflict_resolved(self, sender, rowref):
-        """
-        Callback when a ConflictResolveThread finishes. Deletes the 
-        appropriate conflict from the model. Also looks to see if there
-        are any other conflicts remainng so it can set the sink status and/or
-        delete the partnership
-        """
-        if not self.model.iter_is_valid(rowref):
-            #FIXME: Need to work a way around this before resolution can be threaded
-            log.warn("Iters do not persist throug signal emission!")
-            return
-
-        self.model.remove(rowref)
-        #now look for any sync partnerships with no children
-        empty = False
-        for source,sink in self.partnerships:
-            rowref = self.partnerships[(source,sink)]
-            numChildren = self.model.iter_n_children(rowref)
-            if numChildren == 0:
-                empty = True
-
-        #do in two loops so as to not change the dict while iterating
-        if empty:
-            sink.module.set_status(DataProvider.STATUS_DONE_SYNC_OK)
-            del(self.partnerships[(source,sink)])
-            self.model.remove(rowref)
-        else:
-            sink.module.set_status(DataProvider.STATUS_DONE_SYNC_CONFLICT)
 
-        #FIXME: Do this properly with model signals and a count function
-        self.numConflicts -= 1
-        self._set_conflict_titles()
-
-    def on_conflict(self, thread, conflict):
+    def on_conflict(self, cond, conflict):
         #We start with the expander disabled. Make sure we only enable it once
         if len(self.model) == 0:
             self.expander.set_sensitive(True)
 
         self.numConflicts += 1
-        source = conflict.sourceWrapper
-        sink = conflict.sinkWrapper
+        source,sink = conflict.get_partnership()
         if (source,sink) not in self.partnerships:
             #create a header row
             header = ConflictHeader(source, sink)
-            self.partnerships[(source,sink)] = self.model.append(None, (header, Conflict.CONFLICT_ASK) )
+            rowref = self.model.append(None, (header, Conflict.CONFLICT_ASK))
+            self.partnerships[(source,sink)] = (rowref,conflict)
 
-        self.model.append(self.partnerships[(source,sink)], (conflict, Conflict.CONFLICT_ASK) )  
+        rowref = self.partnerships[(source,sink)][0]
+        self.model.append(rowref, (conflict, Conflict.CONFLICT_ASK))
 
         #FIXME: Do this properly with model signals and a count function
         #update the expander label and the standalone window title
@@ -242,12 +195,6 @@
         return True
 
     def on_resolve_conflicts(self, sender):
-        """
-        According to the users selection, start backgroun threads to
-        resolve the conflicts
-        """
-        IHaveMadeItersPersist = False
-
         #save the resolved rowrefs and remove them at the end
         resolved = []
 
@@ -259,76 +206,30 @@
             direction = model[path][DIRECTION_IDX]
             conflict = model[path][CONFLICT_IDX]
 
-            #do as the user inducated with the arrow
-            if direction == Conflict.CONFLICT_ASK:
-                log.debug("Not resolving")
-                return
-            elif direction == Conflict.CONFLICT_SKIP:
-                log.debug("Skipping conflict")
+            if conflict.resolve(direction):
                 resolved.append(rowref)
-                return
-            elif direction == Conflict.CONFLICT_COPY_SOURCE_TO_SINK:
-                log.debug("Resolving source data --> sink")
-                data = conflict.sourceData
-                dataRid = conflict.sourceDataRid
-                source = conflict.sourceWrapper
-                sink = conflict.sinkWrapper
-            elif direction == Conflict.CONFLICT_COPY_SINK_TO_SOURCE:
-                log.debug("Resolving source <-- sink data")
-                data = conflict.sinkData
-                dataRid = conflict.sinkDataRid
-                source = conflict.sinkWrapper
-                sink = conflict.sourceWrapper
-            elif direction == Conflict.CONFLICT_DELETE:
-                log.debug("Resolving deletion  --->")
-                data = conflict.sinkData
-                dataRid = conflict.sinkDataRid
-                source = conflict.sourceWrapper
-                sink = conflict.sinkWrapper
-            else:
-                log.warn("Unknown resolution")
-
-            deleted = conflict.isDeletion
-
-            #add to resolve thread
-            #FIXME: Think of a way to make rowrefs persist through signals
-            if IHaveMadeItersPersist:
-                self.resolveThreadManager.make_thread(self._conflict_resolved,rowref,source,sink,data,dataRid,deleted)
-            else:
-                try:
-                    if deleted:
-                        log.debug("Resolving conflict. Deleting %s from %s" % (data, sink))
-                        conduit.Synchronization.delete_data(source, sink, data.get_UID())
-                    else:
-                        log.debug("Resolving conflict. Putting %s --> %s" % (data, sink))
-                        conduit.Synchronization.put_data(source, sink, data, dataRid, True)
-
-                    resolved.append(rowref)
-                except Exception:
-                    log.warn("Could not resolve conflict\n%s" % traceback.format_exc())
 
         self.model.foreach(_resolve_func)
         for r in resolved:
             self.model.remove(r)
 
-        if not IHaveMadeItersPersist:
-            #now look for any sync partnerships with no children
-            empty = []
-            for source,sink in self.partnerships:
-                rowref = self.partnerships[(source,sink)]
-                numChildren = self.model.iter_n_children(rowref)
-                if numChildren == 0:
-                    sink.module.set_status(DataProvider.STATUS_DONE_SYNC_OK)
-                    empty.append( (rowref, source, sink) )
-                else:
-                    sink.module.set_status(DataProvider.STATUS_DONE_SYNC_CONFLICT)
-
-            #do in two loops so as to not change the model while iterating
-            for rowref, source, sink in empty:
-                self.model.remove(rowref)
-                try:
-                    del(self.partnerships[(source,sink)])
-                except KeyError: pass
+        #now look for any sync partnerships with no children
+        empty = []
+        for source,sink in self.partnerships:
+            rowref = self.partnerships[(source,sink)][0]
+            numChildren = self.model.iter_n_children(rowref)
+            if numChildren == 0:
+                sink.module.set_status(DataProvider.STATUS_DONE_SYNC_OK)
+                empty.append( (rowref, source, sink) )
+            else:
+                sink.module.set_status(DataProvider.STATUS_DONE_SYNC_CONFLICT)
+
+        #do in two loops so as to not change the model while iterating
+        for rowref, source, sink in empty:
+            self.model.remove(rowref)
+            try:
+                del(self.partnerships[(source,sink)])
+            except KeyError: pass
 
     def on_cancel_conflicts(self, sender):
         self.model.clear()
@@ -432,127 +333,3 @@
             model[path][DIRECTION_IDX] = conflict.choices[curIdx+1]
 
         return True
-
-class _ConflictResolveThread(threading.Thread, gobject.GObject):
-    """
-    Resolves a conflict or deletion event. If a deleted event then
-    calls sink.delete, if a conflict then does put()
-    """
-    __gsignals__ =  { 
-                    "completed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [])
-                    }
-
-    def __init__(self, *args):
-        """
-        Args
-         - arg[0]: source
-         - arg[1]: sink
-         - arg[2]: data
-         - arg[3]: dataRid
-         - arg[4]: isDeleted
-        """
-        threading.Thread.__init__(self)
-        gobject.GObject.__init__(self)
-        
-
-        self.source = args[0]
-        self.sink = args[1]
-        self.data = args[2]
-        self.dataRid = args[3]
-        self.isDeleted = args[4]
-
-        self.setName("ResolveThread for sink: %s. (Delete: %s)" % (self.sink, self.isDeleted))
-
-    def emit(self, *args):
-        """
-        Override the gobject signal emission so that all signals are emitted 
-        from the main loop on an idle handler
-        """
-        gobject.idle_add(gobject.GObject.emit,self,*args)
-
-    def run(self):
-        try:
-            if self.isDeleted:
-                log.debug("Resolving conflict. Deleting %s from %s" % (self.data, self.sink))
-                conduit.Synchronization.delete_data(self.source, self.sink, self.dataRid.get_UID())
-            else:
-                log.debug("Resolving conflict. Putting %s --> %s" % (self.data, self.sink))
-                conduit.Synchronization.put_data(self.source, self.sink, self.data, self.dataRid, True)
-        except Exception:                        
-            log.warn("Could not resolve conflict\n%s" % traceback.format_exc())
-            #sink.module.set_status(DataProvider.STATUS_DONE_SYNC_ERROR)
-
-        self.emit("completed")
-
-class ConflictResolveThreadManager:
-    """
-    Manages many resolve threads. This involves joining and cancelling
-    said threads, and respecting a maximum num of concurrent threads limit
-    """
-    def __init__(self, maxConcurrentThreads):
-        self.maxConcurrentThreads = maxConcurrentThreads
-        #stores all threads, running or stopped
-        self.fooThreads = {}
-        #the pending thread args are used as an index for the stopped threads
-        self.pendingFooThreadArgs = []
-
-    def _register_thread_completed(self, thread, *args):
-        """
-        Decrements the count of concurrent threads and starts any 
-        pending threads if there is space
-        """
-        del(self.fooThreads[args])
-        running = len(self.fooThreads) - len(self.pendingFooThreadArgs)
-
-        log.debug("Thread %s completed. %s running, %s pending" % (
-                            thread, running, len(self.pendingFooThreadArgs)))
-
-        if running < self.maxConcurrentThreads:
-            try:
-                args = self.pendingFooThreadArgs.pop()
-                log.debug("Starting pending %s" % self.fooThreads[args])
-                self.fooThreads[args].start()
-            except IndexError: pass
-
-    def make_thread(self, completedCb, completedCbUserData,  *args):
-        """
-        Makes a thread with args. The thread will be started when there is
-        a free slot
-        """
-        running = len(self.fooThreads) - len(self.pendingFooThreadArgs)
-
-        if args not in self.fooThreads:
-            thread = _ConflictResolveThread(*args)
-            #signals run in the order connected. Connect the user one first 
-            #incase they wish to do something before we delete the thread
-            thread.connect("completed", completedCb, completedCbUserData)
-            thread.connect("completed", self._register_thread_completed, *args)
-            #This is why we use args, not kwargs, because args are hashable
-            self.fooThreads[args] = thread
-
-            if running < self.maxConcurrentThreads:
-                log.debug("Starting %s" % thread)
-                self.fooThreads[args].start()
-            else:
-                log.debug("Queing %s" % thread)
-                self.pendingFooThreadArgs.append(args)
-        else:
-            log.debug("Already resolving conflict")
-
-    def join_all_threads(self):
-        """
-        Joins all threads (blocks)
-
-        Unfortunately we join all the threads do it in a loop to account
-        for join() a non started thread failing. To compensate I time.sleep()
-        to not smoke CPU
-        """
-        joinedThreads = 0
-        while(joinedThreads < len(self.fooThreads)):
-            for thread in self.fooThreads.values():
-                try:
-                    thread.join()
-                    joinedThreads += 1
-                except AssertionError: 
-                    #deal with not started threads
-                    time.sleep(1)

Modified: trunk/conduit/gtkui/UI.py
==============================================================================
--- /trunk/conduit/gtkui/UI.py	(original)
+++ trunk/conduit/gtkui/UI.py	Wed Jul  9 16:07:11 2008
@@ -233,8 +233,8 @@
 
         #Build some liststores to display
         CONVERT_FROM_MESSAGE = _("Convert from")
-        CONVERT_INTO_MESSAGE = _("into") 
-        
+        CONVERT_INTO_MESSAGE = _("into")
+
         convertables = self.type_converter.get_convertables_list()
         converterListStore = gtk.ListStore( str )
         for froms,tos in convertables:
@@ -250,9 +250,10 @@
 
         #construct the dialog
         tree = gtk.glade.XML(self.gladeFile, "PreferencesDialog")
+        notebook = tree.get_widget("prop_notebook")
+
         #Show the DB contents to help debugging
         if conduit.IS_DEVELOPMENT_VERSION:
-            notebook = tree.get_widget("prop_notebook")
             vbox = gtk.VBox(False,5)
             
             #build the treeview to show all column fields. For performance
@@ -284,7 +285,6 @@
             vbox.pack_start(clear, False, False)
 
             notebook.append_page(vbox,gtk.Label('Mapping DB'))
-            notebook.show_all()
         
         converterTreeView = tree.get_widget("dataConversionsTreeView")
         converterTreeView.set_model(converterListStore)
@@ -326,6 +326,20 @@
                 if currentValue == policyValue:
                     widget.set_active(True)
                                         
+        #Add configuration widgets for all factories
+        #The dataprovider factories can provide a configuration widget which is
+        #packed into the notebook
+        factoryConfigurationWidgets = []
+        for i in conduit.GLOBALS.moduleManager.dataproviderFactories:#get_modules_by_type("dataprovider-factory"):
+            widget = i.setup_configuration_widget()
+            if widget:
+                factoryConfigurationWidgets.append(widget)
+                notebook.append_page(
+                            widget,
+                            gtk.Label(i.get_name()))
+
+        notebook.show_all()
+
         #Show the dialog
         dialog = tree.get_widget("PreferencesDialog")
         dialog.set_transient_for(self.mainWindow)
@@ -346,6 +360,11 @@
                         conduit.GLOBALS.settings.set(
                                 "default_policy_%s" % policyName,
                                 policyValue)
+
+        #give the dataprovider factories to ability to save themselves
+        for factory in factoryConfigurationWidgets:
+            factory.save_configuration(response == gtk.RESPONSE_OK)
+
         dialog.destroy()                
 
 
@@ -479,7 +498,9 @@
         The splash can also be destroyed manually by the application
         """
         self.wSplash = gtk.Window(gtk.WINDOW_POPUP)
+        self.wSplash.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_SPLASHSCREEN)
         self.wSplash.set_decorated(False)
+
         wSplashScreen = gtk.Image()
         wSplashScreen.set_from_file(os.path.join(conduit.SHARED_DATA_DIR,"conduit-splash.png"))
 

Modified: trunk/conduit/modules/FileModule/FileConfiguration.py
==============================================================================
--- /trunk/conduit/modules/FileModule/FileConfiguration.py	(original)
+++ trunk/conduit/modules/FileModule/FileConfiguration.py	Wed Jul  9 16:07:11 2008
@@ -300,11 +300,10 @@
                 dialog.emit_stop_by_name("response")
 
 class _FolderTwoWayConfigurator:
-    def __init__(self, mainWindow, folder, folderGroupName, includeHidden, compareIgnoreMtime, followSymlinks):
+    def __init__(self, mainWindow, folder, includeHidden, compareIgnoreMtime, followSymlinks):
         log.debug("Starting new folder chooser at %s" % folder)
         self.folder = folder
         self.includeHidden = includeHidden
-        self.folderGroupName = folderGroupName
         self.compareIgnoreMtime = compareIgnoreMtime
         self.followSymlinks = followSymlinks
 
@@ -315,8 +314,6 @@
 						)
         self.folderChooser = tree.get_widget("filechooserbutton1")
         self.folderChooser.set_current_folder_uri(self.folder)
-        self.folderEntry = tree.get_widget("entry1")
-        self.folderEntry.set_text(self.folderGroupName)
         self.hiddenCb = tree.get_widget("hidden")
         self.hiddenCb.set_active(includeHidden)
         self.mtimeCb = tree.get_widget("ignoreMtime")
@@ -330,37 +327,22 @@
 
     def on_response(self, dialog, response_id):
         if response_id == gtk.RESPONSE_OK:
-            if self.folderEntry.get_text() == "":
-                #stop this dialog from closing, and show a warning to the
-                #user indicating that the folder must be named
-                warning = gtk.MessageDialog(
-                                parent=dialog,
-                                flags=gtk.DIALOG_MODAL, 
-                                type=gtk.MESSAGE_WARNING, 
-                                buttons=gtk.BUTTONS_OK, 
-                                message_format=_("Please Enter a Folder Name"))
-                warning.format_secondary_text(_("All folders require a descriptive name. To name a folder enter its name where indicated"))
-                warning.run()
-                warning.destroy()
-                dialog.emit_stop_by_name("response")
-            else:
-                self.folderGroupName = self.folderEntry.get_text()
-                #I think the logic here is ugly, and rather non-symmetric.
-                #Basically get_uri returns the selected folder uri, OR the current
-                #(read: same as get_folder_uri) uri if none is selected. 
-                #The non symmetric part is that when we pre-load the filechooser
-                #at time+1, we call set_current_folder_uri() #because the set_uri()
-                #call doesnt make sense in a folderchooser where you want 
-                #to be *inside* the selected folder and not one below it.
-                selected = self.folderChooser.get_uri()
-                self.folder = Vfs.uri_make_canonical(selected)
-                log.debug("Folderconfig returned %s (non-canonical: %s)" % (self.folder,selected))
-                self.includeHidden = self.hiddenCb.get_active()
-                self.compareIgnoreMtime = self.mtimeCb.get_active()
-                self.followSymlinks = self.followSymlinksCb.get_active() 
+            #I think the logic here is ugly, and rather non-symmetric.
+            #Basically get_uri returns the selected folder uri, OR the current
+            #(read: same as get_folder_uri) uri if none is selected. 
+            #The non symmetric part is that when we pre-load the filechooser
+            #at time+1, we call set_current_folder_uri() #because the set_uri()
+            #call doesnt make sense in a folderchooser where you want 
+            #to be *inside* the selected folder and not one below it.
+            selected = self.folderChooser.get_uri()
+            self.folder = Vfs.uri_make_canonical(selected)
+            log.debug("Folderconfig returned %s (non-canonical: %s)" % (self.folder,selected))
+            self.includeHidden = self.hiddenCb.get_active()
+            self.compareIgnoreMtime = self.mtimeCb.get_active()
+            self.followSymlinks = self.followSymlinksCb.get_active() 
 
     def show_dialog(self):
         self.dlg.show_all()
         self.dlg.run()
         self.dlg.destroy()
-        return self.folder, self.folderGroupName, self.includeHidden, self.compareIgnoreMtime, self.followSymlinks
+        return self.folder, self.includeHidden, self.compareIgnoreMtime, self.followSymlinks

Modified: trunk/conduit/modules/FileModule/FileModule.py
==============================================================================
--- /trunk/conduit/modules/FileModule/FileModule.py	(original)
+++ trunk/conduit/modules/FileModule/FileModule.py	Wed Jul  9 16:07:11 2008
@@ -13,8 +13,8 @@
 import conduit.Vfs as Vfs
 
 MODULES = {
-	"FileSource" :              { "type": "dataprovider" },
-	"FolderTwoWay" :            { "type": "dataprovider" },
+    "FileSource" :              { "type": "dataprovider" },
+    "FolderTwoWay" :            { "type": "dataprovider" },
     "RemovableDeviceFactory" :  { "type": "dataprovider-factory" }
 }
 
@@ -89,34 +89,35 @@
     def configure(self, window):
         Utils.dataprovider_add_dir_to_path(__file__, "")
         import FileConfiguration
-        f = FileConfiguration._FolderTwoWayConfigurator(window, self.folder, self.folderGroupName,
-                                                        self.includeHidden, self.compareIgnoreMtime, self.followSymlinks)
-        self.folder, self.folderGroupName, self.includeHidden, self.compareIgnoreMtime, self.followSymlinks = f.show_dialog()
+        f = FileConfiguration._FolderTwoWayConfigurator(
+                                    window,
+                                    self.folder,
+                                    self.includeHidden,
+                                    self.compareIgnoreMtime,
+                                    self.followSymlinks)
+        self.folder, self.includeHidden, self.compareIgnoreMtime, self.followSymlinks = f.show_dialog()
         self._monitor_folder()
         
     def set_configuration(self, config):
         self.folder = config.get("folder", self.DEFAULT_FOLDER)
-        self.folderGroupName = config.get("folderGroupName", self.DEFAULT_GROUP)
         self.includeHidden = config.get("includeHidden", self.DEFAULT_HIDDEN)
         self.compareIgnoreMtime = config.get("compareIgnoreMtime", self.DEFAULT_COMPARE_IGNORE_MTIME)
         self.followSymlinks = config.get("followSymlinks", self.DEFAULT_FOLLOW_SYMLINKS)        
         self._monitor_folder()
 
     def get_configuration(self):
-        #_save_config_file_for_dir(self.folder, self.folderGroupName)
         return {
             "folder" : self.folder,
-            "folderGroupName" : self.folderGroupName,
             "includeHidden" : self.includeHidden,
             "compareIgnoreMtime" : self.compareIgnoreMtime,
             "followSymlinks" : self.followSymlinks
             }
 
     def get_UID(self):
-        return "%s:%s" % (self.folder, self.folderGroupName)
+        return self.folder
         
     def get_name(self):
-        return self.folderGroupName
+        return Vfs.uri_get_filename(self.folder)
 
     def _monitor_folder(self):
         if self._monitor_folder_id != None:
@@ -151,7 +152,6 @@
             "_udi_"         :   udi
         }
         if name:
-            info["DEFAULT_GROUP"] = name
             info["_name_"] = name            
         
         klass = type(
@@ -167,7 +167,7 @@
         the folder and the udi to allow multiple preconfigured groups per
         usb key
         """
-        VolumeFactory.VolumeFactory.emit_added(self, 
+        return VolumeFactory.VolumeFactory.emit_added(self, 
                         klass, 
                         initargs, 
                         category, 
@@ -180,10 +180,18 @@
             if prop2.has_key("storage.removable") and prop2["storage.removable"] == True:
                 mount,label = self._get_device_info(props)
                 log.info("Detected removable volume %s %s" % (label,mount))
+
+                #short circuit the logic here to test if this is a volume being
+                #unmounted. Its still interesting, we just dont make a 
+                #klass for it
+                if udi in self._volumes and not mount:
+                    log.debug("This is a FileModule being removed")
+                    del(self._volumes[udi])
+                    return True
+
                 #check for the presence of a mount/.conduit group file
                 #which describe the folder sync groups, and their names,
                 mountUri = "file://%s" % mount
-                
                 try:
                     groups = FileDataProvider.read_removable_volume_group_file(mountUri)
                 except Exception, e:

Modified: trunk/conduit/modules/FileModule/config.glade
==============================================================================
--- /trunk/conduit/modules/FileModule/config.glade	(original)
+++ trunk/conduit/modules/FileModule/config.glade	Wed Jul  9 16:07:11 2008
@@ -233,28 +233,6 @@
             <property name="visible">True</property>
             <property name="spacing">5</property>
             <child>
-              <widget class="GtkLabel" id="label82">
-                <property name="visible">True</property>
-                <property name="xalign">0</property>
-                <property name="label" translatable="yes">Folder Name:</property>
-              </widget>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-              </packing>
-            </child>
-            <child>
-              <widget class="GtkEntry" id="entry1">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-              </widget>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">False</property>
-                <property name="position">1</property>
-              </packing>
-            </child>
-            <child>
               <widget class="GtkLabel" id="label83">
                 <property name="visible">True</property>
                 <property name="xalign">0</property>
@@ -263,7 +241,6 @@
               <packing>
                 <property name="expand">False</property>
                 <property name="fill">False</property>
-                <property name="position">2</property>
               </packing>
             </child>
             <child>
@@ -274,7 +251,7 @@
                 <property name="title" translatable="yes">Select A Folder</property>
               </widget>
               <packing>
-                <property name="position">3</property>
+                <property name="position">1</property>
               </packing>
             </child>
             <child>
@@ -343,7 +320,7 @@
               <packing>
                 <property name="expand">False</property>
                 <property name="fill">False</property>
-                <property name="position">4</property>
+                <property name="position">2</property>
               </packing>
             </child>
           </widget>

Modified: trunk/conduit/modules/TestModule.py
==============================================================================
--- /trunk/conduit/modules/TestModule.py	(original)
+++ trunk/conduit/modules/TestModule.py	Wed Jul  9 16:07:11 2008
@@ -687,9 +687,6 @@
             raise Exceptions.SynchronizeConflictError(conduit.datatypes.COMPARISON_UNKNOWN, data, newData)
         return newData.get_rid()
 
-    def get_UID(self):
-        return Utils.random_string()
-
 class TestConverter(TypeConverter.Converter):
     def __init__(self):
         self.conversions =  {
@@ -714,6 +711,7 @@
         return t
 
 class TestFactory(DataProvider.DataProviderFactory):
+    _configurable_ = True
     def __init__(self, **kwargs):
         DataProvider.DataProviderFactory.__init__(self, **kwargs)
         
@@ -754,6 +752,17 @@
         self.emit_removed(self.key1)
         return False
 
+    def setup_configuration_widget(self):
+        import gtk
+        vb = gtk.VBox(2)
+        vb.pack_start(gtk.Label("Hello World"))
+        self.entry = gtk.Entry()
+        vb.pack_start(self.entry)
+        return vb
+
+    def save_configuration(self, ok):
+        log.debug("OK: %s Message: %s" % (ok,self.entry.get_text()))
+
 class TestFactoryRemoval(DataProvider.DataProviderFactory):
     """
     Repeatedly add/remove a DP/Category to stress test framework

Added: trunk/test/python-tests/TestCoreConflict.py
==============================================================================
--- (empty file)
+++ trunk/test/python-tests/TestCoreConflict.py	Wed Jul  9 16:07:11 2008
@@ -0,0 +1,41 @@
+#common sets up the conduit environment
+from common import *
+
+import conduit.Conflict as Conflict
+
+test = SimpleSyncTest()
+test.prepare(
+        test.get_dataprovider("TestSource"), 
+        test.get_dataprovider("TestConflict")
+        )
+test.set_two_way_policy({"conflict":"ask","deleted":"skip"})
+config = {}
+config["numData"] = 1
+test.configure(source=config)
+test.set_two_way_sync(False)
+
+test.sync(debug=False)
+aborted,errored,conflicted = test.get_sync_result()
+ok("Conflict trapped", conflicted == True and aborted == False)
+
+c = test.conduit._conflicts
+ok("One Conflict", len(c) == 1)
+
+test.sync(debug=False)
+aborted,errored,conflicted = test.get_sync_result()
+ok("Conflict trapped again", conflicted == True and aborted == False)
+
+c = test.conduit._conflicts
+ok("Just One Conflict", len(c) == 1)
+
+#get the one conflict
+for h,conf in c.items():
+    break
+
+resolved = conf.resolve(Conflict.CONFLICT_SKIP)
+ok("Didnt resolve when resolution is skip", resolved == False and len(test.conduit._conflicts) == 1)
+
+resolved = conf.resolve(Conflict.CONFLICT_COPY_SOURCE_TO_SINK)
+ok("Resolved, source -> sink", resolved == True and len(test.conduit._conflicts) == 0)
+
+finished()
\ No newline at end of file



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]