class MonitorCategorizableTest(test.TestCase): def setUp(self): self.monitor = ChangeMonitor() self.monitor.monitorClass(CategorizableCompositeObject) self.obj = CategorizableCompositeObject(subject=u'Object') self.list = ObservableList() self.monitor.monitorCollection(self.list) self.list.append(self.obj) self.cat1 = Category(subject=u'Cat #1') self.cat2 = Category(subject=u'Cat #2') self.catList = ObservableList() self.catList.append(self.cat1) self.catList.append(self.cat2) def tearDown(self): self.monitor.unmonitorClass(CategorizableCompositeObject) self.monitor.unmonitorCollection(self.list) def testAddCategory(self): self.monitor.resetAllChanges() self.obj.addCategory(self.cat1) self.assertEqual(self.monitor.getChanges(self.obj), set(['__add_category:%s' % self.cat1.id()])) def testRemoveCategory(self): self.obj.addCategory(self.cat1) self.monitor.resetAllChanges() self.obj.removeCategory(self.cat1) self.assertEqual(self.monitor.getChanges(self.obj), set(['__del_category:%s' % self.cat1.id()])) def testRemoveBadCategory(self): self.obj.addCategory(self.cat1) self.monitor.resetAllChanges() self.obj.removeCategory(self.cat2) self.assertEqual(self.monitor.getChanges(self.obj), set())
class ChangeSynchronizer(object): def __init__(self, monitor, allChanges): self._monitor = monitor self._allChanges = allChanges @staticmethod def allObjects(theList): result = list() for obj in theList: result.append(obj) if isinstance(obj, CompositeObject): result.extend(ChangeSynchronizer.allObjects(obj.children())) if isinstance(obj, NoteOwner): result.extend(ChangeSynchronizer.allObjects(obj.notes())) if isinstance(obj, AttachmentOwner): result.extend(ChangeSynchronizer.allObjects(obj.attachments())) if isinstance(obj, Task): result.extend(obj.efforts()) return result def sync(self, lists): self.diskChanges = ChangeMonitor() self.conflictChanges = ChangeMonitor() self.notifier = AbstractNotifier.getSimple() self.memMap = dict() self.memOwnerMap = dict() self.diskMap = dict() self.diskOwnerMap = dict() for devGUID, changes in self._allChanges.items(): if devGUID == self._monitor.guid(): self.diskChanges = changes break self._allChanges[self._monitor.guid()] = self._monitor for memList, diskList in lists: self.mergeObjects(memList, diskList) # Cleanup monitor self._monitor.empty() for memList, diskList in lists: for obj in self.allObjects(memList.rootItems()): self._monitor.resetChanges(obj) # Merge conflict changes for devGUID, changes in self._allChanges.items(): if devGUID != self._monitor.guid(): changes.merge(self.conflictChanges) def notify(self, message): self.notifier.notify(_('Task Coach'), message, wx.ArtProvider.GetBitmap('taskcoach', size=wx.Size(32, 32))) def mergeObjects(self, memList, diskList): # Map id to object def addIds(objects, idMap, ownerMap, owner=None): for obj in objects: idMap[obj.id()] = obj if owner is not None: ownerMap[obj.id()] = owner if isinstance(obj, CompositeObject): addIds(obj.children(), idMap, ownerMap) if isinstance(obj, NoteOwner): addIds(obj.notes(), idMap, ownerMap, obj) if isinstance(obj, AttachmentOwner): addIds(obj.attachments(), idMap, ownerMap, obj) if isinstance(obj, Task): addIds(obj.efforts(), idMap, ownerMap) addIds(memList, self.memMap, self.memOwnerMap) addIds(diskList, self.diskMap, self.diskOwnerMap) self.mergeCompositeObjects(memList, diskList) self.mergeOwnedObjectsFromDisk(diskList) self.reparentObjects(memList, diskList) self.deletedObjects(memList) self.deletedOwnedObjects(memList) self.applyChanges(memList) def mergeCompositeObjects(self, memList, diskList): # First pass: new composite objects on disk. Don't handle # other (notes belonging to object, attachments, efforts) yet # because they may belong to a category. This assumes that # categories are the first domain class handled. for diskObject in diskList.allItemsSorted(): memChanges = self._monitor.getChanges(diskObject) deleted = memChanges is not None and '__del__' in memChanges diskChanges = self.diskChanges.getChanges(diskObject) if deleted and diskChanges is not None and '__del__' not in diskChanges and len(diskChanges) > 0: # "undelete" it memChanges.remove('__del__') deleted = False if diskObject.id() not in self.memMap and not deleted: if isinstance(diskObject, CompositeObject): # New children will be handled later. This assumes # that the parent() is not changed when removing a # child. for child in diskObject.children(): diskObject.removeChild(child) parent = diskObject.parent() if parent is not None and parent.id() in self.memMap: parent = self.memMap[parent.id()] parent.addChild(diskObject) diskObject.setParent(parent) elif parent is not None: # Parent deleted from memory; the task will be # top-level. diskObject.setParent(None) self.conflictChanges.addChange(diskObject, '__parent__') self.notify(_('"%s" became top-level because its parent was locally deleted.') % diskObject.subject()) memList.append(diskObject) self.memMap[diskObject.id()] = diskObject def mergeOwnedObjectsFromDisk(self, diskList): # Second pass: '******' objects (notes and attachments # currently) new on disk, and efforts. for obj in diskList.allItemsSorted(): if isinstance(obj, NoteOwner): self._handleNewOwnedObjectsOnDisk(obj.notes()) if isinstance(obj, AttachmentOwner): self._handleNewOwnedObjectsOnDisk(obj.attachments()) if isinstance(obj, Task): self._handleNewEffortsOnDisk(obj.efforts()) def _handleNewOwnedObjectsOnDisk(self, diskObjects): for diskObject in diskObjects: className = diskObject.__class__.__name__ if className.endswith('Attachment'): className = 'Attachment' if isinstance(diskObject, CompositeObject): children = diskObject.children()[:] memChanges = self._monitor.getChanges(diskObject) deleted = memChanges is not None and '__del__' in memChanges if diskObject.id() not in self.memMap and not deleted: addObject = True if isinstance(diskObject, CompositeObject): for child in diskObject.children(): diskObject.removeChild(child) parent = diskObject.parent() if parent is not None and parent.id() in self.memMap: parent = self.memMap[parent.id()] parent.addChild(diskObject) diskObject.setParent(parent) elif parent is not None: # Parent deleted from memory; the object # becomes top-level but its owner stays # the same. diskObject.setParent(None) while parent.parent() is not None: parent = parent.parent() diskOwner = self.diskOwnerMap[parent.id()] if diskOwner.id() in self.memMap: memOwner = self.memMap[diskOwner.id()] getattr(memOwner, 'add%s' % className)(diskObject) self.conflictChanges.addChange(diskObject, '__owner__') self.memOwnerMap[diskObject.id()] = memOwner self.notify(_('"%s" became top-level because its parent was locally deleted.') % diskObject.subject()) else: diskOwner = self.diskOwnerMap[diskObject.id()] if diskOwner.id() in self.memMap: memOwner = self.memMap[diskOwner.id()] getattr(memOwner, 'add%s' % className)(diskObject) self.memOwnerMap[diskObject.id()] = memOwner else: # Owner deleted. Just forget it. self.conflictChanges.addChange(diskObject, '__del__') addObject = False self.notify(_('"%s" was not kept because its owner ("%s") was locally deleted.') % (diskObject.subject(), diskOwner.subject())) else: diskOwner = self.diskOwnerMap[diskObject.id()] if diskOwner.id() in self.memMap: memOwner = self.memMap[diskOwner.id()] getattr(memOwner, 'add%s' % className)(diskObject) self.memOwnerMap[diskObject.id()] = memOwner else: # Forget it again... self.conflictChanges.addChange(diskObject, '__del__') addObject = False self.notify(_('"%s" was not kept because its owner ("%s") was locally deleted.') % (diskObject.subject(), diskOwner.subject())) if addObject: self.memMap[diskObject.id()] = diskObject if diskObject.id() in self.memMap: if isinstance(diskObject, CompositeObject): self._handleNewOwnedObjectsOnDisk(children) if isinstance(diskObject, NoteOwner): self._handleNewOwnedObjectsOnDisk(diskObject.notes()) if isinstance(diskObject, AttachmentOwner): self._handleNewOwnedObjectsOnDisk(diskObject.attachments()) def _handleNewEffortsOnDisk(self, diskEfforts): for diskEffort in diskEfforts: memChanges = self._monitor.getChanges(diskEffort) deleted = memChanges is not None and '__del__' in memChanges if diskEffort.id() not in self.memMap and not deleted: diskTask = diskEffort.parent() if diskTask.id() in self.memMap: memTask = self.memMap[diskTask.id()] diskEffort.setTask(memTask) self.memMap[diskEffort.id()] = diskEffort else: # Task deleted; forget it. self.conflictChanges.addChange(diskEffort, '__del__') self.notify(_('Effort discarded because its owner ("%s") was locally deleted.') % diskTask.subject()) def reparentObjects(self, memList, diskList): # Third pass: objects reparented on disk. for diskObject in self.allObjects(diskList): diskChanges = self.diskChanges.getChanges(diskObject) if diskChanges is not None and '__parent__' in diskChanges: memChanges = self._monitor.getChanges(diskObject) memObject = self.memMap[diskObject.id()] memList.remove(memObject) # Note: no conflict resolution for this one, # it would be a bit tricky... Instead, the # disk version wins. def sameParents(a, b): if a is None and b is None: return True elif a is None or b is None: return False return a.id() == b.id() parentConflict = False if memChanges is not None and '__parent__' in memChanges: if not sameParents(memObject.parent(), diskObject.parent()): parentConflict = True if memObject.parent() is not None: memObject.parent().removeChild(memObject) if parentConflict: diskParent = diskObject.parent() if diskParent is None: memObject.setParent(None) else: if diskParent.id() in self.memMap: memParent = self.memMap[diskParent.id()] memParent.addChild(memObject) memObject.setParent(memParent) else: # New parent deleted from memory... memObject.setParent(None) self.conflictChanges.addChange(memObject, '__parent__') else: diskParent = diskObject.parent() if diskParent is None: memObject.setParent(None) else: if diskParent.id() in self.memMap: memParent = self.memMap[diskParent.id()] memParent.addChild(memObject) memObject.setParent(memParent) else: memObject.setParent(None) self.conflictChanges.addChange(memObject, '__parent__') memList.append(memObject) def deletedObjects(self, memList): # Fourth pass: objects deleted from disk for memObject in memList.allItemsSorted(): diskChanges = self.diskChanges.getChanges(memObject) memChanges = self._monitor.getChanges(memObject) if diskChanges is not None and '__del__' in diskChanges: if (memChanges is None or '__del__' in memChanges or len(memChanges) == 0): memList.remove(memObject) del self.memMap[memObject.id()] if memObject.id() in self.memOwnerMap: del self.memOwnerMap[memObject.id()] else: # If there are local changes they win over deletion. self.diskMap[memObject.id()] = memObject self.diskChanges.resetChanges(memObject) def deletedOwnedObjects(self, memList): for obj in memList.allItemsSorted(): if isinstance(obj, NoteOwner): self._handleOwnedObjectsRemovedFromDisk(obj.notes()) if isinstance(obj, AttachmentOwner): self._handleOwnedObjectsRemovedFromDisk(obj.attachments()) if isinstance(obj, Task): self._handleEffortsRemovedFromDisk(obj.efforts()) def _handleOwnedObjectsRemovedFromDisk(self, memObjects): for memObject in memObjects: className = memObject.__class__.__name__ if className.endswith('Attachment'): className = 'Attachment' if isinstance(memObject, CompositeObject): self._handleOwnedObjectsRemovedFromDisk(memObject.children()) if isinstance(memObject, NoteOwner): self._handleOwnedObjectsRemovedFromDisk(memObject.notes()) if isinstance(memObject, AttachmentOwner): self._handleOwnedObjectsRemovedFromDisk(memObject.attachments()) diskChanges = self.diskChanges.getChanges(memObject) if diskChanges is not None and '__del__' in diskChanges: # Same remark as above if isinstance(memObject, CompositeObject): if memObject.parent() is None: getattr(self.memOwnerMap[memObject.id()], 'remove%s' % className)(memObject) else: self.memMap[memObject.parent().id()].removeChild(memObject) else: getattr(self.memOwnerMap[memObject.id()], 'remove%s' % className)(memObject) del self.memMap[memObject.id()] def _handleEffortsRemovedFromDisk(self, memEfforts): for memEffort in memEfforts: diskChanges = self.diskChanges.getChanges(memEffort) if diskChanges is not None and '__del__' in diskChanges: # Same remark as above self.memMap[memEffort.parent().id()].removeEffort(memEffort) del self.memMap[memEffort.id()] def applyChanges(self, memList): # Final: apply disk changes for memObject in self.allObjects(memList.rootItems()): diskChanges = self.diskChanges.getChanges(memObject) if diskChanges: memChanges = self._monitor.getChanges(memObject) diskObject = self.diskMap[memObject.id()] conflicts = [] for changeName in diskChanges: if changeName == '__parent__': pass # Already handled elif changeName.startswith('__add_category:'): categoryId = changeName[15:] if categoryId not in self.memMap: # Mmmh, deleted... conflicts.append(changeName) self.conflictChanges.addChange(memObject, '__del' + changeName[5:]) else: if memChanges is not None and \ '__del' + changeName[5:] in memChanges: conflicts.append(changeName) self.conflictChanges.addChange(memObject, '__del' + changeName[5:]) else: # Aaaaah finally theCategory = self.memMap[categoryId] memObject.addCategory(theCategory) theCategory.addCategorizable(memObject) elif changeName.startswith('__del_category:'): categoryId = changeName[15:] if categoryId in self.memMap: if memChanges is not None and \ '__add' + changeName[5:] in memChanges: conflicts.append(changeName) self.conflictChanges.addChange(memObject, '__add' + changeName[5:]) else: theCategory = self.memMap[categoryId] memObject.removeCategory(theCategory) theCategory.removeCategorizable(memObject) elif changeName == '__prerequisites__': diskPrereqs = set([self.memMap[obj.id()] for obj in diskObject.prerequisites()]) memPrereqs = set(memObject.prerequisites()) if memChanges is not None and \ '__prerequisites__' in memChanges and \ memPrereqs != diskPrereqs: conflicts.append('__prerequisites__') self.conflictChanges.addChange(memObject, '__prerequisites__') else: memObject.setPrerequisites(diskPrereqs) elif changeName == '__task__': # Effort changed task if memChanges is not None and \ '__task__' in memChanges and \ memObject.parent().id() != diskObject.parent().id(): conflicts.append('__task__') self.conflictChanges.addChange(memObject, '__task__') else: memObject.setTask(self.memMap[diskObject.parent().id()]) elif changeName == '__owner__': # This happens after a conflict if memChanges is not None and \ '__owner__' in memChanges and \ self.memOwnerMap[memObject.id()].id() != self.diskOwnerMap[diskObject.id()].id(): # Yet another conflict... Memory wins conflicts.append('__owner__') self.conflictChanges.addChange(memObject, '__owner__') else: className = memObject.__class__.__name__ if className.endsWith('Attachment'): className = 'Attachment' oldOwner = self.memOwnerMap[memObject.id()] newOwner = self.memOwnerMap[diskObject.id()] getattr(oldOwner, 'remove%s' % className)(memObject) getattr(newOwner, 'add%s' % className)(memObject) elif changeName == 'appearance': attrNames = ['foregroundColor', 'backgroundColor', 'font', 'icon', 'selectedIcon'] if memChanges is not None and \ 'appearance' in memChanges: for attrName in attrNames: if getattr(memObject, attrName)() != getattr(diskObject, attrName)(): conflicts.append(attrName) self.conflictChanges.addChange(memObject, 'appearance') else: for attrName in attrNames: getattr(memObject, 'set' + attrName[0].upper() + attrName[1:])(getattr(diskObject, attrName)()) elif changeName == 'expandedContexts': # Note: no conflict resolution for this one. memObject.expand(diskObject.isExpanded()) else: if changeName in ['start', 'stop']: getterName = 'get' + changeName[0].upper() + changeName[1:] else: getterName = changeName if memChanges is not None and \ changeName in memChanges and \ getattr(memObject, getterName)() != getattr(diskObject, getterName)(): conflicts.append(changeName) self.conflictChanges.addChange(memObject, changeName) else: getattr(memObject, 'set' + changeName[0].upper() + changeName[1:])(getattr(diskObject, getterName)()) if conflicts: self.notify(_('Conflicts detected for "%s".\nThe local version was used.') % memObject.subject())
class ChangeSynchronizer(object): def __init__(self, monitor, allChanges): self._monitor = monitor self._allChanges = allChanges @staticmethod def allObjects(theList): result = list() for obj in theList: result.append(obj) if isinstance(obj, CompositeObject): result.extend(ChangeSynchronizer.allObjects(obj.children())) if isinstance(obj, NoteOwner): result.extend(ChangeSynchronizer.allObjects(obj.notes())) if isinstance(obj, AttachmentOwner): result.extend(ChangeSynchronizer.allObjects(obj.attachments())) if isinstance(obj, Task): result.extend(obj.efforts()) return result def sync(self, lists): self.diskChanges = ChangeMonitor() self.conflictChanges = ChangeMonitor() self.notifier = AbstractNotifier.getSimple() self.memMap = dict() self.memOwnerMap = dict() self.diskMap = dict() self.diskOwnerMap = dict() for devGUID, changes in self._allChanges.items(): if devGUID == self._monitor.guid(): self.diskChanges = changes break self._allChanges[self._monitor.guid()] = self._monitor for memList, diskList in lists: self.mergeObjects(memList, diskList) # Cleanup monitor self._monitor.empty() for memList, diskList in lists: for obj in self.allObjects(memList.rootItems()): self._monitor.resetChanges(obj) # Merge conflict changes for devGUID, changes in self._allChanges.items(): if devGUID != self._monitor.guid(): changes.merge(self.conflictChanges) def notify(self, message): self.notifier.notify(_('Task Coach'), message, wx.ArtProvider.GetBitmap('taskcoach', size=wx.Size(32, 32))) def mergeObjects(self, memList, diskList): # Map id to object def addIds(objects, idMap, ownerMap, owner=None): for obj in objects: idMap[obj.id()] = obj if owner is not None: ownerMap[obj.id()] = owner if isinstance(obj, CompositeObject): addIds(obj.children(), idMap, ownerMap) if isinstance(obj, NoteOwner): addIds(obj.notes(), idMap, ownerMap, obj) if isinstance(obj, AttachmentOwner): addIds(obj.attachments(), idMap, ownerMap, obj) if isinstance(obj, Task): addIds(obj.efforts(), idMap, ownerMap) addIds(memList, self.memMap, self.memOwnerMap) addIds(diskList, self.diskMap, self.diskOwnerMap) self.mergeCompositeObjects(memList, diskList) self.mergeOwnedObjectsFromDisk(diskList) self.reparentObjects(memList, diskList) self.deletedObjects(memList) self.deletedOwnedObjects(memList) self.applyChanges(memList) def mergeCompositeObjects(self, memList, diskList): # First pass: new composite objects on disk. Don't handle # other (notes belonging to object, attachments, efforts) yet # because they may belong to a category. This assumes that # categories are the first domain class handled. for diskObject in diskList.allItemsSorted(): memChanges = self._monitor.getChanges(diskObject) deleted = memChanges is not None and '__del__' in memChanges diskChanges = self.diskChanges.getChanges(diskObject) if deleted and diskChanges is not None and '__del__' not in diskChanges and len(diskChanges) > 0: # "undelete" it memChanges.remove('__del__') deleted = False if diskObject.id() not in self.memMap and not deleted: if isinstance(diskObject, CompositeObject): # New children will be handled later. This assumes # that the parent() is not changed when removing a # child. for child in diskObject.children(): diskObject.removeChild(child) parent = diskObject.parent() if parent is not None and parent.id() in self.memMap: parent = self.memMap[parent.id()] parent.addChild(diskObject) diskObject.setParent(parent) elif parent is not None: # Parent deleted from memory; the task will be # top-level. diskObject.setParent(None) self.conflictChanges.addChange(diskObject, '__parent__') self.notify(_('"%s" became top-level because its parent was locally deleted.') % diskObject.subject()) memList.append(diskObject) self.memMap[diskObject.id()] = diskObject def mergeOwnedObjectsFromDisk(self, diskList): # Second pass: '******' objects (notes and attachments # currently) new on disk, and efforts. for obj in diskList.allItemsSorted(): if isinstance(obj, NoteOwner): self._handleNewOwnedObjectsOnDisk(obj.notes()) if isinstance(obj, AttachmentOwner): self._handleNewOwnedObjectsOnDisk(obj.attachments()) if isinstance(obj, Task): self._handleNewEffortsOnDisk(obj.efforts()) def _handleNewOwnedObjectsOnDisk(self, diskObjects): for diskObject in diskObjects: className = diskObject.__class__.__name__ if className.endswith('Attachment'): className = 'Attachment' if isinstance(diskObject, CompositeObject): children = diskObject.children()[:] memChanges = self._monitor.getChanges(diskObject) deleted = memChanges is not None and '__del__' in memChanges if diskObject.id() not in self.memMap and not deleted: addObject = True if isinstance(diskObject, CompositeObject): for child in diskObject.children(): diskObject.removeChild(child) parent = diskObject.parent() if parent is not None and parent.id() in self.memMap: parent = self.memMap[parent.id()] parent.addChild(diskObject) diskObject.setParent(parent) elif parent is not None: # Parent deleted from memory; the object # becomes top-level but its owner stays # the same. diskObject.setParent(None) while parent.parent() is not None: parent = parent.parent() diskOwner = self.diskOwnerMap[parent.id()] if diskOwner.id() in self.memMap: memOwner = self.memMap[diskOwner.id()] getattr(memOwner, 'add%s' % className)(diskObject) self.conflictChanges.addChange(diskObject, '__owner__') self.memOwnerMap[diskObject.id()] = memOwner self.notify(_('"%s" became top-level because its parent was locally deleted.') % diskObject.subject()) else: diskOwner = self.diskOwnerMap[diskObject.id()] if diskOwner.id() in self.memMap: memOwner = self.memMap[diskOwner.id()] getattr(memOwner, 'add%s' % className)(diskObject) self.memOwnerMap[diskObject.id()] = memOwner else: # Owner deleted. Just forget it. self.conflictChanges.addChange(diskObject, '__del__') addObject = False else: diskOwner = self.diskOwnerMap[diskObject.id()] if diskOwner.id() in self.memMap: memOwner = self.memMap[diskOwner.id()] getattr(memOwner, 'add%s' % className)(diskObject) self.memOwnerMap[diskObject.id()] = memOwner else: # Forget it again... self.conflictChanges.addChange(diskObject, '__del__') addObject = False if addObject: self.memMap[diskObject.id()] = diskObject if diskObject.id() in self.memMap: if isinstance(diskObject, CompositeObject): self._handleNewOwnedObjectsOnDisk(children) if isinstance(diskObject, NoteOwner): self._handleNewOwnedObjectsOnDisk(diskObject.notes()) if isinstance(diskObject, AttachmentOwner): self._handleNewOwnedObjectsOnDisk(diskObject.attachments()) def _handleNewEffortsOnDisk(self, diskEfforts): for diskEffort in diskEfforts: memChanges = self._monitor.getChanges(diskEffort) deleted = memChanges is not None and '__del__' in memChanges if diskEffort.id() not in self.memMap and not deleted: diskTask = diskEffort.parent() if diskTask.id() in self.memMap: memTask = self.memMap[diskTask.id()] diskEffort.setTask(memTask) self.memMap[diskEffort.id()] = diskEffort else: # Task deleted; forget it. self.conflictChanges.addChange(diskEffort, '__del__') def reparentObjects(self, memList, diskList): # Third pass: objects reparented on disk. for diskObject in self.allObjects(diskList): diskChanges = self.diskChanges.getChanges(diskObject) if diskChanges is not None and '__parent__' in diskChanges: memChanges = self._monitor.getChanges(diskObject) memObject = self.memMap[diskObject.id()] memList.remove(memObject) # Note: no conflict resolution for this one, # it would be a bit tricky... Instead, the # disk version wins. def sameParents(a, b): if a is None and b is None: return True elif a is None or b is None: return False return a.id() == b.id() parentConflict = False if memChanges is not None and '__parent__' in memChanges: if not sameParents(memObject.parent(), diskObject.parent()): parentConflict = True if memObject.parent() is not None: memObject.parent().removeChild(memObject) if parentConflict: diskParent = diskObject.parent() if diskParent is None: memObject.setParent(None) else: if diskParent.id() in self.memMap: memParent = self.memMap[diskParent.id()] memParent.addChild(memObject) memObject.setParent(memParent) else: # New parent deleted from memory... memObject.setParent(None) self.conflictChanges.addChange(memObject, '__parent__') else: diskParent = diskObject.parent() if diskParent is None: memObject.setParent(None) else: if diskParent.id() in self.memMap: memParent = self.memMap[diskParent.id()] memParent.addChild(memObject) memObject.setParent(memParent) else: memObject.setParent(None) self.conflictChanges.addChange(memObject, '__parent__') memList.append(memObject) def deletedObjects(self, memList): # Fourth pass: objects deleted from disk for memObject in memList.allItemsSorted(): diskChanges = self.diskChanges.getChanges(memObject) memChanges = self._monitor.getChanges(memObject) if diskChanges is not None and '__del__' in diskChanges: if (memChanges is None or '__del__' in memChanges or len(memChanges) == 0): memList.remove(memObject) del self.memMap[memObject.id()] if memObject.id() in self.memOwnerMap: del self.memOwnerMap[memObject.id()] else: # If there are local changes they win over deletion. self.diskMap[memObject.id()] = memObject self.diskChanges.resetChanges(memObject) def deletedOwnedObjects(self, memList): for obj in memList.allItemsSorted(): if isinstance(obj, NoteOwner): self._handleOwnedObjectsRemovedFromDisk(obj.notes()) if isinstance(obj, AttachmentOwner): self._handleOwnedObjectsRemovedFromDisk(obj.attachments()) if isinstance(obj, Task): self._handleEffortsRemovedFromDisk(obj.efforts()) def _handleOwnedObjectsRemovedFromDisk(self, memObjects): for memObject in memObjects: className = memObject.__class__.__name__ if className.endswith('Attachment'): className = 'Attachment' if isinstance(memObject, CompositeObject): self._handleOwnedObjectsRemovedFromDisk(memObject.children()) if isinstance(memObject, NoteOwner): self._handleOwnedObjectsRemovedFromDisk(memObject.notes()) if isinstance(memObject, AttachmentOwner): self._handleOwnedObjectsRemovedFromDisk(memObject.attachments()) diskChanges = self.diskChanges.getChanges(memObject) if diskChanges is not None and '__del__' in diskChanges: # Same remark as above if isinstance(memObject, CompositeObject): if memObject.parent() is None: getattr(self.memOwnerMap[memObject.id()], 'remove%s' % className)(memObject) else: self.memMap[memObject.parent().id()].removeChild(memObject) else: getattr(self.memOwnerMap[memObject.id()], 'remove%s' % className)(memObject) del self.memMap[memObject.id()] def _handleEffortsRemovedFromDisk(self, memEfforts): for memEffort in memEfforts: diskChanges = self.diskChanges.getChanges(memEffort) if diskChanges is not None and '__del__' in diskChanges: # Same remark as above self.memMap[memEffort.parent().id()].removeEffort(memEffort) del self.memMap[memEffort.id()] def applyChanges(self, memList): # Final: apply disk changes for memObject in self.allObjects(memList.rootItems()): diskChanges = self.diskChanges.getChanges(memObject) if diskChanges: memChanges = self._monitor.getChanges(memObject) diskObject = self.diskMap[memObject.id()] conflicts = [] for changeName in diskChanges: if changeName == '__parent__': pass # Already handled elif changeName.startswith('__add_category:'): categoryId = changeName[15:] if categoryId not in self.memMap: # Mmmh, deleted... conflicts.append(changeName) self.conflictChanges.addChange(memObject, '__del' + changeName[5:]) else: if memChanges is not None and \ '__del' + changeName[5:] in memChanges: conflicts.append(changeName) self.conflictChanges.addChange(memObject, '__del' + changeName[5:]) else: # Aaaaah finally theCategory = self.memMap[categoryId] memObject.addCategory(theCategory) theCategory.addCategorizable(memObject) elif changeName.startswith('__del_category:'): categoryId = changeName[15:] if categoryId in self.memMap: if memChanges is not None and \ '__add' + changeName[5:] in memChanges: conflicts.append(changeName) self.conflictChanges.addChange(memObject, '__add' + changeName[5:]) else: theCategory = self.memMap[categoryId] memObject.removeCategory(theCategory) theCategory.removeCategorizable(memObject) elif changeName == '__prerequisites__': diskPrereqs = set([self.memMap[obj.id()] for obj in diskObject.prerequisites()]) memPrereqs = set(memObject.prerequisites()) if memChanges is not None and \ '__prerequisites__' in memChanges and \ memPrereqs != diskPrereqs: conflicts.append('__prerequisites__') self.conflictChanges.addChange(memObject, '__prerequisites__') else: memObject.setPrerequisites(diskPrereqs) elif changeName == '__task__': # Effort changed task if memChanges is not None and \ '__task__' in memChanges and \ memObject.parent().id() != diskObject.parent().id(): conflicts.append('__task__') self.conflictChanges.addChange(memObject, '__task__') else: memObject.setTask(self.memMap[diskObject.parent().id()]) elif changeName == '__owner__': # This happens after a conflict if memChanges is not None and \ '__owner__' in memChanges and \ self.memOwnerMap[memObject.id()].id() != self.diskOwnerMap[diskObject.id()].id(): # Yet another conflict... Memory wins conflicts.append('__owner__') self.conflictChanges.addChange(memObject, '__owner__') else: className = memObject.__class__.__name__ if className.endsWith('Attachment'): className = 'Attachment' oldOwner = self.memOwnerMap[memObject.id()] newOwner = self.memOwnerMap[diskObject.id()] getattr(oldOwner, 'remove%s' % className)(memObject) getattr(newOwner, 'add%s' % className)(memObject) elif changeName == 'appearance': attrNames = ['foregroundColor', 'backgroundColor', 'font', 'icon', 'selectedIcon'] if memChanges is not None and \ 'appearance' in memChanges: for attrName in attrNames: if getattr(memObject, attrName)() != getattr(diskObject, attrName)(): conflicts.append(attrName) self.conflictChanges.addChange(memObject, 'appearance') else: for attrName in attrNames: getattr(memObject, 'set' + attrName[0].upper() + attrName[1:])(getattr(diskObject, attrName)()) elif changeName == 'expandedContexts': # Note: no conflict resolution for this one. memObject.expand(diskObject.isExpanded()) else: if changeName in ['start', 'stop']: getterName = 'get' + changeName[0].upper() + changeName[1:] else: getterName = changeName if memChanges is not None and \ changeName in memChanges and \ getattr(memObject, getterName)() != getattr(diskObject, getterName)(): conflicts.append(changeName) self.conflictChanges.addChange(memObject, changeName) else: getattr(memObject, 'set' + changeName[0].upper() + changeName[1:])(getattr(diskObject, getterName)()) if conflicts: self.notify(_('Conflicts detected for "%s".\nThe local version was used.') % memObject.subject())