Clean up handling of root and class info for items

This commit is contained in:
Veronica Berglyd Olsen
2022-04-03 17:45:17 +02:00
parent 37970bd446
commit bdde6a2080
7 changed files with 121 additions and 100 deletions
+2 -2
View File
@@ -227,10 +227,10 @@ class NWIndex():
if theItem.itemParent is None: if theItem.itemParent is None:
logger.info("Not indexing orphaned item '%s'", tHandle) logger.info("Not indexing orphaned item '%s'", tHandle)
return False return False
if self.theProject.projTree.isTrashRoot(theItem.itemParent): if theItem.itemClass == nwItemClass.TRASH:
logger.debug("Not indexing trash item '%s'", tHandle) logger.debug("Not indexing trash item '%s'", tHandle)
return False return False
if self.theProject.projTree.getItemClass(tHandle) == nwItemClass.ARCHIVE: if theItem.itemClass == nwItemClass.ARCHIVE:
logger.debug("Not indexing archived item '%s'", tHandle) logger.debug("Not indexing archived item '%s'", tHandle)
return False return False
+14 -14
View File
@@ -53,7 +53,7 @@ logger = logging.getLogger(__name__)
class NWProject(): class NWProject():
FILE_VERSION = "1.4" FILE_VERSION = "1.4" # The current project file format version
def __init__(self, theParent): def __init__(self, theParent):
@@ -129,6 +129,7 @@ class NWProject():
newItem.setClass(rootClass) newItem.setClass(rootClass)
newItem.setStatus(0) newItem.setStatus(0)
self.projTree.append(None, None, newItem) self.projTree.append(None, None, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
def newFolder(self, folderName, folderClass, pHandle): def newFolder(self, folderName, folderClass, pHandle):
@@ -140,6 +141,7 @@ class NWProject():
newItem.setClass(folderClass) newItem.setClass(folderClass)
newItem.setStatus(0) newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem) self.projTree.append(None, pHandle, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
def newFile(self, fileName, fileClass, pHandle): def newFile(self, fileName, fileClass, pHandle):
@@ -156,6 +158,7 @@ class NWProject():
newItem.setClass(fileClass) newItem.setClass(fileClass)
newItem.setStatus(0) newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem) self.projTree.append(None, pHandle, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
def trashFolder(self): def trashFolder(self):
@@ -168,6 +171,7 @@ class NWProject():
newItem.setType(nwItemType.TRASH) newItem.setType(nwItemType.TRASH)
newItem.setClass(nwItemClass.TRASH) newItem.setClass(nwItemClass.TRASH)
self.projTree.append(None, None, newItem) self.projTree.append(None, None, newItem)
self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
return trashHandle return trashHandle
@@ -605,10 +609,15 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache() 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._scanProjectFolder()
self._checkProjectTree()
self._loadProjectLocalisation() self._loadProjectLocalisation()
self.updateWordCounts() self.updateWordCounts()
@@ -617,6 +626,7 @@ class NWProject():
self._writeLockFile() self._writeLockFile()
self.setProjectChanged(False) self.setProjectChanged(False)
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
return True return True
@@ -1341,17 +1351,6 @@ class NWProject():
xEntry.text = aValue xEntry.text = aValue
return 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): def _scanProjectFolder(self):
"""Scan the project folder and check that the files in it are """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 also in the project XML file. If they aren't, import them as
@@ -1441,6 +1440,7 @@ class NWProject():
orphItem.setClass(oClass) orphItem.setClass(oClass)
orphItem.setLayout(oLayout) orphItem.setLayout(oLayout)
self.projTree.append(oHandle, oParent, orphItem) self.projTree.append(oHandle, oParent, orphItem)
self.projTree.updateItemData(orphItem.itemHandle)
if noWhere: if noWhere:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
+65 -66
View File
@@ -113,7 +113,6 @@ class NWTree():
self._projTree[tHandle] = nwItem self._projTree[tHandle] = nwItem
self._treeOrder.append(tHandle) self._treeOrder.append(tHandle)
self.updateItemRoot(tHandle)
self._setTreeChanged(True) self._setTreeChanged(True)
return True return True
@@ -208,9 +207,30 @@ class NWTree():
return novelWords, noteWords 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): def checkType(self, tHandle, itemType):
"""Return true of item exists and is of the specified item type. """Return true of item exists and is of the specified item type.
""" """
@@ -219,70 +239,6 @@ class NWTree():
return False return False
return tItem.itemType == itemType 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): def getItemPath(self, tHandle):
"""Iterate upwards in the tree until we find the item with """Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles. parent None, the root item, and return the list of handles.
@@ -305,6 +261,49 @@ class NWTree():
tTree.append(tHandle) tTree.append(tHandle)
return tTree 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 # Setters
## ##
+1 -1
View File
@@ -280,7 +280,7 @@ class GuiProjectTree(QTreeWidget):
if nwItem.itemType != nwItemType.FILE: if nwItem.itemType != nwItemType.FILE:
return True 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) newDoc = NWDoc(self.theProject, tHandle)
curTxt = newDoc.readDocument() curTxt = newDoc.readDocument()
if curTxt is None: if curTxt is None:
+2 -4
View File
@@ -780,12 +780,13 @@ class GuiBuildNovel(QDialog):
if theItem is None: if theItem is None:
return False return False
if not theItem.isExported and not ignoreFlag: if not (theItem.isExported or ignoreFlag):
return False return False
isNone = theItem.itemType != nwItemType.FILE isNone = theItem.itemType != nwItemType.FILE
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.ARCHIVE
isNone |= theItem.itemClass == nwItemClass.TRASH isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.itemParent == self.theProject.projTree.trashRoot() isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
isNone |= theItem.itemParent is None isNone |= theItem.itemParent is None
@@ -799,9 +800,6 @@ class GuiBuildNovel(QDialog):
if isNovel and not novelFiles: if isNovel and not novelFiles:
return False return False
if self.theProject.projTree.getItemClass(theItem.itemHandle) == nwItemClass.ARCHIVE:
return False
return True return True
def _saveDocument(self, theFmt): def _saveDocument(self, theFmt):
+2 -1
View File
@@ -278,13 +278,14 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
tHandle = theProject.trashFolder() tHandle = theProject.trashFolder()
assert theProject.projTree[tHandle] is not None assert theProject.projTree[tHandle] is not None
xItem.setParent(tHandle) xItem.setParent(tHandle)
theProject.projTree.updateItemData(xItem.itemHandle)
assert theIndex.scanText(xHandle, "Hello World!") is False assert theIndex.scanText(xHandle, "Hello World!") is False
# Create the archive root # Create the archive root
aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE) aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE)
assert theProject.projTree[aHandle] is not None assert theProject.projTree[aHandle] is not None
xItem.setParent(aHandle) xItem.setParent(aHandle)
xItem.setRoot(aHandle) theProject.projTree.updateItemData(xItem.itemHandle)
assert theIndex.scanText(xHandle, "Hello World!") is False assert theIndex.scanText(xHandle, "Hello World!") is False
# Make some usable items # Make some usable items
+35 -12
View File
@@ -103,7 +103,7 @@ def mockItems(mockGUI):
("a000000000002", None, itemE), ("a000000000002", None, itemE),
("a000000000003", None, itemF), ("a000000000003", None, itemF),
("a000000000004", None, itemG), ("a000000000004", None, itemG),
("b000000000002", "a000000000002", itemH), ("b000000000002", "a000000000004", itemH),
] ]
return theItems return theItems
@@ -120,22 +120,23 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree._handleSeed == 42 assert theTree._handleSeed == 42
# Check that tree is empty (calls NWTree.__bool__) # Check that tree is empty (calls NWTree.__bool__)
assert not theTree assert bool(theTree) is False
# Check for archive and trash folders # Check for archive and trash folders
assert theTree.trashRoot() is None assert theTree.trashRoot() is None
assert theTree.archiveRoot() is None assert theTree.archiveRoot() is None
assert not theTree.isTrashRoot("a000000000003") assert theTree.isTrashRoot("a000000000003") is False
aHandles = [] aHandles = []
for tHandle, pHandle, nwItem in mockItems: for tHandle, pHandle, nwItem in mockItems:
aHandles.append(tHandle) 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__) # Check that tree is not empty (calls __bool__)
assert theTree assert bool(theTree) is True
# Check the number of elements (calls __len__) # Check the number of elements (calls __len__)
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
@@ -151,6 +152,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree.trashRoot() == "a000000000003" assert theTree.trashRoot() == "a000000000003"
assert theTree.archiveRoot() == "a000000000002" assert theTree.archiveRoot() == "a000000000002"
assert theTree.isTrashRoot("a000000000003") assert theTree.isTrashRoot("a000000000003")
assert theTree.isRoot("a000000000002")
# Try to add another trash folder # Try to add another trash folder
itemT = NWItem(theProject) itemT = NWItem(theProject)
@@ -169,14 +171,15 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
itemT._class = nwItemClass.NOVEL itemT._class = nwItemClass.NOVEL
itemT._layout = nwItemLayout.DOCUMENT 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 assert len(theTree) == len(mockItems) + 1
theList = theTree.handles() theList = theTree.handles()
assert theList[-1] == "73475cb40a568" assert theList[-1] == "73475cb40a568"
# Try to add existing handle # 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 assert len(theTree) == len(mockItems) + 1
# Delete a non-existing item # Delete a non-existing item
@@ -207,7 +210,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
@pytest.mark.core @pytest.mark.core
def testCoreTree_Methods(mockGUI, mockItems): def testCoreTree_Methods(monkeypatch, mockGUI, mockItems):
"""Test various class methods. """Test various class methods.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -215,9 +218,27 @@ def testCoreTree_Methods(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems: for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem) theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems) 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 # Chech type
assert theTree.checkType("blabla", nwItemType.FILE) is False assert theTree.checkType("blabla", nwItemType.FILE) is False
assert theTree.checkType("b000000000001", 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: for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem) theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
@@ -377,8 +399,8 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
b'<item handle="a000000000001" parent="None" root="a000000000001" order="0" type="ROOT" ' b'<item handle="a000000000001" parent="None" root="a000000000001" order="0" type="ROOT" '
b'class="NOVEL"><meta expanded="True"/><name status="None">Novel</name></item>' b'class="NOVEL"><meta expanded="True"/><name status="None">Novel</name></item>'
b'<item handle="b000000000001" parent="a000000000001" root="a000000000001" order="0" ' b'<item handle="b000000000001" parent="a000000000001" root="a000000000001" order="0" '
b'type="FOLDER" class="NOVEL"><meta expanded="True"/>' b'type="FOLDER" class="NOVEL"><meta expanded="True"/><name status="None">Act One</name>'
b'<name status="None">Act One</name></item>' b'</item>'
b'<item handle="c000000000001" parent="b000000000001" root="a000000000001" order="0" ' b'<item handle="c000000000001" parent="b000000000001" root="a000000000001" order="0" '
b'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="300" wordCount="50" ' b'type="FILE" class="NOVEL" layout="DOCUMENT"><meta charCount="300" wordCount="50" '
b'paraCount="2" cursorPos="0"/><name status="None" exported="True">Chapter One</name>' b'paraCount="2" cursorPos="0"/><name status="None" exported="True">Chapter One</name>'
@@ -393,7 +415,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
b'class="TRASH"><meta expanded="False"/><name status="None">Trash</name></item>' b'class="TRASH"><meta expanded="False"/><name status="None">Trash</name></item>'
b'<item handle="a000000000004" parent="None" root="a000000000004" order="0" type="ROOT" ' b'<item handle="a000000000004" parent="None" root="a000000000004" order="0" type="ROOT" '
b'class="CHARACTER"><meta expanded="True"/><name status="None">Characters</name></item>' b'class="CHARACTER"><meta expanded="True"/><name status="None">Characters</name></item>'
b'<item handle="b000000000002" parent="a000000000002" root="a000000000002" order="0" ' b'<item handle="b000000000002" parent="a000000000004" root="a000000000004" order="0" '
b'type="FILE" class="CHARACTER" layout="NOTE"><meta charCount="2000" wordCount="400" ' b'type="FILE" class="CHARACTER" layout="NOTE"><meta charCount="2000" wordCount="400" '
b'paraCount="16" cursorPos="0"/><name status="None" exported="True">Jane Doe</name></item>' b'paraCount="16" cursorPos="0"/><name status="None" exported="True">Jane Doe</name></item>'
b'</content>' b'</content>'
@@ -418,6 +440,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
for tHandle, pHandle, nwItem in mockItems: for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem) theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
theTree._treeOrder.append("stuff") theTree._treeOrder.append("stuff")