diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index b9b1e5f9..b7332478 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -227,10 +227,10 @@ class NWIndex(): if theItem.itemParent is None: logger.info("Not indexing orphaned item '%s'", tHandle) return False - if self.theProject.projTree.isTrashRoot(theItem.itemParent): + if theItem.itemClass == nwItemClass.TRASH: logger.debug("Not indexing trash item '%s'", tHandle) return False - if self.theProject.projTree.getItemClass(tHandle) == nwItemClass.ARCHIVE: + if theItem.itemClass == nwItemClass.ARCHIVE: logger.debug("Not indexing archived item '%s'", tHandle) return False diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 47397e5c..913e159a 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -53,7 +53,7 @@ logger = logging.getLogger(__name__) class NWProject(): - FILE_VERSION = "1.4" + FILE_VERSION = "1.4" # The current project file format version def __init__(self, theParent): @@ -129,6 +129,7 @@ class NWProject(): newItem.setClass(rootClass) newItem.setStatus(0) self.projTree.append(None, None, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def newFolder(self, folderName, folderClass, pHandle): @@ -140,6 +141,7 @@ class NWProject(): newItem.setClass(folderClass) newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def newFile(self, fileName, fileClass, pHandle): @@ -156,6 +158,7 @@ class NWProject(): newItem.setClass(fileClass) newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def trashFolder(self): @@ -168,6 +171,7 @@ class NWProject(): newItem.setType(nwItemType.TRASH) newItem.setClass(nwItemClass.TRASH) self.projTree.append(None, None, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle return trashHandle @@ -605,10 +609,15 @@ class NWProject(): self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) self.mainConf.saveRecentCache() - self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) + # Check the project tree consistency + for tItem in self.projTree: + tHandle = tItem.itemHandle + logger.verbose("Checking item '%s'", tHandle) + if not self.projTree.updateItemData(tHandle): + logger.error("There was a problem item '%s', and it has been removed", tHandle) + del self.projTree[tHandle] # The file will be re-added as orphaned self._scanProjectFolder() - self._checkProjectTree() self._loadProjectLocalisation() self.updateWordCounts() @@ -617,6 +626,7 @@ class NWProject(): self._writeLockFile() self.setProjectChanged(False) + self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) return True @@ -1341,17 +1351,6 @@ class NWProject(): xEntry.text = aValue return - def _checkProjectTree(self): - """Check the project tree and make sure all items have sensible - values. - """ - for tItem in self.projTree: - tHandle = tItem.itemHandle - logger.verbose("Checking item '%s'", tHandle) - if tItem.itemRoot is None: - self.projTree.updateItemRoot(tHandle) - logger.warning("Corrected the root setting of item '%s'", tHandle) - def _scanProjectFolder(self): """Scan the project folder and check that the files in it are also in the project XML file. If they aren't, import them as @@ -1441,6 +1440,7 @@ class NWProject(): orphItem.setClass(oClass) orphItem.setLayout(oLayout) self.projTree.append(oHandle, oParent, orphItem) + self.projTree.updateItemData(orphItem.itemHandle) if noWhere: self.theParent.makeAlert(self.tr( diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index b146c5a1..ddef88b7 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -113,7 +113,6 @@ class NWTree(): self._projTree[tHandle] = nwItem self._treeOrder.append(tHandle) - self.updateItemRoot(tHandle) self._setTreeChanged(True) return True @@ -208,9 +207,30 @@ class NWTree(): return novelWords, noteWords ## - # Tree Structure Methods + # Tree Item Methods ## + def updateItemData(self, tHandle): + """Update the root item handle of a given item. Returns True if + a root was found and data updated, otherwise False. + """ + tItem = self.__getitem__(tHandle) + if tItem is None: + return False + + iItem = tItem + for _ in range(nwConst.MAX_DEPTH + 1): + if iItem.itemParent is None: + tItem.setRoot(iItem.itemHandle) + tItem.setClass(iItem.itemClass) + return True + else: + iItem = self.__getitem__(iItem.itemParent) + if iItem is None: + return False + + return False + def checkType(self, tHandle, itemType): """Return true of item exists and is of the specified item type. """ @@ -219,70 +239,6 @@ class NWTree(): return False return tItem.itemType == itemType - def trashRoot(self): - """Returns the handle of the trash folder, or None if there - isn't one. - """ - if self._trashRoot: - return self._trashRoot - return None - - def isTrashRoot(self, tHandle): - """Check if a handle is the trash folder. - """ - if self._trashRoot is None: - return False - return tHandle == self._trashRoot - - def archiveRoot(self): - """Returns the handle of the archive folder, or None if there - isn't one. - """ - if self._archRoot: - return self._archRoot - return None - - def findRoot(self, theClass): - """Find the root item for a given class. - Note: This returns the first item for class CUSTOM. - """ - for aRoot in self._treeRoots: - tItem = self.__getitem__(aRoot) - if tItem is None: - continue - if theClass == tItem.itemClass: - return tItem.itemHandle - return None - - def isRoot(self, tHandle): - """Check if a handle is a root item. - """ - return tHandle in self._treeRoots - - def updateItemRoot(self, tHandle): - """Update the root item handle of a given item. - """ - tItem = self.__getitem__(tHandle) - iItem = tItem - if iItem is not None: - for _ in range(nwConst.MAX_DEPTH + 1): - if iItem.itemParent is None: - tItem.setRoot(iItem.itemHandle) - return iItem.itemHandle - else: - tHandle = iItem.itemParent - iItem = self.__getitem__(tHandle) - return None - - def getItemClass(self, tHandle): - """Return the class of a given item. - """ - tItem = self.__getitem__(tHandle) - if tItem is not None: - if tItem.itemRoot in self._treeRoots: - return self._treeRoots[tItem.itemRoot].itemClass - return nwItemClass.NO_CLASS - def getItemPath(self, tHandle): """Iterate upwards in the tree until we find the item with parent None, the root item, and return the list of handles. @@ -305,6 +261,49 @@ class NWTree(): tTree.append(tHandle) return tTree + ## + # Tree Root Methods + ## + + def isRoot(self, tHandle): + """Check if a handle is a root item. + """ + return tHandle in self._treeRoots + + def isTrashRoot(self, tHandle): + """Check if a handle is the trash folder. + """ + if self._trashRoot is None: + return False + return tHandle == self._trashRoot + + def trashRoot(self): + """Returns the handle of the trash folder, or None if there + isn't one. + """ + if self._trashRoot: + return self._trashRoot + return None + + def archiveRoot(self): + """Returns the handle of the archive folder, or None if there + isn't one. + """ + if self._archRoot: + return self._archRoot + return None + + def findRoot(self, theClass): + """Find the first root item for a given class. + """ + for aRoot in self._treeRoots: + tItem = self.__getitem__(aRoot) + if tItem is None: + continue + if theClass == tItem.itemClass: + return tItem.itemHandle + return None + ## # Setters ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index a1cd9081..18b2b9f5 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -280,7 +280,7 @@ class GuiProjectTree(QTreeWidget): if nwItem.itemType != nwItemType.FILE: return True - # This is a new files, so let's add some content + # This is a new file, so let's add some content newDoc = NWDoc(self.theProject, tHandle) curTxt = newDoc.readDocument() if curTxt is None: diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 0bb593a6..feb1662d 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -780,12 +780,13 @@ class GuiBuildNovel(QDialog): if theItem is None: return False - if not theItem.isExported and not ignoreFlag: + if not (theItem.isExported or ignoreFlag): return False isNone = theItem.itemType != nwItemType.FILE isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemClass == nwItemClass.NO_CLASS + isNone |= theItem.itemClass == nwItemClass.ARCHIVE isNone |= theItem.itemClass == nwItemClass.TRASH isNone |= theItem.itemParent == self.theProject.projTree.trashRoot() isNone |= theItem.itemParent is None @@ -799,9 +800,6 @@ class GuiBuildNovel(QDialog): if isNovel and not novelFiles: return False - if self.theProject.projTree.getItemClass(theItem.itemHandle) == nwItemClass.ARCHIVE: - return False - return True def _saveDocument(self, theFmt): diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 5e5d3eb9..8070cbf2 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -278,13 +278,14 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): tHandle = theProject.trashFolder() assert theProject.projTree[tHandle] is not None xItem.setParent(tHandle) + theProject.projTree.updateItemData(xItem.itemHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Create the archive root aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE) assert theProject.projTree[aHandle] is not None xItem.setParent(aHandle) - xItem.setRoot(aHandle) + theProject.projTree.updateItemData(xItem.itemHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 6216a1b1..1e3e7d94 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -103,7 +103,7 @@ def mockItems(mockGUI): ("a000000000002", None, itemE), ("a000000000003", None, itemF), ("a000000000004", None, itemG), - ("b000000000002", "a000000000002", itemH), + ("b000000000002", "a000000000004", itemH), ] return theItems @@ -120,22 +120,23 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert theTree._handleSeed == 42 # Check that tree is empty (calls NWTree.__bool__) - assert not theTree + assert bool(theTree) is False # Check for archive and trash folders assert theTree.trashRoot() is None assert theTree.archiveRoot() is None - assert not theTree.isTrashRoot("a000000000003") + assert theTree.isTrashRoot("a000000000003") is False aHandles = [] for tHandle, pHandle, nwItem in mockItems: aHandles.append(tHandle) - assert theTree.append(tHandle, pHandle, nwItem) + assert theTree.append(tHandle, pHandle, nwItem) is True + assert theTree.updateItemData(tHandle) is True - assert theTree._treeChanged + assert theTree._treeChanged is True # Check that tree is not empty (calls __bool__) - assert theTree + assert bool(theTree) is True # Check the number of elements (calls __len__) assert len(theTree) == len(mockItems) @@ -151,6 +152,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert theTree.trashRoot() == "a000000000003" assert theTree.archiveRoot() == "a000000000002" assert theTree.isTrashRoot("a000000000003") + assert theTree.isRoot("a000000000002") # Try to add another trash folder itemT = NWItem(theProject) @@ -169,14 +171,15 @@ def testCoreTree_BuildTree(mockGUI, mockItems): itemT._class = nwItemClass.NOVEL itemT._layout = nwItemLayout.DOCUMENT - assert theTree.append(None, None, itemT) + assert theTree.append(None, None, itemT) is True + assert theTree.updateItemData(itemT.itemHandle) is True assert len(theTree) == len(mockItems) + 1 theList = theTree.handles() assert theList[-1] == "73475cb40a568" # Try to add existing handle - assert not theTree.append("73475cb40a568", None, itemT) + assert theTree.append("73475cb40a568", None, itemT) is False assert len(theTree) == len(mockItems) + 1 # Delete a non-existing item @@ -207,7 +210,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_Methods(mockGUI, mockItems): +def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): """Test various class methods. """ theProject = NWProject(mockGUI) @@ -215,9 +218,27 @@ def testCoreTree_Methods(mockGUI, mockItems): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) + # Update item data, nonsense handle + assert theTree.updateItemData("stuff") is False + + # Update item data, invalid item parent + corrParent = theTree["b000000000001"].itemParent + theTree["b000000000001"].setParent("0000000000000") + assert theTree.updateItemData("b000000000001") is False + + # Update item data, valid item parent + theTree["b000000000001"].setParent(corrParent) + assert theTree.updateItemData("b000000000001") is True + + # Update item data, root is unreachable + with monkeypatch.context() as mp: + mp.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 0) + assert theTree.updateItemData("b000000000001") is False + # Chech type assert theTree.checkType("blabla", nwItemType.FILE) is False assert theTree.checkType("b000000000001", nwItemType.FILE) is False @@ -366,6 +387,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) @@ -377,8 +399,8 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'Novel' b'' - b'Act One' + b'type="FOLDER" class="NOVEL">Act One' + b'' b'Chapter One' @@ -393,7 +415,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'class="TRASH">Trash' b'Characters' - b'Jane Doe' b'' @@ -418,6 +440,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) theTree._treeOrder.append("stuff")