Пример #1
0
    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)
Пример #2
0
    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)
Пример #3
0
    def setUp(self):
        self.monitor = ChangeMonitor()
        self.monitor.monitorClass(self.klass)

        self.list = self.listClass()
        self.monitor.monitorCollection(self.list)
        self.obj = self.klass(subject=u'New object')
        self.list.append(self.obj)
Пример #4
0
 def read(self):
     allChanges = dict()
     tree = ET.parse(self.__fd)
     for devNode in tree.getroot().findall('device'):
         id_ = devNode.attrib['guid']
         mon = ChangeMonitor(id_)
         for objNode in devNode.findall('obj'):
             if objNode.text:
                 changes = set(objNode.text.split(','))
             else:
                 changes = set()
             mon.setChanges(objNode.attrib['id'], changes)
         allChanges[id_] = mon
     return allChanges
Пример #5
0
 def read(self):
     allChanges = dict()
     tree = ET.parse(self.__fd)
     for devNode in tree.getroot().findall('device'):
         id_ = devNode.attrib['guid']
         mon = ChangeMonitor(id_)
         for objNode in devNode.findall('obj'):
             if objNode.text:
                 changes = set(objNode.text.split(','))
             else:
                 changes = set()
             mon.setChanges(objNode.attrib['id'], changes)
         allChanges[id_] = mon
     return allChanges
Пример #6
0
    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)
Пример #7
0
 def read(self):
     allChanges = dict()
     tree = ET.parse(self.__fd)
     for devNode in tree.getroot().findall("device"):
         id_ = devNode.attrib["guid"]
         mon = ChangeMonitor(id_)
         for objNode in devNode.findall("obj"):
             if objNode.text:
                 changes = set(objNode.text.split(","))
             else:
                 changes = set()
             mon.setChanges(objNode.attrib["id"], changes)
         allChanges[id_] = mon
     return allChanges
Пример #8
0
    def setUp(self):
        self.monitor = ChangeMonitor()
        self.monitor.monitorClass(self.klass)

        self.list = self.listClass()
        self.monitor.monitorCollection(self.list)
        self.obj = self.klass(subject=u'New object')
        self.list.append(self.obj)
Пример #9
0
 def __init__(self, *args, **kwargs):
     self.__filename = self.__lastFilename = ''
     self.__needSave = self.__loading = False
     self.__tasks = task.TaskList()
     self.__categories = category.CategoryList()
     self.__notes = note.NoteContainer()
     self.__efforts = effort.EffortList(self.tasks())
     self.__guid = generate()
     self.__syncMLConfig = createDefaultSyncConfig(self.__guid)
     self.__monitor = ChangeMonitor()
     self.__changes = dict()
     self.__changes[self.__monitor.guid()] = self.__monitor
     self.__changedOnDisk = False
     if kwargs.pop('poll', True):
         self.__notifier = TaskCoachFilesystemPollerNotifier(self)
     else:
         self.__notifier = TaskCoachFilesystemNotifier(self)
     self.__saving = False
     for collection in [self.__tasks, self.__categories, self.__notes]:
         self.__monitor.monitorCollection(collection)
     for domainClass in [task.Task, category.Category, note.Note, effort.Effort,
                         attachment.FileAttachment, attachment.URIAttachment,
                         attachment.MailAttachment]:
         self.__monitor.monitorClass(domainClass)
     super(TaskFile, self).__init__(*args, **kwargs)
     # Register for tasks, categories, efforts and notes being changed so we 
     # can monitor when the task file needs saving (i.e. is 'dirty'):
     for container in self.tasks(), self.categories(), self.notes():
         for eventType in container.modificationEventTypes():
             self.registerObserver(self.onDomainObjectAddedOrRemoved,
                                   eventType, eventSource=container)
         
     for eventType in (base.Object.markDeletedEventType(),
                       base.Object.markNotDeletedEventType()):
         self.registerObserver(self.onDomainObjectAddedOrRemoved, eventType)
         
     for eventType in task.Task.modificationEventTypes():
         if not eventType.startswith('pubsub'):
             self.registerObserver(self.onTaskChanged_Deprecated, eventType)
     pub.subscribe(self.onTaskChanged, 'pubsub.task')
     for eventType in effort.Effort.modificationEventTypes():
         self.registerObserver(self.onEffortChanged, eventType)
     for eventType in note.Note.modificationEventTypes():
         if not eventType.startswith('pubsub'):
             self.registerObserver(self.onNoteChanged_Deprecated, eventType)
     pub.subscribe(self.onNoteChanged, 'pubsub.note')
     for eventType in category.Category.modificationEventTypes():
         if not eventType.startswith('pubsub'):
             self.registerObserver(self.onCategoryChanged_Deprecated, 
                                   eventType)
     pub.subscribe(self.onCategoryChanged, 'pubsub.category')
     for eventType in attachment.FileAttachment.modificationEventTypes() + \
                      attachment.URIAttachment.modificationEventTypes() + \
                      attachment.MailAttachment.modificationEventTypes():
         if not eventType.startswith('pubsub'):
             self.registerObserver(self.onAttachmentChanged_Deprecated, eventType) 
     pub.subscribe(self.onAttachmentChanged, 'pubsub.attachment')
