Files
novelWriter/nw/project/project.py
T

461 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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
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
# Project 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.projName = None
self.bookTitle = None
self.bookAuthors = None
self.statusCols = None
self.clearProject()
return
##
# Item Methods
##
def newRoot(self, rootName, rootClass):
if not self.checkRootUnique(rootClass):
self.theParent.makeAlert("Duplicate root item detected!",2)
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.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.theParent.makeAlert("File not found: %s" % fileName,2)
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.theParent.makeAlert("Project file does not appear to be a novelWriterXML file version 1.0",2)
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 == "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.theParent.makeAlert("Project path not set, cannot save.",2)
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"),
})
xProject = etree.SubElement(nwXML,"project")
xProjName = etree.SubElement(xProject,"name")
xProjName.text = self.projName
xBookTitle = etree.SubElement(xProject,"title")
xBookTitle.text = self.bookTitle
for bookAuthor in self.bookAuthors:
if bookAuthor == "": continue
xBookAuthor = etree.SubElement(xProject,"author")
xBookAuthor.text = bookAuthor
# 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.theParent.makeAlert(["Failed to save project.",str(e)],2)
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 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.theParent.makeAlert(["Could not create folder.",str(e)],2)
return False
return True
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.theParent.makeAlert("Found %d orphaned file(s) in project folder!" % len(orphanFiles),1)
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 = self._checkString(tHandle,self._makeHandle(),False)
pHandle = self._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
def _checkString(self,checkValue,defaultValue,allowNone=False):
if allowNone:
if checkValue == None: return None
if checkValue == "None": return None
if isinstance(checkValue,str): return str(checkValue)
return defaultValue
def _checkInt(self,checkValue,defaultValue,allowNone=False):
if allowNone:
if checkValue == None: return None
if checkValue == "None": return None
try:
return int(checkValue)
except:
return defaultValue
# END Class NWProject