Moved NWTree to core/tree.py
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
novelWriter – Project Item Class
|
novelWriter – Project Item Class
|
||||||
==================================
|
==================================
|
||||||
Class holding the data off a project tree item
|
Class holding the data of a project tree item
|
||||||
|
|
||||||
File History:
|
File History:
|
||||||
Created: 2018-10-27 [0.0.1] NWItem
|
Created: 2018-10-27 [0.0.1] NWItem
|
||||||
|
|||||||
+2
-385
@@ -3,14 +3,13 @@
|
|||||||
|
|
||||||
novelWriter – Project Wrapper
|
novelWriter – Project Wrapper
|
||||||
===============================
|
===============================
|
||||||
Class wrapping the data if a novelWriter project
|
Class wrapping the data of a novelWriter project
|
||||||
|
|
||||||
File History:
|
File History:
|
||||||
Created: 2018-09-29 [0.0.1] NWProject
|
Created: 2018-09-29 [0.0.1] NWProject
|
||||||
Created: 2019-05-19 [0.1.3] NWStatus
|
Created: 2019-05-19 [0.1.3] NWStatus
|
||||||
Created: 2019-10-21 [0.3.1] OptionState
|
Created: 2019-10-21 [0.3.1] OptionState
|
||||||
Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file
|
Merged: 2020-05-07 [0.4.5] Moved NWStatus class to this file
|
||||||
Added: 2020-05-07 [0.4.5] NWTree
|
|
||||||
Rewritten: 2020-02-19 [0.4.5] OptionState
|
Rewritten: 2020-02-19 [0.4.5] OptionState
|
||||||
|
|
||||||
This file is a part of novelWriter
|
This file is a part of novelWriter
|
||||||
@@ -42,6 +41,7 @@ from shutil import make_archive
|
|||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from nw.core.tree import NWTree
|
||||||
from nw.core.item import NWItem
|
from nw.core.item import NWItem
|
||||||
from nw.core.document import NWDoc
|
from nw.core.document import NWDoc
|
||||||
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
|
from nw.common import checkString, checkBool, checkInt, formatTimeStamp
|
||||||
@@ -1224,389 +1224,6 @@ class NWProject():
|
|||||||
|
|
||||||
# END Class NWProject
|
# END Class NWProject
|
||||||
|
|
||||||
# =============================================================================================== #
|
|
||||||
# NWTree
|
|
||||||
# Class holding the project tree for the NWProject
|
|
||||||
# =============================================================================================== #
|
|
||||||
|
|
||||||
class NWTree():
|
|
||||||
|
|
||||||
def __init__(self, theProject):
|
|
||||||
|
|
||||||
self.theProject = theProject
|
|
||||||
|
|
||||||
self._projTree = {} # Holds all the items of the project
|
|
||||||
self._treeOrder = [] # The order of the tree items on the tree view
|
|
||||||
self._treeRoots = [] # The root items of the tree
|
|
||||||
self._trashRoot = None # The handle of the trash root folder
|
|
||||||
self._theLength = 0 # Always the length of _treeOrder
|
|
||||||
self._theIndex = 0 # The current iterator index
|
|
||||||
self._treeChanged = False # True if tree structure has changed
|
|
||||||
self._handleSeed = None # Used for generating handles for testing
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
##
|
|
||||||
# Class Methods
|
|
||||||
##
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Clear the item tree entirely.
|
|
||||||
"""
|
|
||||||
self._projTree = {}
|
|
||||||
self._treeOrder = []
|
|
||||||
self._treeRoots = []
|
|
||||||
self._trashRoot = None
|
|
||||||
self._theLength = 0
|
|
||||||
self._theIndex = 0
|
|
||||||
self._treeChanged = False
|
|
||||||
return
|
|
||||||
|
|
||||||
def handles(self):
|
|
||||||
"""Returns a copy of the list of all the active handles.
|
|
||||||
"""
|
|
||||||
return self._treeOrder.copy()
|
|
||||||
|
|
||||||
def append(self, tHandle, pHandle, nwItem):
|
|
||||||
"""Add a new item to the end of the tree.
|
|
||||||
"""
|
|
||||||
tHandle = checkString(tHandle, None, True)
|
|
||||||
pHandle = checkString(pHandle, None, True)
|
|
||||||
if tHandle is None:
|
|
||||||
tHandle = self._makeHandle()
|
|
||||||
|
|
||||||
logger.verbose("Adding entry %s with parent %s" % (str(tHandle), str(pHandle)))
|
|
||||||
|
|
||||||
nwItem.setHandle(tHandle)
|
|
||||||
nwItem.setParent(pHandle)
|
|
||||||
|
|
||||||
self._projTree[tHandle] = nwItem
|
|
||||||
self._treeOrder.append(tHandle)
|
|
||||||
|
|
||||||
if nwItem.itemType == nwItemType.ROOT:
|
|
||||||
logger.verbose("Entry %s is a root item" % str(tHandle))
|
|
||||||
self._treeRoots.append(tHandle)
|
|
||||||
|
|
||||||
if nwItem.itemType == nwItemType.TRASH:
|
|
||||||
if self._trashRoot is None:
|
|
||||||
logger.verbose("Entry %s is the trash folder" % str(tHandle))
|
|
||||||
self._trashRoot = tHandle
|
|
||||||
else:
|
|
||||||
logger.error("Only one trash folder allowed")
|
|
||||||
|
|
||||||
self._theLength = len(self._treeOrder)
|
|
||||||
self._setTreeChanged(True)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def packXML(self, xParent):
|
|
||||||
"""Pack the content of the tree into an XML object.
|
|
||||||
"""
|
|
||||||
xContent = etree.SubElement(xParent, "content", attrib={
|
|
||||||
"count":str(self._theLength)}
|
|
||||||
)
|
|
||||||
for tHandle in self._treeOrder:
|
|
||||||
tItem = self.__getitem__(tHandle)
|
|
||||||
tItem.packXML(xContent)
|
|
||||||
return
|
|
||||||
|
|
||||||
def unpackXML(self, xContent):
|
|
||||||
"""Iterate through all items of a content XML object and add
|
|
||||||
them to the project tree.
|
|
||||||
"""
|
|
||||||
if xContent.tag != "content":
|
|
||||||
logger.error("XML entry is not a NWTree")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.clear()
|
|
||||||
for xItem in xContent:
|
|
||||||
nwItem = NWItem(self.theProject)
|
|
||||||
if nwItem.unpackXML(xItem):
|
|
||||||
self.append(nwItem.itemHandle, nwItem.parHandle, nwItem)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def writeToCFiles(self):
|
|
||||||
"""Write the convenience table of contents files in the root of
|
|
||||||
the project directory. These files are there to assist the user
|
|
||||||
if they wish to browse the stored files.
|
|
||||||
"""
|
|
||||||
tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT)
|
|
||||||
tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON)
|
|
||||||
|
|
||||||
jsonData = []
|
|
||||||
try:
|
|
||||||
# Dump the text
|
|
||||||
with open(tocText, mode="w", encoding="utf8") as outFile:
|
|
||||||
outFile.write("\n")
|
|
||||||
outFile.write(" Table of Contents\n")
|
|
||||||
outFile.write("===================\n")
|
|
||||||
outFile.write("\n")
|
|
||||||
outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label"))
|
|
||||||
outFile.write("-"*80+"\n")
|
|
||||||
for tHandle in sorted(self._treeOrder):
|
|
||||||
tItem = self.__getitem__(tHandle)
|
|
||||||
if tItem is None:
|
|
||||||
continue
|
|
||||||
tFile = tHandle+".nwd"
|
|
||||||
if path.isfile(path.join(self.theProject.projContent, tFile)):
|
|
||||||
outFile.write(" %-25s %-9s %s\n" %(
|
|
||||||
path.join("content", tFile),
|
|
||||||
tItem.itemClass.name,
|
|
||||||
tItem.itemName,
|
|
||||||
))
|
|
||||||
jsonData.append([
|
|
||||||
path.join("content", tFile),
|
|
||||||
tItem.itemClass.name,
|
|
||||||
tItem.itemName,
|
|
||||||
])
|
|
||||||
outFile.write("\n")
|
|
||||||
|
|
||||||
# Dump the JSON
|
|
||||||
with open(tocJson, mode="w+", encoding="utf8") as outFile:
|
|
||||||
outFile.write(json.dumps(jsonData, indent=2))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(str(e))
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
##
|
|
||||||
# Tree Structure Methods
|
|
||||||
##
|
|
||||||
|
|
||||||
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 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 checkRootUnique(self, theClass):
|
|
||||||
"""Checks if there already is a root entry of class 'theClass'
|
|
||||||
in the root of the project tree. CUSTOM class is skipped as it
|
|
||||||
is not required to be unique.
|
|
||||||
"""
|
|
||||||
if theClass == nwItemClass.CUSTOM:
|
|
||||||
return True
|
|
||||||
for aRoot in self._treeRoots:
|
|
||||||
tItem = self.__getitem__(aRoot)
|
|
||||||
if theClass == tItem.itemClass:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def getRootItem(self, tHandle):
|
|
||||||
"""Iterate upwards in the tree until we find the item with
|
|
||||||
parent None, the root item. We do this with a for loop with a
|
|
||||||
maximum depth of 200 to make infinite loops impossible.
|
|
||||||
"""
|
|
||||||
tItem = self.__getitem__(tHandle)
|
|
||||||
if tItem is not None:
|
|
||||||
for i in range(200):
|
|
||||||
if tItem.parHandle is None:
|
|
||||||
return tHandle
|
|
||||||
else:
|
|
||||||
tHandle = tItem.parHandle
|
|
||||||
tItem = self.__getitem__(tHandle)
|
|
||||||
if tItem is None:
|
|
||||||
return tHandle
|
|
||||||
return None
|
|
||||||
|
|
||||||
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.
|
|
||||||
We do this with a for loop with a maximum depth of 200 to make
|
|
||||||
infinite loops impossible.
|
|
||||||
"""
|
|
||||||
tTree = []
|
|
||||||
tItem = self.__getitem__(tHandle)
|
|
||||||
if tItem is not None:
|
|
||||||
tTree.append(tHandle)
|
|
||||||
for i in range(200):
|
|
||||||
if tItem.parHandle is None:
|
|
||||||
return tTree
|
|
||||||
else:
|
|
||||||
tHandle = tItem.parHandle
|
|
||||||
tItem = self.__getitem__(tHandle)
|
|
||||||
if tItem is None:
|
|
||||||
return tTree
|
|
||||||
else:
|
|
||||||
tTree.append(tHandle)
|
|
||||||
return tTree
|
|
||||||
|
|
||||||
##
|
|
||||||
# Setters
|
|
||||||
##
|
|
||||||
|
|
||||||
def setOrder(self, newOrder):
|
|
||||||
"""Reorders the tree based on a list of items.
|
|
||||||
"""
|
|
||||||
tmpOrder = []
|
|
||||||
|
|
||||||
# Add all known elements to a new temp list
|
|
||||||
for tHandle in newOrder:
|
|
||||||
if tHandle in self._projTree:
|
|
||||||
tmpOrder.append(tHandle)
|
|
||||||
else:
|
|
||||||
logger.error("Handle %s in new tree order is not in project tree" % tHandle)
|
|
||||||
|
|
||||||
# Do a reverse lookup to check for items that will be lost
|
|
||||||
# This is mainly for debugging purposes
|
|
||||||
for tHandle in self._treeOrder:
|
|
||||||
if tHandle not in tmpOrder:
|
|
||||||
logger.warning("Handle %s in old tree order is not in new tree order" % tHandle)
|
|
||||||
|
|
||||||
# Save the temp list
|
|
||||||
self._treeOrder = tmpOrder
|
|
||||||
self._theLength = len(self._treeOrder)
|
|
||||||
self._setTreeChanged(True)
|
|
||||||
logger.verbose("Project tree order updated")
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def setSeed(self, theSeed):
|
|
||||||
"""Used for debugging!
|
|
||||||
Sets a seed for generating handles so that they always come out
|
|
||||||
in a predictable order.
|
|
||||||
"""
|
|
||||||
self._handleSeed = theSeed
|
|
||||||
return
|
|
||||||
|
|
||||||
##
|
|
||||||
# Getters
|
|
||||||
##
|
|
||||||
|
|
||||||
def countTypes(self):
|
|
||||||
"""Count the number of files, folders and roots in the project.
|
|
||||||
"""
|
|
||||||
nRoot = 0
|
|
||||||
nFolder = 0
|
|
||||||
nFile = 0
|
|
||||||
|
|
||||||
for tHandle in self._treeOrder:
|
|
||||||
tItem = self.__getitem__(tHandle)
|
|
||||||
if tItem is None:
|
|
||||||
continue
|
|
||||||
elif tItem.itemType == nwItemType.ROOT:
|
|
||||||
nRoot += 1
|
|
||||||
elif tItem.itemType == nwItemType.FOLDER:
|
|
||||||
nFolder += 1
|
|
||||||
elif tItem.itemType == nwItemType.FILE:
|
|
||||||
nFile += 1
|
|
||||||
|
|
||||||
return nRoot, nFolder, nFile
|
|
||||||
|
|
||||||
##
|
|
||||||
# Meta Methods
|
|
||||||
##
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
"""Return the length counter. Does not check that it is correct!
|
|
||||||
"""
|
|
||||||
return self._theLength
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
"""Returns True if the tree has any entries.
|
|
||||||
"""
|
|
||||||
return self._theLength > 0
|
|
||||||
|
|
||||||
##
|
|
||||||
# Item Access Methods
|
|
||||||
##
|
|
||||||
|
|
||||||
def __getitem__(self, tHandle):
|
|
||||||
"""Return a project item based on its handle. Returns None if
|
|
||||||
the handle doesn't exist in the project.
|
|
||||||
"""
|
|
||||||
if tHandle in self._projTree:
|
|
||||||
return self._projTree[tHandle]
|
|
||||||
logger.error("No tree item with handle %s" % str(tHandle))
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __delitem__(self, tHandle):
|
|
||||||
"""This only removes the item from the order list, but not from
|
|
||||||
the project tree.
|
|
||||||
"""
|
|
||||||
if tHandle not in self._treeOrder:
|
|
||||||
logger.warning(
|
|
||||||
"Could not remove item %s from project tree as it does not exist" % tHandle
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
self._treeOrder.remove(tHandle)
|
|
||||||
self._theLength = len(self._treeOrder)
|
|
||||||
self._setTreeChanged(True)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __contains__(self, tHandle):
|
|
||||||
"""Checks if a handle exists in the tree.
|
|
||||||
"""
|
|
||||||
return tHandle in self._treeOrder
|
|
||||||
|
|
||||||
##
|
|
||||||
# Iterator Methods
|
|
||||||
##
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Initiates the iterator.
|
|
||||||
"""
|
|
||||||
self._theIndex = 0
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
"""Returns the item from the next entry in the _treeOrder list.
|
|
||||||
"""
|
|
||||||
if self._theIndex < self._theLength:
|
|
||||||
theItem = self.__getitem__(self._treeOrder[self._theIndex])
|
|
||||||
self._theIndex += 1
|
|
||||||
return theItem
|
|
||||||
else:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
##
|
|
||||||
# Internal Functions
|
|
||||||
##
|
|
||||||
|
|
||||||
def _setTreeChanged(self, theState):
|
|
||||||
"""Set the changed flag to theState, and if being set to True,
|
|
||||||
propagate that state change to the parent NWProject class.
|
|
||||||
"""
|
|
||||||
self._treeChanged = theState
|
|
||||||
if theState:
|
|
||||||
self.theProject.setProjectChanged(True)
|
|
||||||
return
|
|
||||||
|
|
||||||
def _makeHandle(self, addSeed=""):
|
|
||||||
"""Generate a unique item handle. In the unlikely event that the
|
|
||||||
key already exists, salt the seed and generate a new handle.
|
|
||||||
"""
|
|
||||||
if self._handleSeed is None:
|
|
||||||
newSeed = str(time()) + addSeed
|
|
||||||
else:
|
|
||||||
# This is used for debugging
|
|
||||||
newSeed = str(self._handleSeed)
|
|
||||||
self._handleSeed += 1
|
|
||||||
logger.verbose("Generating handle with seed '%s'" % newSeed)
|
|
||||||
itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
|
|
||||||
if itemHandle in self._projTree:
|
|
||||||
logger.warning("Duplicate handle encountered! Retrying ...")
|
|
||||||
itemHandle = self._makeHandle(addSeed+"!")
|
|
||||||
return itemHandle
|
|
||||||
|
|
||||||
# END Class NWTree
|
|
||||||
|
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
# NWStatus
|
# NWStatus
|
||||||
# Class holding the item status values stored in the NWProject
|
# Class holding the item status values stored in the NWProject
|
||||||
|
|||||||
+421
@@ -0,0 +1,421 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""novelWriter Project Tree Class
|
||||||
|
|
||||||
|
novelWriter – Project Tree Class
|
||||||
|
==================================
|
||||||
|
Class holding the data of the project tree
|
||||||
|
|
||||||
|
File History:
|
||||||
|
Created: 2020-05-07 [0.4.5] NWTree
|
||||||
|
|
||||||
|
This file is a part of novelWriter
|
||||||
|
Copyright 2020, Veronica Berglyd Olsen
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import nw
|
||||||
|
|
||||||
|
from os import path
|
||||||
|
from lxml import etree
|
||||||
|
from hashlib import sha256
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from nw.core.item import NWItem
|
||||||
|
from nw.common import checkString
|
||||||
|
from nw.constants import nwFiles, nwItemType, nwItemClass
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class NWTree():
|
||||||
|
|
||||||
|
def __init__(self, theProject):
|
||||||
|
|
||||||
|
self.theProject = theProject
|
||||||
|
|
||||||
|
self._projTree = {} # Holds all the items of the project
|
||||||
|
self._treeOrder = [] # The order of the tree items on the tree view
|
||||||
|
self._treeRoots = [] # The root items of the tree
|
||||||
|
self._trashRoot = None # The handle of the trash root folder
|
||||||
|
self._theLength = 0 # Always the length of _treeOrder
|
||||||
|
self._theIndex = 0 # The current iterator index
|
||||||
|
self._treeChanged = False # True if tree structure has changed
|
||||||
|
self._handleSeed = None # Used for generating handles for testing
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Class Methods
|
||||||
|
##
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clear the item tree entirely.
|
||||||
|
"""
|
||||||
|
self._projTree = {}
|
||||||
|
self._treeOrder = []
|
||||||
|
self._treeRoots = []
|
||||||
|
self._trashRoot = None
|
||||||
|
self._theLength = 0
|
||||||
|
self._theIndex = 0
|
||||||
|
self._treeChanged = False
|
||||||
|
return
|
||||||
|
|
||||||
|
def handles(self):
|
||||||
|
"""Returns a copy of the list of all the active handles.
|
||||||
|
"""
|
||||||
|
return self._treeOrder.copy()
|
||||||
|
|
||||||
|
def append(self, tHandle, pHandle, nwItem):
|
||||||
|
"""Add a new item to the end of the tree.
|
||||||
|
"""
|
||||||
|
tHandle = checkString(tHandle, None, True)
|
||||||
|
pHandle = checkString(pHandle, None, True)
|
||||||
|
if tHandle is None:
|
||||||
|
tHandle = self._makeHandle()
|
||||||
|
|
||||||
|
logger.verbose("Adding entry %s with parent %s" % (str(tHandle), str(pHandle)))
|
||||||
|
|
||||||
|
nwItem.setHandle(tHandle)
|
||||||
|
nwItem.setParent(pHandle)
|
||||||
|
|
||||||
|
self._projTree[tHandle] = nwItem
|
||||||
|
self._treeOrder.append(tHandle)
|
||||||
|
|
||||||
|
if nwItem.itemType == nwItemType.ROOT:
|
||||||
|
logger.verbose("Entry %s is a root item" % str(tHandle))
|
||||||
|
self._treeRoots.append(tHandle)
|
||||||
|
|
||||||
|
if nwItem.itemType == nwItemType.TRASH:
|
||||||
|
if self._trashRoot is None:
|
||||||
|
logger.verbose("Entry %s is the trash folder" % str(tHandle))
|
||||||
|
self._trashRoot = tHandle
|
||||||
|
else:
|
||||||
|
logger.error("Only one trash folder allowed")
|
||||||
|
|
||||||
|
self._theLength = len(self._treeOrder)
|
||||||
|
self._setTreeChanged(True)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def packXML(self, xParent):
|
||||||
|
"""Pack the content of the tree into an XML object.
|
||||||
|
"""
|
||||||
|
xContent = etree.SubElement(xParent, "content", attrib={
|
||||||
|
"count":str(self._theLength)}
|
||||||
|
)
|
||||||
|
for tHandle in self._treeOrder:
|
||||||
|
tItem = self.__getitem__(tHandle)
|
||||||
|
tItem.packXML(xContent)
|
||||||
|
return
|
||||||
|
|
||||||
|
def unpackXML(self, xContent):
|
||||||
|
"""Iterate through all items of a content XML object and add
|
||||||
|
them to the project tree.
|
||||||
|
"""
|
||||||
|
if xContent.tag != "content":
|
||||||
|
logger.error("XML entry is not a NWTree")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.clear()
|
||||||
|
for xItem in xContent:
|
||||||
|
nwItem = NWItem(self.theProject)
|
||||||
|
if nwItem.unpackXML(xItem):
|
||||||
|
self.append(nwItem.itemHandle, nwItem.parHandle, nwItem)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def writeToCFiles(self):
|
||||||
|
"""Write the convenience table of contents files in the root of
|
||||||
|
the project directory. These files are there to assist the user
|
||||||
|
if they wish to browse the stored files.
|
||||||
|
"""
|
||||||
|
tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT)
|
||||||
|
tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON)
|
||||||
|
|
||||||
|
jsonData = []
|
||||||
|
try:
|
||||||
|
# Dump the text
|
||||||
|
with open(tocText, mode="w", encoding="utf8") as outFile:
|
||||||
|
outFile.write("\n")
|
||||||
|
outFile.write(" Table of Contents\n")
|
||||||
|
outFile.write("===================\n")
|
||||||
|
outFile.write("\n")
|
||||||
|
outFile.write(" %-25s %-9s %s\n" %("File Name","Class","Document Label"))
|
||||||
|
outFile.write("-"*80+"\n")
|
||||||
|
for tHandle in sorted(self._treeOrder):
|
||||||
|
tItem = self.__getitem__(tHandle)
|
||||||
|
if tItem is None:
|
||||||
|
continue
|
||||||
|
tFile = tHandle+".nwd"
|
||||||
|
if path.isfile(path.join(self.theProject.projContent, tFile)):
|
||||||
|
outFile.write(" %-25s %-9s %s\n" %(
|
||||||
|
path.join("content", tFile),
|
||||||
|
tItem.itemClass.name,
|
||||||
|
tItem.itemName,
|
||||||
|
))
|
||||||
|
jsonData.append([
|
||||||
|
path.join("content", tFile),
|
||||||
|
tItem.itemClass.name,
|
||||||
|
tItem.itemName,
|
||||||
|
])
|
||||||
|
outFile.write("\n")
|
||||||
|
|
||||||
|
# Dump the JSON
|
||||||
|
with open(tocJson, mode="w+", encoding="utf8") as outFile:
|
||||||
|
outFile.write(json.dumps(jsonData, indent=2))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Tree Structure Methods
|
||||||
|
##
|
||||||
|
|
||||||
|
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 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 checkRootUnique(self, theClass):
|
||||||
|
"""Checks if there already is a root entry of class 'theClass'
|
||||||
|
in the root of the project tree. CUSTOM class is skipped as it
|
||||||
|
is not required to be unique.
|
||||||
|
"""
|
||||||
|
if theClass == nwItemClass.CUSTOM:
|
||||||
|
return True
|
||||||
|
for aRoot in self._treeRoots:
|
||||||
|
tItem = self.__getitem__(aRoot)
|
||||||
|
if theClass == tItem.itemClass:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getRootItem(self, tHandle):
|
||||||
|
"""Iterate upwards in the tree until we find the item with
|
||||||
|
parent None, the root item. We do this with a for loop with a
|
||||||
|
maximum depth of 200 to make infinite loops impossible.
|
||||||
|
"""
|
||||||
|
tItem = self.__getitem__(tHandle)
|
||||||
|
if tItem is not None:
|
||||||
|
for i in range(200):
|
||||||
|
if tItem.parHandle is None:
|
||||||
|
return tHandle
|
||||||
|
else:
|
||||||
|
tHandle = tItem.parHandle
|
||||||
|
tItem = self.__getitem__(tHandle)
|
||||||
|
if tItem is None:
|
||||||
|
return tHandle
|
||||||
|
return None
|
||||||
|
|
||||||
|
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.
|
||||||
|
We do this with a for loop with a maximum depth of 200 to make
|
||||||
|
infinite loops impossible.
|
||||||
|
"""
|
||||||
|
tTree = []
|
||||||
|
tItem = self.__getitem__(tHandle)
|
||||||
|
if tItem is not None:
|
||||||
|
tTree.append(tHandle)
|
||||||
|
for i in range(200):
|
||||||
|
if tItem.parHandle is None:
|
||||||
|
return tTree
|
||||||
|
else:
|
||||||
|
tHandle = tItem.parHandle
|
||||||
|
tItem = self.__getitem__(tHandle)
|
||||||
|
if tItem is None:
|
||||||
|
return tTree
|
||||||
|
else:
|
||||||
|
tTree.append(tHandle)
|
||||||
|
return tTree
|
||||||
|
|
||||||
|
##
|
||||||
|
# Setters
|
||||||
|
##
|
||||||
|
|
||||||
|
def setOrder(self, newOrder):
|
||||||
|
"""Reorders the tree based on a list of items.
|
||||||
|
"""
|
||||||
|
tmpOrder = []
|
||||||
|
|
||||||
|
# Add all known elements to a new temp list
|
||||||
|
for tHandle in newOrder:
|
||||||
|
if tHandle in self._projTree:
|
||||||
|
tmpOrder.append(tHandle)
|
||||||
|
else:
|
||||||
|
logger.error("Handle %s in new tree order is not in project tree" % tHandle)
|
||||||
|
|
||||||
|
# Do a reverse lookup to check for items that will be lost
|
||||||
|
# This is mainly for debugging purposes
|
||||||
|
for tHandle in self._treeOrder:
|
||||||
|
if tHandle not in tmpOrder:
|
||||||
|
logger.warning("Handle %s in old tree order is not in new tree order" % tHandle)
|
||||||
|
|
||||||
|
# Save the temp list
|
||||||
|
self._treeOrder = tmpOrder
|
||||||
|
self._theLength = len(self._treeOrder)
|
||||||
|
self._setTreeChanged(True)
|
||||||
|
logger.verbose("Project tree order updated")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def setSeed(self, theSeed):
|
||||||
|
"""Used for debugging!
|
||||||
|
Sets a seed for generating handles so that they always come out
|
||||||
|
in a predictable order.
|
||||||
|
"""
|
||||||
|
self._handleSeed = theSeed
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Getters
|
||||||
|
##
|
||||||
|
|
||||||
|
def countTypes(self):
|
||||||
|
"""Count the number of files, folders and roots in the project.
|
||||||
|
"""
|
||||||
|
nRoot = 0
|
||||||
|
nFolder = 0
|
||||||
|
nFile = 0
|
||||||
|
|
||||||
|
for tHandle in self._treeOrder:
|
||||||
|
tItem = self.__getitem__(tHandle)
|
||||||
|
if tItem is None:
|
||||||
|
continue
|
||||||
|
elif tItem.itemType == nwItemType.ROOT:
|
||||||
|
nRoot += 1
|
||||||
|
elif tItem.itemType == nwItemType.FOLDER:
|
||||||
|
nFolder += 1
|
||||||
|
elif tItem.itemType == nwItemType.FILE:
|
||||||
|
nFile += 1
|
||||||
|
|
||||||
|
return nRoot, nFolder, nFile
|
||||||
|
|
||||||
|
##
|
||||||
|
# Meta Methods
|
||||||
|
##
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""Return the length counter. Does not check that it is correct!
|
||||||
|
"""
|
||||||
|
return self._theLength
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
"""Returns True if the tree has any entries.
|
||||||
|
"""
|
||||||
|
return self._theLength > 0
|
||||||
|
|
||||||
|
##
|
||||||
|
# Item Access Methods
|
||||||
|
##
|
||||||
|
|
||||||
|
def __getitem__(self, tHandle):
|
||||||
|
"""Return a project item based on its handle. Returns None if
|
||||||
|
the handle doesn't exist in the project.
|
||||||
|
"""
|
||||||
|
if tHandle in self._projTree:
|
||||||
|
return self._projTree[tHandle]
|
||||||
|
logger.error("No tree item with handle %s" % str(tHandle))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __delitem__(self, tHandle):
|
||||||
|
"""This only removes the item from the order list, but not from
|
||||||
|
the project tree.
|
||||||
|
"""
|
||||||
|
if tHandle not in self._treeOrder:
|
||||||
|
logger.warning(
|
||||||
|
"Could not remove item %s from project tree as it does not exist" % tHandle
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
self._treeOrder.remove(tHandle)
|
||||||
|
self._theLength = len(self._treeOrder)
|
||||||
|
self._setTreeChanged(True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __contains__(self, tHandle):
|
||||||
|
"""Checks if a handle exists in the tree.
|
||||||
|
"""
|
||||||
|
return tHandle in self._treeOrder
|
||||||
|
|
||||||
|
##
|
||||||
|
# Iterator Methods
|
||||||
|
##
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""Initiates the iterator.
|
||||||
|
"""
|
||||||
|
self._theIndex = 0
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
"""Returns the item from the next entry in the _treeOrder list.
|
||||||
|
"""
|
||||||
|
if self._theIndex < self._theLength:
|
||||||
|
theItem = self.__getitem__(self._treeOrder[self._theIndex])
|
||||||
|
self._theIndex += 1
|
||||||
|
return theItem
|
||||||
|
else:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
##
|
||||||
|
# Internal Functions
|
||||||
|
##
|
||||||
|
|
||||||
|
def _setTreeChanged(self, theState):
|
||||||
|
"""Set the changed flag to theState, and if being set to True,
|
||||||
|
propagate that state change to the parent NWProject class.
|
||||||
|
"""
|
||||||
|
self._treeChanged = theState
|
||||||
|
if theState:
|
||||||
|
self.theProject.setProjectChanged(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _makeHandle(self, addSeed=""):
|
||||||
|
"""Generate a unique item handle. In the unlikely event that the
|
||||||
|
key already exists, salt the seed and generate a new handle.
|
||||||
|
"""
|
||||||
|
if self._handleSeed is None:
|
||||||
|
newSeed = str(time()) + addSeed
|
||||||
|
else:
|
||||||
|
# This is used for debugging
|
||||||
|
newSeed = str(self._handleSeed)
|
||||||
|
self._handleSeed += 1
|
||||||
|
logger.verbose("Generating handle with seed '%s'" % newSeed)
|
||||||
|
itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
|
||||||
|
if itemHandle in self._projTree:
|
||||||
|
logger.warning("Duplicate handle encountered! Retrying ...")
|
||||||
|
itemHandle = self._makeHandle(addSeed+"!")
|
||||||
|
return itemHandle
|
||||||
|
|
||||||
|
# END Class NWTree
|
||||||
Reference in New Issue
Block a user