# -*- coding: utf-8 -*- """novelWriter Project Wrapper novelWriter – Project Wrapper =============================== Class holding a project File History: Created: 2018-09-29 [0.0.1] """ import logging import nw from os import path, mkdir, listdir from lxml import etree from hashlib import sha256 from datetime import datetime from time import time from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from nw.common import checkString, checkBool from nw.project.item import NWItem logger = logging.getLogger(__name__) class NWProject(): def __init__(self, theParent): # Internal self.theParent = theParent self.mainConf = self.theParent.mainConf self.projChanged = None # Debug self.handleSeed = None # Class Settings self.projTree = None self.treeOrder = None self.treeRoots = None self.trashRoot = None self.projPath = None self.projMeta = None self.projCache = None self.projFile = None self.statusCols = None # Project Meta self.projName = None self.bookTitle = None self.bookAuthors = None # Project Settings self.spellCheck = False # Set Defaults self.clearProject() # Internal Mapping self.makeAlert = self.theParent.makeAlert return ## # Item Methods ## def newRoot(self, rootName, rootClass): if not self.checkRootUnique(rootClass): self.makeAlert("Duplicate root item detected!", nwAlert.ERROR) return None newItem = NWItem() newItem.setName(rootName) newItem.setType(nwItemType.ROOT) newItem.setClass(rootClass) self._appendItem(None,None,newItem) return newItem.itemHandle def newFolder(self, folderName, folderClass, pHandle): newItem = NWItem() newItem.setName(folderName) newItem.setType(nwItemType.FOLDER) newItem.setClass(folderClass) self._appendItem(None,pHandle,newItem) return newItem.itemHandle def newFile(self, fileName, fileClass, pHandle): newItem = NWItem() newItem.setName(fileName) newItem.setType(nwItemType.FILE) if fileClass == nwItemClass.NOVEL: newItem.setLayout(nwItemLayout.SCENE) else: newItem.setLayout(nwItemLayout.NOTE) newItem.setClass(fileClass) self._appendItem(None,pHandle,newItem) return newItem.itemHandle def addTrash(self): newItem = NWItem() newItem.setName("Trash") newItem.setType(nwItemType.TRASH) newItem.setClass(nwItemClass.TRASH) self._appendItem(None,None,newItem) return newItem.itemHandle ## # Project Methods ## def newProject(self): self.clearProject() hNovel = self.newRoot("Novel", nwItemClass.NOVEL) hChars = self.newRoot("Characters", nwItemClass.CHARACTER) hWorld = self.newRoot("Plot", nwItemClass.PLOT) hWorld = self.newRoot("World", nwItemClass.WORLD) hChapt = self.newFolder("New Chapter", nwItemClass.NOVEL, hNovel) hScene = self.newFile("New Scene", nwItemClass.NOVEL, hChapt) return True def clearProject(self): self.projChanged = None # Project Settings self.projTree = {} self.treeOrder = [] self.treeRoots = [] self.trashRoot = None self.projPath = None self.projMeta = None self.projCache = None self.projFile = "nwProject.nwx" self.projName = "" self.bookTitle = "" self.bookAuthors = [] self.spellCheck = False self.statusCols = [ ("New", 100,100,100), ("Note", 200, 50, 0), ("Draft", 200,150, 0), ("Finished", 50,200, 0), ] return def openProject(self, fileName): if not path.isfile(fileName): fileName = path.join(fileName, "nwProject.nwx") if not path.isfile(fileName): self.makeAlert("File not found: %s" % fileName, nwAlert.ERROR) return False self.clearProject() self.projPath = path.dirname(fileName) logger.debug("Opening project: %s" % self.projPath) self.projMeta = path.join(self.projPath,"meta") self.projCache = path.join(self.projPath,"cache") if not self._checkFolder(self.projMeta): return if not self._checkFolder(self.projCache): return nwXML = etree.parse(fileName) xRoot = nwXML.getroot() nwxRoot = xRoot.tag appVersion = xRoot.attrib["appVersion"] fileVersion = xRoot.attrib["fileVersion"] logger.verbose("XML root is %s" % nwxRoot) logger.verbose("File version is %s" % fileVersion) if not nwxRoot == "novelWriterXML" or not fileVersion == "1.0": self.makeAlert("Project file does not appear to be a novelWriterXML file version 1.0", nwAlert.ERROR) return False for xChild in xRoot: if xChild.tag == "project": logger.debug("Found project meta") for xItem in xChild: if xItem.text is None: continue if xItem.tag == "name": logger.verbose("Working Title: '%s'" % xItem.text) self.projName = xItem.text elif xItem.tag == "title": logger.verbose("Title is '%s'" % xItem.text) self.bookTitle = xItem.text elif xItem.tag == "author": logger.verbose("Author: '%s'" % xItem.text) self.bookAuthors.append(xItem.text) elif xChild.tag == "settings": logger.debug("Found project settings") for xItem in xChild: if xItem.text is None: continue if xItem.tag == "spellCheck": self.spellCheck = checkBool(xItem.text,False) elif xChild.tag == "content": logger.debug("Found project content") for xItem in xChild: itemAttrib = xItem.attrib if "handle" in xItem.attrib: tHandle = itemAttrib["handle"] else: logger.error("Skipping entry missing handle") continue if "parent" in xItem.attrib: pHandle = itemAttrib["parent"] else: pHandle = None nwItem = NWItem() for xValue in xItem: nwItem.setFromTag(xValue.tag,xValue.text) self._appendItem(tHandle,pHandle,nwItem) self.mainConf.setRecent(self.projPath) self.theParent.setStatus("Opened Project: %s" % self.projName) self._scanProjectFolder() self.setProjectChanged(False) return True def saveProject(self): if self.projPath is None: self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR) return False self.projMeta = path.join(self.projPath,"meta") self.projCache = path.join(self.projPath,"cache") if not self._checkFolder(self.projPath): return if not self._checkFolder(self.projMeta): return if not self._checkFolder(self.projCache): return logger.debug("Saving project: %s" % self.projPath) # Root element and project details logger.debug("Writing project meta") nwXML = etree.Element("novelWriterXML",attrib={ "fileVersion" : "1.0", "appVersion" : str(nw.__version__), "timeStamp" : datetime.now().strftime("%Y-%m-%d %H:%M:%S"), }) # Save Project Meta xProject = etree.SubElement(nwXML,"project") self._saveProjectValue(xProject,"name", self.projName, True) self._saveProjectValue(xProject,"title", self.bookTitle, True) self._saveProjectValue(xProject,"author",self.bookAuthors) # Save Project Settings xSettings = etree.SubElement(nwXML,"settings") self._saveProjectValue(xSettings,"spellCheck",self.spellCheck) # Save Tree Content logger.debug("Writing project content") xContent = etree.SubElement(nwXML,"content",attrib={"count":str(len(self.treeOrder))}) for tHandle in self.treeOrder: self.projTree[tHandle].packXML(xContent) # Write the xml tree to file saveFile = path.join(self.projPath,self.projFile) try: with open(saveFile,"wb") as outFile: outFile.write(etree.tostring( nwXML, pretty_print = True, encoding = "utf-8", xml_declaration = True )) except Exception as e: self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR) return False self.mainConf.setRecent(self.projPath) self.theParent.setStatus("Saved Project: %s" % self.projName) self.setProjectChanged(False) return True ## # Set Functions ## def setProjectPath(self, projPath): self.projPath = projPath self.setProjectChanged(True) return True def setProjectName(self, projName): self.projName = projName.strip() self.setProjectChanged(True) return True def setBookTitle(self, bookTitle): self.bookTitle = bookTitle.strip() self.setProjectChanged(True) return True def setBookAuthors(self, bookAuthors): self.bookAuthors = [] for bookAuthor in bookAuthors.split("\n"): bookAuthor = bookAuthor.strip() if bookAuthor == "": continue self.bookAuthors.append(bookAuthor) self.setProjectChanged(True) return True def setSpellCheck(self, theMode): self.spellCheck = theMode self.setProjectChanged(True) return True def setTreeOrder(self, newOrder): if len(self.treeOrder) != len(newOrder): logger.warning("Size of new and old tree order does not match") self.treeOrder = newOrder self.setProjectChanged(True) return True def setProjectChanged(self, bValue): self.projChanged = bValue self.theParent.setProjectStatus(self.projChanged) return ## # Get Functions ## def getItem(self, tHandle): if tHandle in self.projTree: return self.projTree[tHandle] logger.error("No tree item with handle %s" % str(tHandle)) return None def findRootItem(self, theClass): for aRoot in self.treeRoots: if theClass == self.projTree[aRoot].itemClass: return self.projTree[aRoot].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. """ if theClass == nwItemClass.CUSTOM: return True for aRoot in self.treeRoots: if theClass == self.projTree[aRoot].itemClass: return False return True ## # Internal Functions ## def _checkFolder(self, thePath): if not path.isdir(thePath): try: mkdir(thePath) logger.info("Created folder %s" % thePath) except Exception as e: self.makeAlert(["Could not create folder.",str(e)], nwAlert.ERROR) return False return True def _saveProjectValue(self, xParent, theName, theValue, allowNone=True): if not isinstance(theValue, list): theValue = [theValue] for aValue in theValue: if not isinstance(aValue, str): aValue = str(aValue) if aValue == "" and not allowNone: continue xItem = etree.SubElement(xParent,theName) xItem.text = aValue return def _scanProjectFolder(self): if self.projPath is None: return # First, scan the project data folders itemList = [] for subItem in listdir(self.projPath): if subItem[:5] != "data_": continue dataDir = path.join(self.projPath,subItem) for subFile in listdir(dataDir): if subFile[-4:] == ".nwd": newItem = path.join(subItem,subFile) itemList.append(newItem) # Then check the valid files orphanFiles = [] for fileItem in itemList: if len(fileItem) != 28: # Just to be safe, shouldn't happen logger.warning("Skipping file %s" % fileItem) continue fHandle = fileItem[5]+fileItem[7:19] if fHandle in self.treeOrder: logger.debug("Checking file %s, handle %s: OK" % (fileItem,fHandle)) else: logger.debug("Checking file %s, handle %s: Orphaned" % (fileItem,fHandle)) orphanFiles.append(fHandle) # Report status if len(orphanFiles) > 0: self.makeAlert("Found %d orphaned file(s) in project folder!" % len(orphanFiles), nwAlert.WARN) else: logger.debug("File check OK") return # Handle orphans nOrph = 0 for oHandle in orphanFiles: nOrph += 1 orItem = NWItem() orItem.setName("Orphaned File %d" % nOrph) orItem.setType(nwItemType.FILE) orItem.setClass(nwItemClass.NO_CLASS) orItem.setLayout(nwItemLayout.NO_LAYOUT) self._appendItem(oHandle,None,orItem) return def _appendItem(self, tHandle, pHandle, nwItem): tHandle = checkString(tHandle,self._makeHandle(),False) pHandle = checkString(pHandle,None,True) 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.setProjectChanged(True) return def _makeHandle(self, addSeed=""): 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.keys(): logger.warning("Duplicate handle encountered! Retrying ...") itemHandle = self._makeHandle(addSeed+"!") return itemHandle # END Class NWProject