Пример #10
0
class MonitorBaseTest(test.TestCase):
    klass = Object
    listClass = ObservableList

    def setUp(self):
        self.monitor = ChangeMonitor()
        self.monitor.monitorClass(self.klass)

        self.list = self.listClass()
        self.monitor.monitorCollection(self.list)
        self.obj = self.klass(subject=u'New object')
        self.list.append(self.obj)

    def tearDown(self):
        self.monitor.unmonitorClass(self.klass)
        self.monitor.unmonitorCollection(self.list)
    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)
Пример #12
0
class MonitorBaseTest(test.TestCase):
    klass = Object
    listClass = ObservableList

    def setUp(self):
        self.monitor = ChangeMonitor()
        self.monitor.monitorClass(self.klass)

        self.list = self.listClass()
        self.monitor.monitorCollection(self.list)
        self.obj = self.klass(subject=u'New object')
        self.list.append(self.obj)

    def tearDown(self):
        self.monitor.unmonitorClass(self.klass)
        self.monitor.unmonitorCollection(self.list)
Пример #13
0
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())
Пример #14
0
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())
Пример #15
0
    def __init__(self, *args, **kwargs):
        self.__filename = self.__lastFilename = ''
        self.__needSave = self.__loading = False
        self.__tasks = task.TaskList()
        self.__categories = category.CategoryList()
        self.__notes = note.NoteContainer()
        self.__efforts = effort.EffortList(self.tasks())
        self.__guid = generate()
        self.__syncMLConfig = createDefaultSyncConfig(self.__guid)
        self.__monitor = ChangeMonitor()
        self.__changes = dict()
        self.__changes[self.__monitor.guid()] = self.__monitor
        self.__changedOnDisk = False
        if kwargs.pop('poll', True):
            self.__notifier = TaskCoachFilesystemPollerNotifier(self)
        else:
            self.__notifier = TaskCoachFilesystemNotifier(self)
        self.__saving = False
        for collection in [self.__tasks, self.__categories, self.__notes]:
            self.__monitor.monitorCollection(collection)
        for domainClass in [
                task.Task, category.Category, note.Note, effort.Effort,
                attachment.FileAttachment, attachment.URIAttachment,
                attachment.MailAttachment
        ]:
            self.__monitor.monitorClass(domainClass)
        super(TaskFile, self).__init__(*args, **kwargs)
        # Register for tasks, categories, efforts and notes being changed so we
        # can monitor when the task file needs saving (i.e. is 'dirty'):
        for container in self.tasks(), self.categories(), self.notes():
            for eventType in container.modificationEventTypes():
                self.registerObserver(self.onDomainObjectAddedOrRemoved,
                                      eventType,
                                      eventSource=container)

        for eventType in (base.Object.markDeletedEventType(),
                          base.Object.markNotDeletedEventType()):
            self.registerObserver(self.onDomainObjectAddedOrRemoved, eventType)

        for eventType in task.Task.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onTaskChanged_Deprecated, eventType)
        pub.subscribe(self.onTaskChanged, 'pubsub.task')
        for eventType in effort.Effort.modificationEventTypes():
            self.registerObserver(self.onEffortChanged, eventType)
        for eventType in note.Note.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onNoteChanged_Deprecated, eventType)
        pub.subscribe(self.onNoteChanged, 'pubsub.note')
        for eventType in category.Category.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onCategoryChanged_Deprecated,
                                      eventType)
        pub.subscribe(self.onCategoryChanged, 'pubsub.category')
        for eventType in attachment.FileAttachment.modificationEventTypes() + \
                         attachment.URIAttachment.modificationEventTypes() + \
                         attachment.MailAttachment.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onAttachmentChanged_Deprecated,
                                      eventType)
        pub.subscribe(self.onAttachmentChanged, 'pubsub.attachment')
