Пример #1
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)
Пример #2
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)
Пример #3
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())
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())
Пример #5
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()
Пример #6
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()