Пример #16
0
class TaskFile(patterns.Observer):
    def __init__(self, *args, **kwargs):
        self.__filename = self.__lastFilename = ''
        self.__needSave = self.__loading = False
        self.__tasks = task.TaskList()
        self.__categories = category.CategoryList()
        self.__notes = note.NoteContainer()
        self.__efforts = effort.EffortList(self.tasks())
        self.__guid = generate()
        self.__syncMLConfig = createDefaultSyncConfig(self.__guid)
        self.__monitor = ChangeMonitor()
        self.__changes = dict()
        self.__changes[self.__monitor.guid()] = self.__monitor
        self.__changedOnDisk = False
        if kwargs.pop('poll', True):
            self.__notifier = TaskCoachFilesystemPollerNotifier(self)
        else:
            self.__notifier = TaskCoachFilesystemNotifier(self)
        self.__saving = False
        for collection in [self.__tasks, self.__categories, self.__notes]:
            self.__monitor.monitorCollection(collection)
        for domainClass in [task.Task, category.Category, note.Note, effort.Effort,
                            attachment.FileAttachment, attachment.URIAttachment,
                            attachment.MailAttachment]:
            self.__monitor.monitorClass(domainClass)
        super(TaskFile, self).__init__(*args, **kwargs)
        # Register for tasks, categories, efforts and notes being changed so we 
        # can monitor when the task file needs saving (i.e. is 'dirty'):
        for container in self.tasks(), self.categories(), self.notes():
            for eventType in container.modificationEventTypes():
                self.registerObserver(self.onDomainObjectAddedOrRemoved,
                                      eventType, eventSource=container)
            
        for eventType in (base.Object.markDeletedEventType(),
                          base.Object.markNotDeletedEventType()):
            self.registerObserver(self.onDomainObjectAddedOrRemoved, eventType)
            
        for eventType in task.Task.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onTaskChanged_Deprecated, eventType)
        pub.subscribe(self.onTaskChanged, 'pubsub.task')
        for eventType in effort.Effort.modificationEventTypes():
            self.registerObserver(self.onEffortChanged, eventType)
        for eventType in note.Note.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onNoteChanged_Deprecated, eventType)
        pub.subscribe(self.onNoteChanged, 'pubsub.note')
        for eventType in category.Category.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onCategoryChanged_Deprecated, 
                                      eventType)
        pub.subscribe(self.onCategoryChanged, 'pubsub.category')
        for eventType in attachment.FileAttachment.modificationEventTypes() + \
                         attachment.URIAttachment.modificationEventTypes() + \
                         attachment.MailAttachment.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onAttachmentChanged_Deprecated, eventType) 
        pub.subscribe(self.onAttachmentChanged, 'pubsub.attachment')

    def __str__(self):
        return self.filename()
    
    def __contains__(self, item):
        return item in self.tasks() or item in self.notes() or \
               item in self.categories() or item in self.efforts()

    def monitor(self):
        return self.__monitor

    def categories(self):
        return self.__categories
    
    def notes(self):
        return self.__notes
    
    def tasks(self):
        return self.__tasks
    
    def efforts(self):
        return self.__efforts

    def syncMLConfig(self):
        return self.__syncMLConfig

    def guid(self):
        return self.__guid

    def changes(self):
        return self.__changes

    def setSyncMLConfig(self, config):
        self.__syncMLConfig = config
        self.markDirty()

    def isEmpty(self):
        return 0 == len(self.categories()) == len(self.tasks()) == len(self.notes())
            
    def onDomainObjectAddedOrRemoved(self, event):  # pylint: disable=W0613
        if self.__loading or self.__saving:
            return
        self.markDirty()

    def onTaskChanged(self, newValue, sender):
        if self.__loading or self.__saving:
            return
        if sender in self.tasks():
            self.markDirty()
                    
    def onTaskChanged_Deprecated(self, event):
        if self.__loading:
            return
        changedTasks = [changedTask for changedTask in event.sources() \
                        if changedTask in self.tasks()]
        if changedTasks:
            self.markDirty()
            for changedTask in changedTasks:
                changedTask.markDirty()
            
    def onEffortChanged(self, event):
        if self.__loading or self.__saving:
            return
        changedEfforts = [changedEffort for changedEffort in event.sources() if \
                          changedEffort.task() in self.tasks()]
        if changedEfforts:
            self.markDirty()
            for changedEffort in changedEfforts:
                changedEffort.markDirty()
                
    def onCategoryChanged_Deprecated(self, event):
        if self.__loading or self.__saving:
            return
        changedCategories = [changedCategory for changedCategory in event.sources() if \
                             changedCategory in self.categories()]
        if changedCategories:
            self.markDirty()
            # Mark all categorizables belonging to the changed category dirty; 
            # this is needed because in SyncML/vcard world, categories are not 
            # first-class objects. Instead, each task/contact/etc has a 
            # categories property which is a comma-separated list of category
            # names. So, when a category name changes, every associated
            # categorizable changes.
            for changedCategory in changedCategories:
                for categorizable in changedCategory.categorizables():
                    categorizable.markDirty()
            
    def onCategoryChanged(self, newValue, sender):
        if self.__loading or self.__saving:
            return
        changedCategories = [changedCategory for changedCategory in [sender] if \
                             changedCategory in self.categories()]
        if changedCategories:
            self.markDirty()
            # Mark all categorizables belonging to the changed category dirty; 
            # this is needed because in SyncML/vcard world, categories are not 
            # first-class objects. Instead, each task/contact/etc has a 
            # categories property which is a comma-separated list of category
            # names. So, when a category name changes, every associated
            # categorizable changes.
            for changedCategory in changedCategories:
                for categorizable in changedCategory.categorizables():
                    categorizable.markDirty()
            
    def onNoteChanged_Deprecated(self, event):
        if self.__loading:
            return
        # A note may be in self.notes() or it may be a note of another 
        # domain object.
        self.markDirty()
        for changedNote in event.sources():
            changedNote.markDirty()
            
    def onNoteChanged(self, newValue, sender):
        if self.__loading:
            return
        # A note may be in self.notes() or it may be a note of another 
        # domain object.
        self.markDirty()
        sender.markDirty()
            
    def onAttachmentChanged(self, newValue, sender):
        if self.__loading or self.__saving:
            return
        # Attachments don't know their owner, so we can't check whether the
        # attachment is actually in the task file. Assume it is.
        self.markDirty()
            
    def onAttachmentChanged_Deprecated(self, event):
        if self.__loading:
            return
        # Attachments don't know their owner, so we can't check whether the
        # attachment is actually in the task file. Assume it is.
        self.markDirty()
        for changedAttachment in event.sources():
            changedAttachment.markDirty()

    def setFilename(self, filename):
        if filename == self.__filename:
            return
        self.__lastFilename = filename or self.__filename
        self.__filename = filename
        self.__notifier.setFilename(filename)
        pub.sendMessage('taskfile.filenameChanged', filename=filename)
    
    def setLastFilename(self, filename):
        self.__lastFilename = filename
        
    def filename(self):
        return self.__filename
        
    def lastFilename(self):
        return self.__lastFilename

    def isDirty(self):
        return self.__needSave

    def markDirty(self, force=False):
        if force or not self.__needSave:
            self.__needSave = True
            pub.sendMessage('taskfile.dirty', taskFile=self)
                
    def markClean(self):
        if self.__needSave:
            self.__needSave = False
            pub.sendMessage('taskfile.clean', taskFile=self)

    def onFileChanged(self):
        if not self.__saving:
            import wx # Not really clean but we're in another thread...
            self.__changedOnDisk = True
            wx.CallAfter(pub.sendMessage, 'taskfile.changed', taskFile=self)

    @patterns.eventSource
    def clear(self, regenerate=True, event=None):
        pub.sendMessage('taskfile.aboutToClear', taskFile=self)
        try:
            self.tasks().clear(event=event)
            self.categories().clear(event=event)
            self.notes().clear(event=event)
            if regenerate:
                self.__guid = generate()
                self.__syncMLConfig = createDefaultSyncConfig(self.__guid)
        finally:
            pub.sendMessage('taskfile.justCleared', taskFile=self)

    def close(self):
        if os.path.exists(self.filename()):
            changes = xml.ChangesXMLReader(self.filename() + '.delta').read()
            del changes[self.__monitor.guid()]
            xml.ChangesXMLWriter(file(self.filename() + '.delta', 'wb')).write(changes)

        self.setFilename('')
        self.__guid = generate()
        self.clear()
        self.__monitor.reset()
        self.markClean()
        self.__changedOnDisk = False

    def stop(self):
        self.__notifier.stop()

    def _read(self, fd):
        return xml.XMLReader(fd).read()
        
    def exists(self):
        return os.path.isfile(self.__filename)
        
    def _openForRead(self):
        return file(self.__filename, 'rU')

    def _openForWrite(self):
        name = getTemporaryFileName(os.path.split(self.__filename)[0])
        return name, file(name, 'w')
    
    def load(self, filename=None):
        pub.sendMessage('taskfile.aboutToRead', taskFile=self)
        self.__loading = True
        if filename:
            self.setFilename(filename)
        try:
            if self.exists():
                fd = self._openForRead()
                tasks, categories, globalcategories, notes, syncMLConfig, changes, guid = self._read(fd)
                fd.close()
            else:
                tasks = []
                categories = []
                notes = []
                changes = dict()
                guid = generate()
                syncMLConfig = createDefaultSyncConfig(guid)
            self.clear()
            self.__monitor.reset()
            self.__changes = changes
            self.__changes[self.__monitor.guid()] = self.__monitor
            self.categories().extend(categories)
            self.tasks().extend(tasks)
            self.notes().extend(notes)
            def registerOtherObjects(objects):
                for obj in objects:
                    if isinstance(obj, base.CompositeObject):
                        registerOtherObjects(obj.children())
                    if isinstance(obj, note.NoteOwner):
                        registerOtherObjects(obj.notes())
                    if isinstance(obj, attachment.AttachmentOwner):
                        registerOtherObjects(obj.attachments())
                    if isinstance(obj, task.Task):
                        registerOtherObjects(obj.efforts())
                    if isinstance(obj, note.Note) or \
                           isinstance(obj, attachment.Attachment) or \
                           isinstance(obj, effort.Effort):
                        self.__monitor.setChanges(obj.id(), set())
            registerOtherObjects(self.categories().rootItems())
            registerOtherObjects(self.tasks().rootItems())
            registerOtherObjects(self.notes().rootItems())
            self.__monitor.resetAllChanges()
            self.__syncMLConfig = syncMLConfig
            self.__guid = guid

            if os.path.exists(self.filename()):
                # We need to reset the changes on disk because we're up to date.
                xml.ChangesXMLWriter(file(self.filename() + '.delta', 'wb')).write(self.__changes)
        except:
            self.setFilename('')
            raise
        finally:
            self.__loading = False
            self.markClean()
            self.__changedOnDisk = False
        pub.sendMessage('taskfile.justRead', taskFile=self)
        
    def save(self):
        pub.sendMessage('taskfile.aboutToSave', taskFile=self)
        # When encountering a problem while saving (disk full,
        # computer on fire), if we were writing directly to the file,
        # it's lost. So write to a temporary file and rename it if
        # everything went OK.
        self.__saving = True
        try:
            self.mergeDiskChanges()

            if self.__needSave or not os.path.exists(self.__filename):
                name, fd = self._openForWrite()
                xml.XMLWriter(fd).write(self.tasks(), self.categories(), self.notes(),
                                        self.syncMLConfig(), self.guid())
                fd.close()
                if os.path.exists(self.__filename):  # Not using self.exists() because DummyFile.exists returns True
                    os.remove(self.__filename)
                if name is not None:  # Unit tests (AutoSaver)
                    os.rename(name, self.__filename)

            self.markClean()
        finally:
            self.__saving = False
            self.__notifier.saved()

    def mergeDiskChanges(self):
        self.__loading = True
        try:
            if os.path.exists(self.__filename): # Not using self.exists() because DummyFile.exists returns True
                # Instead of writing the content of memory, merge changes
                # with the on-disk version and save the result.
                self.__monitor.freeze()
                try:
                    fd = self._openForRead()
                    tasks, categories, globalcategories, notes, syncMLConfig, allChanges, guid = self._read(fd)
                    fd.close()

                    self.__changes = allChanges

                    if self.__saving:
                        for devGUID, changes in self.__changes.items():
                            if devGUID != self.__monitor.guid():
                                changes.merge(self.__monitor)

                    sync = ChangeSynchronizer(self.__monitor, allChanges)

                    sync.sync(
                        [(self.categories(), category.CategoryList(categories)),
                         (self.tasks(), task.TaskList(tasks)),
                         (self.notes(), note.NoteContainer(notes))]
                        )

                    self.__changes[self.__monitor.guid()] = self.__monitor
                finally:
                    self.__monitor.thaw()
            else:
                self.__changes = {self.__monitor.guid(): self.__monitor}

            self.__monitor.resetAllChanges()
            name, fd = self._openForWrite()
            xml.ChangesXMLWriter(fd).write(self.changes())
            fd.close()
            if os.path.exists(self.__filename + '.delta'):
                os.remove(self.__filename + '.delta')
            if name is not None: # Unit tests (AutoSaver)
                os.rename(name, self.__filename + '.delta')

            self.__changedOnDisk = False
        finally:
            self.__loading = False

    def saveas(self, filename):
        self.setFilename(filename)
        self.save()

    def merge(self, filename):
        mergeFile = self.__class__()
        mergeFile.load(filename)
        self.__loading = True
        categoryMap = dict()
        self.tasks().removeItems(self.objectsToOverwrite(self.tasks(), mergeFile.tasks()))
        self.rememberCategoryLinks(categoryMap, self.tasks())
        self.tasks().extend(mergeFile.tasks().rootItems())
        self.notes().removeItems(self.objectsToOverwrite(self.notes(), mergeFile.notes()))
        self.rememberCategoryLinks(categoryMap, self.notes())
        self.notes().extend(mergeFile.notes().rootItems())
        self.categories().removeItems(self.objectsToOverwrite(self.categories(),
                                                              mergeFile.categories()))
        self.categories().extend(mergeFile.categories().rootItems())
        self.restoreCategoryLinks(categoryMap)
        mergeFile.close()
        self.__loading = False
        self.markDirty(force=True)

    def objectsToOverwrite(self, originalObjects, objectsToMerge):
        objectsToOverwrite = []
        for domainObject in objectsToMerge:
            try:
                objectsToOverwrite.append(originalObjects.getObjectById(domainObject.id()))
            except IndexError:
                pass
        return objectsToOverwrite
        
    def rememberCategoryLinks(self, categoryMap, categorizables):
        for categorizable in categorizables:
            for categoryToLinkLater in categorizable.categories():
                categoryMap.setdefault(categoryToLinkLater.id(), []).append(categorizable)
            
    def restoreCategoryLinks(self, categoryMap):
        categories = self.categories()
        for categoryId, categorizables in categoryMap.iteritems():
            try:
                categoryToLink = categories.getObjectById(categoryId)
            except IndexError:
                continue  # Subcategory was removed by the merge
            for categorizable in categorizables:
                categorizable.addCategory(categoryToLink)
                categoryToLink.addCategorizable(categorizable)
    
    def needSave(self):
        return not self.__loading and self.__needSave

    def changedOnDisk(self):
        return self.__changedOnDisk

    def beginSync(self):
        self.__loading = True

    def endSync(self):
        self.__loading = False
        self.markDirty()
Пример #17
0
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())
Пример #18
0
class TaskFile(patterns.Observer):
    def __init__(self, *args, **kwargs):
        self.__filename = self.__lastFilename = ''
        self.__needSave = self.__loading = False
        self.__tasks = task.TaskList()
        self.__categories = category.CategoryList()
        self.__notes = note.NoteContainer()
        self.__efforts = effort.EffortList(self.tasks())
        self.__guid = generate()
        self.__syncMLConfig = createDefaultSyncConfig(self.__guid)
        self.__monitor = ChangeMonitor()
        self.__changes = dict()
        self.__changes[self.__monitor.guid()] = self.__monitor
        self.__changedOnDisk = False
        if kwargs.pop('poll', True):
            self.__notifier = TaskCoachFilesystemPollerNotifier(self)
        else:
            self.__notifier = TaskCoachFilesystemNotifier(self)
        self.__saving = False
        for collection in [self.__tasks, self.__categories, self.__notes]:
            self.__monitor.monitorCollection(collection)
        for domainClass in [
                task.Task, category.Category, note.Note, effort.Effort,
                attachment.FileAttachment, attachment.URIAttachment,
                attachment.MailAttachment
        ]:
            self.__monitor.monitorClass(domainClass)
        super(TaskFile, self).__init__(*args, **kwargs)
        # Register for tasks, categories, efforts and notes being changed so we
        # can monitor when the task file needs saving (i.e. is 'dirty'):
        for container in self.tasks(), self.categories(), self.notes():
            for eventType in container.modificationEventTypes():
                self.registerObserver(self.onDomainObjectAddedOrRemoved,
                                      eventType,
                                      eventSource=container)

        for eventType in (base.Object.markDeletedEventType(),
                          base.Object.markNotDeletedEventType()):
            self.registerObserver(self.onDomainObjectAddedOrRemoved, eventType)

        for eventType in task.Task.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onTaskChanged_Deprecated, eventType)
        pub.subscribe(self.onTaskChanged, 'pubsub.task')
        for eventType in effort.Effort.modificationEventTypes():
            self.registerObserver(self.onEffortChanged, eventType)
        for eventType in note.Note.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onNoteChanged_Deprecated, eventType)
        pub.subscribe(self.onNoteChanged, 'pubsub.note')
        for eventType in category.Category.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onCategoryChanged_Deprecated,
                                      eventType)
        pub.subscribe(self.onCategoryChanged, 'pubsub.category')
        for eventType in attachment.FileAttachment.modificationEventTypes() + \
                         attachment.URIAttachment.modificationEventTypes() + \
                         attachment.MailAttachment.modificationEventTypes():
            if not eventType.startswith('pubsub'):
                self.registerObserver(self.onAttachmentChanged_Deprecated,
                                      eventType)
        pub.subscribe(self.onAttachmentChanged, 'pubsub.attachment')

    def __str__(self):
        return self.filename()

    def __contains__(self, item):
        return item in self.tasks() or item in self.notes() or \
               item in self.categories() or item in self.efforts()

    def monitor(self):
        return self.__monitor

    def categories(self):
        return self.__categories

    def notes(self):
        return self.__notes

    def tasks(self):
        return self.__tasks

    def efforts(self):
        return self.__efforts

    def syncMLConfig(self):
        return self.__syncMLConfig

    def guid(self):
        return self.__guid

    def changes(self):
        return self.__changes

    def setSyncMLConfig(self, config):
        self.__syncMLConfig = config
        self.markDirty()

    def isEmpty(self):
        return 0 == len(self.categories()) == len(self.tasks()) == len(
            self.notes())

    def onDomainObjectAddedOrRemoved(self, event):  # pylint: disable=W0613
        if self.__loading or self.__saving:
            return
        self.markDirty()

    def onTaskChanged(self, newValue, sender):
        if self.__loading or self.__saving:
            return
        if sender in self.tasks():
            self.markDirty()

    def onTaskChanged_Deprecated(self, event):
        if self.__loading:
            return
        changedTasks = [changedTask for changedTask in event.sources() \
                        if changedTask in self.tasks()]
        if changedTasks:
            self.markDirty()
            for changedTask in changedTasks:
                changedTask.markDirty()

    def onEffortChanged(self, event):
        if self.__loading or self.__saving:
            return
        changedEfforts = [changedEffort for changedEffort in event.sources() if \
                          changedEffort.task() in self.tasks()]
        if changedEfforts:
            self.markDirty()
            for changedEffort in changedEfforts:
                changedEffort.markDirty()

    def onCategoryChanged_Deprecated(self, event):
        if self.__loading or self.__saving:
            return
        changedCategories = [changedCategory for changedCategory in event.sources() if \
                             changedCategory in self.categories()]
        if changedCategories:
            self.markDirty()
            # Mark all categorizables belonging to the changed category dirty;
            # this is needed because in SyncML/vcard world, categories are not
            # first-class objects. Instead, each task/contact/etc has a
            # categories property which is a comma-separated list of category
            # names. So, when a category name changes, every associated
            # categorizable changes.
            for changedCategory in changedCategories:
                for categorizable in changedCategory.categorizables():
                    categorizable.markDirty()

    def onCategoryChanged(self, newValue, sender):
        if self.__loading or self.__saving:
            return
        changedCategories = [changedCategory for changedCategory in [sender] if \
                             changedCategory in self.categories()]
        if changedCategories:
            self.markDirty()
            # Mark all categorizables belonging to the changed category dirty;
            # this is needed because in SyncML/vcard world, categories are not
            # first-class objects. Instead, each task/contact/etc has a
            # categories property which is a comma-separated list of category
            # names. So, when a category name changes, every associated
            # categorizable changes.
            for changedCategory in changedCategories:
                for categorizable in changedCategory.categorizables():
                    categorizable.markDirty()

    def onNoteChanged_Deprecated(self, event):
        if self.__loading:
            return
        # A note may be in self.notes() or it may be a note of another
        # domain object.
        self.markDirty()
        for changedNote in event.sources():
            changedNote.markDirty()

    def onNoteChanged(self, newValue, sender):
        if self.__loading:
            return
        # A note may be in self.notes() or it may be a note of another
        # domain object.
        self.markDirty()
        sender.markDirty()

    def onAttachmentChanged(self, newValue, sender):
        if self.__loading or self.__saving:
            return
        # Attachments don't know their owner, so we can't check whether the
        # attachment is actually in the task file. Assume it is.
        self.markDirty()

    def onAttachmentChanged_Deprecated(self, event):
        if self.__loading:
            return
        # Attachments don't know their owner, so we can't check whether the
        # attachment is actually in the task file. Assume it is.
        self.markDirty()
        for changedAttachment in event.sources():
            changedAttachment.markDirty()

    def setFilename(self, filename):
        if filename == self.__filename:
            return
        self.__lastFilename = filename or self.__filename
        self.__filename = filename
        self.__notifier.setFilename(filename)
        pub.sendMessage('taskfile.filenameChanged', filename=filename)

    def filename(self):
        return self.__filename

    def lastFilename(self):
        return self.__lastFilename

    def isDirty(self):
        return self.__needSave

    def markDirty(self, force=False):
        if force or not self.__needSave:
            self.__needSave = True
            pub.sendMessage('taskfile.dirty', taskFile=self)

    def markClean(self):
        if self.__needSave:
            self.__needSave = False
            pub.sendMessage('taskfile.clean', taskFile=self)

    def onFileChanged(self):
        if not self.__saving:
            import wx  # Not really clean but we're in another thread...
            self.__changedOnDisk = True
            wx.CallAfter(pub.sendMessage, 'taskfile.changed', taskFile=self)

    @patterns.eventSource
    def clear(self, regenerate=True, event=None):
        pub.sendMessage('taskfile.aboutToClear', taskFile=self)
        try:
            self.tasks().clear(event=event)
            self.categories().clear(event=event)
            self.notes().clear(event=event)
            if regenerate:
                self.__guid = generate()
                self.__syncMLConfig = createDefaultSyncConfig(self.__guid)
        finally:
            pub.sendMessage('taskfile.justCleared', taskFile=self)

    def close(self):
        if os.path.exists(self.filename()):
            changes = xml.ChangesXMLReader(self.filename() + '.delta').read()
            del changes[self.__monitor.guid()]
            xml.ChangesXMLWriter(file(self.filename() + '.delta',
                                      'wb')).write(changes)

        self.setFilename('')
        self.__guid = generate()
        self.clear()
        self.__monitor.reset()
        self.markClean()
        self.__changedOnDisk = False

    def stop(self):
        self.__notifier.stop()

    def _read(self, fd):
        return xml.XMLReader(fd).read()

    def exists(self):
        return os.path.isfile(self.__filename)

    def _openForWrite(self, suffix=''):
        return SafeWriteFile(self.__filename + suffix)

    def _openForRead(self):
        return file(self.__filename, 'rU')

    def load(self, filename=None):
        pub.sendMessage('taskfile.aboutToRead', taskFile=self)
        self.__loading = True
        if filename:
            self.setFilename(filename)
        try:
            if self.exists():
                fd = self._openForRead()
                try:
                    tasks, categories, notes, syncMLConfig, changes, guid = self._read(
                        fd)
                finally:
                    fd.close()
            else:
                tasks = []
                categories = []
                notes = []
                changes = dict()
                guid = generate()
                syncMLConfig = createDefaultSyncConfig(guid)
            self.clear()
            self.__monitor.reset()
            self.__changes = changes
            self.__changes[self.__monitor.guid()] = self.__monitor
            self.categories().extend(categories)
            self.tasks().extend(tasks)
            self.notes().extend(notes)

            def registerOtherObjects(objects):
                for obj in objects:
                    if isinstance(obj, base.CompositeObject):
                        registerOtherObjects(obj.children())
                    if isinstance(obj, note.NoteOwner):
                        registerOtherObjects(obj.notes())
                    if isinstance(obj, attachment.AttachmentOwner):
                        registerOtherObjects(obj.attachments())
                    if isinstance(obj, task.Task):
                        registerOtherObjects(obj.efforts())
                    if isinstance(obj, note.Note) or \
                           isinstance(obj, attachment.Attachment) or \
                           isinstance(obj, effort.Effort):
                        self.__monitor.setChanges(obj.id(), set())

            registerOtherObjects(self.categories().rootItems())
            registerOtherObjects(self.tasks().rootItems())
            registerOtherObjects(self.notes().rootItems())
            self.__monitor.resetAllChanges()
            self.__syncMLConfig = syncMLConfig
            self.__guid = guid

            if os.path.exists(self.filename()):
                # We need to reset the changes on disk because we're up to date.
                xml.ChangesXMLWriter(file(self.filename() + '.delta',
                                          'wb')).write(self.__changes)
        except:
            self.setFilename('')
            raise
        finally:
            self.__loading = False
            self.markClean()
            self.__changedOnDisk = False
            pub.sendMessage('taskfile.justRead', taskFile=self)

    def save(self):
        try:
            pub.sendMessage('taskfile.aboutToSave', taskFile=self)
        except:
            pass
        # When encountering a problem while saving (disk full,
        # computer on fire), if we were writing directly to the file,
        # it's lost. So write to a temporary file and rename it if
        # everything went OK.
        self.__saving = True
        try:
            self.mergeDiskChanges()

            if self.__needSave or not os.path.exists(self.__filename):
                fd = self._openForWrite()
                try:
                    xml.XMLWriter(fd).write(self.tasks(), self.categories(),
                                            self.notes(), self.syncMLConfig(),
                                            self.guid())
                finally:
                    fd.close()

            self.markClean()
        finally:
            self.__saving = False
            self.__notifier.saved()
            try:
                pub.sendMessage('taskfile.justSaved', taskFile=self)
            except:
                pass

    def mergeDiskChanges(self):
        self.__loading = True
        try:
            if os.path.exists(
                    self.__filename
            ):  # Not using self.exists() because DummyFile.exists returns True
                # Instead of writing the content of memory, merge changes
                # with the on-disk version and save the result.
                self.__monitor.freeze()
                try:
                    fd = self._openForRead()
                    tasks, categories, notes, syncMLConfig, allChanges, guid = self._read(
                        fd)
                    fd.close()

                    self.__changes = allChanges

                    if self.__saving:
                        for devGUID, changes in self.__changes.items():
                            if devGUID != self.__monitor.guid():
                                changes.merge(self.__monitor)

                    sync = ChangeSynchronizer(self.__monitor, allChanges)

                    sync.sync([(self.categories(),
                                category.CategoryList(categories)),
                               (self.tasks(), task.TaskList(tasks)),
                               (self.notes(), note.NoteContainer(notes))])

                    self.__changes[self.__monitor.guid()] = self.__monitor
                finally:
                    self.__monitor.thaw()
            else:
                self.__changes = {self.__monitor.guid(): self.__monitor}

            self.__monitor.resetAllChanges()
            fd = self._openForWrite('.delta')
            try:
                xml.ChangesXMLWriter(fd).write(self.changes())
            finally:
                fd.close()

            self.__changedOnDisk = False
        finally:
            self.__loading = False

    def saveas(self, filename):
        if os.path.exists(filename):
            os.remove(filename)
        if os.path.exists(filename + '.delta'):
            os.remove(filename + '.delta')
        self.setFilename(filename)
        self.save()

    def merge(self, filename):
        mergeFile = self.__class__()
        mergeFile.load(filename)
        self.__loading = True
        categoryMap = dict()
        self.tasks().removeItems(
            self.objectsToOverwrite(self.tasks(), mergeFile.tasks()))
        self.rememberCategoryLinks(categoryMap, self.tasks())
        self.tasks().extend(mergeFile.tasks().rootItems())
        self.notes().removeItems(
            self.objectsToOverwrite(self.notes(), mergeFile.notes()))
        self.rememberCategoryLinks(categoryMap, self.notes())
        self.notes().extend(mergeFile.notes().rootItems())
        self.categories().removeItems(
            self.objectsToOverwrite(self.categories(), mergeFile.categories()))
        self.categories().extend(mergeFile.categories().rootItems())
        self.restoreCategoryLinks(categoryMap)
        mergeFile.close()
        self.__loading = False
        self.markDirty(force=True)

    def objectsToOverwrite(self, originalObjects, objectsToMerge):
        objectsToOverwrite = []
        for domainObject in objectsToMerge:
            try:
                objectsToOverwrite.append(
                    originalObjects.getObjectById(domainObject.id()))
            except IndexError:
                pass
        return objectsToOverwrite

    def rememberCategoryLinks(self, categoryMap, categorizables):
        for categorizable in categorizables:
            for categoryToLinkLater in categorizable.categories():
                categoryMap.setdefault(categoryToLinkLater.id(),
                                       []).append(categorizable)

    def restoreCategoryLinks(self, categoryMap):
        categories = self.categories()
        for categoryId, categorizables in categoryMap.iteritems():
            try:
                categoryToLink = categories.getObjectById(categoryId)
            except IndexError:
                continue  # Subcategory was removed by the merge
            for categorizable in categorizables:
                categorizable.addCategory(categoryToLink)
                categoryToLink.addCategorizable(categorizable)

    def needSave(self):
        return not self.__loading and self.__needSave

    def changedOnDisk(self):
        return self.__changedOnDisk

    def beginSync(self):
        self.__loading = True

    def endSync(self):
        self.__loading = False
        self.markDirty()
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())