From 717d516f6ac8dc498120e4874a1ea17c3a9611e6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 1 Nov 2022 22:32:29 +0100
Subject: [PATCH 01/26] Delegate responsibility for the index to the project
class
---
novelwriter/core/project.py | 110 +++++++++++++++--------------
novelwriter/guimain.py | 7 +-
tests/test_core/test_core_index.py | 3 +-
3 files changed, 61 insertions(+), 59 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index cf48c23d..dc33513e 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -67,14 +67,14 @@ class NWProject(QObject):
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
- # Project Data
- self._data = NWProjectData(self)
-
# Core Elements
- self._optState = OptionState(self) # Project-specific GUI options
- self._projTree = NWTree(self) # The project tree
- self._projIndex = NWIndex(self) # The projecty index
- self._langData = {} # Localisation data
+ self._data = NWProjectData(self) # The project settings
+ self._options = OptionState(self) # Project-specific GUI options
+ self._tree = NWTree(self) # The project tree
+ self._index = NWIndex(self) # The projecty index
+
+ # Data Cache
+ self._langData = {} # Localisation data
# Project Status
self._projOpened = 0 # The time stamp of when the project file was opened
@@ -108,15 +108,15 @@ class NWProject(QObject):
@property
def index(self):
- return self._projIndex
+ return self._index
@property
def tree(self):
- return self._projTree
+ return self._tree
@property
def options(self):
- return self._optState
+ return self._options
@property
def projOpened(self):
@@ -143,39 +143,39 @@ class NWProject(QObject):
newItem.setName(label)
newItem.setType(nwItemType.ROOT)
newItem.setClass(itemClass)
- self._projTree.append(None, None, newItem)
- self._projTree.updateItemData(newItem.itemHandle)
+ self._tree.append(None, None, newItem)
+ self._tree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFolder(self, label, pHandle):
"""Add a new folder with a given label and parent item.
"""
- if pHandle not in self._projTree:
+ if pHandle not in self._tree:
return None
newItem = NWItem(self)
newItem.setName(label)
newItem.setType(nwItemType.FOLDER)
- self._projTree.append(None, pHandle, newItem)
- self._projTree.updateItemData(newItem.itemHandle)
+ self._tree.append(None, pHandle, newItem)
+ self._tree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFile(self, label, pHandle):
"""Add a new file with a given label and parent item.
"""
- if pHandle not in self._projTree:
+ if pHandle not in self._tree:
return None
newItem = NWItem(self)
newItem.setName(label)
newItem.setType(nwItemType.FILE)
- self._projTree.append(None, pHandle, newItem)
- self._projTree.updateItemData(newItem.itemHandle)
+ self._tree.append(None, pHandle, newItem)
+ self._tree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def writeNewFile(self, tHandle, hLevel, isDocument, addText=""):
"""Write content to a new document after it is created. This
will not run if the file exists and is not empty.
"""
- tItem = self._projTree[tHandle]
+ tItem = self._tree[tHandle]
if tItem is None:
return False
if not tItem.isFileType():
@@ -193,7 +193,7 @@ class NWProject(QObject):
tItem.setLayout(nwItemLayout.NOTE)
newDoc.writeDocument(newText)
- self._projIndex.scanText(tHandle, newText)
+ self._index.scanText(tHandle, newText)
return True
@@ -201,7 +201,7 @@ class NWProject(QObject):
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
"""
- if self._projTree.checkType(tHandle, nwItemType.FILE):
+ if self._tree.checkType(tHandle, nwItemType.FILE):
delDoc = NWDoc(self, tHandle)
if not delDoc.deleteDocument():
self.mainGui.makeAlert([
@@ -209,22 +209,22 @@ class NWProject(QObject):
], nwAlert.ERROR)
return False
- self._projIndex.deleteHandle(tHandle)
- del self._projTree[tHandle]
+ self._index.deleteHandle(tHandle)
+ del self._tree[tHandle]
return True
def trashFolder(self):
"""Add the special trash root folder to the project.
"""
- trashHandle = self._projTree.trashRoot()
+ trashHandle = self._tree.trashRoot()
if trashHandle is None:
newItem = NWItem(self)
newItem.setName(trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH]))
newItem.setType(nwItemType.ROOT)
newItem.setClass(nwItemClass.TRASH)
- self._projTree.append(None, None, newItem)
- self._projTree.updateItemData(newItem.itemHandle)
+ self._tree.append(None, None, newItem)
+ self._tree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
return trashHandle
@@ -243,8 +243,8 @@ class NWProject(QObject):
self._projAltered = False
# Project Tree
- self._projTree.clear()
-
+ self._tree.clear()
+ self._index.clearIndex()
self._data = NWProjectData(self)
# Project Settings
@@ -307,7 +307,9 @@ class NWProject(QObject):
hNovelRoot = self.newRoot(nwItemClass.NOVEL)
hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot)
- titlePage = "#! %s\n\n" % (self._data.title if self._data.title else self._data.name)
+ titlePage = "#! %s\n\n" % (
+ self._data.title if self._data.title else self._data.name
+ )
if self._data.authors:
titlePage = "%s>> %s %s <<\n" % (
titlePage, self.tr("By"), self.getFormattedAuthors()
@@ -519,8 +521,9 @@ class NWProject(QObject):
# Extract Data
# ============
- self._projTree.unpack(projContent)
- self._optState.loadSettings()
+ self._tree.unpack(projContent)
+ self._options.loadSettings()
+ self._index.loadIndex()
# Sort out old file locations
if legacyList:
@@ -543,12 +546,12 @@ class NWProject(QObject):
self.mainConf.saveRecentCache()
# Check the project tree consistency
- for tItem in self._projTree:
+ for tItem in self._tree:
tHandle = tItem.itemHandle
logger.debug("Checking item '%s'", tHandle)
- if not self._projTree.updateItemData(tHandle):
+ if not self._tree.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
+ del self._tree[tHandle] # The file will be re-added as orphaned
self._scanProjectFolder()
self._loadProjectLocalisation()
@@ -592,7 +595,7 @@ class NWProject(QObject):
saveTime = time()
editTime = int(self._data.editTime + saveTime - self._projOpened)
- content = self._projTree.pack()
+ content = self._tree.pack()
xmlWriter = ProjectXMLWriter(self.projPath)
if not xmlWriter.write(self._data, content, saveTime, editTime):
self.mainGui.makeAlert(self.tr(
@@ -600,8 +603,9 @@ class NWProject(QObject):
), nwAlert.ERROR, exception=xmlWriter.error)
return False
- # Save project GUI options
- self._optState.saveSettings()
+ # Save other project data
+ self._options.saveSettings()
+ self._index.saveIndex()
# Update recent projects
self.mainConf.updateRecentCache(
@@ -619,8 +623,8 @@ class NWProject(QObject):
"""Close the current project and clear all meta data.
"""
logger.info("Closing project: %s", self.projPath)
- self._optState.saveSettings()
- self._projTree.writeToCFile()
+ self._options.saveSettings()
+ self._tree.writeToCFile()
self._appendSessionStats(idleTime)
self._clearLockFile()
self.clearProject()
@@ -840,9 +844,9 @@ class NWProject(QObject):
items in the GUI project tree. The user can rearrange the order
by drag-and-drop. Forwarded to the NWTree class.
"""
- if len(self._projTree) != len(newOrder):
+ if len(self._tree) != len(newOrder):
logger.warning("Sizes of new and old tree order do not match")
- self._projTree.setOrder(newOrder)
+ self._tree.setOrder(newOrder)
self.setProjectChanged(True)
return True
@@ -903,12 +907,12 @@ class NWProject(QObject):
capable of handling it.
"""
sentItems = []
- iterItems = self._projTree.handles()
+ iterItems = self._tree.handles()
n = 0
nMax = min(len(iterItems), 10000)
while n < nMax:
tHandle = iterItems[n]
- tItem = self._projTree[tHandle]
+ tItem = self._tree[tHandle]
n += 1
if tItem is None:
# Technically a bug since treeOrder is built from the
@@ -943,7 +947,7 @@ class NWProject(QObject):
def updateWordCounts(self):
"""Update the total word count values.
"""
- novel, notes = self._projTree.sumWords()
+ novel, notes = self._tree.sumWords()
self._data.setCurrCounts(novel=novel, notes=notes)
return
@@ -954,7 +958,7 @@ class NWProject(QObject):
"""
self._data.itemStatus.resetCounts()
self._data.itemImport.resetCounts()
- for nwItem in self._projTree:
+ for nwItem in self._tree:
if nwItem.isNovelLike():
self._data.itemStatus.increment(nwItem.itemStatus)
else:
@@ -1001,7 +1005,9 @@ class NWProject(QObject):
self._langData = {}
return False
- langFile = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self._data.language)
+ langFile = os.path.join(
+ self.mainConf.nwLangPath, "project_%s.json" % self._data.language
+ )
if not os.path.isfile(langFile):
langFile = os.path.join(self.mainConf.nwLangPath, "project_en_GB.json")
@@ -1120,7 +1126,7 @@ class NWProject(QObject):
logger.warning("Skipping file: %s", fileItem)
continue
- if fHandle in self._projTree:
+ if fHandle in self._tree:
self.projFiles.append(fHandle)
logger.debug("Checking file %s, handle '%s': OK", fileItem, fHandle)
else:
@@ -1167,10 +1173,10 @@ class NWProject(QObject):
if oLayout is None:
oLayout = nwItemLayout.NOTE
- if oParent is None or oParent not in self._projTree:
- oParent = self._projTree.findRoot(oClass)
+ if oParent is None or oParent not in self._tree:
+ oParent = self._tree.findRoot(oClass)
if oParent is None:
- oParent = self._projTree.findRoot(nwItemClass.NOVEL)
+ oParent = self._tree.findRoot(nwItemClass.NOVEL)
# If the file still has no parent item, skip it
if oParent is None:
@@ -1182,8 +1188,8 @@ class NWProject(QObject):
orphItem.setType(nwItemType.FILE)
orphItem.setClass(oClass)
orphItem.setLayout(oLayout)
- self._projTree.append(oHandle, oParent, orphItem)
- self._projTree.updateItemData(orphItem.itemHandle)
+ self._tree.append(oHandle, oParent, orphItem)
+ self._tree.updateItemData(orphItem.itemHandle)
if noWhere:
self.mainGui.makeAlert(self.tr(
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index ae9529e6..516cec42 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -443,7 +443,6 @@ class GuiMain(QMainWindow):
self.idleRefTime = time()
self.idleTime = 0.0
- self.theProject.index.clearIndex()
self.clearGUI()
self.hasProject = False
self._changeView(nwView.PROJECT)
@@ -519,9 +518,6 @@ class GuiMain(QMainWindow):
self.idleRefTime = time()
self.idleTime = 0.0
- # Load the tag index
- self.theProject.index.loadIndex()
-
# Update GUI
self._updateWindowTitle(self.theProject.data.name)
self.rebuildTrees()
@@ -573,8 +569,7 @@ class GuiMain(QMainWindow):
return False
self.projView.saveProjectTasks()
- if self.theProject.saveProject(autoSave=autoSave):
- self.theProject.index.saveIndex()
+ self.theProject.saveProject(autoSave=autoSave)
return True
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 0e416e09..624f6ec8 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -752,7 +752,6 @@ def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd):
assert theIndex.saveIndex() is True
assert theProject.saveProject() is True
- assert theProject.closeProject() is True
# Header Record
bHandle = "0000000000000"
@@ -764,6 +763,8 @@ def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd):
("T000001", "H1", "Hello World!"), ("T000011", "H1", "Hello World!")
]
+ assert theProject.closeProject() is True
+
# END Test testCoreIndex_ExtractData
From da4f4b0801281a72e5ab1edc4c1ea6c18f967aaf Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 1 Nov 2022 22:58:12 +0100
Subject: [PATCH 02/26] Add prototyype storage class
---
novelwriter/core/project.py | 4 +-
novelwriter/core/storage.py | 79 +++++++++++++++++++++++++++++++++++++
2 files changed, 82 insertions(+), 1 deletion(-)
create mode 100644 novelwriter/core/storage.py
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index dc33513e..750e68db 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -45,6 +45,7 @@ from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex
from novelwriter.core.status import NWStatus
from novelwriter.core.options import OptionState
+from novelwriter.core.storage import NWStorage
from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.common import (
@@ -68,8 +69,9 @@ class NWProject(QObject):
self.mainGui = mainGui
# Core Elements
- self._data = NWProjectData(self) # The project settings
self._options = OptionState(self) # Project-specific GUI options
+ self._storage = NWStorage(self) # The project storage handler
+ self._data = NWProjectData(self) # The project settings
self._tree = NWTree(self) # The project tree
self._index = NWIndex(self) # The projecty index
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
new file mode 100644
index 00000000..7382c336
--- /dev/null
+++ b/novelwriter/core/storage.py
@@ -0,0 +1,79 @@
+"""
+novelWriter – Project Storage Class
+===================================
+The main class handling the project storage
+
+File History:
+Created: 2022-11-01 [2.0rc1] NWStorage
+
+This file is a part of novelWriter
+Copyright 2018–2022, 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 .
+"""
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class NWStorage:
+
+ def __init__(self, theProject):
+ self.theProject = theProject
+ return
+
+ ##
+ # Core Methods
+ ##
+
+ def openProjectFolder(self, path):
+ pass
+
+ def openProjectArchive(self, path):
+ pass
+
+ def close(self):
+ pass
+
+ ##
+ # Content Access Methods
+ ##
+
+ def getXmlReader(self):
+ pass
+
+ def getXmlWriter(self):
+ pass
+
+ def getDocument(self, tHandle):
+ pass
+
+ def getMetaFile(self, kind):
+ pass
+
+ ##
+ # Internal Functions
+ ##
+
+ def _zipIt(self, target):
+ pass
+
+ def _reeadLockFile(self):
+ pass
+
+ def _writeLockFile(self):
+ pass
+
+# END Class NWStorage
From 3eee8442a7cfd78b94c266eddfea41dc17f5da8c Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 3 Nov 2022 17:30:54 +0100
Subject: [PATCH 03/26] Make packing and unpacking item data consistent
---
novelwriter/core/item.py | 41 +-
novelwriter/core/projectxml.py | 102 +-
tests/reference/projectXML_ReadCurrent.json | 1082 ++++++++++--------
tests/reference/projectXML_ReadLegacy10.json | 766 ++++++++-----
tests/reference/projectXML_ReadLegacy11.json | 734 +++++++-----
tests/reference/projectXML_ReadLegacy12.json | 822 +++++++------
tests/reference/projectXML_ReadLegacy13.json | 822 +++++++------
tests/test_core/test_core_item.py | 120 +-
tests/test_core/test_core_projectxml.py | 34 +-
9 files changed, 2650 insertions(+), 1873 deletions(-)
diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py
index 8a45fa35..7b6530e0 100644
--- a/novelwriter/core/item.py
+++ b/novelwriter/core/item.py
@@ -187,29 +187,34 @@ class NWItem:
def unpack(self, data):
"""Set the values from a data dictionary.
"""
- if "handle" in data:
- self.setHandle(data["handle"])
+ item = data.get("itemAttr", {})
+ meta = data.get("metaAttr", {})
+ name = data.get("nameAttr", {})
+
+ if "handle" in item:
+ self.setHandle(item["handle"])
else:
logger.error("Item does not have a handle")
return False
- self.setName(data.get("label", ""))
- self.setParent(data.get("parent", None))
- self.setRoot(data.get("root", None))
- self.setOrder(data.get("order", 0))
- self.setType(data.get("type", nwItemType.NO_TYPE))
- self.setClass(data.get("class", nwItemClass.NO_CLASS))
- self.setLayout(data.get("layout", nwItemLayout.NO_LAYOUT))
+ self.setName(data.get("name", ""))
+ self.setParent(item.get("parent", None))
+ self.setRoot(item.get("root", None))
+ self.setOrder(item.get("order", 0))
+ self.setType(item.get("type", nwItemType.NO_TYPE))
+ self.setClass(item.get("class", nwItemClass.NO_CLASS))
+ self.setExpanded(meta.get("expanded", False))
+ self.setStatus(name.get("status", None))
+ self.setImport(name.get("import", None))
- self.setExpanded(data.get("expanded", False))
- self.setStatus(data.get("status", None))
- self.setImport(data.get("import", None))
- self.setMainHeading(data.get("heading", "H0"))
- self.setCharCount(data.get("charCount", 0))
- self.setWordCount(data.get("wordCount", 0))
- self.setParaCount(data.get("paraCount", 0))
- self.setCursorPos(data.get("cursorPos", 0))
- self.setActive(data.get("active", True))
+ if self._type == nwItemType.FILE:
+ self.setLayout(item.get("layout", nwItemLayout.NO_LAYOUT))
+ self.setMainHeading(meta.get("heading", "H0"))
+ self.setCharCount(meta.get("charCount", 0))
+ self.setWordCount(meta.get("wordCount", 0))
+ self.setParaCount(meta.get("paraCount", 0))
+ self.setCursorPos(meta.get("cursorPos", 0))
+ self.setActive(name.get("active", True))
# Make some checks to ensure consistency
if self._type == nwItemType.ROOT:
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index dc08573a..18020c9a 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -68,24 +68,24 @@ class ProjectXMLReader:
"""The main project XML file reader class. All data is read into a
NWProjectData instance, which must be provided.
- Version Change History
- ======================
+ File Format Version Change History
+ ==================================
1.0 Original file format.
1.1 Changes the way documents are structured in the project folder
from data_X, where X is the first hex value of the handle, to a
single content folder. Introduced in version 0.7.
- 1.2 Changes the way autoReplace entries are stored. The 1.1 parser
- will lose the autoReplace settings if allowed to read the file.
- Introduced in version 0.10.
+ 1.2 Changes the way autoReplace entries are stored. Introduced in
+ version 0.10.
1.3 Reduces the number of layouts to only two. One for novel
documents and one for project notes. Introduced in version 1.5.
1.4 Introduces a more compact format for storing items. All settings
aside from name are now attributes. This format also changes the
- way satus and importance labels are stored and handled.
+ way satus and importance labels, last used handles, and title
+ formats are stored. They are now all stored as key/value sets.
Introduced in version 2.0.
"""
@@ -158,7 +158,7 @@ class ProjectXMLReader:
except Exception as exc:
# Trying to open backup file instead
- logger.error("Failed to parse project xml", exc_info=exc)
+ logger.error("Failed to parse project XML", exc_info=exc)
self._state = XMLReadState.CANNOT_PARSE
backFile = self._path[:-3]+"bak"
@@ -168,7 +168,7 @@ class ProjectXMLReader:
self._state = XMLReadState.PARSED_BACKUP
logger.info("Backup project file parsed")
except Exception as exc:
- logger.error("Failed to parse backup project xml", exc_info=exc)
+ logger.error("Failed to parse backup project XML", exc_info=exc)
self._state = XMLReadState.CANNOT_PARSE
return False
else:
@@ -205,7 +205,7 @@ class ProjectXMLReader:
else:
self._parseProjectContentLegacy(xSection, projContent, projData)
else:
- logger.warning("Ignored in xml", xSection.tag)
+ logger.warning("Ignored in XML", xSection.tag)
if self._version == 0x0104:
self._state = XMLReadState.PARSED_OK
@@ -238,7 +238,7 @@ class ProjectXMLReader:
elif xItem.tag == "editTime":
projData.setEditTime(xItem.text)
else:
- logger.warning("Ignored in xml", xItem.tag)
+ logger.warning("Ignored in XML", xItem.tag)
return
@@ -277,7 +277,7 @@ class ProjectXMLReader:
else: # Pre 1.4 format
projData.setTitleFormat(self._parseDictTagText(xItem))
else:
- logger.warning("Ignored in xml", xItem.tag)
+ logger.warning("Ignored in XML", xItem.tag)
return
@@ -289,6 +289,10 @@ class ProjectXMLReader:
for xItem in xSection:
if xItem.tag == "item":
item = {}
+ meta = {}
+ name = {}
+ itemName = ""
+
item["handle"] = checkStringNone(xItem.attrib.get("handle"), None)
item["parent"] = checkStringNone(xItem.attrib.get("parent"), None)
item["root"] = checkStringNone(xItem.attrib.get("root"), None)
@@ -298,28 +302,33 @@ class ProjectXMLReader:
item["layout"] = checkString(xItem.attrib.get("layout"), "NO_LAYOUT")
for xVal in xItem:
if xVal.tag == "meta":
- item["expanded"] = checkBool(xVal.attrib.get("expanded"), False)
- item["heading"] = checkString(xVal.attrib.get("heading"), "H0")
- item["charCount"] = checkInt(xVal.attrib.get("charCount"), 0)
- item["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0)
- item["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0)
- item["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0)
+ meta["expanded"] = checkBool(xVal.attrib.get("expanded"), False)
+ meta["heading"] = checkString(xVal.attrib.get("heading"), "H0")
+ meta["charCount"] = checkInt(xVal.attrib.get("charCount"), 0)
+ meta["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0)
+ meta["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0)
+ meta["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0)
elif xVal.tag == "name":
- item["label"] = simplified(checkString(xVal.text, ""))
- item["status"] = checkStringNone(xVal.attrib.get("status"), None)
- item["import"] = checkStringNone(xVal.attrib.get("import"), None)
- item["active"] = checkBool(xVal.attrib.get("active"), False)
+ itemName = simplified(checkString(xVal.text, ""))
+ name["status"] = checkStringNone(xVal.attrib.get("status"), None)
+ name["import"] = checkStringNone(xVal.attrib.get("import"), None)
+ name["active"] = checkBool(xVal.attrib.get("active"), False)
# ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases.
if "exported" in xVal.attrib:
- item["active"] = checkBool(xVal.attrib.get("exported"), False)
+ name["active"] = checkBool(xVal.attrib.get("exported"), False)
else:
- logger.warning("Ignored in xml", xVal.tag)
+ logger.warning("Ignored in XML", xVal.tag)
- projContent.append(item)
+ projContent.append({
+ "name": itemName,
+ "itemAttr": item,
+ "metaAttr": meta,
+ "nameAttr": name,
+ })
else:
- logger.warning("Ignored item in xml", xItem.tag)
+ logger.warning("Ignored item in XML", xItem.tag)
return
@@ -334,17 +343,21 @@ class ProjectXMLReader:
for xItem in xSection:
item = {}
+ meta = {}
+ name = {}
+ itemName = ""
+
if xItem.tag == "item":
item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None)
item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None)
item["root"] = None # Value was added in 1.4
item["order"] = checkInt(xItem.attrib.get("order", 0), 0)
- item["heading"] = "H0" # Value was added in 1.4
+ meta["heading"] = "H0" # Value was added in 1.4
tmpStatus = ""
for xVal in xItem:
if xVal.tag == "name":
- item["label"] = simplified(checkString(xVal.text, ""))
+ itemName = simplified(checkString(xVal.text, ""))
elif xVal.tag == "status":
tmpStatus = checkStringNone(xVal.text, None)
elif xVal.tag == "type":
@@ -354,25 +367,25 @@ class ProjectXMLReader:
elif xVal.tag == "layout":
item["layout"] = checkString(xVal.text, "")
elif xVal.tag == "expanded":
- item["expanded"] = checkBool(xVal.text, False)
+ meta["expanded"] = checkBool(xVal.text, False)
elif xVal.tag == "exported": # Renamed to active in 1.4
- item["active"] = checkBool(xVal.text, False)
+ name["active"] = checkBool(xVal.text, False)
elif xVal.tag == "charCount":
- item["charCount"] = checkInt(xVal.text, 0)
+ meta["charCount"] = checkInt(xVal.text, 0)
elif xVal.tag == "wordCount":
- item["wordCount"] = checkInt(xVal.text, 0)
+ meta["wordCount"] = checkInt(xVal.text, 0)
elif xVal.tag == "paraCount":
- item["paraCount"] = checkInt(xVal.text, 0)
+ meta["paraCount"] = checkInt(xVal.text, 0)
elif xVal.tag == "cursorPos":
- item["cursorPos"] = checkInt(xVal.text, 0)
+ meta["cursorPos"] = checkInt(xVal.text, 0)
else:
- logger.warning("Ignored in xml", xVal.tag)
+ logger.warning("Ignored in XML", xVal.tag)
# Status was split into separate status/import with a key in 1.4
if item.get("class", "") in ("NOVEL", "ARCHIVE"):
- item["status"] = statusMap.get(tmpStatus, None)
+ name["status"] = statusMap.get(tmpStatus, None)
else:
- item["import"] = importMap.get(tmpStatus, None)
+ name["import"] = importMap.get(tmpStatus, None)
# A number of layouts were removed in 1.3
if item.get("layout", "") in (
@@ -384,10 +397,15 @@ class ProjectXMLReader:
if item.get("type", "") == "TRASH":
item["type"] = "ROOT"
- projContent.append(item)
+ projContent.append({
+ "name": itemName,
+ "itemAttr": item,
+ "metaAttr": meta,
+ "nameAttr": name,
+ })
else:
- logger.warning("Ignored in xml", xItem.tag)
+ logger.warning("Ignored in XML", xItem.tag)
return
@@ -495,7 +513,7 @@ class ProjectXMLWriter:
xName = etree.SubElement(xItem, "name", attrib=item.get("nameAttr", {}))
xName.text = item["name"]
- # Write the xml tree to file
+ # Write the XML tree to file
saveFile = os.path.join(self._path, nwFiles.PROJ_FILE)
tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~")
backFile = os.path.join(self._path, nwFiles.PROJ_FILE[:-3]+"bak")
@@ -530,14 +548,14 @@ class ProjectXMLWriter:
##
def _packSingleValue(self, xParent, name, value, attrib=None):
- """Pack a single value into an xml element.
+ """Pack a single value into an XML element.
"""
xItem = etree.SubElement(xParent, name, attrib=attrib)
xItem.text = str(value) or ""
return
def _packListValue(self, xParent, name, data):
- """Pack a list of values into an xml element.
+ """Pack a list of values into an XML element.
"""
for value in data:
xItem = etree.SubElement(xParent, name)
@@ -545,7 +563,7 @@ class ProjectXMLWriter:
return
def _packDictKeyValue(self, xParent, name, data):
- """Pack the entries of a dictionary into an xml element.
+ """Pack the entries of a dictionary into an XML element.
"""
xItem = etree.SubElement(xParent, name)
for key, value in data.items():
diff --git a/tests/reference/projectXML_ReadCurrent.json b/tests/reference/projectXML_ReadCurrent.json
index 141c3c3a..758a1f56 100644
--- a/tests/reference/projectXML_ReadCurrent.json
+++ b/tests/reference/projectXML_ReadCurrent.json
@@ -1,515 +1,677 @@
[
{
- "handle": "7031beac91f75",
- "parent": null,
- "root": "7031beac91f75",
- "order": 0,
- "type": "ROOT",
- "class": "NOVEL",
- "layout": "NO_LAYOUT",
- "expanded": true,
- "heading": "H0",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 0,
- "label": "Novel",
- "status": "sc24b8f",
- "import": "ia857f0",
- "active": false
+ "name": "Novel",
+ "itemAttr": {
+ "handle": "7031beac91f75",
+ "parent": null,
+ "root": "7031beac91f75",
+ "order": 0,
+ "type": "ROOT",
+ "class": "NOVEL",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sc24b8f",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "53b69b83cdafc",
- "parent": "7031beac91f75",
- "root": "7031beac91f75",
- "order": 0,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H1",
- "charCount": 93,
- "wordCount": 19,
- "paraCount": 2,
- "cursorPos": 119,
- "label": "Title Page",
- "status": "sc24b8f",
- "import": "ia857f0",
- "active": true
+ "name": "Title Page",
+ "itemAttr": {
+ "handle": "53b69b83cdafc",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 93,
+ "wordCount": 19,
+ "paraCount": 2,
+ "cursorPos": 119
+ },
+ "nameAttr": {
+ "status": "sc24b8f",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "974e400180a99",
- "parent": "7031beac91f75",
- "root": "7031beac91f75",
- "order": 1,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H0",
- "charCount": 251,
- "wordCount": 50,
- "paraCount": 2,
- "cursorPos": 277,
- "label": "Page",
- "status": "sf12341",
- "import": "ia857f0",
- "active": true
+ "name": "Page",
+ "itemAttr": {
+ "handle": "974e400180a99",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 251,
+ "wordCount": 50,
+ "paraCount": 2,
+ "cursorPos": 277
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "edca4be2fcaf8",
- "parent": "7031beac91f75",
- "root": "7031beac91f75",
- "order": 2,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H1",
- "charCount": 26,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 36,
- "label": "Part One",
- "status": "s90e6c9",
- "import": "ia857f0",
- "active": true
+ "name": "Part One",
+ "itemAttr": {
+ "handle": "edca4be2fcaf8",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 26,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 36
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "6a2d6d5f4f401",
- "parent": "7031beac91f75",
- "root": "7031beac91f75",
- "order": 3,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": true,
- "heading": "H2",
- "charCount": 95,
- "wordCount": 18,
- "paraCount": 1,
- "cursorPos": 291,
- "label": "Chapter One",
- "status": "sf24ce6",
- "import": "ia857f0",
- "active": true
+ "name": "Chapter One",
+ "itemAttr": {
+ "handle": "6a2d6d5f4f401",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 3,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H2",
+ "charCount": 95,
+ "wordCount": 18,
+ "paraCount": 1,
+ "cursorPos": 291
+ },
+ "nameAttr": {
+ "status": "sf24ce6",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "636b6aa9b697b",
- "parent": "6a2d6d5f4f401",
- "root": "7031beac91f75",
- "order": 0,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H3",
- "charCount": 2687,
- "wordCount": 479,
- "paraCount": 14,
- "cursorPos": 67,
- "label": "Making a Scene",
- "status": "s90e6c9",
- "import": "ia857f0",
- "active": true
+ "name": "Making a Scene",
+ "itemAttr": {
+ "handle": "636b6aa9b697b",
+ "parent": "6a2d6d5f4f401",
+ "root": "7031beac91f75",
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H3",
+ "charCount": 2687,
+ "wordCount": 479,
+ "paraCount": 14,
+ "cursorPos": 67
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "bc0cbd2a407f3",
- "parent": "6a2d6d5f4f401",
- "root": "7031beac91f75",
- "order": 1,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H3",
- "charCount": 548,
- "wordCount": 108,
- "paraCount": 3,
- "cursorPos": 465,
- "label": "Another Scene",
- "status": "s90e6c9",
- "import": "ia857f0",
- "active": true
+ "name": "Another Scene",
+ "itemAttr": {
+ "handle": "bc0cbd2a407f3",
+ "parent": "6a2d6d5f4f401",
+ "root": "7031beac91f75",
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H3",
+ "charCount": 548,
+ "wordCount": 108,
+ "paraCount": 3,
+ "cursorPos": 465
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "ba8a28a246524",
- "parent": "7031beac91f75",
- "root": "7031beac91f75",
- "order": 4,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H2",
- "charCount": 617,
- "wordCount": 101,
- "paraCount": 3,
- "cursorPos": 310,
- "label": "Interlude",
- "status": "s78ea90",
- "import": "ia857f0",
- "active": true
+ "name": "Interlude",
+ "itemAttr": {
+ "handle": "ba8a28a246524",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 4,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H2",
+ "charCount": 617,
+ "wordCount": 101,
+ "paraCount": 3,
+ "cursorPos": 310
+ },
+ "nameAttr": {
+ "status": "s78ea90",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "96b68994dfa3d",
- "parent": "7031beac91f75",
- "root": "7031beac91f75",
- "order": 5,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "NOTE",
- "expanded": false,
- "heading": "H1",
- "charCount": 1909,
- "wordCount": 346,
- "paraCount": 7,
- "cursorPos": 0,
- "label": "A Note on Structure",
- "status": "sf24ce6",
- "import": "ia857f0",
- "active": false
+ "name": "A Note on Structure",
+ "itemAttr": {
+ "handle": "96b68994dfa3d",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 5,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 1909,
+ "wordCount": 346,
+ "paraCount": 7,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf24ce6",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "88706ddc78b1b",
- "parent": "7031beac91f75",
- "root": "7031beac91f75",
- "order": 6,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": true,
- "heading": "H2",
- "charCount": 139,
- "wordCount": 28,
- "paraCount": 1,
- "cursorPos": 188,
- "label": "Chapter Two",
- "status": "s90e6c9",
- "import": "ia857f0",
- "active": true
+ "name": "Chapter Two",
+ "itemAttr": {
+ "handle": "88706ddc78b1b",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 6,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H2",
+ "charCount": 139,
+ "wordCount": 28,
+ "paraCount": 1,
+ "cursorPos": 188
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "ae7339df26ded",
- "parent": "88706ddc78b1b",
- "root": "7031beac91f75",
- "order": 0,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H3",
- "charCount": 189,
- "wordCount": 37,
- "paraCount": 1,
- "cursorPos": 0,
- "label": "We Found John!",
- "status": "s90e6c9",
- "import": "ia857f0",
- "active": true
+ "name": "We Found John!",
+ "itemAttr": {
+ "handle": "ae7339df26ded",
+ "parent": "88706ddc78b1b",
+ "root": "7031beac91f75",
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H3",
+ "charCount": 189,
+ "wordCount": 37,
+ "paraCount": 1,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "e5e47ebf63b1c",
- "parent": null,
- "root": "e5e47ebf63b1c",
- "order": 1,
- "type": "ROOT",
- "class": "NOVEL",
- "layout": "NO_LAYOUT",
- "expanded": true,
- "heading": "H0",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 0,
- "label": "Sequel",
- "status": "sf12341",
- "import": "ia857f0",
- "active": false
+ "name": "Sequel",
+ "itemAttr": {
+ "handle": "e5e47ebf63b1c",
+ "parent": null,
+ "root": "e5e47ebf63b1c",
+ "order": 1,
+ "type": "ROOT",
+ "class": "NOVEL",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "bacb7059e3083",
- "parent": "e5e47ebf63b1c",
- "root": "e5e47ebf63b1c",
- "order": 0,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H1",
- "charCount": 27,
- "wordCount": 5,
- "paraCount": 1,
- "cursorPos": 100,
- "label": "Title Page",
- "status": "sc24b8f",
- "import": "ia857f0",
- "active": true
+ "name": "Title Page",
+ "itemAttr": {
+ "handle": "bacb7059e3083",
+ "parent": "e5e47ebf63b1c",
+ "root": "e5e47ebf63b1c",
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 27,
+ "wordCount": 5,
+ "paraCount": 1,
+ "cursorPos": 100
+ },
+ "nameAttr": {
+ "status": "sc24b8f",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "a520879ca0b45",
- "parent": "e5e47ebf63b1c",
- "root": "e5e47ebf63b1c",
- "order": 1,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H2",
- "charCount": 299,
- "wordCount": 55,
- "paraCount": 2,
- "cursorPos": 104,
- "label": "Chapter One",
- "status": "s90e6c9",
- "import": "ia857f0",
- "active": true
+ "name": "Chapter One",
+ "itemAttr": {
+ "handle": "a520879ca0b45",
+ "parent": "e5e47ebf63b1c",
+ "root": "e5e47ebf63b1c",
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H2",
+ "charCount": 299,
+ "wordCount": 55,
+ "paraCount": 2,
+ "cursorPos": 104
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "f6622b4617424",
- "parent": null,
- "root": "f6622b4617424",
- "order": 2,
- "type": "ROOT",
- "class": "CHARACTER",
- "layout": "NO_LAYOUT",
- "expanded": true,
- "heading": "H0",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 0,
- "label": "Characters",
- "status": "sf12341",
- "import": "ia857f0",
- "active": false
+ "name": "Characters",
+ "itemAttr": {
+ "handle": "f6622b4617424",
+ "parent": null,
+ "root": "f6622b4617424",
+ "order": 2,
+ "type": "ROOT",
+ "class": "CHARACTER",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "f7e2d9f330615",
- "parent": "f6622b4617424",
- "root": "f6622b4617424",
- "order": 0,
- "type": "FOLDER",
- "class": "CHARACTER",
- "layout": "NO_LAYOUT",
- "expanded": true,
- "heading": "H0",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 0,
- "label": "Main Characters",
- "status": "sf12341",
- "import": "ia857f0",
- "active": false
+ "name": "Main Characters",
+ "itemAttr": {
+ "handle": "f7e2d9f330615",
+ "parent": "f6622b4617424",
+ "root": "f6622b4617424",
+ "order": 0,
+ "type": "FOLDER",
+ "class": "CHARACTER",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "14298de4d9524",
- "parent": "f7e2d9f330615",
- "root": "f6622b4617424",
- "order": 0,
- "type": "FILE",
- "class": "CHARACTER",
- "layout": "NOTE",
- "expanded": false,
- "heading": "H1",
- "charCount": 49,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 24,
- "label": "John Smith",
- "status": "sf12341",
- "import": "icfb3a5",
- "active": true
+ "name": "John Smith",
+ "itemAttr": {
+ "handle": "14298de4d9524",
+ "parent": "f7e2d9f330615",
+ "root": "f6622b4617424",
+ "order": 0,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 49,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 24
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "icfb3a5",
+ "active": true
+ }
},
{
- "handle": "bb2c23b3c42cc",
- "parent": "f7e2d9f330615",
- "root": "f6622b4617424",
- "order": 1,
- "type": "FILE",
- "class": "CHARACTER",
- "layout": "NOTE",
- "expanded": false,
- "heading": "H1",
- "charCount": 55,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 25,
- "label": "Jane Smith",
- "status": "sf12341",
- "import": "i2d7a54",
- "active": true
+ "name": "Jane Smith",
+ "itemAttr": {
+ "handle": "bb2c23b3c42cc",
+ "parent": "f7e2d9f330615",
+ "root": "f6622b4617424",
+ "order": 1,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 55,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 25
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "i2d7a54",
+ "active": true
+ }
},
{
- "handle": "15c4492bd5107",
- "parent": null,
- "root": "15c4492bd5107",
- "order": 3,
- "type": "ROOT",
- "class": "WORLD",
- "layout": "NO_LAYOUT",
- "expanded": true,
- "heading": "H0",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 0,
- "label": "Locations",
- "status": "sf12341",
- "import": "ia857f0",
- "active": false
+ "name": "Locations",
+ "itemAttr": {
+ "handle": "15c4492bd5107",
+ "parent": null,
+ "root": "15c4492bd5107",
+ "order": 3,
+ "type": "ROOT",
+ "class": "WORLD",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "b3e74dbc1f584",
- "parent": "15c4492bd5107",
- "root": "15c4492bd5107",
- "order": 0,
- "type": "FILE",
- "class": "WORLD",
- "layout": "NOTE",
- "expanded": false,
- "heading": "H1",
- "charCount": 76,
- "wordCount": 15,
- "paraCount": 1,
- "cursorPos": 20,
- "label": "Earth",
- "status": "sf12341",
- "import": "i56be10",
- "active": true
+ "name": "Earth",
+ "itemAttr": {
+ "handle": "b3e74dbc1f584",
+ "parent": "15c4492bd5107",
+ "root": "15c4492bd5107",
+ "order": 0,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 76,
+ "wordCount": 15,
+ "paraCount": 1,
+ "cursorPos": 20
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "i56be10",
+ "active": true
+ }
},
{
- "handle": "f1471bef9f2ae",
- "parent": "15c4492bd5107",
- "root": "15c4492bd5107",
- "order": 1,
- "type": "FILE",
- "class": "WORLD",
- "layout": "NOTE",
- "expanded": false,
- "heading": "H1",
- "charCount": 115,
- "wordCount": 24,
- "paraCount": 1,
- "cursorPos": 133,
- "label": "Space",
- "status": "sf12341",
- "import": "icfb3a5",
- "active": true
+ "name": "Space",
+ "itemAttr": {
+ "handle": "f1471bef9f2ae",
+ "parent": "15c4492bd5107",
+ "root": "15c4492bd5107",
+ "order": 1,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 115,
+ "wordCount": 24,
+ "paraCount": 1,
+ "cursorPos": 133
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "icfb3a5",
+ "active": true
+ }
},
{
- "handle": "5eaea4e8cdee8",
- "parent": "15c4492bd5107",
- "root": "15c4492bd5107",
- "order": 2,
- "type": "FILE",
- "class": "WORLD",
- "layout": "NOTE",
- "expanded": false,
- "heading": "H1",
- "charCount": 28,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 45,
- "label": "Mars",
- "status": "sf12341",
- "import": "i2d7a54",
- "active": true
+ "name": "Mars",
+ "itemAttr": {
+ "handle": "5eaea4e8cdee8",
+ "parent": "15c4492bd5107",
+ "root": "15c4492bd5107",
+ "order": 2,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H1",
+ "charCount": 28,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 45
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "i2d7a54",
+ "active": true
+ }
},
{
- "handle": "6827118336ac1",
- "parent": null,
- "root": "6827118336ac1",
- "order": 4,
- "type": "ROOT",
- "class": "ARCHIVE",
- "layout": "NO_LAYOUT",
- "expanded": true,
- "heading": "H0",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 0,
- "label": "Archive",
- "status": "sf12341",
- "import": "ia857f0",
- "active": false
+ "name": "Archive",
+ "itemAttr": {
+ "handle": "6827118336ac1",
+ "parent": null,
+ "root": "6827118336ac1",
+ "order": 4,
+ "type": "ROOT",
+ "class": "ARCHIVE",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "ae9bf3c3ea159",
- "parent": "6827118336ac1",
- "root": "6827118336ac1",
- "order": 0,
- "type": "FOLDER",
- "class": "ARCHIVE",
- "layout": "NO_LAYOUT",
- "expanded": true,
- "heading": "H0",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 0,
- "label": "Scenes",
- "status": "sf12341",
- "import": "ia857f0",
- "active": false
+ "name": "Scenes",
+ "itemAttr": {
+ "handle": "ae9bf3c3ea159",
+ "parent": "6827118336ac1",
+ "root": "6827118336ac1",
+ "order": 0,
+ "type": "FOLDER",
+ "class": "ARCHIVE",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "8a5deb88c0e97",
- "parent": "ae9bf3c3ea159",
- "root": "6827118336ac1",
- "order": 0,
- "type": "FILE",
- "class": "ARCHIVE",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H3",
- "charCount": 232,
- "wordCount": 42,
- "paraCount": 1,
- "cursorPos": 239,
- "label": "Old File",
- "status": "s90e6c9",
- "import": "ia857f0",
- "active": true
+ "name": "Old File",
+ "itemAttr": {
+ "handle": "8a5deb88c0e97",
+ "parent": "ae9bf3c3ea159",
+ "root": "6827118336ac1",
+ "order": 0,
+ "type": "FILE",
+ "class": "ARCHIVE",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H3",
+ "charCount": 232,
+ "wordCount": 42,
+ "paraCount": 1,
+ "cursorPos": 239
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
},
{
- "handle": "98acd8c76c93a",
- "parent": null,
- "root": "98acd8c76c93a",
- "order": 5,
- "type": "ROOT",
- "class": "TRASH",
- "layout": "NO_LAYOUT",
- "expanded": true,
- "heading": "H0",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 0,
- "label": "Trash",
- "status": "sf12341",
- "import": "ia857f0",
- "active": false
+ "name": "Trash",
+ "itemAttr": {
+ "handle": "98acd8c76c93a",
+ "parent": null,
+ "root": "98acd8c76c93a",
+ "order": 5,
+ "type": "ROOT",
+ "class": "TRASH",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
},
{
- "handle": "b8136a5a774a0",
- "parent": "98acd8c76c93a",
- "root": "98acd8c76c93a",
- "order": 0,
- "type": "FILE",
- "class": "TRASH",
- "layout": "DOCUMENT",
- "expanded": false,
- "heading": "H3",
- "charCount": 30,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 36,
- "label": "Delete Me!",
- "status": "sf12341",
- "import": "ia857f0",
- "active": true
+ "name": "Delete Me!",
+ "itemAttr": {
+ "handle": "b8136a5a774a0",
+ "parent": "98acd8c76c93a",
+ "root": "98acd8c76c93a",
+ "order": 0,
+ "type": "FILE",
+ "class": "TRASH",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H3",
+ "charCount": 30,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 36
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": true
+ }
}
-]
\ No newline at end of file
+]
diff --git a/tests/reference/projectXML_ReadLegacy10.json b/tests/reference/projectXML_ReadLegacy10.json
index 27e9da17..2a44ede2 100644
--- a/tests/reference/projectXML_ReadLegacy10.json
+++ b/tests/reference/projectXML_ReadLegacy10.json
@@ -1,362 +1,494 @@
[
{
- "handle": "7031beac91f75",
- "parent": null,
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Novel",
- "type": "ROOT",
- "class": "NOVEL",
- "expanded": true,
- "status": "s000002"
+ "name": "Novel",
+ "itemAttr": {
+ "handle": "7031beac91f75",
+ "parent": null,
+ "root": null,
+ "order": 0,
+ "type": "ROOT",
+ "class": "NOVEL"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000002"
+ }
},
{
- "handle": "53b69b83cdafc",
- "parent": "7031beac91f75",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Title Page",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 72,
- "wordCount": 15,
- "paraCount": 2,
- "cursorPos": 78,
- "status": "s000002"
+ "name": "Title Page",
+ "itemAttr": {
+ "handle": "53b69b83cdafc",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 72,
+ "wordCount": 15,
+ "paraCount": 2,
+ "cursorPos": 78
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000002"
+ }
},
{
- "handle": "974e400180a99",
- "parent": "7031beac91f75",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Page",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 208,
- "wordCount": 40,
- "paraCount": 2,
- "cursorPos": 213,
- "status": "s000000"
+ "name": "Page",
+ "itemAttr": {
+ "handle": "974e400180a99",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 208,
+ "wordCount": 40,
+ "paraCount": 2,
+ "cursorPos": 213
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "edca4be2fcaf8",
- "parent": "7031beac91f75",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Part One",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 23,
- "wordCount": 5,
- "paraCount": 1,
- "cursorPos": 0,
- "status": "s000000"
+ "name": "Part One",
+ "itemAttr": {
+ "handle": "edca4be2fcaf8",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 23,
+ "wordCount": 5,
+ "paraCount": 1,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "e7ded148d6e4a",
- "parent": "7031beac91f75",
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "A Folder",
- "type": "FOLDER",
- "class": "NOVEL",
- "expanded": true,
- "status": "s000003"
+ "name": "A Folder",
+ "itemAttr": {
+ "handle": "e7ded148d6e4a",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 3,
+ "type": "FOLDER",
+ "class": "NOVEL"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000003"
+ }
},
{
- "handle": "6a2d6d5f4f401",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Chapter One",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 12,
- "wordCount": 3,
- "paraCount": 0,
- "cursorPos": 215,
- "status": "s000001"
+ "name": "Chapter One",
+ "itemAttr": {
+ "handle": "6a2d6d5f4f401",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 12,
+ "wordCount": 3,
+ "paraCount": 0,
+ "cursorPos": 215
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000001"
+ }
},
{
- "handle": "636b6aa9b697b",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Making a Scene",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 1199,
- "wordCount": 216,
- "paraCount": 7,
- "cursorPos": 527,
- "status": "s000003"
+ "name": "Making a Scene",
+ "itemAttr": {
+ "handle": "636b6aa9b697b",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 1199,
+ "wordCount": 216,
+ "paraCount": 7,
+ "cursorPos": 527
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "bc0cbd2a407f3",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Another Scene",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 476,
- "wordCount": 93,
- "paraCount": 3,
- "cursorPos": 551,
- "status": "s000003"
+ "name": "Another Scene",
+ "itemAttr": {
+ "handle": "bc0cbd2a407f3",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 476,
+ "wordCount": 93,
+ "paraCount": 3,
+ "cursorPos": 551
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "ba8a28a246524",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "Interlude",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 633,
- "wordCount": 101,
- "paraCount": 3,
- "cursorPos": 1238,
- "status": "s000006"
+ "name": "Interlude",
+ "itemAttr": {
+ "handle": "ba8a28a246524",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 3,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 633,
+ "wordCount": 101,
+ "paraCount": 3,
+ "cursorPos": 1238
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000006"
+ }
},
{
- "handle": "96b68994dfa3d",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 4,
- "heading": "H0",
- "label": "A Note on Structure",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": false,
- "layout": "NOTE",
- "charCount": 1692,
- "wordCount": 313,
- "paraCount": 6,
- "cursorPos": 1721,
- "status": "s000004"
+ "name": "A Note on Structure",
+ "itemAttr": {
+ "handle": "96b68994dfa3d",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 4,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 1692,
+ "wordCount": 313,
+ "paraCount": 6,
+ "cursorPos": 1721
+ },
+ "nameAttr": {
+ "active": false,
+ "status": "s000004"
+ }
},
{
- "handle": "88706ddc78b1b",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 5,
- "heading": "H0",
- "label": "Chapter Two",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 139,
- "wordCount": 28,
- "paraCount": 1,
- "cursorPos": 343,
- "status": "s000003"
+ "name": "Chapter Two",
+ "itemAttr": {
+ "handle": "88706ddc78b1b",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 5,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 139,
+ "wordCount": 28,
+ "paraCount": 1,
+ "cursorPos": 343
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "ae7339df26ded",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 6,
- "heading": "H0",
- "label": "We Found John!",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 189,
- "wordCount": 37,
- "paraCount": 1,
- "cursorPos": 224,
- "status": "s000003"
+ "name": "We Found John!",
+ "itemAttr": {
+ "handle": "ae7339df26ded",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 6,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 189,
+ "wordCount": 37,
+ "paraCount": 1,
+ "cursorPos": 224
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "f6622b4617424",
- "parent": null,
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Characters",
- "type": "ROOT",
- "class": "CHARACTER",
- "expanded": true,
- "import": null
+ "name": "Characters",
+ "itemAttr": {
+ "handle": "f6622b4617424",
+ "parent": null,
+ "root": null,
+ "order": 1,
+ "type": "ROOT",
+ "class": "CHARACTER"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "f7e2d9f330615",
- "parent": "f6622b4617424",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Main Characters",
- "type": "FOLDER",
- "class": "CHARACTER",
- "expanded": true,
- "import": null
+ "name": "Main Characters",
+ "itemAttr": {
+ "handle": "f7e2d9f330615",
+ "parent": "f6622b4617424",
+ "root": null,
+ "order": 0,
+ "type": "FOLDER",
+ "class": "CHARACTER"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "14298de4d9524",
- "parent": "f7e2d9f330615",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "John Smith",
- "type": "FILE",
- "class": "CHARACTER",
- "expanded": false,
- "active": true,
- "layout": "NOTE",
- "charCount": 49,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 24,
- "import": "i000008"
+ "name": "John Smith",
+ "itemAttr": {
+ "handle": "14298de4d9524",
+ "parent": "f7e2d9f330615",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 49,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 24
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000008"
+ }
},
{
- "handle": "bb2c23b3c42cc",
- "parent": "f7e2d9f330615",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Jane Smith",
- "type": "FILE",
- "class": "CHARACTER",
- "expanded": false,
- "active": true,
- "layout": "NOTE",
- "charCount": 55,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 25,
- "import": "i000009"
+ "name": "Jane Smith",
+ "itemAttr": {
+ "handle": "bb2c23b3c42cc",
+ "parent": "f7e2d9f330615",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 55,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 25
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000009"
+ }
},
{
- "handle": "15c4492bd5107",
- "parent": null,
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Locations",
- "type": "ROOT",
- "class": "WORLD",
- "expanded": true,
- "import": null
+ "name": "Locations",
+ "itemAttr": {
+ "handle": "15c4492bd5107",
+ "parent": null,
+ "root": null,
+ "order": 2,
+ "type": "ROOT",
+ "class": "WORLD"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "b3e74dbc1f584",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Earth",
- "type": "FILE",
- "class": "WORLD",
- "expanded": false,
- "active": true,
- "layout": "NOTE",
- "charCount": 76,
- "wordCount": 15,
- "paraCount": 1,
- "cursorPos": 20,
- "import": "i00000a"
+ "name": "Earth",
+ "itemAttr": {
+ "handle": "b3e74dbc1f584",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 76,
+ "wordCount": 15,
+ "paraCount": 1,
+ "cursorPos": 20
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i00000a"
+ }
},
{
- "handle": "f1471bef9f2ae",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Space",
- "type": "FILE",
- "class": "WORLD",
- "expanded": false,
- "active": true,
- "layout": "NOTE",
- "charCount": 115,
- "wordCount": 24,
- "paraCount": 1,
- "cursorPos": 133,
- "import": "i000008"
+ "name": "Space",
+ "itemAttr": {
+ "handle": "f1471bef9f2ae",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 115,
+ "wordCount": 24,
+ "paraCount": 1,
+ "cursorPos": 133
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000008"
+ }
},
{
- "handle": "5eaea4e8cdee8",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Mars",
- "type": "FILE",
- "class": "WORLD",
- "expanded": false,
- "active": true,
- "layout": "NOTE",
- "charCount": 28,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 45,
- "import": "i000009"
+ "name": "Mars",
+ "itemAttr": {
+ "handle": "5eaea4e8cdee8",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 28,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 45
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000009"
+ }
},
{
- "handle": "98acd8c76c93a",
- "parent": null,
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "Trash",
- "type": "ROOT",
- "class": "TRASH",
- "expanded": true,
- "import": null
+ "name": "Trash",
+ "itemAttr": {
+ "handle": "98acd8c76c93a",
+ "parent": null,
+ "root": null,
+ "order": 3,
+ "type": "ROOT",
+ "class": "TRASH"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "b8136a5a774a0",
- "parent": "98acd8c76c93a",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Delete Me!",
- "type": "FILE",
- "class": "NOVEL",
- "expanded": false,
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 0,
- "wordCount": 0,
- "paraCount": 0,
- "cursorPos": 36,
- "status": "s000000"
+ "name": "Delete Me!",
+ "itemAttr": {
+ "handle": "b8136a5a774a0",
+ "parent": "98acd8c76c93a",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": false,
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 36
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
}
-]
\ No newline at end of file
+]
diff --git a/tests/reference/projectXML_ReadLegacy11.json b/tests/reference/projectXML_ReadLegacy11.json
index 16e84adc..20ed9609 100644
--- a/tests/reference/projectXML_ReadLegacy11.json
+++ b/tests/reference/projectXML_ReadLegacy11.json
@@ -1,346 +1,478 @@
[
{
- "handle": "7031beac91f75",
- "parent": null,
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Novel",
- "type": "ROOT",
- "class": "NOVEL",
- "expanded": true,
- "status": "s000002"
+ "name": "Novel",
+ "itemAttr": {
+ "handle": "7031beac91f75",
+ "parent": null,
+ "root": null,
+ "order": 0,
+ "type": "ROOT",
+ "class": "NOVEL"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000002"
+ }
},
{
- "handle": "53b69b83cdafc",
- "parent": "7031beac91f75",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Title Page",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 72,
- "wordCount": 15,
- "paraCount": 2,
- "cursorPos": 78,
- "status": "s000002"
+ "name": "Title Page",
+ "itemAttr": {
+ "handle": "53b69b83cdafc",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 72,
+ "wordCount": 15,
+ "paraCount": 2,
+ "cursorPos": 78
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000002"
+ }
},
{
- "handle": "974e400180a99",
- "parent": "7031beac91f75",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Page",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 210,
- "wordCount": 40,
- "paraCount": 2,
- "cursorPos": 213,
- "status": "s000000"
+ "name": "Page",
+ "itemAttr": {
+ "handle": "974e400180a99",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 210,
+ "wordCount": 40,
+ "paraCount": 2,
+ "cursorPos": 213
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "edca4be2fcaf8",
- "parent": "7031beac91f75",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Part One",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 23,
- "wordCount": 5,
- "paraCount": 1,
- "cursorPos": 0,
- "status": "s000000"
+ "name": "Part One",
+ "itemAttr": {
+ "handle": "edca4be2fcaf8",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 23,
+ "wordCount": 5,
+ "paraCount": 1,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "e7ded148d6e4a",
- "parent": "7031beac91f75",
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "A Folder",
- "type": "FOLDER",
- "class": "NOVEL",
- "expanded": true,
- "status": "s000003"
+ "name": "A Folder",
+ "itemAttr": {
+ "handle": "e7ded148d6e4a",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 3,
+ "type": "FOLDER",
+ "class": "NOVEL"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000003"
+ }
},
{
- "handle": "6a2d6d5f4f401",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Chapter One",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 12,
- "wordCount": 3,
- "paraCount": 0,
- "cursorPos": 215,
- "status": "s000001"
+ "name": "Chapter One",
+ "itemAttr": {
+ "handle": "6a2d6d5f4f401",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 12,
+ "wordCount": 3,
+ "paraCount": 0,
+ "cursorPos": 215
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000001"
+ }
},
{
- "handle": "636b6aa9b697b",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Making a Scene",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 1483,
- "wordCount": 263,
- "paraCount": 8,
- "cursorPos": 1086,
- "status": "s000003"
+ "name": "Making a Scene",
+ "itemAttr": {
+ "handle": "636b6aa9b697b",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 1483,
+ "wordCount": 263,
+ "paraCount": 8,
+ "cursorPos": 1086
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "bc0cbd2a407f3",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Another Scene",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 476,
- "wordCount": 93,
- "paraCount": 3,
- "cursorPos": 428,
- "status": "s000003"
+ "name": "Another Scene",
+ "itemAttr": {
+ "handle": "bc0cbd2a407f3",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 476,
+ "wordCount": 93,
+ "paraCount": 3,
+ "cursorPos": 428
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "ba8a28a246524",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "Interlude",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 633,
- "wordCount": 101,
- "paraCount": 3,
- "cursorPos": 1238,
- "status": "s000006"
+ "name": "Interlude",
+ "itemAttr": {
+ "handle": "ba8a28a246524",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 3,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 633,
+ "wordCount": 101,
+ "paraCount": 3,
+ "cursorPos": 1238
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000006"
+ }
},
{
- "handle": "96b68994dfa3d",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 4,
- "heading": "H0",
- "label": "A Note on Structure",
- "type": "FILE",
- "class": "NOVEL",
- "active": false,
- "layout": "NOTE",
- "charCount": 1692,
- "wordCount": 313,
- "paraCount": 6,
- "cursorPos": 1721,
- "status": "s000004"
+ "name": "A Note on Structure",
+ "itemAttr": {
+ "handle": "96b68994dfa3d",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 4,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 1692,
+ "wordCount": 313,
+ "paraCount": 6,
+ "cursorPos": 1721
+ },
+ "nameAttr": {
+ "active": false,
+ "status": "s000004"
+ }
},
{
- "handle": "88706ddc78b1b",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 5,
- "heading": "H0",
- "label": "Chapter Two",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 139,
- "wordCount": 28,
- "paraCount": 1,
- "cursorPos": 343,
- "status": "s000003"
+ "name": "Chapter Two",
+ "itemAttr": {
+ "handle": "88706ddc78b1b",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 5,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 139,
+ "wordCount": 28,
+ "paraCount": 1,
+ "cursorPos": 343
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "ae7339df26ded",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 6,
- "heading": "H0",
- "label": "We Found John!",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 189,
- "wordCount": 37,
- "paraCount": 1,
- "cursorPos": 224,
- "status": "s000003"
+ "name": "We Found John!",
+ "itemAttr": {
+ "handle": "ae7339df26ded",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 6,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 189,
+ "wordCount": 37,
+ "paraCount": 1,
+ "cursorPos": 224
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "f6622b4617424",
- "parent": null,
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Characters",
- "type": "ROOT",
- "class": "CHARACTER",
- "expanded": true,
- "import": null
+ "name": "Characters",
+ "itemAttr": {
+ "handle": "f6622b4617424",
+ "parent": null,
+ "root": null,
+ "order": 1,
+ "type": "ROOT",
+ "class": "CHARACTER"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "f7e2d9f330615",
- "parent": "f6622b4617424",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Main Characters",
- "type": "FOLDER",
- "class": "CHARACTER",
- "expanded": true,
- "import": null
+ "name": "Main Characters",
+ "itemAttr": {
+ "handle": "f7e2d9f330615",
+ "parent": "f6622b4617424",
+ "root": null,
+ "order": 0,
+ "type": "FOLDER",
+ "class": "CHARACTER"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "14298de4d9524",
- "parent": "f7e2d9f330615",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "John Smith",
- "type": "FILE",
- "class": "CHARACTER",
- "active": true,
- "layout": "NOTE",
- "charCount": 49,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 24,
- "import": "i000008"
+ "name": "John Smith",
+ "itemAttr": {
+ "handle": "14298de4d9524",
+ "parent": "f7e2d9f330615",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 49,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 24
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000008"
+ }
},
{
- "handle": "bb2c23b3c42cc",
- "parent": "f7e2d9f330615",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Jane Smith",
- "type": "FILE",
- "class": "CHARACTER",
- "active": true,
- "layout": "NOTE",
- "charCount": 55,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 25,
- "import": "i000009"
+ "name": "Jane Smith",
+ "itemAttr": {
+ "handle": "bb2c23b3c42cc",
+ "parent": "f7e2d9f330615",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 55,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 25
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000009"
+ }
},
{
- "handle": "15c4492bd5107",
- "parent": null,
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Locations",
- "type": "ROOT",
- "class": "WORLD",
- "expanded": true,
- "import": null
+ "name": "Locations",
+ "itemAttr": {
+ "handle": "15c4492bd5107",
+ "parent": null,
+ "root": null,
+ "order": 2,
+ "type": "ROOT",
+ "class": "WORLD"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "b3e74dbc1f584",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Earth",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 76,
- "wordCount": 15,
- "paraCount": 1,
- "cursorPos": 20,
- "import": "i00000a"
+ "name": "Earth",
+ "itemAttr": {
+ "handle": "b3e74dbc1f584",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 76,
+ "wordCount": 15,
+ "paraCount": 1,
+ "cursorPos": 20
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i00000a"
+ }
},
{
- "handle": "f1471bef9f2ae",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Space",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 115,
- "wordCount": 24,
- "paraCount": 1,
- "cursorPos": 133,
- "import": "i000008"
+ "name": "Space",
+ "itemAttr": {
+ "handle": "f1471bef9f2ae",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 115,
+ "wordCount": 24,
+ "paraCount": 1,
+ "cursorPos": 133
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000008"
+ }
},
{
- "handle": "5eaea4e8cdee8",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Mars",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 28,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 45,
- "import": "i000009"
+ "name": "Mars",
+ "itemAttr": {
+ "handle": "5eaea4e8cdee8",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 28,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 45
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000009"
+ }
},
{
- "handle": "98acd8c76c93a",
- "parent": null,
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "Trash",
- "type": "ROOT",
- "class": "TRASH",
- "expanded": true,
- "import": null
+ "name": "Trash",
+ "itemAttr": {
+ "handle": "98acd8c76c93a",
+ "parent": null,
+ "root": null,
+ "order": 3,
+ "type": "ROOT",
+ "class": "TRASH"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "b8136a5a774a0",
- "parent": "98acd8c76c93a",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Delete Me!",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 30,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 36,
- "status": "s000000"
+ "name": "Delete Me!",
+ "itemAttr": {
+ "handle": "b8136a5a774a0",
+ "parent": "98acd8c76c93a",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 30,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 36
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
}
-]
\ No newline at end of file
+]
diff --git a/tests/reference/projectXML_ReadLegacy12.json b/tests/reference/projectXML_ReadLegacy12.json
index 917cb633..2826bffa 100644
--- a/tests/reference/projectXML_ReadLegacy12.json
+++ b/tests/reference/projectXML_ReadLegacy12.json
@@ -1,387 +1,537 @@
[
{
- "handle": "7031beac91f75",
- "parent": null,
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Novel",
- "type": "ROOT",
- "class": "NOVEL",
- "expanded": true,
- "status": "s000002"
+ "name": "Novel",
+ "itemAttr": {
+ "handle": "7031beac91f75",
+ "parent": null,
+ "root": null,
+ "order": 0,
+ "type": "ROOT",
+ "class": "NOVEL"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000002"
+ }
},
{
- "handle": "53b69b83cdafc",
- "parent": "7031beac91f75",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Title Page",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 241,
- "wordCount": 42,
- "paraCount": 3,
- "cursorPos": 252,
- "status": "s000002"
+ "name": "Title Page",
+ "itemAttr": {
+ "handle": "53b69b83cdafc",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 241,
+ "wordCount": 42,
+ "paraCount": 3,
+ "cursorPos": 252
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000002"
+ }
},
{
- "handle": "974e400180a99",
- "parent": "7031beac91f75",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Page",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 125,
- "wordCount": 26,
- "paraCount": 2,
- "cursorPos": 127,
- "status": "s000000"
+ "name": "Page",
+ "itemAttr": {
+ "handle": "974e400180a99",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 125,
+ "wordCount": 26,
+ "paraCount": 2,
+ "cursorPos": 127
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "edca4be2fcaf8",
- "parent": "7031beac91f75",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Part One",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 26,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 30,
- "status": "s000000"
+ "name": "Part One",
+ "itemAttr": {
+ "handle": "edca4be2fcaf8",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 26,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 30
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "e7ded148d6e4a",
- "parent": "7031beac91f75",
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "A Folder",
- "type": "FOLDER",
- "class": "NOVEL",
- "expanded": true,
- "status": "s000003"
+ "name": "A Folder",
+ "itemAttr": {
+ "handle": "e7ded148d6e4a",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 3,
+ "type": "FOLDER",
+ "class": "NOVEL"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000003"
+ }
},
{
- "handle": "6a2d6d5f4f401",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Chapter One",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 75,
- "wordCount": 14,
- "paraCount": 1,
- "cursorPos": 279,
- "status": "s000001"
+ "name": "Chapter One",
+ "itemAttr": {
+ "handle": "6a2d6d5f4f401",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 75,
+ "wordCount": 14,
+ "paraCount": 1,
+ "cursorPos": 279
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000001"
+ }
},
{
- "handle": "636b6aa9b697b",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Making a Scene",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 2429,
- "wordCount": 432,
- "paraCount": 14,
- "cursorPos": 61,
- "status": "s000003"
+ "name": "Making a Scene",
+ "itemAttr": {
+ "handle": "636b6aa9b697b",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 2429,
+ "wordCount": 432,
+ "paraCount": 14,
+ "cursorPos": 61
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "bc0cbd2a407f3",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Another Scene",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 476,
- "wordCount": 93,
- "paraCount": 3,
- "cursorPos": 577,
- "status": "s000003"
+ "name": "Another Scene",
+ "itemAttr": {
+ "handle": "bc0cbd2a407f3",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 476,
+ "wordCount": 93,
+ "paraCount": 3,
+ "cursorPos": 577
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "ba8a28a246524",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "Interlude",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 617,
- "wordCount": 101,
- "paraCount": 3,
- "cursorPos": 1137,
- "status": "s000000"
+ "name": "Interlude",
+ "itemAttr": {
+ "handle": "ba8a28a246524",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 3,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 617,
+ "wordCount": 101,
+ "paraCount": 3,
+ "cursorPos": 1137
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "96b68994dfa3d",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 4,
- "heading": "H0",
- "label": "A Note on Structure",
- "type": "FILE",
- "class": "NOVEL",
- "active": false,
- "layout": "NOTE",
- "charCount": 1692,
- "wordCount": 313,
- "paraCount": 6,
- "cursorPos": 1110,
- "status": "s000004"
+ "name": "A Note on Structure",
+ "itemAttr": {
+ "handle": "96b68994dfa3d",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 4,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 1692,
+ "wordCount": 313,
+ "paraCount": 6,
+ "cursorPos": 1110
+ },
+ "nameAttr": {
+ "active": false,
+ "status": "s000004"
+ }
},
{
- "handle": "88706ddc78b1b",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 5,
- "heading": "H0",
- "label": "Chapter Two",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 139,
- "wordCount": 28,
- "paraCount": 1,
- "cursorPos": 343,
- "status": "s000003"
+ "name": "Chapter Two",
+ "itemAttr": {
+ "handle": "88706ddc78b1b",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 5,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 139,
+ "wordCount": 28,
+ "paraCount": 1,
+ "cursorPos": 343
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "ae7339df26ded",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 6,
- "heading": "H0",
- "label": "We Found John!",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 189,
- "wordCount": 37,
- "paraCount": 1,
- "cursorPos": 224,
- "status": "s000003"
+ "name": "We Found John!",
+ "itemAttr": {
+ "handle": "ae7339df26ded",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 6,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 189,
+ "wordCount": 37,
+ "paraCount": 1,
+ "cursorPos": 224
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "f6622b4617424",
- "parent": null,
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Characters",
- "type": "ROOT",
- "class": "CHARACTER",
- "expanded": true,
- "import": null
+ "name": "Characters",
+ "itemAttr": {
+ "handle": "f6622b4617424",
+ "parent": null,
+ "root": null,
+ "order": 1,
+ "type": "ROOT",
+ "class": "CHARACTER"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "f7e2d9f330615",
- "parent": "f6622b4617424",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Main Characters",
- "type": "FOLDER",
- "class": "CHARACTER",
- "expanded": true,
- "import": null
+ "name": "Main Characters",
+ "itemAttr": {
+ "handle": "f7e2d9f330615",
+ "parent": "f6622b4617424",
+ "root": null,
+ "order": 0,
+ "type": "FOLDER",
+ "class": "CHARACTER"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "14298de4d9524",
- "parent": "f7e2d9f330615",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "John Smith",
- "type": "FILE",
- "class": "CHARACTER",
- "active": true,
- "layout": "NOTE",
- "charCount": 49,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 24,
- "import": "i000008"
+ "name": "John Smith",
+ "itemAttr": {
+ "handle": "14298de4d9524",
+ "parent": "f7e2d9f330615",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 49,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 24
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000008"
+ }
},
{
- "handle": "bb2c23b3c42cc",
- "parent": "f7e2d9f330615",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Jane Smith",
- "type": "FILE",
- "class": "CHARACTER",
- "active": true,
- "layout": "NOTE",
- "charCount": 55,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 25,
- "import": "i000009"
+ "name": "Jane Smith",
+ "itemAttr": {
+ "handle": "bb2c23b3c42cc",
+ "parent": "f7e2d9f330615",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 55,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 25
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000009"
+ }
},
{
- "handle": "15c4492bd5107",
- "parent": null,
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Locations",
- "type": "ROOT",
- "class": "WORLD",
- "expanded": true,
- "import": null
+ "name": "Locations",
+ "itemAttr": {
+ "handle": "15c4492bd5107",
+ "parent": null,
+ "root": null,
+ "order": 2,
+ "type": "ROOT",
+ "class": "WORLD"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "b3e74dbc1f584",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Earth",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 76,
- "wordCount": 15,
- "paraCount": 1,
- "cursorPos": 20,
- "import": "i00000a"
+ "name": "Earth",
+ "itemAttr": {
+ "handle": "b3e74dbc1f584",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 76,
+ "wordCount": 15,
+ "paraCount": 1,
+ "cursorPos": 20
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i00000a"
+ }
},
{
- "handle": "f1471bef9f2ae",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Space",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 115,
- "wordCount": 24,
- "paraCount": 1,
- "cursorPos": 133,
- "import": "i000008"
+ "name": "Space",
+ "itemAttr": {
+ "handle": "f1471bef9f2ae",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 115,
+ "wordCount": 24,
+ "paraCount": 1,
+ "cursorPos": 133
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000008"
+ }
},
{
- "handle": "5eaea4e8cdee8",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Mars",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 28,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 45,
- "import": "i000009"
+ "name": "Mars",
+ "itemAttr": {
+ "handle": "5eaea4e8cdee8",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 28,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 45
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000009"
+ }
},
{
- "handle": "6827118336ac1",
- "parent": null,
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "Outtakes",
- "type": "ROOT",
- "class": "ARCHIVE",
- "expanded": true,
- "status": null
+ "name": "Outtakes",
+ "itemAttr": {
+ "handle": "6827118336ac1",
+ "parent": null,
+ "root": null,
+ "order": 3,
+ "type": "ROOT",
+ "class": "ARCHIVE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": null
+ }
},
{
- "handle": "ae9bf3c3ea159",
- "parent": "6827118336ac1",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Scenes",
- "type": "FOLDER",
- "class": "ARCHIVE",
- "expanded": true,
- "status": null
+ "name": "Scenes",
+ "itemAttr": {
+ "handle": "ae9bf3c3ea159",
+ "parent": "6827118336ac1",
+ "root": null,
+ "order": 0,
+ "type": "FOLDER",
+ "class": "ARCHIVE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": null
+ }
},
{
- "handle": "8a5deb88c0e97",
- "parent": "ae9bf3c3ea159",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Old File",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 315,
- "wordCount": 55,
- "paraCount": 1,
- "cursorPos": 322,
- "status": "s000003"
+ "name": "Old File",
+ "itemAttr": {
+ "handle": "8a5deb88c0e97",
+ "parent": "ae9bf3c3ea159",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 315,
+ "wordCount": 55,
+ "paraCount": 1,
+ "cursorPos": 322
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "98acd8c76c93a",
- "parent": null,
- "root": null,
- "order": 4,
- "heading": "H0",
- "label": "Trash",
- "type": "ROOT",
- "class": "TRASH",
- "expanded": true,
- "import": null
+ "name": "Trash",
+ "itemAttr": {
+ "handle": "98acd8c76c93a",
+ "parent": null,
+ "root": null,
+ "order": 4,
+ "type": "ROOT",
+ "class": "TRASH"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "b8136a5a774a0",
- "parent": "98acd8c76c93a",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Delete Me!",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 30,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 36,
- "status": "s000000"
+ "name": "Delete Me!",
+ "itemAttr": {
+ "handle": "b8136a5a774a0",
+ "parent": "98acd8c76c93a",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 30,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 36
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
}
-]
\ No newline at end of file
+]
diff --git a/tests/reference/projectXML_ReadLegacy13.json b/tests/reference/projectXML_ReadLegacy13.json
index 55508758..4398ac76 100644
--- a/tests/reference/projectXML_ReadLegacy13.json
+++ b/tests/reference/projectXML_ReadLegacy13.json
@@ -1,387 +1,537 @@
[
{
- "handle": "7031beac91f75",
- "parent": null,
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Novel",
- "type": "ROOT",
- "class": "NOVEL",
- "expanded": true,
- "status": "s000002"
+ "name": "Novel",
+ "itemAttr": {
+ "handle": "7031beac91f75",
+ "parent": null,
+ "root": null,
+ "order": 0,
+ "type": "ROOT",
+ "class": "NOVEL"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000002"
+ }
},
{
- "handle": "53b69b83cdafc",
- "parent": "7031beac91f75",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Title Page",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 93,
- "wordCount": 19,
- "paraCount": 2,
- "cursorPos": 2,
- "status": "s000002"
+ "name": "Title Page",
+ "itemAttr": {
+ "handle": "53b69b83cdafc",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 93,
+ "wordCount": 19,
+ "paraCount": 2,
+ "cursorPos": 2
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000002"
+ }
},
{
- "handle": "974e400180a99",
- "parent": "7031beac91f75",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Page",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 186,
- "wordCount": 39,
- "paraCount": 2,
- "cursorPos": 212,
- "status": "s000000"
+ "name": "Page",
+ "itemAttr": {
+ "handle": "974e400180a99",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 186,
+ "wordCount": 39,
+ "paraCount": 2,
+ "cursorPos": 212
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "edca4be2fcaf8",
- "parent": "7031beac91f75",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Part One",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 26,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 33,
- "status": "s000000"
+ "name": "Part One",
+ "itemAttr": {
+ "handle": "edca4be2fcaf8",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 26,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 33
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "e7ded148d6e4a",
- "parent": "7031beac91f75",
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "A Folder",
- "type": "FOLDER",
- "class": "NOVEL",
- "expanded": true,
- "status": "s000003"
+ "name": "A Folder",
+ "itemAttr": {
+ "handle": "e7ded148d6e4a",
+ "parent": "7031beac91f75",
+ "root": null,
+ "order": 3,
+ "type": "FOLDER",
+ "class": "NOVEL"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000003"
+ }
},
{
- "handle": "6a2d6d5f4f401",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Chapter One",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 75,
- "wordCount": 14,
- "paraCount": 1,
- "cursorPos": 279,
- "status": "s000001"
+ "name": "Chapter One",
+ "itemAttr": {
+ "handle": "6a2d6d5f4f401",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 75,
+ "wordCount": 14,
+ "paraCount": 1,
+ "cursorPos": 279
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000001"
+ }
},
{
- "handle": "636b6aa9b697b",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Making a Scene",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 2429,
- "wordCount": 432,
- "paraCount": 14,
- "cursorPos": 62,
- "status": "s000003"
+ "name": "Making a Scene",
+ "itemAttr": {
+ "handle": "636b6aa9b697b",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 2429,
+ "wordCount": 432,
+ "paraCount": 14,
+ "cursorPos": 62
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "bc0cbd2a407f3",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Another Scene",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 476,
- "wordCount": 93,
- "paraCount": 3,
- "cursorPos": 577,
- "status": "s000003"
+ "name": "Another Scene",
+ "itemAttr": {
+ "handle": "bc0cbd2a407f3",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 476,
+ "wordCount": 93,
+ "paraCount": 3,
+ "cursorPos": 577
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "ba8a28a246524",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "Interlude",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 617,
- "wordCount": 101,
- "paraCount": 3,
- "cursorPos": 4,
- "status": "s000000"
+ "name": "Interlude",
+ "itemAttr": {
+ "handle": "ba8a28a246524",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 3,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 617,
+ "wordCount": 101,
+ "paraCount": 3,
+ "cursorPos": 4
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
},
{
- "handle": "96b68994dfa3d",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 4,
- "heading": "H0",
- "label": "A Note on Structure",
- "type": "FILE",
- "class": "NOVEL",
- "active": false,
- "layout": "NOTE",
- "charCount": 1692,
- "wordCount": 313,
- "paraCount": 6,
- "cursorPos": 1110,
- "status": "s000004"
+ "name": "A Note on Structure",
+ "itemAttr": {
+ "handle": "96b68994dfa3d",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 4,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 1692,
+ "wordCount": 313,
+ "paraCount": 6,
+ "cursorPos": 1110
+ },
+ "nameAttr": {
+ "active": false,
+ "status": "s000004"
+ }
},
{
- "handle": "88706ddc78b1b",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 5,
- "heading": "H0",
- "label": "Chapter Two",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 139,
- "wordCount": 28,
- "paraCount": 1,
- "cursorPos": 343,
- "status": "s000003"
+ "name": "Chapter Two",
+ "itemAttr": {
+ "handle": "88706ddc78b1b",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 5,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 139,
+ "wordCount": 28,
+ "paraCount": 1,
+ "cursorPos": 343
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "ae7339df26ded",
- "parent": "e7ded148d6e4a",
- "root": null,
- "order": 6,
- "heading": "H0",
- "label": "We Found John!",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 189,
- "wordCount": 37,
- "paraCount": 1,
- "cursorPos": 224,
- "status": "s000003"
+ "name": "We Found John!",
+ "itemAttr": {
+ "handle": "ae7339df26ded",
+ "parent": "e7ded148d6e4a",
+ "root": null,
+ "order": 6,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 189,
+ "wordCount": 37,
+ "paraCount": 1,
+ "cursorPos": 224
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "f6622b4617424",
- "parent": null,
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Characters",
- "type": "ROOT",
- "class": "CHARACTER",
- "expanded": true,
- "import": null
+ "name": "Characters",
+ "itemAttr": {
+ "handle": "f6622b4617424",
+ "parent": null,
+ "root": null,
+ "order": 1,
+ "type": "ROOT",
+ "class": "CHARACTER"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "f7e2d9f330615",
- "parent": "f6622b4617424",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Main Characters",
- "type": "FOLDER",
- "class": "CHARACTER",
- "expanded": true,
- "import": null
+ "name": "Main Characters",
+ "itemAttr": {
+ "handle": "f7e2d9f330615",
+ "parent": "f6622b4617424",
+ "root": null,
+ "order": 0,
+ "type": "FOLDER",
+ "class": "CHARACTER"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "14298de4d9524",
- "parent": "f7e2d9f330615",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "John Smith",
- "type": "FILE",
- "class": "CHARACTER",
- "active": true,
- "layout": "NOTE",
- "charCount": 49,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 24,
- "import": "i000008"
+ "name": "John Smith",
+ "itemAttr": {
+ "handle": "14298de4d9524",
+ "parent": "f7e2d9f330615",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 49,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 24
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000008"
+ }
},
{
- "handle": "bb2c23b3c42cc",
- "parent": "f7e2d9f330615",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Jane Smith",
- "type": "FILE",
- "class": "CHARACTER",
- "active": true,
- "layout": "NOTE",
- "charCount": 55,
- "wordCount": 9,
- "paraCount": 1,
- "cursorPos": 25,
- "import": "i000009"
+ "name": "Jane Smith",
+ "itemAttr": {
+ "handle": "bb2c23b3c42cc",
+ "parent": "f7e2d9f330615",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 55,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 25
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000009"
+ }
},
{
- "handle": "15c4492bd5107",
- "parent": null,
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Locations",
- "type": "ROOT",
- "class": "WORLD",
- "expanded": true,
- "import": null
+ "name": "Locations",
+ "itemAttr": {
+ "handle": "15c4492bd5107",
+ "parent": null,
+ "root": null,
+ "order": 2,
+ "type": "ROOT",
+ "class": "WORLD"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "b3e74dbc1f584",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Earth",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 76,
- "wordCount": 15,
- "paraCount": 1,
- "cursorPos": 20,
- "import": "i00000a"
+ "name": "Earth",
+ "itemAttr": {
+ "handle": "b3e74dbc1f584",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 76,
+ "wordCount": 15,
+ "paraCount": 1,
+ "cursorPos": 20
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i00000a"
+ }
},
{
- "handle": "f1471bef9f2ae",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 1,
- "heading": "H0",
- "label": "Space",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 115,
- "wordCount": 24,
- "paraCount": 1,
- "cursorPos": 133,
- "import": "i000008"
+ "name": "Space",
+ "itemAttr": {
+ "handle": "f1471bef9f2ae",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 1,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 115,
+ "wordCount": 24,
+ "paraCount": 1,
+ "cursorPos": 133
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000008"
+ }
},
{
- "handle": "5eaea4e8cdee8",
- "parent": "15c4492bd5107",
- "root": null,
- "order": 2,
- "heading": "H0",
- "label": "Mars",
- "type": "FILE",
- "class": "WORLD",
- "active": true,
- "layout": "NOTE",
- "charCount": 28,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 45,
- "import": "i000009"
+ "name": "Mars",
+ "itemAttr": {
+ "handle": "5eaea4e8cdee8",
+ "parent": "15c4492bd5107",
+ "root": null,
+ "order": 2,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 28,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 45
+ },
+ "nameAttr": {
+ "active": true,
+ "import": "i000009"
+ }
},
{
- "handle": "6827118336ac1",
- "parent": null,
- "root": null,
- "order": 3,
- "heading": "H0",
- "label": "Archive",
- "type": "ROOT",
- "class": "ARCHIVE",
- "expanded": true,
- "status": "s000000"
+ "name": "Archive",
+ "itemAttr": {
+ "handle": "6827118336ac1",
+ "parent": null,
+ "root": null,
+ "order": 3,
+ "type": "ROOT",
+ "class": "ARCHIVE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000000"
+ }
},
{
- "handle": "ae9bf3c3ea159",
- "parent": "6827118336ac1",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Scenes",
- "type": "FOLDER",
- "class": "ARCHIVE",
- "expanded": true,
- "status": "s000000"
+ "name": "Scenes",
+ "itemAttr": {
+ "handle": "ae9bf3c3ea159",
+ "parent": "6827118336ac1",
+ "root": null,
+ "order": 0,
+ "type": "FOLDER",
+ "class": "ARCHIVE"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "status": "s000000"
+ }
},
{
- "handle": "8a5deb88c0e97",
- "parent": "ae9bf3c3ea159",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Old File",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 314,
- "wordCount": 55,
- "paraCount": 1,
- "cursorPos": 322,
- "status": "s000003"
+ "name": "Old File",
+ "itemAttr": {
+ "handle": "8a5deb88c0e97",
+ "parent": "ae9bf3c3ea159",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 314,
+ "wordCount": 55,
+ "paraCount": 1,
+ "cursorPos": 322
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000003"
+ }
},
{
- "handle": "98acd8c76c93a",
- "parent": null,
- "root": null,
- "order": 4,
- "heading": "H0",
- "label": "Trash",
- "type": "ROOT",
- "class": "TRASH",
- "expanded": true,
- "import": null
+ "name": "Trash",
+ "itemAttr": {
+ "handle": "98acd8c76c93a",
+ "parent": null,
+ "root": null,
+ "order": 4,
+ "type": "ROOT",
+ "class": "TRASH"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "expanded": true
+ },
+ "nameAttr": {
+ "import": null
+ }
},
{
- "handle": "b8136a5a774a0",
- "parent": "98acd8c76c93a",
- "root": null,
- "order": 0,
- "heading": "H0",
- "label": "Delete Me!",
- "type": "FILE",
- "class": "NOVEL",
- "active": true,
- "layout": "DOCUMENT",
- "charCount": 30,
- "wordCount": 6,
- "paraCount": 1,
- "cursorPos": 36,
- "status": "s000000"
+ "name": "Delete Me!",
+ "itemAttr": {
+ "handle": "b8136a5a774a0",
+ "parent": "98acd8c76c93a",
+ "root": null,
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "heading": "H0",
+ "charCount": 30,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 36
+ },
+ "nameAttr": {
+ "active": true,
+ "status": "s000000"
+ }
}
-]
\ No newline at end of file
+]
diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py
index 714d6d57..f02f21f3 100644
--- a/tests/test_core/test_core_item.py
+++ b/tests/test_core/test_core_item.py
@@ -509,23 +509,29 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
# File
theItem = NWItem(theProject)
assert theItem.unpack({
- "label": "A File",
- "handle": "0000000000003",
- "parent": "0000000000002",
- "root": "0000000000001",
- "order": 1,
- "type": "FILE",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": True,
- "status": None,
- "import": None,
- "heading": "H1",
- "charCount": 100,
- "wordCount": 20,
- "paraCount": 2,
- "cursorPos": 50,
- "active": False,
+ "name": "A File",
+ "itemAttr": {
+ "handle": "0000000000003",
+ "parent": "0000000000002",
+ "root": "0000000000001",
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT",
+ },
+ "metaAttr": {
+ "expanded": True,
+ "heading": "H1",
+ "charCount": 100,
+ "wordCount": 20,
+ "paraCount": 2,
+ "cursorPos": 50,
+ },
+ "nameAttr": {
+ "status": None,
+ "import": None,
+ "active": False,
+ },
}) is True
assert theItem.itemName == "A File"
@@ -575,23 +581,29 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
# Folder
theItem = NWItem(theProject)
assert theItem.unpack({
- "label": "A Folder",
- "handle": "0000000000003",
- "parent": "0000000000002",
- "root": "0000000000001",
- "order": 1,
- "type": "FOLDER",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": True,
- "status": "",
- "import": "",
- "heading": "H1",
- "charCount": 100,
- "wordCount": 20,
- "paraCount": 2,
- "cursorPos": 50,
- "active": True,
+ "name": "A Folder",
+ "itemAttr": {
+ "handle": "0000000000003",
+ "parent": "0000000000002",
+ "root": "0000000000001",
+ "order": 1,
+ "type": "FOLDER",
+ "class": "NOVEL",
+ "layout": "DOCUMENT",
+ },
+ "metaAttr": {
+ "expanded": True,
+ "heading": "H1",
+ "charCount": 100,
+ "wordCount": 20,
+ "paraCount": 2,
+ "cursorPos": 50,
+ },
+ "nameAttr": {
+ "status": "",
+ "import": "",
+ "active": True,
+ }
}) is True
assert theItem.itemName == "A Folder"
@@ -634,23 +646,29 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
# Root
theItem = NWItem(theProject)
assert theItem.unpack({
- "label": "A Novel",
- "handle": "0000000000003",
- "parent": "0000000000002",
- "root": "0000000000001",
- "order": 1,
- "type": "ROOT",
- "class": "NOVEL",
- "layout": "DOCUMENT",
- "expanded": True,
- "status": None,
- "import": None,
- "heading": "H1",
- "charCount": 100,
- "wordCount": 20,
- "paraCount": 2,
- "cursorPos": 50,
- "active": True,
+ "name": "A Novel",
+ "itemAttr": {
+ "handle": "0000000000003",
+ "parent": "0000000000002",
+ "root": "0000000000001",
+ "order": 1,
+ "type": "ROOT",
+ "class": "NOVEL",
+ "layout": "DOCUMENT",
+ },
+ "metaAttr": {
+ "expanded": True,
+ "heading": "H1",
+ "charCount": 100,
+ "wordCount": 20,
+ "paraCount": 2,
+ "cursorPos": 50,
+ },
+ "nameAttr": {
+ "status": None,
+ "import": None,
+ "active": True,
+ },
}) is True
assert theItem.itemName == "A Novel"
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index 6ad1174c..05580607 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -19,11 +19,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import json
import os
+import json
import pytest
-import shutil
+from shutil import copyfile
from datetime import datetime
from mock import causeOSError
@@ -47,6 +47,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
xmlFile = os.path.join(fncDir, "nwProject-1.4.nwx")
bakFile = os.path.join(fncDir, "nwProject-1.4.bak")
outFile = os.path.join(fncDir, "nwProject.nwx")
+ tstFile = os.path.join(outDir, "ProjectXML_ReadCurrent.nwx")
xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION
@@ -125,7 +126,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
content = []
# Parse a valid, complete file
- shutil.copy(refFile, xmlFile)
+ copyfile(refFile, xmlFile)
assert xmlReader.read(data, content) is True
assert xmlReader.state == XMLReadState.PARSED_OK
assert xmlReader.xmlRoot == "novelWriterXML"
@@ -231,7 +232,8 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
# Successful save (should be twice)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
- assert cmpFiles(outFile, xmlFile)
+ copyfile(outFile, tstFile)
+ assert cmpFiles(tstFile, xmlFile)
# END Test testCoreProjectXML_ReadCurrent
@@ -243,7 +245,7 @@ def testCoreProjectXML_ReadLegacy10(filesDir, fncDir, outDir, refDir, mockRnd):
refFile = os.path.join(filesDir, "nwProject-1.0.nwx")
xmlFile = os.path.join(fncDir, "nwProject-1.0.nwx")
outFile = os.path.join(fncDir, "nwProject.nwx")
- shutil.copy(refFile, xmlFile)
+ copyfile(refFile, xmlFile)
xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION
@@ -369,8 +371,10 @@ def testCoreProjectXML_ReadLegacy10(filesDir, fncDir, outDir, refDir, mockRnd):
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncDir)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
+ testFile = os.path.join(outDir, "projectXML_ReadLegacy10.nwx")
compFile = os.path.join(refDir, "projectXML_ReadLegacy10.nwx")
- assert cmpFiles(outFile, compFile)
+ copyfile(outFile, testFile)
+ assert cmpFiles(testFile, compFile)
# END Test testCoreProjectXML_ReadLegacy10
@@ -382,7 +386,7 @@ def testCoreProjectXML_ReadLegacy11(filesDir, fncDir, outDir, refDir, mockRnd):
refFile = os.path.join(filesDir, "nwProject-1.1.nwx")
xmlFile = os.path.join(fncDir, "nwProject-1.1.nwx")
outFile = os.path.join(fncDir, "nwProject.nwx")
- shutil.copy(refFile, xmlFile)
+ copyfile(refFile, xmlFile)
xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION
@@ -508,8 +512,10 @@ def testCoreProjectXML_ReadLegacy11(filesDir, fncDir, outDir, refDir, mockRnd):
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncDir)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
+ testFile = os.path.join(outDir, "projectXML_ReadLegacy11.nwx")
compFile = os.path.join(refDir, "projectXML_ReadLegacy11.nwx")
- assert cmpFiles(outFile, compFile)
+ copyfile(outFile, testFile)
+ assert cmpFiles(testFile, compFile)
# END Test testCoreProjectXML_ReadLegacy11
@@ -521,7 +527,7 @@ def testCoreProjectXML_ReadLegacy12(filesDir, fncDir, outDir, refDir, mockRnd):
refFile = os.path.join(filesDir, "nwProject-1.2.nwx")
xmlFile = os.path.join(fncDir, "nwProject-1.2.nwx")
outFile = os.path.join(fncDir, "nwProject.nwx")
- shutil.copy(refFile, xmlFile)
+ copyfile(refFile, xmlFile)
xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION
@@ -650,8 +656,10 @@ def testCoreProjectXML_ReadLegacy12(filesDir, fncDir, outDir, refDir, mockRnd):
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncDir)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
+ testFile = os.path.join(outDir, "projectXML_ReadLegacy12.nwx")
compFile = os.path.join(refDir, "projectXML_ReadLegacy12.nwx")
- assert cmpFiles(outFile, compFile)
+ copyfile(outFile, testFile)
+ assert cmpFiles(testFile, compFile)
# END Test testCoreProjectXML_ReadLegacy12
@@ -663,7 +671,7 @@ def testCoreProjectXML_ReadLegacy13(filesDir, fncDir, outDir, refDir, mockRnd):
refFile = os.path.join(filesDir, "nwProject-1.3.nwx")
xmlFile = os.path.join(fncDir, "nwProject-1.3.nwx")
outFile = os.path.join(fncDir, "nwProject.nwx")
- shutil.copy(refFile, xmlFile)
+ copyfile(refFile, xmlFile)
xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION
@@ -792,7 +800,9 @@ def testCoreProjectXML_ReadLegacy13(filesDir, fncDir, outDir, refDir, mockRnd):
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncDir)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
+ testFile = os.path.join(outDir, "projectXML_ReadLegacy13.nwx")
compFile = os.path.join(refDir, "projectXML_ReadLegacy13.nwx")
- assert cmpFiles(outFile, compFile)
+ copyfile(outFile, testFile)
+ assert cmpFiles(testFile, compFile)
# END Test testCoreProjectXML_ReadLegacy13
From 96b011cf70f7daf5cef406fe049fa04706cc836c Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 3 Nov 2022 21:41:57 +0100
Subject: [PATCH 04/26] Rename doctools module to coretools
---
novelwriter/core/__init__.py | 2 +-
novelwriter/core/{doctools.py => coretools.py} | 0
.../{test_core_doctools.py => test_core_coretools.py} | 10 +++++-----
3 files changed, 6 insertions(+), 6 deletions(-)
rename novelwriter/core/{doctools.py => coretools.py} (100%)
rename tests/test_core/{test_core_doctools.py => test_core_coretools.py} (96%)
diff --git a/novelwriter/core/__init__.py b/novelwriter/core/__init__.py
index f1b5dfd3..684ce53d 100644
--- a/novelwriter/core/__init__.py
+++ b/novelwriter/core/__init__.py
@@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-from novelwriter.core.doctools import DocMerger, DocSplitter
+from novelwriter.core.coretools import DocMerger, DocSplitter
from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
diff --git a/novelwriter/core/doctools.py b/novelwriter/core/coretools.py
similarity index 100%
rename from novelwriter/core/doctools.py
rename to novelwriter/core/coretools.py
diff --git a/tests/test_core/test_core_doctools.py b/tests/test_core/test_core_coretools.py
similarity index 96%
rename from tests/test_core/test_core_doctools.py
rename to tests/test_core/test_core_coretools.py
index 02d87866..4bf4c407 100644
--- a/tests/test_core/test_core_doctools.py
+++ b/tests/test_core/test_core_coretools.py
@@ -28,12 +28,12 @@ from mock import causeOSError
from tools import C, buildTestProject, cmpFiles
from novelwriter.core.project import NWProject
-from novelwriter.core.doctools import DocMerger, DocSplitter
from novelwriter.core.document import NWDoc
+from novelwriter.core.coretools import DocMerger, DocSplitter
@pytest.mark.core
-def testCoreDocTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText):
+def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText):
"""Test the DocMerger utility.
"""
theProject = NWProject(mockGUI)
@@ -118,11 +118,11 @@ def testCoreDocTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, moc
# Just for debugging
docMerger.writeTargetDoc()
-# END Test testCoreDocTools_DocMerger
+# END Test testCoreTools_DocMerger
@pytest.mark.core
-def testCoreDocTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText):
+def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText):
"""Test the DocSplitter utility.
"""
theProject = NWProject(mockGUI)
@@ -258,4 +258,4 @@ def testCoreDocTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, m
theProject.saveProject()
-# END Test testCoreDocTools_DocSplitter
+# END Test testCoreTools_DocSplitter
From dfd9e159515722e7d08cb7614e3564a1e37fc0a8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 3 Nov 2022 21:51:56 +0100
Subject: [PATCH 05/26] Use storage class to open and save project XML
---
novelwriter/core/project.py | 41 +++++++++---
novelwriter/core/projectxml.py | 35 +++++------
novelwriter/core/storage.py | 84 +++++++++++++++++++++++--
tests/test_core/test_core_projectxml.py | 4 +-
tests/tools.py | 1 +
5 files changed, 129 insertions(+), 36 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 750e68db..709310a2 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -105,20 +105,24 @@ class NWProject(QObject):
##
@property
- def data(self):
- return self._data
+ def options(self):
+ return self._options
@property
- def index(self):
- return self._index
+ def storage(self):
+ return self._storage
+
+ @property
+ def data(self):
+ return self._data
@property
def tree(self):
return self._tree
@property
- def options(self):
- return self._options
+ def index(self):
+ return self._index
@property
def projOpened(self):
@@ -245,6 +249,7 @@ class NWProject(QObject):
self._projAltered = False
# Project Tree
+ self._storage.clear()
self._tree.clear()
self._index.clearIndex()
self._data = NWProjectData(self)
@@ -302,6 +307,8 @@ class NWProject(QObject):
if not self.setProjectPath(projPath, newProject=True):
return False
+ self._storage.openProjectInPlace(self.projPath)
+
self._data.setName(projName)
self._data.setTitle(projTitle)
self._data.setAuthors(projAuthors)
@@ -458,10 +465,18 @@ class NWProject(QObject):
# Open The Project XML File
# =========================
+ if not self._storage.openProjectInPlace(self.projPath):
+ self.clearProject()
+ return False
+
+ xmlReader = self._storage.getXmlReader()
+ if not isinstance(xmlReader, ProjectXMLReader):
+ self.clearProject()
+ return False
+
self._data = NWProjectData(self)
projContent = []
- xmlReader = ProjectXMLReader(fileName)
xmlParsed = xmlReader.read(self._data, projContent)
appVersion = xmlReader.appVersion or self.tr("Unknown")
@@ -580,6 +595,12 @@ class NWProject(QObject):
), nwAlert.ERROR)
return False
+ if not self._storage.isOpen():
+ self.mainGui.makeAlert(self.tr(
+ "There is no project open."
+ ), nwAlert.ERROR)
+ return False
+
saveTime = time()
if not self.ensureFolderStructure():
return False
@@ -594,11 +615,13 @@ class NWProject(QObject):
self.updateWordCounts()
self.countStatus()
+ xmlWriter = self._storage.getXmlWriter()
+ if not isinstance(xmlWriter, ProjectXMLWriter):
+ return False
+
saveTime = time()
editTime = int(self._data.editTime + saveTime - self._projOpened)
-
content = self._tree.pack()
- xmlWriter = ProjectXMLWriter(self.projPath)
if not xmlWriter.write(self._data, content, saveTime, editTime):
self.mainGui.makeAlert(self.tr(
"Failed to save project."
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index 18020c9a..1cfe770f 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -31,6 +31,7 @@ import novelwriter
from enum import Enum
from lxml import etree
from time import time
+from pathlib import Path
from novelwriter.common import (
checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString
@@ -91,7 +92,7 @@ class ProjectXMLReader:
def __init__(self, path):
- self._path = path
+ self._path = Path(path)
self._state = XMLReadState.NO_ACTION
self._root = ""
@@ -153,7 +154,7 @@ class ProjectXMLReader:
logger.debug("Reading project XML")
try:
- xml = etree.parse(self._path)
+ xml = etree.parse(str(self._path))
self._state = XMLReadState.NO_ERROR
except Exception as exc:
@@ -161,10 +162,10 @@ class ProjectXMLReader:
logger.error("Failed to parse project XML", exc_info=exc)
self._state = XMLReadState.CANNOT_PARSE
- backFile = self._path[:-3]+"bak"
+ backFile = self._path.with_suffix(".bak")
if os.path.isfile(backFile):
try:
- xml = etree.parse(backFile)
+ xml = etree.parse(str(backFile))
self._state = XMLReadState.PARSED_BACKUP
logger.info("Backup project file parsed")
except Exception as exc:
@@ -445,7 +446,7 @@ class ProjectXMLWriter:
def __init__(self, path):
- self._path = path
+ self._path = Path(path)
self._error = None
return
@@ -514,17 +515,13 @@ class ProjectXMLWriter:
xName.text = item["name"]
# Write the XML tree to file
- saveFile = os.path.join(self._path, nwFiles.PROJ_FILE)
- tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~")
- backFile = os.path.join(self._path, nwFiles.PROJ_FILE[:-3]+"bak")
+ saveFile = self._path / nwFiles.PROJ_FILE
+ tempFile = saveFile.with_suffix(".tmp")
+ backFile = saveFile.with_suffix(".bak")
try:
- with open(tempFile, mode="wb") as outFile:
- outFile.write(etree.tostring(
- xRoot,
- pretty_print=True,
- encoding="utf-8",
- xml_declaration=True
- ))
+ tempFile.write_bytes(etree.tostring(
+ xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True
+ ))
except Exception as exc:
self._error = exc
return False
@@ -532,10 +529,10 @@ class ProjectXMLWriter:
# If we're here, the file was successfully saved,
# so let's sort out the temps and backups
try:
- if os.path.isfile(saveFile):
- os.replace(saveFile, backFile)
- os.replace(tempFile, saveFile)
- except OSError as exc:
+ if saveFile.exists():
+ saveFile.replace(backFile)
+ tempFile.replace(saveFile)
+ except Exception as exc:
self._error = exc
return False
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index 7382c336..43e5d618 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -25,37 +25,109 @@ along with this program. If not, see .
import logging
+from pathlib import Path
+
+from novelwriter.constants import nwFiles
+from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
+
logger = logging.getLogger(__name__)
class NWStorage:
+ MODE_INACTIVE = 0
+ MODE_INPLACE = 1
+ MODE_ARCHIVE = 2
+
def __init__(self, theProject):
+
self.theProject = theProject
+
+ self._storagePath = None
+ self._runtimePath = None
+ self._openMode = self.MODE_INACTIVE
+
+ return
+
+ def clear(self):
+ """Reset internal variables.
+ """
+ self._storagePath = None
+ self._runtimePath = None
+ self._openMode = self.MODE_INACTIVE
return
##
# Core Methods
##
- def openProjectFolder(self, path):
- pass
+ def isOpen(self):
+ """Check if the storage location is open.
+ """
+ return self._runtimePath is not None
+
+ def openProjectInPlace(self, path):
+ """Open a novelWriter project in-place. That is, it is opened
+ directly from a project folder.
+ """
+ inPath = Path(path)
+ if inPath.is_file():
+ inPath = inPath.parent
+
+ if not inPath.is_dir():
+ logger.error("No such folder: %s", inPath)
+ self.clear()
+ return False
+
+ self._storagePath = inPath
+ self._runtimePath = inPath
+ self._openMode = self.MODE_INPLACE
+
+ return True
def openProjectArchive(self, path):
pass
- def close(self):
- pass
+ def runPostSaveTasks(self, autoSave=False):
+ """Run tasks after the project has been saved.
+ """
+ if self._openMode == self.MODE_INPLACE:
+ # Nothing to do, so we just return
+ return True
+
+ return True
+
+ def closeSession(self):
+ """Run tasks related to closing the session.
+ """
+ # Clear lockfile
+ self.clear()
+ return
##
# Content Access Methods
##
def getXmlReader(self):
- pass
+ """
+ """
+ if self._runtimePath is None:
+ return None
+
+ projFile = self._runtimePath / nwFiles.PROJ_FILE
+ xmlReader = ProjectXMLReader(projFile)
+
+ return xmlReader
def getXmlWriter(self):
- pass
+ """
+ """
+ if self._runtimePath is None:
+ return None
+
+ xmlWriter = ProjectXMLWriter(self._runtimePath)
+
+ return xmlWriter
def getDocument(self, tHandle):
pass
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index 05580607..59943220 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -220,12 +220,12 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
# Fail saving
with monkeypatch.context() as mp:
- mp.setattr("builtins.open", causeOSError)
+ mp.setattr("pathlib.Path.write_bytes", causeOSError)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is False
assert str(xmlWriter.error) == "Mock OSError"
with monkeypatch.context() as mp:
- mp.setattr("os.replace", causeOSError)
+ mp.setattr("pathlib.Path.replace", causeOSError)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is False
assert str(xmlWriter.error) == "Mock OSError"
diff --git a/tests/tools.py b/tests/tools.py
index 0da846cc..9a2e4dd8 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -167,6 +167,7 @@ def buildTestProject(theObject, projPath):
theProject.clearProject()
theProject.setProjectPath(projPath, newProject=True)
+ theProject.storage.openProjectInPlace(theProject.projPath)
theProject.data.itemStatus.write(None, "New", (100, 100, 100))
theProject.data.itemStatus.write(None, "Note", (200, 50, 0))
From bd81ea088054679fee6cc7f2f628bee92065e75b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 3 Nov 2022 22:17:25 +0100
Subject: [PATCH 06/26] Make project xml class and tests use pathlib instead of
os
---
novelwriter/core/projectxml.py | 3 +-
tests/conftest.py | 31 +++++++++
tests/test_core/test_core_projectxml.py | 91 ++++++++++++-------------
3 files changed, 77 insertions(+), 48 deletions(-)
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index 1cfe770f..8f15b0f5 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -24,7 +24,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import logging
import novelwriter
@@ -163,7 +162,7 @@ class ProjectXMLReader:
self._state = XMLReadState.CANNOT_PARSE
backFile = self._path.with_suffix(".bak")
- if os.path.isfile(backFile):
+ if backFile.is_file():
try:
xml = etree.parse(str(backFile))
self._state = XMLReadState.PARSED_BACKUP
diff --git a/tests/conftest.py b/tests/conftest.py
index 50955a31..33cd3574 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -24,6 +24,8 @@ import sys
import pytest
import shutil
+from pathlib import Path
+
from mock import MockGuiMain
from tools import cleanProject
@@ -62,6 +64,35 @@ def tmpDir():
return theDir
+@pytest.fixture(scope="session")
+def tstPaths(tmpDir):
+ """Returns an object that can provide the various paths needed for
+ running tests.
+ """
+ class _Store:
+ testDir = Path(__file__).parent
+ filesDir = testDir / "files"
+ refDir = testDir / "reference"
+ outDir = testDir / tmpDir / "results"
+
+ store = _Store()
+ store.outDir.mkdir(exist_ok=True)
+
+ return store
+
+
+@pytest.fixture(scope="function")
+def fncPath(tmpDir):
+ """A temporary folder for a single test function.
+ """
+ fncPath = Path(tmpDir) / "f_temp"
+ if fncPath.is_dir():
+ shutil.rmtree(fncPath)
+ if not fncPath.is_dir():
+ fncPath.mkdir()
+ return fncPath
+
+
@pytest.fixture(scope="session")
def refDir():
"""The folder where all the reference files are stored for verifying
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index 59943220..efe25df9 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import json
import pytest
@@ -40,14 +39,14 @@ class MockProject:
@pytest.mark.core
-def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir):
+def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
"""Test reading the current XML file format.
"""
- refFile = os.path.join(filesDir, "nwProject-1.4.nwx")
- xmlFile = os.path.join(fncDir, "nwProject-1.4.nwx")
- bakFile = os.path.join(fncDir, "nwProject-1.4.bak")
- outFile = os.path.join(fncDir, "nwProject.nwx")
- tstFile = os.path.join(outDir, "ProjectXML_ReadCurrent.nwx")
+ refFile = tstPaths.filesDir / "nwProject-1.4.nwx"
+ tstFile = tstPaths.outDir / "ProjectXML_ReadCurrent.nwx"
+ xmlFile = fncPath / "nwProject-1.4.nwx"
+ bakFile = fncPath / "nwProject-1.4.bak"
+ outFile = fncPath / "nwProject.nwx"
xmlReader = ProjectXMLReader(xmlFile)
assert xmlReader.state == XMLReadState.NO_ACTION
@@ -200,8 +199,8 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
assert data.itemImport.count("i56be10") == 1
# Compare content
- dumpFile = os.path.join(outDir, "projectXML_ReadCurrent.json")
- compFile = os.path.join(refDir, "projectXML_ReadCurrent.json")
+ dumpFile = tstPaths.outDir / "projectXML_ReadCurrent.json"
+ compFile = tstPaths.refDir / "projectXML_ReadCurrent.json"
with open(dumpFile, mode="w", encoding="utf-8") as dump:
json.dump(content, dump, indent=2)
assert cmpFiles(dumpFile, compFile)
@@ -216,7 +215,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
- xmlWriter = ProjectXMLWriter(fncDir)
+ xmlWriter = ProjectXMLWriter(fncPath)
# Fail saving
with monkeypatch.context() as mp:
@@ -239,12 +238,12 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
@pytest.mark.core
-def testCoreProjectXML_ReadLegacy10(filesDir, fncDir, outDir, refDir, mockRnd):
+def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
"""Test reading the version 1.0 XML file format.
"""
- refFile = os.path.join(filesDir, "nwProject-1.0.nwx")
- xmlFile = os.path.join(fncDir, "nwProject-1.0.nwx")
- outFile = os.path.join(fncDir, "nwProject.nwx")
+ refFile = tstPaths.filesDir / "nwProject-1.0.nwx"
+ xmlFile = fncPath / "nwProject-1.0.nwx"
+ outFile = fncPath / "nwProject.nwx"
copyfile(refFile, xmlFile)
xmlReader = ProjectXMLReader(xmlFile)
@@ -326,8 +325,8 @@ def testCoreProjectXML_ReadLegacy10(filesDir, fncDir, outDir, refDir, mockRnd):
assert data.itemImport.count("i00000a") == 0
# Compare content
- dumpFile = os.path.join(outDir, "projectXML_ReadLegacy10.json")
- compFile = os.path.join(refDir, "projectXML_ReadLegacy10.json")
+ dumpFile = tstPaths.outDir / "projectXML_ReadLegacy10.json"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy10.json"
with open(dumpFile, mode="w", encoding="utf-8") as dump:
json.dump(content, dump, indent=2)
assert cmpFiles(dumpFile, compFile)
@@ -369,10 +368,10 @@ def testCoreProjectXML_ReadLegacy10(filesDir, fncDir, outDir, refDir, mockRnd):
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
- xmlWriter = ProjectXMLWriter(fncDir)
+ xmlWriter = ProjectXMLWriter(fncPath)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
- testFile = os.path.join(outDir, "projectXML_ReadLegacy10.nwx")
- compFile = os.path.join(refDir, "projectXML_ReadLegacy10.nwx")
+ testFile = tstPaths.outDir / "projectXML_ReadLegacy10.nwx"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy10.nwx"
copyfile(outFile, testFile)
assert cmpFiles(testFile, compFile)
@@ -380,12 +379,12 @@ def testCoreProjectXML_ReadLegacy10(filesDir, fncDir, outDir, refDir, mockRnd):
@pytest.mark.core
-def testCoreProjectXML_ReadLegacy11(filesDir, fncDir, outDir, refDir, mockRnd):
+def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
"""Test reading the version 1.1 XML file format.
"""
- refFile = os.path.join(filesDir, "nwProject-1.1.nwx")
- xmlFile = os.path.join(fncDir, "nwProject-1.1.nwx")
- outFile = os.path.join(fncDir, "nwProject.nwx")
+ refFile = tstPaths.filesDir / "nwProject-1.1.nwx"
+ xmlFile = fncPath / "nwProject-1.1.nwx"
+ outFile = fncPath / "nwProject.nwx"
copyfile(refFile, xmlFile)
xmlReader = ProjectXMLReader(xmlFile)
@@ -467,8 +466,8 @@ def testCoreProjectXML_ReadLegacy11(filesDir, fncDir, outDir, refDir, mockRnd):
assert data.itemImport.count("i00000a") == 0
# Compare content
- dumpFile = os.path.join(outDir, "projectXML_ReadLegacy11.json")
- compFile = os.path.join(refDir, "projectXML_ReadLegacy11.json")
+ dumpFile = tstPaths.outDir / "projectXML_ReadLegacy11.json"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy11.json"
with open(dumpFile, mode="w", encoding="utf-8") as dump:
json.dump(content, dump, indent=2)
assert cmpFiles(dumpFile, compFile)
@@ -510,10 +509,10 @@ def testCoreProjectXML_ReadLegacy11(filesDir, fncDir, outDir, refDir, mockRnd):
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
- xmlWriter = ProjectXMLWriter(fncDir)
+ xmlWriter = ProjectXMLWriter(fncPath)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
- testFile = os.path.join(outDir, "projectXML_ReadLegacy11.nwx")
- compFile = os.path.join(refDir, "projectXML_ReadLegacy11.nwx")
+ testFile = tstPaths.outDir / "projectXML_ReadLegacy11.nwx"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy11.nwx"
copyfile(outFile, testFile)
assert cmpFiles(testFile, compFile)
@@ -521,12 +520,12 @@ def testCoreProjectXML_ReadLegacy11(filesDir, fncDir, outDir, refDir, mockRnd):
@pytest.mark.core
-def testCoreProjectXML_ReadLegacy12(filesDir, fncDir, outDir, refDir, mockRnd):
+def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
"""Test reading the version 1.2 XML file format.
"""
- refFile = os.path.join(filesDir, "nwProject-1.2.nwx")
- xmlFile = os.path.join(fncDir, "nwProject-1.2.nwx")
- outFile = os.path.join(fncDir, "nwProject.nwx")
+ refFile = tstPaths.filesDir / "nwProject-1.2.nwx"
+ xmlFile = fncPath / "nwProject-1.2.nwx"
+ outFile = fncPath / "nwProject.nwx"
copyfile(refFile, xmlFile)
xmlReader = ProjectXMLReader(xmlFile)
@@ -608,8 +607,8 @@ def testCoreProjectXML_ReadLegacy12(filesDir, fncDir, outDir, refDir, mockRnd):
assert data.itemImport.count("i00000a") == 0
# Compare content
- dumpFile = os.path.join(outDir, "projectXML_ReadLegacy12.json")
- compFile = os.path.join(refDir, "projectXML_ReadLegacy12.json")
+ dumpFile = tstPaths.outDir / "projectXML_ReadLegacy12.json"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy12.json"
with open(dumpFile, mode="w", encoding="utf-8") as dump:
json.dump(content, dump, indent=2)
assert cmpFiles(dumpFile, compFile)
@@ -654,10 +653,10 @@ def testCoreProjectXML_ReadLegacy12(filesDir, fncDir, outDir, refDir, mockRnd):
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
- xmlWriter = ProjectXMLWriter(fncDir)
+ xmlWriter = ProjectXMLWriter(fncPath)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
- testFile = os.path.join(outDir, "projectXML_ReadLegacy12.nwx")
- compFile = os.path.join(refDir, "projectXML_ReadLegacy12.nwx")
+ testFile = tstPaths.outDir / "projectXML_ReadLegacy12.nwx"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy12.nwx"
copyfile(outFile, testFile)
assert cmpFiles(testFile, compFile)
@@ -665,12 +664,12 @@ def testCoreProjectXML_ReadLegacy12(filesDir, fncDir, outDir, refDir, mockRnd):
@pytest.mark.core
-def testCoreProjectXML_ReadLegacy13(filesDir, fncDir, outDir, refDir, mockRnd):
+def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
"""Test reading the version 1.3 XML file format.
"""
- refFile = os.path.join(filesDir, "nwProject-1.3.nwx")
- xmlFile = os.path.join(fncDir, "nwProject-1.3.nwx")
- outFile = os.path.join(fncDir, "nwProject.nwx")
+ refFile = tstPaths.filesDir / "nwProject-1.3.nwx"
+ xmlFile = fncPath / "nwProject-1.3.nwx"
+ outFile = fncPath / "nwProject.nwx"
copyfile(refFile, xmlFile)
xmlReader = ProjectXMLReader(xmlFile)
@@ -752,8 +751,8 @@ def testCoreProjectXML_ReadLegacy13(filesDir, fncDir, outDir, refDir, mockRnd):
assert data.itemImport.count("i00000a") == 0
# Compare content
- dumpFile = os.path.join(outDir, "projectXML_ReadLegacy13.json")
- compFile = os.path.join(refDir, "projectXML_ReadLegacy13.json")
+ dumpFile = tstPaths.outDir / "projectXML_ReadLegacy13.json"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy13.json"
with open(dumpFile, mode="w", encoding="utf-8") as dump:
json.dump(content, dump, indent=2)
assert cmpFiles(dumpFile, compFile)
@@ -798,10 +797,10 @@ def testCoreProjectXML_ReadLegacy13(filesDir, fncDir, outDir, refDir, mockRnd):
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
- xmlWriter = ProjectXMLWriter(fncDir)
+ xmlWriter = ProjectXMLWriter(fncPath)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
- testFile = os.path.join(outDir, "projectXML_ReadLegacy13.nwx")
- compFile = os.path.join(refDir, "projectXML_ReadLegacy13.nwx")
+ testFile = tstPaths.outDir / "projectXML_ReadLegacy13.nwx"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy13.nwx"
copyfile(outFile, testFile)
assert cmpFiles(testFile, compFile)
From 2e590b742fa00f37d4db3e035b9a0bc445398c01 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 3 Nov 2022 23:47:51 +0100
Subject: [PATCH 07/26] Move new project tool to core tools (resolves #1152)
---
novelwriter/core/__init__.py | 3 +-
novelwriter/core/coretools.py | 215 ++++++++++++++++-
novelwriter/core/project.py | 219 ++----------------
novelwriter/guimain.py | 30 +--
...nwx => coreTools_NewCustomA_nwProject.nwx} | 4 +-
...nwx => coreTools_NewCustomB_nwProject.nwx} | 4 +-
...nwx => coreTools_NewMinimal_nwProject.nwx} | 8 +-
tests/test_core/test_core_coretools.py | 156 ++++++++++++-
tests/test_core/test_core_project.py | 210 +----------------
tests/tools.py | 10 +-
10 files changed, 398 insertions(+), 461 deletions(-)
rename tests/reference/{coreProject_NewCustomA_nwProject.nwx => coreTools_NewCustomA_nwProject.nwx} (99%)
rename tests/reference/{coreProject_NewCustomB_nwProject.nwx => coreTools_NewCustomB_nwProject.nwx} (98%)
rename tests/reference/{coreProject_NewMinimal_nwProject.nwx => coreTools_NewMinimal_nwProject.nwx} (96%)
diff --git a/novelwriter/core/__init__.py b/novelwriter/core/__init__.py
index 684ce53d..b535785b 100644
--- a/novelwriter/core/__init__.py
+++ b/novelwriter/core/__init__.py
@@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-from novelwriter.core.coretools import DocMerger, DocSplitter
+from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
@@ -31,6 +31,7 @@ from novelwriter.core.tomd import ToMarkdown
__all__ = [
"DocMerger",
"DocSplitter",
+ "ProjectBuilder",
"countWords",
"NWDoc",
"NWProject",
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 95fec897..0c24fc29 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -24,15 +24,30 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import os
+import shutil
import logging
+import novelwriter
-from novelwriter.common import minmax
+from time import time
+from functools import partial
+
+from PyQt5.QtCore import QCoreApplication
+
+from novelwriter.enum import nwAlert
+from novelwriter.common import minmax, simplified
+from novelwriter.constants import nwItemClass
+from novelwriter.core.project import NWProject
from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
class DocMerger:
+ """Document tool for merging a set of documents into a single new
+ document. The parameters are defined by the user using the
+ GuiDocMerge dialog.
+ """
def __init__(self, theProject):
@@ -122,6 +137,10 @@ class DocMerger:
class DocSplitter:
+ """Document tool for splitting a document into a set of new
+ documents. The parameters are defined by the user using the
+ GuiDocSplit dialog.
+ """
def __init__(self, theProject, sHandle):
@@ -242,3 +261,197 @@ class DocSplitter:
return
# END Class DocSplitter
+
+
+class ProjectBuilder:
+ """A class to build a new project from a set of user-defined
+ parameter provided by the New Projecty Wizard.
+ """
+
+ def __init__(self, mainGui):
+
+ self.mainGui = mainGui
+ self.mainConf = novelwriter.CONFIG
+
+ self.tr = partial(QCoreApplication.translate, "NWProject")
+
+ return
+
+ ##
+ # Methods
+ ##
+
+ def buildProject(self, data):
+ """Build a project from a data dictionary of specifications
+ provided by the wizard.
+ """
+ if not isinstance(data, dict):
+ logger.error("Invalid call to newProject function")
+ return False
+
+ popMinimal = data.get("popMinimal", True)
+ popCustom = data.get("popCustom", False)
+ popSample = data.get("popSample", False)
+
+ # Check if we're extracting the sample project. This is handled
+ # differently as it isn't actually a new project, so we forward
+ # this to another function and return here.
+ if popSample:
+ return self._extractSampleProject(data)
+
+ projPath = data.get("projPath", None)
+ if projPath is None:
+ logger.error("No project path set for the new project")
+ return False
+
+ project = NWProject(self.mainGui)
+ if not project.setProjectPath(projPath, newProject=True):
+ return False
+
+ if not project.storage.openProjectInPlace(projPath):
+ return False
+
+ lblNewProject = self.tr("New Project")
+ lblNewChapter = self.tr("New Chapter")
+ lblNewScene = self.tr("New Scene")
+ lblTitlePage = self.tr("Title Page")
+ lblByAuthors = self.tr("By")
+
+ # Settings
+ projName = data.get("projName", lblNewProject)
+ projTitle = data.get("projTitle", lblNewProject)
+ projAuthors = data.get("projAuthors", "")
+
+ project.data.setName(projName)
+ project.data.setTitle(projTitle)
+ project.data.setAuthors(projAuthors)
+ project.setDefaultStatusImport()
+ project._projOpened = int(time())
+
+ # Add Root Folders
+ hNovelRoot = project.newRoot(nwItemClass.NOVEL)
+ hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
+ novelTitle = project.data.title if project.data.title else project.data.name
+
+ titlePage = f"#! {novelTitle}\n\n"
+ if project.data.authors:
+ titlePage += f">> {lblByAuthors} {project.getFormattedAuthors()} <<\n\n"
+
+ aDoc = NWDoc(project, hTitlePage)
+ aDoc.writeDocument(titlePage)
+
+ if popMinimal:
+ # Creating a minimal project with a few root folders and a
+ # single chapter with a single scene.
+ hChapter = project.newFile(lblNewChapter, hNovelRoot)
+ aDoc = NWDoc(project, hChapter)
+ aDoc.writeDocument(f"## {lblNewChapter}\n\n")
+
+ hScene = project.newFile(lblNewScene, hChapter)
+ aDoc = NWDoc(project, hScene)
+ aDoc.writeDocument(f"### {lblNewScene}\n\n")
+
+ project.newRoot(nwItemClass.PLOT)
+ project.newRoot(nwItemClass.CHARACTER)
+ project.newRoot(nwItemClass.WORLD)
+ project.newRoot(nwItemClass.ARCHIVE)
+
+ project.saveProject()
+ project.closeProject()
+
+ elif popCustom:
+ # Create a project structure based on selected root folders
+ # and a number of chapters and scenes selected in the
+ # wizard's custom page.
+
+ # Create chapters and scenes
+ numChapters = data.get("numChapters", 0)
+ numScenes = data.get("numScenes", 0)
+
+ chSynop = self.tr("Summary of the chapter.")
+ scSynop = self.tr("Summary of the scene.")
+
+ # Create chapters
+ if numChapters > 0:
+ for ch in range(numChapters):
+ chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
+ cHandle = project.newFile(chTitle, hNovelRoot)
+ aDoc = NWDoc(project, cHandle)
+ aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
+
+ # Create chapter scenes
+ if numScenes > 0:
+ for sc in range(numScenes):
+ scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
+ sHandle = project.newFile(scTitle, cHandle)
+ aDoc = NWDoc(project, sHandle)
+ aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
+
+ # Create scenes (no chapters)
+ elif numScenes > 0:
+ for sc in range(numScenes):
+ scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
+ sHandle = project.newFile(scTitle, hNovelRoot)
+ aDoc = NWDoc(project, sHandle)
+ aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
+
+ # Create notes folders
+ noteTitles = {
+ nwItemClass.PLOT: self.tr("Main Plot"),
+ nwItemClass.CHARACTER: self.tr("Protagonist"),
+ nwItemClass.WORLD: self.tr("Main Location"),
+ }
+
+ addNotes = data.get("addNotes", False)
+ for newRoot in data.get("addRoots", []):
+ if newRoot in nwItemClass:
+ rHandle = project.newRoot(newRoot)
+ if addNotes:
+ aHandle = project.newFile(noteTitles[newRoot], rHandle)
+ ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
+ aDoc = NWDoc(project, aHandle)
+ aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
+
+ # Also add the archive and trash folders
+ project.newRoot(nwItemClass.ARCHIVE)
+ project.trashFolder()
+
+ project.saveProject()
+ project.closeProject()
+
+ return True
+
+ ##
+ # Internal Functions
+ ##
+
+ def _extractSampleProject(self, data):
+ """Make a copy of the sample project by extracting the
+ sample.zip file to the new path.
+ """
+ projPath = data.get("projPath", None)
+ if projPath is None:
+ logger.error("No project path set for the example project")
+ return False
+
+ pkgSample = os.path.join(self.mainConf.assetPath, "sample.zip")
+ if os.path.isfile(pkgSample):
+ try:
+ shutil.unpack_archive(pkgSample, projPath)
+ except Exception as exc:
+ self.mainGui.makeAlert(self.tr(
+ "Failed to create a new example project."
+ ), nwAlert.ERROR, exception=exc)
+ return False
+
+ else:
+ self.mainGui.makeAlert(self.tr(
+ "Failed to create a new example project. "
+ "Could not find the necessary files. "
+ "They seem to be missing from this installation."
+ ), nwAlert.ERROR)
+ return False
+
+ return True
+
+# END Class ProjectBuilder
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 709310a2..1714cfd2 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -264,150 +264,6 @@ class NWProject(QObject):
return
- def newProject(self, projData):
- """Create a new project by populating the project tree with a
- few starter items.
- """
- if not isinstance(projData, dict):
- logger.error("Invalid call to newProject function")
- return False
-
- popMinimal = projData.get("popMinimal", True)
- popCustom = projData.get("popCustom", False)
- popSample = projData.get("popSample", False)
-
- # Check if we're extracting the sample project. This is handled
- # differently as it isn't actually a new project, so we forward
- # this to another function and return here.
- if popSample:
- return self.extractSampleProject(projData)
-
- # Project Settings
- projPath = projData.get("projPath", None)
- projName = projData.get("projName", self.tr("New Project"))
- projTitle = projData.get("projTitle", "")
- projAuthors = projData.get("projAuthors", "")
-
- if projPath is None:
- logger.error("No project path set for the new project")
- return False
-
- self.clearProject()
-
- self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100))
- self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0))
- self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0))
- self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0))
-
- self._data.itemImport.write(None, self.tr("New"), (100, 100, 100))
- self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0))
- self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0))
- self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
-
- if not self.setProjectPath(projPath, newProject=True):
- return False
-
- self._storage.openProjectInPlace(self.projPath)
-
- self._data.setName(projName)
- self._data.setTitle(projTitle)
- self._data.setAuthors(projAuthors)
-
- hNovelRoot = self.newRoot(nwItemClass.NOVEL)
- hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot)
-
- titlePage = "#! %s\n\n" % (
- self._data.title if self._data.title else self._data.name
- )
- if self._data.authors:
- titlePage = "%s>> %s %s <<\n" % (
- titlePage, self.tr("By"), self.getFormattedAuthors()
- )
-
- aDoc = NWDoc(self, hTitlePage)
- aDoc.writeDocument(titlePage)
-
- if popMinimal:
- # Creating a minimal project with a few root folders and a
- # single chapter with a single scene.
- hChapter = self.newFile(self.tr("New Chapter"), hNovelRoot)
- aDoc = NWDoc(self, hChapter)
- aDoc.writeDocument("## %s\n\n" % self.tr("New Chapter"))
-
- hScene = self.newFile(self.tr("New Scene"), hChapter)
- aDoc = NWDoc(self, hScene)
- aDoc.writeDocument("### %s\n\n" % self.tr("New Scene"))
-
- self.newRoot(nwItemClass.PLOT)
- self.newRoot(nwItemClass.CHARACTER)
- self.newRoot(nwItemClass.WORLD)
- self.newRoot(nwItemClass.ARCHIVE)
-
- elif popCustom:
- # Create a project structure based on selected root folders
- # and a number of chapters and scenes selected in the
- # wizard's custom page.
-
- # Create chapters and scenes
- numChapters = projData.get("numChapters", 0)
- numScenes = projData.get("numScenes", 0)
-
- chSynop = self.tr("Summary of the chapter.")
- scSynop = self.tr("Summary of the scene.")
-
- # Create chapters
- if numChapters > 0:
- for ch in range(numChapters):
- chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
- cHandle = self.newFile(chTitle, hNovelRoot)
- aDoc = NWDoc(self, cHandle)
- aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
-
- # Create chapter scenes
- if numScenes > 0:
- for sc in range(numScenes):
- scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
- sHandle = self.newFile(scTitle, cHandle)
- aDoc = NWDoc(self, sHandle)
- aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
-
- # Create scenes (no chapters)
- elif numScenes > 0:
- for sc in range(numScenes):
- scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
- sHandle = self.newFile(scTitle, hNovelRoot)
- aDoc = NWDoc(self, sHandle)
- aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
-
- # Create notes folders
- noteTitles = {
- nwItemClass.PLOT: self.tr("Main Plot"),
- nwItemClass.CHARACTER: self.tr("Protagonist"),
- nwItemClass.WORLD: self.tr("Main Location"),
- }
-
- addNotes = projData.get("addNotes", False)
- for newRoot in projData.get("addRoots", []):
- if newRoot in nwItemClass:
- rHandle = self.newRoot(newRoot)
- if addNotes:
- aHandle = self.newFile(noteTitles[newRoot], rHandle)
- ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
- aDoc = NWDoc(self, aHandle)
- aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
-
- # Also add the archive and trash folders
- self.newRoot(nwItemClass.ARCHIVE)
- self.trashFolder()
-
- # Finalise
- if popCustom or popMinimal:
- self._projOpened = time()
- self.setProjectChanged(True)
- self.saveProject(autoSave=True)
-
- return True
-
def openProject(self, fileName, overrideLock=False):
"""Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful,
@@ -680,6 +536,19 @@ class NWProject(QObject):
return True
+ def setDefaultStatusImport(self):
+ """Set the default status and importance values.
+ """
+ self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100))
+ self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0))
+ self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0))
+ self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0))
+ self._data.itemImport.write(None, self.tr("New"), (100, 100, 100))
+ self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0))
+ self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0))
+ self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
+ return
+
##
# Zip/Unzip Project
##
@@ -753,68 +622,6 @@ class NWProject(QObject):
return True
- def extractSampleProject(self, projData):
- """Make a copy of the sample project.
- First, look for the sample.zip file in the assets folder and
- unpack it. If it doesn't exist, try to copy the content of the
- sample folder to the new project path. If neither exits, error.
- """
- projPath = projData.get("projPath", None)
- if projPath is None:
- logger.error("No project path set for the example project")
- return False
-
- srcSample = os.path.abspath(os.path.join(self.mainConf.appRoot, "sample"))
- pkgSample = os.path.join(self.mainConf.assetPath, "sample.zip")
-
- isSuccess = False
- if os.path.isfile(pkgSample):
-
- self.setProjectPath(projPath, newProject=True)
- try:
- shutil.unpack_archive(pkgSample, projPath)
- isSuccess = True
- except Exception as exc:
- self.mainGui.makeAlert(self.tr(
- "Failed to create a new example project."
- ), nwAlert.ERROR, exception=exc)
-
- elif os.path.isdir(srcSample):
-
- self.setProjectPath(projPath, newProject=True)
- try:
- srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE)
- dstProj = os.path.join(projPath, nwFiles.PROJ_FILE)
- shutil.copyfile(srcProj, dstProj)
-
- srcContent = os.path.join(srcSample, "content")
- dstContent = os.path.join(projPath, "content")
- for srcFile in os.listdir(srcContent):
- srcDoc = os.path.join(srcContent, srcFile)
- dstDoc = os.path.join(dstContent, srcFile)
- shutil.copyfile(srcDoc, dstDoc)
-
- isSuccess = True
-
- except Exception as exc:
- self.mainGui.makeAlert(self.tr(
- "Failed to create a new example project."
- ), nwAlert.ERROR, exception=exc)
-
- else:
- self.mainGui.makeAlert(self.tr(
- "Failed to create a new example project. "
- "Could not find the necessary files. "
- "They seem to be missing from this installation."
- ), nwAlert.ERROR)
-
- if isSuccess:
- self.clearProject()
- self.mainGui.openProject(projPath)
- self.mainGui.rebuildIndex()
-
- return isSuccess
-
##
# Setters
##
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 516cec42..c0745460 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -50,9 +50,9 @@ from novelwriter.dialogs import (
from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
)
-from novelwriter.core import NWProject
+from novelwriter.core import NWProject, ProjectBuilder
from novelwriter.enum import (
- nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
+ nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView
)
from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwFiles
@@ -365,30 +365,10 @@ class GuiMain(QMainWindow):
return False
logger.info("Creating new project")
- if self.theProject.newProject(projData):
-
- self.hasProject = True
- self.idleRefTime = time()
- self.idleTime = 0.0
-
- self.rebuildTrees()
- self.saveProject()
-
- self.docEditor.setDictionaries()
- self.projView.openProjectTasks()
- self.novelView.openProjectTasks()
- self.outlineView.openProjectTasks()
- self.rebuildIndex(beQuiet=True)
-
- self.mainStatus.setRefTime(self.theProject.projOpened)
- self.mainStatus.setProjectStatus(nwState.GOOD)
- self.mainStatus.setDocumentStatus(nwState.NONE)
- self.mainStatus.setStatus(self.tr("New project created ..."))
-
- self._updateWindowTitle(self.theProject.data.name)
-
+ nwProject = ProjectBuilder(self)
+ if nwProject.buildProject(projData):
+ self.openProject(projPath)
else:
- self.theProject.clearProject()
return False
return True
diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreTools_NewCustomA_nwProject.nwx
similarity index 99%
rename from tests/reference/coreProject_NewCustomA_nwProject.nwx
rename to tests/reference/coreTools_NewCustomA_nwProject.nwx
index 76509948..f6314c1a 100644
--- a/tests/reference/coreProject_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomA_nwProject.nwx
@@ -1,12 +1,12 @@
-
+
Test Custom
Test Novel
Jane Doe
John Doh
1
- 1
+ 0
0
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreTools_NewCustomB_nwProject.nwx
similarity index 98%
rename from tests/reference/coreProject_NewCustomB_nwProject.nwx
rename to tests/reference/coreTools_NewCustomB_nwProject.nwx
index afd7627c..23e0c509 100644
--- a/tests/reference/coreProject_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomB_nwProject.nwx
@@ -1,12 +1,12 @@
-
+
Test Custom
Test Novel
Jane Doe
John Doh
1
- 1
+ 0
0
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreTools_NewMinimal_nwProject.nwx
similarity index 96%
rename from tests/reference/coreProject_NewMinimal_nwProject.nwx
rename to tests/reference/coreTools_NewMinimal_nwProject.nwx
index d43aee14..0f2388f5 100644
--- a/tests/reference/coreProject_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreTools_NewMinimal_nwProject.nwx
@@ -1,10 +1,10 @@
-
+
New Project
- None
- 2
- 1
+ New Project
+ 1
+ 0
0
diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py
index 4bf4c407..f98e32f1 100644
--- a/tests/test_core/test_core_coretools.py
+++ b/tests/test_core/test_core_coretools.py
@@ -23,13 +23,15 @@ import os
import pytest
from shutil import copyfile
+from zipfile import ZipFile
from mock import causeOSError
-from tools import C, buildTestProject, cmpFiles
+from tools import C, buildTestProject, cmpFiles, XML_IGNORE
+from novelwriter.constants import nwItemClass
from novelwriter.core.project import NWProject
from novelwriter.core.document import NWDoc
-from novelwriter.core.coretools import DocMerger, DocSplitter
+from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
@pytest.mark.core
@@ -259,3 +261,153 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mock
theProject.saveProject()
# END Test testCoreTools_DocSplitter
+
+
+@pytest.mark.core
+def testCoreTools_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd):
+ """Create a new project from a project wizard dictionary. With
+ default setting, creating a Minimal project.
+ """
+ projFile = os.path.join(fncDir, "nwProject.nwx")
+ testFile = os.path.join(outDir, "coreTools_NewMinimal_nwProject.nwx")
+ compFile = os.path.join(refDir, "coreTools_NewMinimal_nwProject.nwx")
+
+ projBuild = ProjectBuilder(mockGUI)
+
+ # Setting no data should fail
+ assert projBuild.buildProject({}) is False
+
+ # Wrong type should also fail
+ assert projBuild.buildProject("stuff") is False
+
+ # Try again with a proper path
+ assert projBuild.buildProject({"projPath": fncDir}) is True
+
+ # Creating the project once more should fail
+ assert projBuild.buildProject({"projPath": fncDir}) is False
+
+ # Save and close
+ copyfile(projFile, testFile)
+ assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
+
+# END Test testCoreTools_NewMinimal
+
+
+@pytest.mark.core
+def testCoreTools_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd):
+ """Create a new project from a project wizard dictionary.
+ Custom type with chapters and scenes.
+ """
+ projFile = os.path.join(fncDir, "nwProject.nwx")
+ testFile = os.path.join(outDir, "coreTools_NewCustomA_nwProject.nwx")
+ compFile = os.path.join(refDir, "coreTools_NewCustomA_nwProject.nwx")
+
+ projData = {
+ "projName": "Test Custom",
+ "projTitle": "Test Novel",
+ "projAuthors": "Jane Doe\nJohn Doh\n",
+ "projPath": fncDir,
+ "popSample": False,
+ "popMinimal": False,
+ "popCustom": True,
+ "addRoots": [
+ nwItemClass.PLOT,
+ nwItemClass.CHARACTER,
+ nwItemClass.WORLD,
+ ],
+ "addNotes": True,
+ "numChapters": 3,
+ "numScenes": 3,
+ }
+
+ projBuild = ProjectBuilder(mockGUI)
+ assert projBuild.buildProject(projData) is True
+
+ copyfile(projFile, testFile)
+ assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
+
+# END Test testCoreTools_NewCustomA
+
+
+@pytest.mark.core
+def testCoreTools_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd):
+ """Create a new project from a project wizard dictionary.
+ Custom type without chapters, but with scenes.
+ """
+ projFile = os.path.join(fncDir, "nwProject.nwx")
+ testFile = os.path.join(outDir, "coreTools_NewCustomB_nwProject.nwx")
+ compFile = os.path.join(refDir, "coreTools_NewCustomB_nwProject.nwx")
+
+ projData = {
+ "projName": "Test Custom",
+ "projTitle": "Test Novel",
+ "projAuthors": "Jane Doe\nJohn Doh\n",
+ "projPath": fncDir,
+ "popSample": False,
+ "popMinimal": False,
+ "popCustom": True,
+ "addRoots": [
+ nwItemClass.PLOT,
+ nwItemClass.CHARACTER,
+ nwItemClass.WORLD,
+ ],
+ "addNotes": True,
+ "numChapters": 0,
+ "numScenes": 6,
+ }
+
+ projBuild = ProjectBuilder(mockGUI)
+ assert projBuild.buildProject(projData) is True
+
+ copyfile(projFile, testFile)
+ assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
+
+# END Test testCoreTools_NewCustomB
+
+
+@pytest.mark.core
+def testCoreTools_NewSample(fncDir, tmpConf, mockGUI, tmpDir):
+ """Check that we can create a new project can be created from the
+ provided sample project via a zip file.
+ """
+ projData = {
+ "projName": "Test Sample",
+ "projTitle": "Test Novel",
+ "projAuthors": "Jane Doe\nJohn Doh\n",
+ "projPath": fncDir,
+ "popSample": True,
+ "popMinimal": False,
+ "popCustom": False,
+ }
+
+ projBuild = ProjectBuilder(mockGUI)
+
+ # No path set
+ assert projBuild.buildProject({"popSample": True}) is False
+
+ # Force the lookup path for assets to our temp folder
+ srcSample = os.path.abspath(os.path.join(tmpConf.appRoot, "sample"))
+ dstSample = os.path.join(tmpDir, "sample.zip")
+ tmpConf.assetPath = tmpDir
+
+ # Cannot extract when the zip does not exist
+ assert projBuild.buildProject(projData) is False
+
+ # Create and open a defective zip file
+ with open(dstSample, mode="w+") as outFile:
+ outFile.write("foo")
+
+ assert projBuild.buildProject(projData) is False
+ os.unlink(dstSample)
+
+ # Create a real zip file, and unpack it
+ with ZipFile(dstSample, "w") as zipObj:
+ zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
+ for docFile in os.listdir(os.path.join(srcSample, "content")):
+ srcDoc = os.path.join(srcSample, "content", docFile)
+ zipObj.write(srcDoc, "content/"+docFile)
+
+ assert projBuild.buildProject(projData) is True
+ os.unlink(dstSample)
+
+# END Test testCoreTools_NewSample
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 02ce6c27..9d7b12e5 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -27,7 +27,7 @@ from shutil import copyfile
from zipfile import ZipFile
from mock import causeOSError
-from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE, C
+from tools import C, cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.common import formatTimeStamp
@@ -40,214 +40,6 @@ from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
-@pytest.mark.core
-def testCoreProject_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd):
- """Create a new project from a project wizard dictionary. With
- default setting, creating a Minimal project.
- """
- projFile = os.path.join(fncDir, "nwProject.nwx")
- testFile = os.path.join(outDir, "coreProject_NewMinimal_nwProject.nwx")
- compFile = os.path.join(refDir, "coreProject_NewMinimal_nwProject.nwx")
-
- theProject = NWProject(mockGUI)
-
- # Setting no data should fail
- assert theProject.newProject({}) is False
-
- # Wrong type should also fail
- assert theProject.newProject("stuff") is False
-
- # Try again with a proper path
- assert theProject.newProject({"projPath": fncDir}) is True
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
-
- # Creating the project once more should fail
- assert theProject.newProject({"projPath": fncDir}) is False
-
- # Open again
- assert theProject.openProject(projFile) is True
-
- # Save and close
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
- copyfile(projFile, testFile)
- assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
- assert theProject.projChanged is False
-
- # Open a second time
- assert theProject.openProject(projFile) is True
- assert theProject.openProject(projFile) is False
- assert theProject.openProject(projFile, overrideLock=True) is True
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
- copyfile(projFile, testFile)
- assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
-
-# END Test testCoreProject_NewMinimal
-
-
-@pytest.mark.core
-def testCoreProject_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd):
- """Create a new project from a project wizard dictionary.
- Custom type with chapters and scenes.
- """
- projFile = os.path.join(fncDir, "nwProject.nwx")
- testFile = os.path.join(outDir, "coreProject_NewCustomA_nwProject.nwx")
- compFile = os.path.join(refDir, "coreProject_NewCustomA_nwProject.nwx")
-
- projData = {
- "projName": "Test Custom",
- "projTitle": "Test Novel",
- "projAuthors": "Jane Doe\nJohn Doh\n",
- "projPath": fncDir,
- "popSample": False,
- "popMinimal": False,
- "popCustom": True,
- "addRoots": [
- nwItemClass.PLOT,
- nwItemClass.CHARACTER,
- nwItemClass.WORLD,
- ],
- "addNotes": True,
- "numChapters": 3,
- "numScenes": 3,
- }
- theProject = NWProject(mockGUI)
-
- assert theProject.newProject(projData) is True
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
-
- copyfile(projFile, testFile)
- assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
-
-# END Test testCoreProject_NewCustomA
-
-
-@pytest.mark.core
-def testCoreProject_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd):
- """Create a new project from a project wizard dictionary.
- Custom type without chapters, but with scenes.
- """
- projFile = os.path.join(fncDir, "nwProject.nwx")
- testFile = os.path.join(outDir, "coreProject_NewCustomB_nwProject.nwx")
- compFile = os.path.join(refDir, "coreProject_NewCustomB_nwProject.nwx")
-
- projData = {
- "projName": "Test Custom",
- "projTitle": "Test Novel",
- "projAuthors": "Jane Doe\nJohn Doh\n",
- "projPath": fncDir,
- "popSample": False,
- "popMinimal": False,
- "popCustom": True,
- "addRoots": [
- nwItemClass.PLOT,
- nwItemClass.CHARACTER,
- nwItemClass.WORLD,
- ],
- "addNotes": True,
- "numChapters": 0,
- "numScenes": 6,
- }
- theProject = NWProject(mockGUI)
-
- assert theProject.newProject(projData) is True
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
-
- copyfile(projFile, testFile)
- assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
-
-# END Test testCoreProject_NewCustomB
-
-
-@pytest.mark.core
-def testCoreProject_NewSampleA(fncDir, tmpConf, mockGUI, tmpDir):
- """Check that we can create a new project can be created from the
- provided sample project via a zip file.
- """
- projData = {
- "projName": "Test Sample",
- "projTitle": "Test Novel",
- "projAuthors": "Jane Doe\nJohn Doh\n",
- "projPath": fncDir,
- "popSample": True,
- "popMinimal": False,
- "popCustom": False,
- }
- theProject = NWProject(mockGUI)
-
- # Sample set, but no path
- assert not theProject.newProject({"popSample": True})
-
- # Force the lookup path for assets to our temp folder
- srcSample = os.path.abspath(os.path.join(tmpConf.appRoot, "sample"))
- dstSample = os.path.join(tmpDir, "sample.zip")
- tmpConf.assetPath = tmpDir
-
- # Create and open a defective zip file
- with open(dstSample, mode="w+") as outFile:
- outFile.write("foo")
-
- assert not theProject.newProject(projData)
- os.unlink(dstSample)
-
- # Create a real zip file, and unpack it
- with ZipFile(dstSample, "w") as zipObj:
- zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
- for docFile in os.listdir(os.path.join(srcSample, "content")):
- srcDoc = os.path.join(srcSample, "content", docFile)
- zipObj.write(srcDoc, "content/"+docFile)
-
- assert theProject.newProject(projData) is True
- assert theProject.openProject(fncDir) is True
- assert theProject.data.name == "Sample Project"
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
- os.unlink(dstSample)
-
-# END Test testCoreProject_NewSampleA
-
-
-@pytest.mark.core
-def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir):
- """Check that we can create a new project can be created from the
- provided sample project folder.
- """
- projData = {
- "projName": "Test Sample",
- "projTitle": "Test Novel",
- "projAuthors": "Jane Doe\nJohn Doh\n",
- "projPath": fncDir,
- "popSample": True,
- "popMinimal": False,
- "popCustom": False,
- }
- theProject = NWProject(mockGUI)
-
- # Make sure we do not pick up the novelwriter/assets/sample.zip file
- tmpConf.assetPath = tmpDir
-
- # Set a fake project file name
- monkeypatch.setattr(nwFiles, "PROJ_FILE", "nothing.nwx")
- assert not theProject.newProject(projData)
-
- monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx")
- assert theProject.newProject(projData) is True
- assert theProject.openProject(fncDir) is True
- assert theProject.data.name == "Sample Project"
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
-
- # Misdirect the appRoot path so neither is possible
- tmpConf.appRoot = tmpDir
- assert not theProject.newProject(projData)
-
-# END Test testCoreProject_NewSampleB
-
-
@pytest.mark.core
def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
"""Check that new root folders can be added to the project.
diff --git a/tests/tools.py b/tests/tools.py
index 9a2e4dd8..5094fa62 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -168,15 +168,7 @@ def buildTestProject(theObject, projPath):
theProject.clearProject()
theProject.setProjectPath(projPath, newProject=True)
theProject.storage.openProjectInPlace(theProject.projPath)
-
- theProject.data.itemStatus.write(None, "New", (100, 100, 100))
- theProject.data.itemStatus.write(None, "Note", (200, 50, 0))
- theProject.data.itemStatus.write(None, "Draft", (200, 150, 0))
- theProject.data.itemStatus.write(None, "Finished", (50, 200, 0))
- theProject.data.itemImport.write(None, "New", (100, 100, 100))
- theProject.data.itemImport.write(None, "Minor", (200, 50, 0))
- theProject.data.itemImport.write(None, "Major", (200, 150, 0))
- theProject.data.itemImport.write(None, "Main", (50, 200, 0))
+ theProject.setDefaultStatusImport()
theProject.data.setName("New Project")
theProject.data.setTitle("New Novel")
From 407e7fa69e401ccea1c86de84657d6e6ecdcccd3 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 5 Nov 2022 16:15:26 +0100
Subject: [PATCH 08/26] Add storage code to handle projecty structure and old
projects
---
CHANGELOG.md | 8 +-
novelwriter/core/project.py | 51 ++--------
novelwriter/core/storage.py | 147 +++++++++++++++++++++++++--
tests/test_core/test_core_project.py | 10 --
4 files changed, 154 insertions(+), 62 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06f51d75..8f15afd2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3707,8 +3707,8 @@ helpful feedback and issue reports for the new features added in this, and previ
**User Interface**
-* Added a preferences dialog for the program settings. No longer necessary to edit the config file.
- PR #30.
+* Added a preferences dialog for the program settings. It is no longer necessary to edit the config
+ file. PR #30.
* The document viewer remembers scroll bar position when pressing `Ctrl+R` on a document already
being viewed. PR #28.
* Removed version number from windows title. PR #28.
@@ -3749,8 +3749,8 @@ helpful feedback and issue reports for the new features added in this, and previ
**Status Bar**
* Redesign of the status bar adding project and session stats as well as a session timer. PR #21.
-* Project word count is written to the project file, which is needed for the session word count. PR
- #21.
+* Project word count is written to the project file, which is needed for the session word count.
+ PR #21.
* Closing a project now clears the status bar. PR #21.
**Editor**
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 1714cfd2..00a01ff6 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -264,42 +264,26 @@ class NWProject(QObject):
return
- def openProject(self, fileName, overrideLock=False):
+ def openProject(self, projPath, overrideLock=False):
"""Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful,
parse the XML of the file and populate the project variables and
build the tree of project items.
"""
- if not os.path.isfile(fileName):
- fileName = os.path.join(fileName, nwFiles.PROJ_FILE)
- if not os.path.isfile(fileName):
- self.mainGui.makeAlert(self.tr(
- "File not found: {0}"
- ).format(fileName), nwAlert.ERROR)
- return False
-
self.clearProject()
- self.projPath = os.path.abspath(os.path.dirname(fileName))
- logger.info("Opening project: %s", self.projPath)
-
- # Standard Folders and Files
- # ==========================
-
- if not self.ensureFolderStructure():
- self.clearProject()
+ if not self._storage.openProjectInPlace(projPath):
return False
+ # ToDo: These should not be set explicitly, and should stay as Path
+ self.projPath = str(self._storage.runtimePath)
+ self.projContent = str(self._storage.contentPath)
+ self.projCache = str(self._storage.cachePath)
+ self.projMeta = str(self._storage.metaPath)
+
+ logger.info("Opening project: %s", self.projPath)
+
self.projDict = os.path.join(self.projMeta, nwFiles.PROJ_DICT)
- # Check for Old Legacy Data
- # =========================
-
- legacyList = [] # Cleanup is done later
- for projItem in os.listdir(self.projPath):
- logger.debug("Project contains: %s", projItem)
- if projItem.startswith("data_") and len(projItem) == 6:
- legacyList.append(projItem)
-
# Project Lock
# ============
@@ -321,10 +305,6 @@ class NWProject(QObject):
# Open The Project XML File
# =========================
- if not self._storage.openProjectInPlace(self.projPath):
- self.clearProject()
- return False
-
xmlReader = self._storage.getXmlReader()
if not isinstance(xmlReader, ProjectXMLReader):
self.clearProject()
@@ -398,17 +378,6 @@ class NWProject(QObject):
self._options.loadSettings()
self._index.loadIndex()
- # Sort out old file locations
- if legacyList:
- try:
- for projItem in legacyList:
- self._legacyDataFolder(projItem)
- except Exception:
- self.mainGui.makeAlert(self.tr(
- "There was an error updating the project. "
- "Some data may not have been preserved."
- ), nwAlert.ERROR)
-
# Clean up no longer used files
self._deprecatedFiles()
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index 43e5d618..dfa4a2a2 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -57,6 +57,32 @@ class NWStorage:
self._openMode = self.MODE_INACTIVE
return
+ ##
+ # Properties
+ ##
+
+ @property
+ def runtimePath(self):
+ return self._runtimePath
+
+ @property
+ def contentPath(self):
+ if self._runtimePath is not None:
+ return self._runtimePath / "content"
+ return None
+
+ @property
+ def metaPath(self):
+ if self._runtimePath is not None:
+ return self._runtimePath / "meta"
+ return None
+
+ @property
+ def cachePath(self):
+ if self._runtimePath is not None:
+ return self._runtimePath / "cache"
+ return None
+
##
# Core Methods
##
@@ -70,19 +96,20 @@ class NWStorage:
"""Open a novelWriter project in-place. That is, it is opened
directly from a project folder.
"""
- inPath = Path(path)
+ inPath = Path(path).resolve()
if inPath.is_file():
+ # The path should not point to an exisitng file,
+ # but it can point to a folder containing files
inPath = inPath.parent
- if not inPath.is_dir():
- logger.error("No such folder: %s", inPath)
- self.clear()
- return False
-
self._storagePath = inPath
self._runtimePath = inPath
self._openMode = self.MODE_INPLACE
+ if self._prepareStorage(checkLegacy=True) is False:
+ self.clear()
+ return False
+
return True
def openProjectArchive(self, path):
@@ -142,10 +169,116 @@ class NWStorage:
def _zipIt(self, target):
pass
- def _reeadLockFile(self):
+ def _readLockFile(self):
pass
def _writeLockFile(self):
pass
+ def _prepareStorage(self, checkLegacy=True):
+ """Prepare the storage area for the project.
+ """
+ path = self._runtimePath
+ if path is None:
+ logger.error("No path set")
+ self.clear()
+ return False
+
+ if path == Path.home().absolute():
+ logger.error("Cannot use the user's home path as the root of a project")
+ self.clear()
+ return False
+
+ # The folder is not required to exist, as it could be a new
+ # project, so we make sure it does. Then we add subfolders.
+ try:
+ path.mkdir(exist_ok=True)
+ (path / "content").mkdir(exist_ok=True)
+ (path / "cache").mkdir(exist_ok=True)
+ (path / "meta").mkdir(exist_ok=True)
+ except Exception as exc:
+ logger.error("Failed to create required project folders", exc_info=exc)
+ return False
+
+ if not checkLegacy:
+ # The legacy content check is only needed for project folder
+ # storage, so if it is not expected to be that, there's no
+ # need for the remaning checks.
+ return True
+
+ # Check for legacy data folders
+ for child in path.iterdir():
+ if child.is_dir() and child.name.startswith("data_"):
+ self._legacyDataFolder(path, child)
+
+ # Check for no longer used files, and delete them
+ self._deleteDeprecatedFiles(path)
+
+ return True
+
+ ##
+ # Legacy Project Data Handlers
+ ##
+
+ def _legacyDataFolder(self, path: Path, child: Path):
+ """Handle the content of a legacy data folder from a version 1.0
+ project.
+ """
+ logger.info("Processing legacy data folder: %s", path)
+
+ # Move Documents to Content
+ first = child.name[-1]
+ if first not in "0123456789abcdef":
+ return
+
+ for item in child.iterdir():
+ if not item.is_file():
+ continue
+
+ name = item.name
+ if len(name) == 21 and name.endswith("_main.nwd"):
+ newPath = path / "content" / f"{first}{name[:12]}.nwd"
+ try:
+ item.rename(newPath)
+ logger.info("Moved file: %s", newPath)
+ except Exception as exc:
+ logger.warning("Failed to move: %s", item, exc_info=exc)
+ elif len(name) == 21 and name.endswith("_main.bak"):
+ try:
+ item.unlink()
+ logger.info("Deleted file: %s", item)
+ except Exception as exc:
+ logger.warning("Failed to delete: %s", item, exc_info=exc)
+
+ # Remove Data Folder
+ try:
+ child.rmdir()
+ logger.info("Deleted folder: %s", child)
+ except Exception as exc:
+ logger.warning("Failed to delete: %s", child, exc_info=exc)
+
+ return
+
+ def _deleteDeprecatedFiles(self, path: Path):
+ """Delete files that are no longer used by novelWriter.
+ """
+ remove = [
+ path / "meta" / "mainOptions.json",
+ path / "meta" / "exportOptions.json",
+ path / "meta" / "outlineOptions.json",
+ path / "meta" / "timelineOptions.json",
+ path / "meta" / "docMergeOptions.json",
+ path / "meta" / "sessionLogOptions.json",
+ path / "ToC.json",
+ ]
+ for item in remove:
+ if item.is_file():
+ try:
+ item.unlink()
+ logger.info("Deleted: %s", item)
+ except Exception as exc:
+ logger.warning("Failed to delete: %s", item, exc_info=exc)
+
+ return
+
# END Class NWStorage
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 9d7b12e5..29e1a561 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -250,16 +250,6 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
assert "This project was saved by a newer version" in mockGUI.lastQuestion[1]
mockGUI.askResponse = True
- # Add some legacy stuff that cannot be removed
- with monkeypatch.context() as mp:
- mp.setattr(theProject, "_legacyDataFolder", causeOSError)
- os.mkdir(os.path.join(fncDir, "data_0"))
- writeFile(os.path.join(fncDir, "data_0", "123456789abc_main.nwd"), "stuff")
- writeFile(os.path.join(fncDir, "data_0", "123456789abc_main.bak"), "stuff")
- mockGUI.clear()
- assert theProject.openProject(fncDir) is True
- assert "There was an error updating the project." in mockGUI.lastAlert
-
assert theProject.closeProject()
# END Test testCoreProject_Open
From 783a849e09edd2c04defbcdf5a3e28793771984b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 5 Nov 2022 16:19:19 +0100
Subject: [PATCH 09/26] Remove the old legacy format code
---
novelwriter/core/project.py | 74 ----------
tests/conftest.py | 21 ---
tests/oldproj/data_1/9752e7f9d8af_main.nwd | 4 -
tests/oldproj/data_7/ff63b8afc4cd_main.nwd | 4 -
tests/oldproj/data_8/8124a4292d8b_main.nwd | 4 -
tests/oldproj/data_9/058ae29f0dfd_main.nwd | 4 -
tests/oldproj/data_9/1239bf2f8b69_main.nwd | 4 -
tests/oldproj/data_a/764d5acf5a21_main.nwd | 4 -
tests/oldproj/data_f/528d831f5b24_main.nwd | 4 -
tests/oldproj/meta/sessionInfo.log | 2 -
tests/oldproj/meta/tagsIndex.json | 72 ----------
tests/oldproj/nwProject.nwx | 148 --------------------
tests/test_core/test_core_project.py | 151 ---------------------
13 files changed, 496 deletions(-)
delete mode 100644 tests/oldproj/data_1/9752e7f9d8af_main.nwd
delete mode 100644 tests/oldproj/data_7/ff63b8afc4cd_main.nwd
delete mode 100644 tests/oldproj/data_8/8124a4292d8b_main.nwd
delete mode 100644 tests/oldproj/data_9/058ae29f0dfd_main.nwd
delete mode 100644 tests/oldproj/data_9/1239bf2f8b69_main.nwd
delete mode 100644 tests/oldproj/data_a/764d5acf5a21_main.nwd
delete mode 100644 tests/oldproj/data_f/528d831f5b24_main.nwd
delete mode 100644 tests/oldproj/meta/sessionInfo.log
delete mode 100644 tests/oldproj/meta/tagsIndex.json
delete mode 100644 tests/oldproj/nwProject.nwx
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 00a01ff6..85bba2fa 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -378,9 +378,6 @@ class NWProject(QObject):
self._options.loadSettings()
self._index.loadIndex()
- # Clean up no longer used files
- self._deprecatedFiles()
-
# Update recent projects
self.mainConf.updateRecentCache(
self.projPath, self._data.name, sum(self._data.initCounts), time()
@@ -1046,77 +1043,6 @@ class NWProject(QObject):
return True
- ##
- # Legacy Data Structure Handlers
- ##
-
- def _legacyDataFolder(self, dataDir):
- """Clean up legacy data folders.
- """
- dataPath = os.path.join(self.projPath, dataDir)
- if not os.path.isdir(dataPath):
- return False
-
- logger.info("Old data folder found: %s", dataDir)
-
- # Move Documents to Content
- for dataItem in os.listdir(dataPath):
- dataFile = os.path.join(dataPath, dataItem)
- if not os.path.isfile(dataFile):
- continue
-
- if len(dataItem) == 21 and dataItem.endswith("_main.nwd"):
- tHandle = dataDir[-1] + dataItem[:12]
- newPath = os.path.join(self.projContent, f"{tHandle}.nwd")
- os.rename(dataFile, newPath)
- logger.info("Moved file: %s", dataFile)
-
- elif len(dataItem) == 21 and dataItem.endswith("_main.bak"):
- os.unlink(dataFile)
- logger.info("Deleted file: %s", dataFile)
-
- # Remove Data Folder
- if not os.listdir(dataPath):
- os.rmdir(dataPath)
- logger.info("Deleted folder: %s", dataDir)
-
- return True
-
- def _deprecatedFiles(self):
- """Delete files that are no longer used by novelWriter.
- """
- rmList = [
- os.path.join(self.projCache, "nwProject.nwx.0"),
- os.path.join(self.projCache, "nwProject.nwx.1"),
- os.path.join(self.projCache, "nwProject.nwx.2"),
- os.path.join(self.projCache, "nwProject.nwx.3"),
- os.path.join(self.projCache, "nwProject.nwx.4"),
- os.path.join(self.projCache, "nwProject.nwx.5"),
- os.path.join(self.projCache, "nwProject.nwx.6"),
- os.path.join(self.projCache, "nwProject.nwx.7"),
- os.path.join(self.projCache, "nwProject.nwx.8"),
- os.path.join(self.projCache, "nwProject.nwx.9"),
- os.path.join(self.projMeta, "mainOptions.json"),
- os.path.join(self.projMeta, "exportOptions.json"),
- os.path.join(self.projMeta, "outlineOptions.json"),
- os.path.join(self.projMeta, "timelineOptions.json"),
- os.path.join(self.projMeta, "docMergeOptions.json"),
- os.path.join(self.projMeta, "sessionLogOptions.json"),
- os.path.join(self.projPath, "ToC.json"),
- ]
-
- for rmFile in rmList:
- if os.path.isfile(rmFile):
- logger.info("Deleting: %s", rmFile)
- try:
- os.unlink(rmFile)
- except Exception:
- logger.error("Could not delete: %s", rmFile)
- logException()
- return False
-
- return True
-
# END Class NWProject
diff --git a/tests/conftest.py b/tests/conftest.py
index 33cd3574..758d46da 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -267,27 +267,6 @@ def nwLipsum(tmpDir):
return
-@pytest.fixture(scope="function")
-def nwOldProj(tmpDir):
- """A minimal movelWriter project using the old folder structure used
- for storage versions < 1.2.
- """
- tstDir = os.path.dirname(__file__)
- srcDir = os.path.join(tstDir, "oldproj")
- dstDir = os.path.join(tmpDir, "oldproj")
- if os.path.isdir(dstDir):
- shutil.rmtree(dstDir)
-
- shutil.copytree(srcDir, dstDir)
-
- yield dstDir
-
- if os.path.isdir(dstDir):
- shutil.rmtree(dstDir)
-
- return
-
-
@pytest.fixture(scope="session")
def ipsumText():
"""Return five paragraphs of Lorem Ipsum text.
diff --git a/tests/oldproj/data_1/9752e7f9d8af_main.nwd b/tests/oldproj/data_1/9752e7f9d8af_main.nwd
deleted file mode 100644
index 5b25ad52..00000000
--- a/tests/oldproj/data_1/9752e7f9d8af_main.nwd
+++ /dev/null
@@ -1,4 +0,0 @@
-### Scene Four
-
-Scene Four
-
diff --git a/tests/oldproj/data_7/ff63b8afc4cd_main.nwd b/tests/oldproj/data_7/ff63b8afc4cd_main.nwd
deleted file mode 100644
index 818712f7..00000000
--- a/tests/oldproj/data_7/ff63b8afc4cd_main.nwd
+++ /dev/null
@@ -1,4 +0,0 @@
-# Antagonist
-
-Antagonist
-
diff --git a/tests/oldproj/data_8/8124a4292d8b_main.nwd b/tests/oldproj/data_8/8124a4292d8b_main.nwd
deleted file mode 100644
index 5fe1d9fe..00000000
--- a/tests/oldproj/data_8/8124a4292d8b_main.nwd
+++ /dev/null
@@ -1,4 +0,0 @@
-### Scene Two
-
-Scene Two
-
diff --git a/tests/oldproj/data_9/058ae29f0dfd_main.nwd b/tests/oldproj/data_9/058ae29f0dfd_main.nwd
deleted file mode 100644
index 79e4dc06..00000000
--- a/tests/oldproj/data_9/058ae29f0dfd_main.nwd
+++ /dev/null
@@ -1,4 +0,0 @@
-# Protagonist
-
-Protagonist
-
diff --git a/tests/oldproj/data_9/1239bf2f8b69_main.nwd b/tests/oldproj/data_9/1239bf2f8b69_main.nwd
deleted file mode 100644
index 2d701cd4..00000000
--- a/tests/oldproj/data_9/1239bf2f8b69_main.nwd
+++ /dev/null
@@ -1,4 +0,0 @@
-### Scene Three
-
-Scene Three
-
diff --git a/tests/oldproj/data_a/764d5acf5a21_main.nwd b/tests/oldproj/data_a/764d5acf5a21_main.nwd
deleted file mode 100644
index 7ef7c622..00000000
--- a/tests/oldproj/data_a/764d5acf5a21_main.nwd
+++ /dev/null
@@ -1,4 +0,0 @@
-### Scene Five
-
-Scene Five
-
diff --git a/tests/oldproj/data_f/528d831f5b24_main.nwd b/tests/oldproj/data_f/528d831f5b24_main.nwd
deleted file mode 100644
index 8fecdb8e..00000000
--- a/tests/oldproj/data_f/528d831f5b24_main.nwd
+++ /dev/null
@@ -1,4 +0,0 @@
-### Scene One
-
-Scene One
-
diff --git a/tests/oldproj/meta/sessionInfo.log b/tests/oldproj/meta/sessionInfo.log
deleted file mode 100644
index 92da5e9a..00000000
--- a/tests/oldproj/meta/sessionInfo.log
+++ /dev/null
@@ -1,2 +0,0 @@
-Start: 2020-09-26 16:13:00 End: 2020-09-26 16:15:54 Words: 24
-Start: 2020-09-26 16:16:28 End: 2020-09-26 16:16:40 Words: -1
diff --git a/tests/oldproj/meta/tagsIndex.json b/tests/oldproj/meta/tagsIndex.json
deleted file mode 100644
index d02515f6..00000000
--- a/tests/oldproj/meta/tagsIndex.json
+++ /dev/null
@@ -1,72 +0,0 @@
-{
- "tagIndex": {},
- "refIndex": {
- "f528d831f5b24": [],
- "88124a4292d8b": [],
- "91239bf2f8b69": [],
- "19752e7f9d8af": [],
- "a764d5acf5a21": [],
- "9058ae29f0dfd": [],
- "7ff63b8afc4cd": []
- },
- "novelIndex": {
- "f528d831f5b24": [
- [
- 1,
- 3,
- "Scene One",
- "SCENE"
- ]
- ],
- "88124a4292d8b": [
- [
- 1,
- 3,
- "Scene Two",
- "SCENE"
- ]
- ],
- "91239bf2f8b69": [
- [
- 1,
- 3,
- "Scene Three",
- "SCENE"
- ]
- ],
- "19752e7f9d8af": [
- [
- 1,
- 3,
- "Scene Four",
- "SCENE"
- ]
- ],
- "a764d5acf5a21": [
- [
- 1,
- 3,
- "Scene Five",
- "SCENE"
- ]
- ]
- },
- "noteIndex": {
- "9058ae29f0dfd": [
- [
- 1,
- 1,
- "Protagonist",
- "NOTE"
- ]
- ],
- "7ff63b8afc4cd": [
- [
- 1,
- 1,
- "Antagonist",
- "NOTE"
- ]
- ]
- }
-}
\ No newline at end of file
diff --git a/tests/oldproj/nwProject.nwx b/tests/oldproj/nwProject.nwx
deleted file mode 100644
index 8ba21098..00000000
--- a/tests/oldproj/nwProject.nwx
+++ /dev/null
@@ -1,148 +0,0 @@
-
-
-
-
-
- True
-
-
- False
- a764d5acf5a21
- None
- 23
-
-
- New
- Note
- Draft
- Finished
-
-
- New
- Minor
- Major
- Main
-
-
-
- -
- Novel
- ROOT
- NOVEL
- New
- True
-
- -
- Chapter One
- FOLDER
- NOVEL
- New
- True
-
- -
- Scene One
- FILE
- NOVEL
- New
- False
- SCENE
- 18
- 4
- 1
- 3
-
- -
- Scene Two
- FILE
- NOVEL
- New
- False
- SCENE
- 18
- 4
- 1
- 2
-
- -
- Scene Three
- FILE
- NOVEL
- New
- False
- SCENE
- 22
- 4
- 1
- 2
-
- -
- Scene Four
- FILE
- NOVEL
- New
- False
- SCENE
- 20
- 4
- 1
- 2
-
- -
- Scene Five
- FILE
- NOVEL
- New
- False
- SCENE
- 20
- 4
- 1
- 2
-
- -
- Characters
- ROOT
- CHARACTER
- New
- True
-
- -
- Protagonist
- FILE
- CHARACTER
- New
- False
- NOTE
- 11
- 1
- 0
- 28
-
- -
- Antagonist
- FILE
- CHARACTER
- New
- False
- NOTE
- 13
- 2
- 1
- 26
-
- -
- Plot
- ROOT
- PLOT
- New
- False
-
- -
- World
- ROOT
- WORLD
- New
- False
-
-
-
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 29e1a561..118c0cb7 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -812,157 +812,6 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
# END Test testCoreProject_OrphanedFiles
-@pytest.mark.core
-def testCoreProject_OldFormat(mockGUI, nwOldProj):
- """Test that a project folder structure of version 1.0 can be
- converted to the latest folder structure. Version 1.0 split the
- documents into 'data_0' ... 'data_f' folders, which are now all
- contained in a single 'content' folder.
- """
- theProject = NWProject(mockGUI)
-
- # Create mock files for known legacy files
- deleteFiles = [
- os.path.join(nwOldProj, "cache", "nwProject.nwx.0"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.1"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.2"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.3"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.4"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.5"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.6"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.7"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.8"),
- os.path.join(nwOldProj, "cache", "nwProject.nwx.9"),
- os.path.join(nwOldProj, "meta", "mainOptions.json"),
- os.path.join(nwOldProj, "meta", "exportOptions.json"),
- os.path.join(nwOldProj, "meta", "outlineOptions.json"),
- os.path.join(nwOldProj, "meta", "timelineOptions.json"),
- os.path.join(nwOldProj, "meta", "docMergeOptions.json"),
- os.path.join(nwOldProj, "meta", "sessionLogOptions.json"),
- ]
-
- # Create mock files
- os.mkdir(os.path.join(nwOldProj, "cache"))
- for aFile in deleteFiles:
- writeFile(aFile, "Hi")
- for aFile in deleteFiles:
- assert os.path.isfile(aFile)
-
- # Open project and check that files that are not supposed to be
- # there have been removed
- assert theProject.openProject(nwOldProj)
- for aFile in deleteFiles:
- assert not os.path.isfile(aFile)
-
- assert not os.path.isdir(os.path.join(nwOldProj, "data_1"))
- assert not os.path.isdir(os.path.join(nwOldProj, "data_7"))
- assert not os.path.isdir(os.path.join(nwOldProj, "data_8"))
- assert not os.path.isdir(os.path.join(nwOldProj, "data_9"))
- assert not os.path.isdir(os.path.join(nwOldProj, "data_a"))
- assert not os.path.isdir(os.path.join(nwOldProj, "data_f"))
-
- # Check that files we want to keep are in the right place
- assert os.path.isdir(os.path.join(nwOldProj, "cache"))
- assert os.path.isdir(os.path.join(nwOldProj, "content"))
- assert os.path.isdir(os.path.join(nwOldProj, "meta"))
-
- assert os.path.isfile(os.path.join(nwOldProj, "content", "f528d831f5b24.nwd"))
- assert os.path.isfile(os.path.join(nwOldProj, "content", "88124a4292d8b.nwd"))
- assert os.path.isfile(os.path.join(nwOldProj, "content", "91239bf2f8b69.nwd"))
- assert os.path.isfile(os.path.join(nwOldProj, "content", "19752e7f9d8af.nwd"))
- assert os.path.isfile(os.path.join(nwOldProj, "content", "a764d5acf5a21.nwd"))
- assert os.path.isfile(os.path.join(nwOldProj, "content", "9058ae29f0dfd.nwd"))
- assert os.path.isfile(os.path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd"))
-
- assert os.path.isfile(os.path.join(nwOldProj, "meta", "tagsIndex.json"))
- assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionInfo.log"))
-
- # Close the project
- theProject.closeProject()
-
- # Check that new files have been created
- assert os.path.isfile(os.path.join(nwOldProj, "meta", "guiOptions.json"))
- assert os.path.isfile(os.path.join(nwOldProj, "ToC.txt"))
-
-# END Test testCoreProject_OldFormat
-
-
-@pytest.mark.core
-def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir):
- """Test the functins that handle legacy data folders and structure
- with additional tests of failure handling.
- """
- theProject = NWProject(mockGUI)
- theProject.setProjectPath(fncDir)
-
- # Check behaviour of deprecated files function on OSError
- tstFile = os.path.join(fncDir, "ToC.json")
- writeFile(tstFile, "stuff")
- assert os.path.isfile(tstFile)
-
- with monkeypatch.context() as mp:
- mp.setattr("os.unlink", causeOSError)
- assert theProject._deprecatedFiles() is False
-
- assert theProject._deprecatedFiles()
- assert not os.path.isfile(tstFile)
-
- # Check processing non-folders
- tstFile = os.path.join(fncDir, "data_0")
- writeFile(tstFile, "stuff")
- assert os.path.isfile(tstFile)
- assert theProject._legacyDataFolder(tstFile) is False
-
- # Check renaming/deleting of old document files
- tstData2 = os.path.join(fncDir, "data_2")
- tstData3 = os.path.join(fncDir, "data_3")
- tstDoc1m = os.path.join(tstData2, "000000000001_main.nwd")
- tstDoc1b = os.path.join(tstData2, "000000000001_main.bak")
- tstDoc2m = os.path.join(tstData2, "000000000002_main.nwd")
- tstDoc2b = os.path.join(tstData2, "000000000002_main.bak")
- tstDoc3m = os.path.join(tstData3, "tooshort003_main.nwd")
- tstDoc3b = os.path.join(tstData3, "tooshort003_main.bak")
- tstDir4a = os.path.join(tstData3, "stuff")
-
- os.mkdir(tstData2)
- os.mkdir(tstData3)
- writeFile(tstDoc1m, "stuff")
- writeFile(tstDoc1b, "stuff")
- writeFile(tstDoc2m, "stuff")
- writeFile(tstDoc2b, "stuff")
- writeFile(tstDoc3m, "stuff")
- writeFile(tstDoc3b, "stuff")
- os.mkdir(tstDir4a)
-
- # Make the above fail
- with monkeypatch.context() as mp:
- mp.setattr("os.rename", causeOSError)
- mp.setattr("os.unlink", causeOSError)
- with pytest.raises(OSError):
- theProject._legacyDataFolder(tstData2)
- theProject._legacyDataFolder(tstData3)
- assert os.path.isfile(tstDoc1m)
- assert os.path.isfile(tstDoc1b)
- assert os.path.isfile(tstDoc2m)
- assert os.path.isfile(tstDoc2b)
- assert os.path.isfile(tstDoc3m)
- assert os.path.isfile(tstDoc3b)
-
- # And succeed ...
- assert theProject._legacyDataFolder(tstData2) is True
- assert theProject._legacyDataFolder(tstData3) is True
-
- assert not os.path.isdir(tstData2)
- assert os.path.isdir(tstData3)
- assert os.path.isfile(os.path.join(fncDir, "content", "2000000000001.nwd"))
- assert os.path.isfile(os.path.join(fncDir, "content", "2000000000002.nwd"))
- assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.nwd"))
- assert os.path.isfile(os.path.join(fncDir, tstData3, "tooshort003_main.bak"))
- assert os.path.isdir(tstDir4a)
-
-# END Test testCoreProject_LegacyData
-
-
@pytest.mark.core
def testCoreProject_Backup(monkeypatch, mockGUI, fncDir, tmpDir):
"""Test the automated backup feature of the project class. The test
From 8b4dac4e13cd783157665d232cf6862954796ecd Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 5 Nov 2022 16:24:57 +0100
Subject: [PATCH 10/26] Split project data class back into a separate file
---
novelwriter/core/project.py | 312 +---------------------
novelwriter/core/projectdata.py | 340 ++++++++++++++++++++++++
tests/test_core/test_core_projectxml.py | 2 +-
3 files changed, 345 insertions(+), 309 deletions(-)
create mode 100644 novelwriter/core/projectdata.py
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 85bba2fa..7892d16e 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -1,11 +1,10 @@
"""
novelWriter – Project Wrapper
=============================
-Data class for novelWriter projects
+The parent class for a novelWriter project
File History:
-Created: 2018-09-29 [0.0.1] NWProject
-Created: 2022-10-30 [2.0rc1] NWProjectData
+Created: 2018-09-29 [0.0.1]
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -43,14 +42,13 @@ from novelwriter.constants import trConst, nwFiles, nwLabels
from novelwriter.core.tree import NWTree
from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex
-from novelwriter.core.status import NWStatus
from novelwriter.core.options import OptionState
from novelwriter.core.storage import NWStorage
from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
+from novelwriter.core.projectdata import NWProjectData
from novelwriter.common import (
- checkBool, checkInt, checkStringNone, formatTimeStamp, hexToInt, isHandle,
- makeFileNameSafe, minmax, simplified,
+ checkStringNone, formatTimeStamp, hexToInt, isHandle, makeFileNameSafe, minmax
)
@@ -1044,305 +1042,3 @@ class NWProject(QObject):
return True
# END Class NWProject
-
-
-class NWProjectData:
-
- def __init__(self, theProject):
-
- self.theProject = theProject
-
- # Project Meta
- self._name = ""
- self._title = ""
- self._authors = []
- self._saveCount = 0
- self._autoCount = 0
- self._editTime = 0
-
- # Project Settings
- self._doBackup = True
- self._language = None
- self._spellCheck = False
- self._spellLang = None
-
- # Project Dictionaries
- self._initCounts = [0, 0]
- self._currCounts = [0, 0]
- self._lastHandle: dict[str, str | None] = {
- "editor": None,
- "viewer": None,
- "novelTree": None,
- "outline": None,
- }
- self._autoReplace: dict[str, str] = {}
- self._titleFormat: dict[str, str] = {
- "title": "%title%",
- "chapter": "%title%",
- "unnumbered": "%title%",
- "scene": "* * *",
- "section": "",
- }
-
- self._status = NWStatus(NWStatus.STATUS)
- self._import = NWStatus(NWStatus.IMPORT)
-
- return
-
- ##
- # Properties
- ##
-
- @property
- def name(self):
- return self._name
-
- @property
- def title(self):
- return self._title
-
- @property
- def authors(self):
- return self._authors
-
- @property
- def saveCount(self):
- return self._saveCount
-
- @property
- def autoCount(self):
- return self._autoCount
-
- @property
- def editTime(self):
- return self._editTime
-
- @property
- def doBackup(self):
- return self._doBackup
-
- @property
- def language(self):
- return self._language
-
- @property
- def spellCheck(self):
- return self._spellCheck
-
- @property
- def spellLang(self):
- return self._spellLang
-
- @property
- def initCounts(self):
- return tuple(self._initCounts)
-
- @property
- def currCounts(self):
- return tuple(self._currCounts)
-
- @property
- def lastHandle(self):
- return self._lastHandle
-
- @property
- def autoReplace(self):
- return self._autoReplace
-
- @property
- def titleFormat(self):
- return self._titleFormat
-
- @property
- def itemStatus(self):
- return self._status
-
- @property
- def itemImport(self):
- return self._import
-
- ##
- # Methods
- ##
-
- def addAuthor(self, value):
- """Add an author to the authors list.
- """
- self._authors.append(simplified(str(value)))
- self.theProject.setProjectChanged(True)
- return
-
- def incSaveCount(self):
- """Increment the save count by one.
- """
- self._saveCount += 1
- self.theProject.setProjectChanged(True)
- return
-
- def incAutoCount(self):
- """Increment the auto save count by one.
- """
- self._autoCount += 1
- self.theProject.setProjectChanged(True)
- return
-
- ##
- # Getters
- ##
-
- def getLastHandle(self, component):
- """Retrieve the last used handle for a given component.
- """
- return self._lastHandle.get(component, None)
-
- def getTitleFormat(self, kind):
- """Retrieve the title format string for a given kind of header.
- """
- return self._titleFormat.get(kind, "%title%")
-
- ##
- # Setters
- ##
-
- def setName(self, value):
- """Set a new project name.
- """
- if value != self._name:
- self._name = simplified(str(value))
- self.theProject.setProjectChanged(True)
- return
-
- def setTitle(self, value):
- """Set a new novel title.
- """
- if value != self._title:
- self._title = simplified(str(value))
- self.theProject.setProjectChanged(True)
- return
-
- def setAuthors(self, value):
- """Set the list of authors from either a string with one author
- per line, or a list of authors.
- """
- self._authors = []
- self.theProject.setProjectChanged(True)
- if isinstance(value, str):
- for author in value.splitlines():
- author = simplified(author)
- if author:
- self._authors.append(author)
- self.theProject.setProjectChanged(True)
- elif isinstance(value, list):
- self._authors = value
- return
-
- def setSaveCount(self, value):
- """Set the save count from last session.
- """
- self._saveCount = checkInt(value, 0)
- self.theProject.setProjectChanged(True)
- return
-
- def setAutoCount(self, value):
- """Set the auto save count from last session.
- """
- self._autoCount = checkInt(value, 0)
- self.theProject.setProjectChanged(True)
- return
-
- def setEditTime(self, value):
- """Set tyje edit time from last session.
- """
- self._editTime = checkInt(value, 0)
- self.theProject.setProjectChanged(True)
- return
-
- def setDoBackup(self, value):
- """Set the do write backup flag.
- """
- if value != self._doBackup:
- self._doBackup = checkBool(value, False)
- self.theProject.setProjectChanged(True)
- return
-
- def setLanguage(self, value):
- """Set the project language.
- """
- if value != self._language:
- self._language = checkStringNone(value, None)
- self.theProject.setProjectChanged(True)
- return
-
- def setSpellCheck(self, value):
- """Set the spell check flag.
- """
- if value != self._spellCheck:
- self._spellCheck = checkBool(value, False)
- self.theProject.setProjectChanged(True)
- return
-
- def setSpellLang(self, value):
- """Set the spell check language.
- """
- if value != self._spellLang:
- self._spellLang = checkStringNone(value, None)
- self.theProject.setProjectChanged(True)
- return
-
- def setLastHandle(self, value, component=None):
- """Set a last used handle into the handle registry. If component
- is None, the value is assumed to be the whole dictionary of
- values.
- """
- if isinstance(component, str):
- self._lastHandle[component] = checkStringNone(value, None)
- self.theProject.setProjectChanged(True)
- elif isinstance(value, dict):
- for key, entry in value.items():
- if key in self._lastHandle:
- self._lastHandle[key] = str(entry) if isHandle(entry) else None
- self.theProject.setProjectChanged(True)
- return
-
- def setInitCounts(self, novel=None, notes=None):
- """Set the worc count totals for novel and note files.
- """
- if novel is not None:
- self._initCounts[0] = checkInt(novel, 0)
- self._currCounts[0] = checkInt(novel, 0)
- if notes is not None:
- self._initCounts[1] = checkInt(notes, 0)
- self._currCounts[1] = checkInt(notes, 0)
- return
-
- def setCurrCounts(self, novel=None, notes=None):
- """Set the worc count totals for novel and note files.
- """
- if novel is not None:
- self._currCounts[0] = checkInt(novel, 0)
- if notes is not None:
- self._currCounts[1] = checkInt(notes, 0)
- return
-
- def setAutoReplace(self, value):
- """Set the auto-replace dictionary.
- """
- if isinstance(value, dict):
- self._autoReplace = {}
- for key, entry in value.items():
- if isinstance(entry, str):
- self._autoReplace[key] = simplified(entry)
- self.theProject.setProjectChanged(True)
- return
-
- def setTitleFormat(self, value):
- """Set the title formats.
- """
- if isinstance(value, dict):
- for key, entry in value.items():
- if key in self._titleFormat and isinstance(entry, str):
- self._titleFormat[key] = simplified(entry)
- self.theProject.setProjectChanged(True)
- return
-
-# END Class NWProjectData
diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py
new file mode 100644
index 00000000..913a8aa8
--- /dev/null
+++ b/novelwriter/core/projectdata.py
@@ -0,0 +1,340 @@
+"""
+novelWriter – Project Data Class
+================================
+Data class for novelWriter projects
+
+File History:
+Created: 2022-10-30 [2.0rc1]
+
+This file is a part of novelWriter
+Copyright 2018–2022, 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 .
+"""
+
+from __future__ import annotations
+
+import logging
+
+from novelwriter.common import checkBool, checkInt, checkStringNone, isHandle, simplified
+from novelwriter.core.status import NWStatus
+
+logger = logging.getLogger(__name__)
+
+
+class NWProjectData:
+
+ def __init__(self, theProject):
+
+ self.theProject = theProject
+
+ # Project Meta
+ self._uuid = ""
+ self._name = ""
+ self._title = ""
+ self._authors = []
+ self._saveCount = 0
+ self._autoCount = 0
+ self._editTime = 0
+
+ # Project Settings
+ self._doBackup = True
+ self._language = None
+ self._spellCheck = False
+ self._spellLang = None
+
+ # Project Dictionaries
+ self._initCounts = [0, 0]
+ self._currCounts = [0, 0]
+ self._lastHandle: dict[str, str | None] = {
+ "editor": None,
+ "viewer": None,
+ "novelTree": None,
+ "outline": None,
+ }
+ self._autoReplace: dict[str, str] = {}
+ self._titleFormat: dict[str, str] = {
+ "title": "%title%",
+ "chapter": "%title%",
+ "unnumbered": "%title%",
+ "scene": "* * *",
+ "section": "",
+ }
+
+ self._status = NWStatus(NWStatus.STATUS)
+ self._import = NWStatus(NWStatus.IMPORT)
+
+ return
+
+ ##
+ # Properties
+ ##
+
+ @property
+ def uuid(self):
+ return self._uuid
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def title(self):
+ return self._title
+
+ @property
+ def authors(self):
+ return self._authors
+
+ @property
+ def saveCount(self):
+ return self._saveCount
+
+ @property
+ def autoCount(self):
+ return self._autoCount
+
+ @property
+ def editTime(self):
+ return self._editTime
+
+ @property
+ def doBackup(self):
+ return self._doBackup
+
+ @property
+ def language(self):
+ return self._language
+
+ @property
+ def spellCheck(self):
+ return self._spellCheck
+
+ @property
+ def spellLang(self):
+ return self._spellLang
+
+ @property
+ def initCounts(self):
+ return tuple(self._initCounts)
+
+ @property
+ def currCounts(self):
+ return tuple(self._currCounts)
+
+ @property
+ def lastHandle(self):
+ return self._lastHandle
+
+ @property
+ def autoReplace(self):
+ return self._autoReplace
+
+ @property
+ def titleFormat(self):
+ return self._titleFormat
+
+ @property
+ def itemStatus(self):
+ return self._status
+
+ @property
+ def itemImport(self):
+ return self._import
+
+ ##
+ # Methods
+ ##
+
+ def addAuthor(self, value):
+ """Add an author to the authors list.
+ """
+ self._authors.append(simplified(str(value)))
+ self.theProject.setProjectChanged(True)
+ return
+
+ def incSaveCount(self):
+ """Increment the save count by one.
+ """
+ self._saveCount += 1
+ self.theProject.setProjectChanged(True)
+ return
+
+ def incAutoCount(self):
+ """Increment the auto save count by one.
+ """
+ self._autoCount += 1
+ self.theProject.setProjectChanged(True)
+ return
+
+ ##
+ # Getters
+ ##
+
+ def getLastHandle(self, component):
+ """Retrieve the last used handle for a given component.
+ """
+ return self._lastHandle.get(component, None)
+
+ def getTitleFormat(self, kind):
+ """Retrieve the title format string for a given kind of header.
+ """
+ return self._titleFormat.get(kind, "%title%")
+
+ ##
+ # Setters
+ ##
+
+ def setName(self, value):
+ """Set a new project name.
+ """
+ if value != self._name:
+ self._name = simplified(str(value))
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setTitle(self, value):
+ """Set a new novel title.
+ """
+ if value != self._title:
+ self._title = simplified(str(value))
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setAuthors(self, value):
+ """Set the list of authors from either a string with one author
+ per line, or a list of authors.
+ """
+ self._authors = []
+ self.theProject.setProjectChanged(True)
+ if isinstance(value, str):
+ for author in value.splitlines():
+ author = simplified(author)
+ if author:
+ self._authors.append(author)
+ self.theProject.setProjectChanged(True)
+ elif isinstance(value, list):
+ self._authors = value
+ return
+
+ def setSaveCount(self, value):
+ """Set the save count from last session.
+ """
+ self._saveCount = checkInt(value, 0)
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setAutoCount(self, value):
+ """Set the auto save count from last session.
+ """
+ self._autoCount = checkInt(value, 0)
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setEditTime(self, value):
+ """Set tyje edit time from last session.
+ """
+ self._editTime = checkInt(value, 0)
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setDoBackup(self, value):
+ """Set the do write backup flag.
+ """
+ if value != self._doBackup:
+ self._doBackup = checkBool(value, False)
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setLanguage(self, value):
+ """Set the project language.
+ """
+ if value != self._language:
+ self._language = checkStringNone(value, None)
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setSpellCheck(self, value):
+ """Set the spell check flag.
+ """
+ if value != self._spellCheck:
+ self._spellCheck = checkBool(value, False)
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setSpellLang(self, value):
+ """Set the spell check language.
+ """
+ if value != self._spellLang:
+ self._spellLang = checkStringNone(value, None)
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setLastHandle(self, value, component=None):
+ """Set a last used handle into the handle registry. If component
+ is None, the value is assumed to be the whole dictionary of
+ values.
+ """
+ if isinstance(component, str):
+ self._lastHandle[component] = checkStringNone(value, None)
+ self.theProject.setProjectChanged(True)
+ elif isinstance(value, dict):
+ for key, entry in value.items():
+ if key in self._lastHandle:
+ self._lastHandle[key] = str(entry) if isHandle(entry) else None
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setInitCounts(self, novel=None, notes=None):
+ """Set the worc count totals for novel and note files.
+ """
+ if novel is not None:
+ self._initCounts[0] = checkInt(novel, 0)
+ self._currCounts[0] = checkInt(novel, 0)
+ if notes is not None:
+ self._initCounts[1] = checkInt(notes, 0)
+ self._currCounts[1] = checkInt(notes, 0)
+ return
+
+ def setCurrCounts(self, novel=None, notes=None):
+ """Set the worc count totals for novel and note files.
+ """
+ if novel is not None:
+ self._currCounts[0] = checkInt(novel, 0)
+ if notes is not None:
+ self._currCounts[1] = checkInt(notes, 0)
+ return
+
+ def setAutoReplace(self, value):
+ """Set the auto-replace dictionary.
+ """
+ if isinstance(value, dict):
+ self._autoReplace = {}
+ for key, entry in value.items():
+ if isinstance(entry, str):
+ self._autoReplace[key] = simplified(entry)
+ self.theProject.setProjectChanged(True)
+ return
+
+ def setTitleFormat(self, value):
+ """Set the title formats.
+ """
+ if isinstance(value, dict):
+ for key, entry in value.items():
+ if key in self._titleFormat and isinstance(entry, str):
+ self._titleFormat[key] = simplified(entry)
+ self.theProject.setProjectChanged(True)
+ return
+
+# END Class NWProjectData
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index efe25df9..ac2b4a3c 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -29,8 +29,8 @@ from mock import causeOSError
from tools import cmpFiles, writeFile
from novelwriter.core.item import NWItem
-from novelwriter.core.project import NWProjectData
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
+from novelwriter.core.projectdata import NWProjectData
class MockProject:
From c6d3f680ccc752656b97e498d037e4757adec89e Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 5 Nov 2022 17:03:45 +0100
Subject: [PATCH 11/26] Add a UUID to projects
---
novelwriter/common.py | 10 ++++++++
novelwriter/core/coretools.py | 1 +
novelwriter/core/projectdata.py | 16 ++++++++++++-
novelwriter/core/projectxml.py | 3 ++-
sample/nwProject.nwx | 8 +++----
tests/files/nwProject-1.4.nwx | 2 +-
tests/lipsum/nwProject.nwx | 9 ++++---
.../coreProject_NewFileFolder_nwProject.nwx | 4 ++--
.../coreProject_NewRoot_nwProject.nwx | 4 ++--
.../coreTools_NewCustomA_nwProject.nwx | 4 ++--
.../coreTools_NewCustomB_nwProject.nwx | 4 ++--
.../coreTools_NewMinimal_nwProject.nwx | 4 ++--
.../guiEditor_Main_Final_nwProject.nwx | 6 ++---
.../guiEditor_Main_Initial_nwProject.nwx | 4 ++--
tests/reference/projectXML_ReadLegacy10.nwx | 2 +-
tests/reference/projectXML_ReadLegacy11.nwx | 2 +-
tests/reference/projectXML_ReadLegacy12.nwx | 2 +-
tests/reference/projectXML_ReadLegacy13.nwx | 2 +-
tests/test_base/test_base_common.py | 24 +++++++++++++++----
tests/test_core/test_core_coretools.py | 13 +++++++---
tests/test_core/test_core_projectxml.py | 6 ++++-
tests/tools.py | 1 +
22 files changed, 91 insertions(+), 40 deletions(-)
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 60d1f7fa..5a84302d 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -25,6 +25,7 @@ along with this program. If not, see .
import os
import json
+import uuid
import hashlib
import logging
@@ -113,6 +114,15 @@ def checkHandle(value, default, allowNone=False):
return default
+def checkUuid(value, default):
+ """Try to process a value as an uuid, or return a default.
+ """
+ try:
+ return str(uuid.UUID(value))
+ except Exception:
+ return default
+
+
# =============================================================================================== #
# Validator Functions
# =============================================================================================== #
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 0c24fc29..1ff73d2e 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -322,6 +322,7 @@ class ProjectBuilder:
projTitle = data.get("projTitle", lblNewProject)
projAuthors = data.get("projAuthors", "")
+ project.data.setUuid(None)
project.data.setName(projName)
project.data.setTitle(projTitle)
project.data.setAuthors(projAuthors)
diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py
index 913a8aa8..40bce1a7 100644
--- a/novelwriter/core/projectdata.py
+++ b/novelwriter/core/projectdata.py
@@ -25,9 +25,12 @@ along with this program. If not, see .
from __future__ import annotations
+import uuid
import logging
-from novelwriter.common import checkBool, checkInt, checkStringNone, isHandle, simplified
+from novelwriter.common import (
+ checkBool, checkInt, checkStringNone, checkUuid, isHandle, simplified
+)
from novelwriter.core.status import NWStatus
logger = logging.getLogger(__name__)
@@ -196,6 +199,17 @@ class NWProjectData:
# Setters
##
+ def setUuid(self, value):
+ """Set the project id.
+ """
+ value = checkUuid(value, "")
+ if not value:
+ self._uuid = str(uuid.uuid4())
+ elif value != self._uuid:
+ self._uuid = value
+ self.theProject.setProjectChanged(True)
+ return
+
def setName(self, value):
"""Set a new project name.
"""
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index 8f15b0f5..ed0d2e9a 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -224,6 +224,7 @@ class ProjectXMLReader:
"""Parse the project section of the XML file.
"""
logger.debug("Parsing section")
+ projData.setUuid(xSection.attrib.get("id", None))
for xItem in xSection:
if xItem.tag == "name":
projData.setName(xItem.text)
@@ -476,7 +477,7 @@ class ProjectXMLWriter:
})
# Save Project Meta
- xProject = etree.SubElement(xRoot, "project")
+ xProject = etree.SubElement(xRoot, "project", attrib={"id": projData.uuid})
self._packSingleValue(xProject, "name", projData.name)
self._packSingleValue(xProject, "title", projData.title)
self._packListValue(xProject, "author", projData.authors)
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 1dba26ca..d7d7bde1 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,13 +1,13 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 1409
+ 1421
236
- 69427
+ 69454
False
diff --git a/tests/files/nwProject-1.4.nwx b/tests/files/nwProject-1.4.nwx
index 706b2266..71baddbc 100644
--- a/tests/files/nwProject-1.4.nwx
+++ b/tests/files/nwProject-1.4.nwx
@@ -1,6 +1,6 @@
-
+
Sample Project
Sample Project
Jane Smith
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx
index 5ac1b4de..d78a4aa0 100644
--- a/tests/lipsum/nwProject.nwx
+++ b/tests/lipsum/nwProject.nwx
@@ -1,19 +1,18 @@
-
-
+
+
Lorem Ipsum
Lorem Ipsum
lipsum.com
- 32
+ 34
24
- 1889
+ 1893
False
en_GB
False
None
- 3847
3109
738
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index b1370820..973306cf 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
New Project
New Novel
Jane Doe
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index ed50e9ca..6aea2f22 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
New Project
New Novel
Jane Doe
diff --git a/tests/reference/coreTools_NewCustomA_nwProject.nwx b/tests/reference/coreTools_NewCustomA_nwProject.nwx
index f6314c1a..1b083ddc 100644
--- a/tests/reference/coreTools_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomA_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
Test Custom
Test Novel
Jane Doe
diff --git a/tests/reference/coreTools_NewCustomB_nwProject.nwx b/tests/reference/coreTools_NewCustomB_nwProject.nwx
index 23e0c509..637afa4f 100644
--- a/tests/reference/coreTools_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomB_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
Test Custom
Test Novel
Jane Doe
diff --git a/tests/reference/coreTools_NewMinimal_nwProject.nwx b/tests/reference/coreTools_NewMinimal_nwProject.nwx
index 0f2388f5..bed1b6bf 100644
--- a/tests/reference/coreTools_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreTools_NewMinimal_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
New Project
New Project
1
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index 853ea1d9..2c849f63 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,12 +1,12 @@
-
-
+
+
New Project
New Novel
Jane Doe
4
2
- 3
+ 4
True
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index 8108fba8..84c353e8 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,6 +1,6 @@
-
-
+
+
New Project
New Novel
Jane Doe
diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx
index 8e7f0a99..b557dc19 100644
--- a/tests/reference/projectXML_ReadLegacy10.nwx
+++ b/tests/reference/projectXML_ReadLegacy10.nwx
@@ -1,6 +1,6 @@
-
+
Sample Project
Sample Project
Jane Smith
diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx
index fce991d3..84df9eef 100644
--- a/tests/reference/projectXML_ReadLegacy11.nwx
+++ b/tests/reference/projectXML_ReadLegacy11.nwx
@@ -1,6 +1,6 @@
-
+
Sample Project
Sample Project
Jane Smith
diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx
index 9de6966b..e87015fe 100644
--- a/tests/reference/projectXML_ReadLegacy12.nwx
+++ b/tests/reference/projectXML_ReadLegacy12.nwx
@@ -1,6 +1,6 @@
-
+
Sample Project
Sample Project
Jane Smith
diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx
index 7fc4cfea..a8be3ecd 100644
--- a/tests/reference/projectXML_ReadLegacy13.nwx
+++ b/tests/reference/projectXML_ReadLegacy13.nwx
@@ -1,6 +1,6 @@
-
+
Sample Project
Sample Project
Jane Smith
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index bb05265f..ca487aa2 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -30,11 +30,11 @@ from tools import writeFile
from novelwriter.guimain import GuiMain
from novelwriter.common import (
checkStringNone, checkString, checkInt, checkFloat, checkBool, checkHandle,
- isHandle, isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt,
- minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified,
- splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode,
- readTextFile, makeFileNameSafe, ensureFolder, sha256sum, getGuiItem,
- NWConfigParser
+ checkUuid, isHandle, isTitleTag, isItemClass, isItemType, isItemLayout,
+ hexToInt, minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime,
+ simplified, splitVersionNumber, transferCase, fuzzyTime, numberToRoman,
+ jsonEncode, readTextFile, makeFileNameSafe, ensureFolder, sha256sum,
+ getGuiItem, NWConfigParser
)
@@ -137,6 +137,20 @@ def testBaseCommon_CheckHandle():
# END Test testBaseCommon_CheckHandle
+@pytest.mark.base
+def testBaseCommon_CheckUuid():
+ """Test the checkUuid function.
+ """
+ testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea"
+ assert checkUuid("", None) is None
+ assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None
+ assert checkUuid("e2be99af-f9bf-qq03-857a-c3d1ac25abea", None) is None
+ assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abeaa", None) is None
+ assert checkUuid(testUuid, None) == testUuid
+
+# END Test testBaseCommon_CheckUuid
+
+
@pytest.mark.base
def testBaseCommon_IsHandle():
"""Test the isHandle function.
diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py
index f98e32f1..afb4d07b 100644
--- a/tests/test_core/test_core_coretools.py
+++ b/tests/test_core/test_core_coretools.py
@@ -20,6 +20,7 @@ along with this program. If not, see .
"""
import os
+import uuid
import pytest
from shutil import copyfile
@@ -264,10 +265,12 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mock
@pytest.mark.core
-def testCoreTools_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd):
+def testCoreTools_NewMinimal(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd):
"""Create a new project from a project wizard dictionary. With
default setting, creating a Minimal project.
"""
+ monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
+
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreTools_NewMinimal_nwProject.nwx")
compFile = os.path.join(refDir, "coreTools_NewMinimal_nwProject.nwx")
@@ -294,10 +297,12 @@ def testCoreTools_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd):
@pytest.mark.core
-def testCoreTools_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd):
+def testCoreTools_NewCustomA(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd):
"""Create a new project from a project wizard dictionary.
Custom type with chapters and scenes.
"""
+ monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
+
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreTools_NewCustomA_nwProject.nwx")
compFile = os.path.join(refDir, "coreTools_NewCustomA_nwProject.nwx")
@@ -330,10 +335,12 @@ def testCoreTools_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd):
@pytest.mark.core
-def testCoreTools_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd):
+def testCoreTools_NewCustomB(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd):
"""Create a new project from a project wizard dictionary.
Custom type without chapters, but with scenes.
"""
+ monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
+
projFile = os.path.join(fncDir, "nwProject.nwx")
testFile = os.path.join(outDir, "coreTools_NewCustomB_nwProject.nwx")
compFile = os.path.join(refDir, "coreTools_NewCustomB_nwProject.nwx")
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index ac2b4a3c..b3af7289 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -232,7 +232,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
copyfile(outFile, tstFile)
- assert cmpFiles(tstFile, xmlFile)
+ assert cmpFiles(tstFile, refFile)
# END Test testCoreProjectXML_ReadCurrent
@@ -369,6 +369,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath)
+ data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy10.nwx"
compFile = tstPaths.refDir / "projectXML_ReadLegacy10.nwx"
@@ -510,6 +511,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath)
+ data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy11.nwx"
compFile = tstPaths.refDir / "projectXML_ReadLegacy11.nwx"
@@ -654,6 +656,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath)
+ data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy12.nwx"
compFile = tstPaths.refDir / "projectXML_ReadLegacy12.nwx"
@@ -798,6 +801,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
# Save the project again, which should produce an identical project xml
timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
xmlWriter = ProjectXMLWriter(fncPath)
+ data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
testFile = tstPaths.outDir / "projectXML_ReadLegacy13.nwx"
compFile = tstPaths.refDir / "projectXML_ReadLegacy13.nwx"
diff --git a/tests/tools.py b/tests/tools.py
index 5094fa62..7824268b 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -170,6 +170,7 @@ def buildTestProject(theObject, projPath):
theProject.storage.openProjectInPlace(theProject.projPath)
theProject.setDefaultStatusImport()
+ theProject.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
theProject.data.setName("New Project")
theProject.data.setTitle("New Novel")
theProject.data.setAuthors("Jane Doe")
From 72cc0cf0aa22c3b27a973b74280bdc2394dc0763 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 5 Nov 2022 21:13:18 +0100
Subject: [PATCH 12/26] Add project file format 1.5
---
novelwriter/core/projectxml.py | 182 +++--
sample/nwProject.nwx | 11 +-
tests/files/nwProject-1.4.nwx | 101 ++-
tests/files/nwProject-1.5.nwx | 158 ++++
tests/lipsum/nwProject.nwx | 11 +-
.../coreProject_NewFileFolder_nwProject.nwx | 11 +-
.../coreProject_NewRoot_nwProject.nwx | 11 +-
.../coreTools_NewCustomA_nwProject.nwx | 11 +-
.../coreTools_NewCustomB_nwProject.nwx | 11 +-
.../coreTools_NewMinimal_nwProject.nwx | 11 +-
.../guiEditor_Main_Final_nwProject.nwx | 11 +-
.../guiEditor_Main_Initial_nwProject.nwx | 11 +-
tests/reference/projectXML_ReadLegacy10.nwx | 11 +-
tests/reference/projectXML_ReadLegacy11.nwx | 11 +-
tests/reference/projectXML_ReadLegacy12.nwx | 11 +-
tests/reference/projectXML_ReadLegacy13.nwx | 11 +-
tests/reference/projectXML_ReadLegacy14.json | 677 ++++++++++++++++++
tests/reference/projectXML_ReadLegacy14.nwx | 158 ++++
tests/test_core/test_core_projectxml.py | 157 +++-
tests/tools.py | 2 +-
20 files changed, 1343 insertions(+), 235 deletions(-)
create mode 100644 tests/files/nwProject-1.5.nwx
create mode 100644 tests/reference/projectXML_ReadLegacy14.json
create mode 100644 tests/reference/projectXML_ReadLegacy14.nwx
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index ed0d2e9a..e005a3ec 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -39,14 +39,16 @@ from novelwriter.constants import nwFiles
logger = logging.getLogger(__name__)
-FILE_VERSION = "1.4" # The current project file format version
+FILE_VERSION = "1.5" # The current project file format version
+HEX_VERSION = 0x0105
NUM_VERSION = {
- "1.0": 0x0100,
- "1.1": 0x0101,
- "1.2": 0x0102,
- "1.3": 0x0103,
- "1.4": 0x0104,
+ "1.0": 0x0100, # Up to 0.7
+ "1.1": 0x0101, # Up to 0.10
+ "1.2": 0x0102, # Up to 1.5
+ "1.3": 0x0103, # Up to 2.0 Beta 1
+ "1.4": 0x0104, # Up to 2.0 RC 2
+ "1.5": 0x0105, # Current
}
@@ -84,9 +86,15 @@ class ProjectXMLReader:
1.4 Introduces a more compact format for storing items. All settings
aside from name are now attributes. This format also changes the
- way satus and importance labels, last used handles, and title
- formats are stored. They are now all stored as key/value sets.
- Introduced in version 2.0.
+ way satus and importance labels are stored. This format was only
+ a part of version 2.0 RC 1
+
+ 1.5 The actual format released for 2.0. It moves last used handles
+ and title formats into a key/value format similar to auto-
+ replace, status and imporetance. It adds the heading value to
+ the content item meta entry. It also moves meta data related to
+ the project or the content into their respective section nodes
+ as attributes. The id attribute was also added to the project.
"""
def __init__(self, path):
@@ -201,13 +209,13 @@ class ProjectXMLReader:
self._parseProjectSettings(xSection, projData)
elif xSection.tag == "content":
if self._version >= 0x0104:
- self._parseProjectContent(xSection, projContent)
+ self._parseProjectContent(xSection, projData, projContent)
else:
- self._parseProjectContentLegacy(xSection, projContent, projData)
+ self._parseProjectContentLegacy(xSection, projData, projContent)
else:
logger.warning("Ignored in XML", xSection.tag)
- if self._version == 0x0104:
+ if self._version == HEX_VERSION:
self._state = XMLReadState.PARSED_OK
else:
self._state = XMLReadState.WAS_LEGACY
@@ -224,7 +232,12 @@ class ProjectXMLReader:
"""Parse the project section of the XML file.
"""
logger.debug("Parsing section")
- projData.setUuid(xSection.attrib.get("id", None))
+
+ projData.setUuid(xSection.attrib.get("id", None)) # Added in 1.5
+ projData.setSaveCount(xSection.attrib.get("saveCount", 0)) # Moved in 1.5
+ projData.setAutoCount(xSection.attrib.get("autoCount", 0)) # Moved in 1.5
+ projData.setEditTime(xSection.attrib.get("editTime", 0)) # Moved in 1.5
+
for xItem in xSection:
if xItem.tag == "name":
projData.setName(xItem.text)
@@ -232,15 +245,19 @@ class ProjectXMLReader:
projData.setTitle(xItem.text)
elif xItem.tag == "author":
projData.addAuthor(xItem.text)
- elif xItem.tag == "saveCount":
- projData.setSaveCount(xItem.text)
- elif xItem.tag == "autoCount":
- projData.setAutoCount(xItem.text)
- elif xItem.tag == "editTime":
- projData.setEditTime(xItem.text)
else:
logger.warning("Ignored in XML", xItem.tag)
+ # Deprecated Nodes
+ if self._version < HEX_VERSION:
+ for xItem in xSection:
+ if xItem.tag == "saveCount": # Moved to attribute in 1.5
+ projData.setSaveCount(xItem.text)
+ elif xItem.tag == "autoCount": # Moved to attribute in 1.5
+ projData.setAutoCount(xItem.text)
+ elif xItem.tag == "editTime": # Moved to attribute in 1.5
+ projData.setEditTime(xItem.text)
+
return
def _parseProjectSettings(self, xSection, projData):
@@ -257,10 +274,6 @@ class ProjectXMLReader:
projData.setSpellCheck(xItem.text)
elif xItem.tag == "spellLang":
projData.setSpellLang(xItem.text)
- elif xItem.tag == "novelWordCount":
- projData.setInitCounts(novel=xItem.text)
- elif xItem.tag == "notesWordCount":
- projData.setInitCounts(notes=xItem.text)
elif xItem.tag == "status":
self._parseStatusImport(xItem, projData.itemStatus)
elif xItem.tag in ("import", "importance"):
@@ -273,67 +286,80 @@ class ProjectXMLReader:
else: # Pre 1.2 format
projData.setAutoReplace(self._parseDictTagText(xItem))
elif xItem.tag == "titleFormat":
- if self._version >= 0x0104:
+ if self._version >= 0x0105:
projData.setTitleFormat(self._parseDictKeyText(xItem))
else: # Pre 1.4 format
projData.setTitleFormat(self._parseDictTagText(xItem))
else:
logger.warning("Ignored in XML", xItem.tag)
+ # Deprecated Nodes
+ if self._version < HEX_VERSION:
+ for xItem in xSection:
+ if xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
+ projData.setInitCounts(novel=xItem.text)
+ elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
+ projData.setInitCounts(notes=xItem.text)
+
return
- def _parseProjectContent(self, xSection, projContent):
+ def _parseProjectContent(self, xSection, projData, projContent):
"""Parse the content section of the XML file.
"""
logger.debug("Parsing section")
+ projData.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5
+ projData.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5
+
for xItem in xSection:
- if xItem.tag == "item":
- item = {}
- meta = {}
- name = {}
- itemName = ""
-
- item["handle"] = checkStringNone(xItem.attrib.get("handle"), None)
- item["parent"] = checkStringNone(xItem.attrib.get("parent"), None)
- item["root"] = checkStringNone(xItem.attrib.get("root"), None)
- item["order"] = checkInt(xItem.attrib.get("order"), 0)
- item["type"] = checkString(xItem.attrib.get("type"), "NO_TYPE")
- item["class"] = checkString(xItem.attrib.get("class"), "NO_CLASS")
- item["layout"] = checkString(xItem.attrib.get("layout"), "NO_LAYOUT")
- for xVal in xItem:
- if xVal.tag == "meta":
- meta["expanded"] = checkBool(xVal.attrib.get("expanded"), False)
- meta["heading"] = checkString(xVal.attrib.get("heading"), "H0")
- meta["charCount"] = checkInt(xVal.attrib.get("charCount"), 0)
- meta["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0)
- meta["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0)
- meta["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0)
- elif xVal.tag == "name":
- itemName = simplified(checkString(xVal.text, ""))
- name["status"] = checkStringNone(xVal.attrib.get("status"), None)
- name["import"] = checkStringNone(xVal.attrib.get("import"), None)
- name["active"] = checkBool(xVal.attrib.get("active"), False)
-
- # ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases.
- if "exported" in xVal.attrib:
- name["active"] = checkBool(xVal.attrib.get("exported"), False)
- else:
- logger.warning("Ignored in XML", xVal.tag)
-
- projContent.append({
- "name": itemName,
- "itemAttr": item,
- "metaAttr": meta,
- "nameAttr": name,
- })
-
- else:
+ if xItem.tag != "item":
logger.warning("Ignored item in XML", xItem.tag)
+ continue
+
+ item = {}
+ meta = {}
+ name = {}
+ itemName = ""
+
+ item["handle"] = checkStringNone(xItem.attrib.get("handle"), None)
+ item["parent"] = checkStringNone(xItem.attrib.get("parent"), None)
+ item["root"] = checkStringNone(xItem.attrib.get("root"), None)
+ item["order"] = checkInt(xItem.attrib.get("order"), 0)
+ item["type"] = checkString(xItem.attrib.get("type"), "NO_TYPE")
+ item["class"] = checkString(xItem.attrib.get("class"), "NO_CLASS")
+ item["layout"] = checkString(xItem.attrib.get("layout"), "NO_LAYOUT")
+ for xVal in xItem:
+ if xVal.tag == "meta":
+ meta["expanded"] = checkBool(xVal.attrib.get("expanded"), False)
+ meta["heading"] = checkString(xVal.attrib.get("heading"), "H0")
+ meta["charCount"] = checkInt(xVal.attrib.get("charCount"), 0)
+ meta["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0)
+ meta["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0)
+ meta["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0)
+ elif xVal.tag == "name":
+ itemName = simplified(checkString(xVal.text, ""))
+ name["status"] = checkStringNone(xVal.attrib.get("status"), None)
+ name["import"] = checkStringNone(xVal.attrib.get("import"), None)
+ name["active"] = checkBool(xVal.attrib.get("active"), False)
+ else:
+ logger.warning("Ignored in XML", xVal.tag)
+
+ # Deprecated Nodes
+ if self._version < HEX_VERSION:
+ for xVal in xItem:
+ if xVal.tag == "name":
+ name["active"] = checkBool(xVal.attrib.get("exported"), name["active"])
+
+ projContent.append({
+ "name": itemName,
+ "itemAttr": item,
+ "metaAttr": meta,
+ "nameAttr": name,
+ })
return
- def _parseProjectContentLegacy(self, xSection, projContent, projData):
+ def _parseProjectContentLegacy(self, xSection, projData, projContent):
"""Parse the content section of the XML file for older versions.
"""
logger.debug("Parsing section (legacy format)")
@@ -477,13 +503,17 @@ class ProjectXMLWriter:
})
# Save Project Meta
- xProject = etree.SubElement(xRoot, "project", attrib={"id": projData.uuid})
+ projAttr = {
+ "id": projData.uuid,
+ "saveCount": str(projData.saveCount),
+ "autoCount": str(projData.autoCount),
+ "editTime": str(editTime),
+ }
+
+ xProject = etree.SubElement(xRoot, "project", attrib=projAttr)
self._packSingleValue(xProject, "name", projData.name)
self._packSingleValue(xProject, "title", projData.title)
self._packListValue(xProject, "author", projData.authors)
- self._packSingleValue(xProject, "saveCount", projData.saveCount)
- self._packSingleValue(xProject, "autoCount", projData.autoCount)
- self._packSingleValue(xProject, "editTime", editTime)
# Save Project Settings
xSettings = etree.SubElement(xRoot, "settings")
@@ -491,8 +521,6 @@ class ProjectXMLWriter:
self._packSingleValue(xSettings, "language", projData.language)
self._packSingleValue(xSettings, "spellCheck", projData.spellCheck)
self._packSingleValue(xSettings, "spellLang", projData.spellLang)
- self._packSingleValue(xSettings, "novelWordCount", projData.currCounts[0])
- self._packSingleValue(xSettings, "notesWordCount", projData.currCounts[1])
self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle)
self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace)
self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat)
@@ -507,7 +535,13 @@ class ProjectXMLWriter:
self._packSingleValue(xImport, "entry", label, attrib=attrib)
# Save Tree Content
- xContent = etree.SubElement(xRoot, "content", attrib={"count": str(len(projContent))})
+ contAttr = {
+ "itemCount": str(len(projContent)),
+ "novelWords": str(projData.currCounts[0]),
+ "notesWords": str(projData.currCounts[1]),
+ }
+
+ xContent = etree.SubElement(xRoot, "content", attrib=contAttr)
for item in projContent:
xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {}))
etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {}))
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index d7d7bde1..b1f151ac 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,21 +1,16 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 1421
- 236
- 69454
False
en_GB
True
None
- 954
- 409
636b6aa9b697b
636b6aa9b697b
@@ -50,7 +45,7 @@
Main
-
+
-
Novel
diff --git a/tests/files/nwProject-1.4.nwx b/tests/files/nwProject-1.4.nwx
index 71baddbc..85ff2f63 100644
--- a/tests/files/nwProject-1.4.nwx
+++ b/tests/files/nwProject-1.4.nwx
@@ -1,6 +1,6 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
@@ -14,25 +14,24 @@
en_GB
True
en_GB
+ 636b6aa9b697b
+ 636b6aa9b697b
+ 7031beac91f75
+ 7031beac91f75
+ 1363
954
409
-
- 636b6aa9b697b
- 636b6aa9b697b
- 7031beac91f75
- 7031beac91f75
-
B
E
D
- %title%
- Chapter %chw%: %title%
- %title%
- Scene %ch%.%sc%: %title%
-
+ %title%
+ Chapter %chw%: %title%
+ %title%
+ Scene %ch%.%sc%: %title%
+
New
@@ -56,56 +55,56 @@
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Page
+
+ Page
-
-
- Part One
+
+ Part One
-
-
- Chapter One
+
+ Chapter One
-
-
- Making a Scene
+
+ Making a Scene
-
-
- Another Scene
+
+ Another Scene
-
-
- Interlude
+
+ Interlude
-
-
- A Note on Structure
+
+ A Note on Structure
-
-
- Chapter Two
+
+ Chapter Two
-
-
- We Found John!
+
+ We Found John!
-
Sequel
-
-
- Title Page
+
+ Title Page
-
-
- Chapter One
+
+ Chapter One
-
@@ -116,28 +115,28 @@
Main Characters
-
-
- John Smith
+
+ John Smith
-
-
- Jane Smith
+
+ Jane Smith
-
Locations
-
-
- Earth
+
+ Earth
-
-
- Space
+
+ Space
-
-
- Mars
+
+ Mars
-
@@ -148,16 +147,16 @@
Scenes
-
-
- Old File
+
+ Old File
-
Trash
-
-
- Delete Me!
+
+ Delete Me!
diff --git a/tests/files/nwProject-1.5.nwx b/tests/files/nwProject-1.5.nwx
new file mode 100644
index 00000000..28f03cef
--- /dev/null
+++ b/tests/files/nwProject-1.5.nwx
@@ -0,0 +1,158 @@
+
+
+
+ Sample Project
+ Sample Project
+ Jane Smith
+ Jay Doh
+
+
+ True
+ en_GB
+ True
+ en_GB
+
+ 636b6aa9b697b
+ 636b6aa9b697b
+ 7031beac91f75
+ 7031beac91f75
+
+
+ B
+ E
+ D
+
+
+ %title%
+ Chapter %chw%: %title%
+ %title%
+ Scene %ch%.%sc%: %title%
+
+
+
+ New
+ Notes
+ Started
+ 1st Draft
+ 2nd Draft
+ 3rd Draft
+ Finished
+
+
+ None
+ Minor
+ Major
+ Main
+
+
+
+ -
+
+ Novel
+
+ -
+
+ Title Page
+
+ -
+
+ Page
+
+ -
+
+ Part One
+
+ -
+
+ Chapter One
+
+ -
+
+ Making a Scene
+
+ -
+
+ Another Scene
+
+ -
+
+ Interlude
+
+ -
+
+ A Note on Structure
+
+ -
+
+ Chapter Two
+
+ -
+
+ We Found John!
+
+ -
+
+ Sequel
+
+ -
+
+ Title Page
+
+ -
+
+ Chapter One
+
+ -
+
+ Characters
+
+ -
+
+ Main Characters
+
+ -
+
+ John Smith
+
+ -
+
+ Jane Smith
+
+ -
+
+ Locations
+
+ -
+
+ Earth
+
+ -
+
+ Space
+
+ -
+
+ Mars
+
+ -
+
+ Archive
+
+ -
+
+ Scenes
+
+ -
+
+ Old File
+
+ -
+
+ Trash
+
+ -
+
+ Delete Me!
+
+
+
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx
index d78a4aa0..d5b2b2c7 100644
--- a/tests/lipsum/nwProject.nwx
+++ b/tests/lipsum/nwProject.nwx
@@ -1,20 +1,15 @@
-
-
+
+
Lorem Ipsum
Lorem Ipsum
lipsum.com
- 34
- 24
- 1893
False
en_GB
False
None
- 3109
- 738
7a992350f3eb6
None
@@ -45,7 +40,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index 973306cf..520883e5 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,20 +1,15 @@
-
-
+
+
New Project
New Novel
Jane Doe
- 2
- 1
- 0
True
None
False
None
- 10
- 3
None
None
@@ -42,7 +37,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 6aea2f22..6e2c26af 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,20 +1,15 @@
-
-
+
+
New Project
New Novel
Jane Doe
- 2
- 1
- 0
True
None
False
None
- 9
- 0
None
None
@@ -42,7 +37,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/coreTools_NewCustomA_nwProject.nwx b/tests/reference/coreTools_NewCustomA_nwProject.nwx
index 1b083ddc..e7775cd5 100644
--- a/tests/reference/coreTools_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomA_nwProject.nwx
@@ -1,21 +1,16 @@
-
-
+
+
Test Custom
Test Novel
Jane Doe
John Doh
- 1
- 0
- 0
True
None
False
None
- 0
- 0
None
None
@@ -43,7 +38,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/coreTools_NewCustomB_nwProject.nwx b/tests/reference/coreTools_NewCustomB_nwProject.nwx
index 637afa4f..6937cb0a 100644
--- a/tests/reference/coreTools_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomB_nwProject.nwx
@@ -1,21 +1,16 @@
-
-
+
+
Test Custom
Test Novel
Jane Doe
John Doh
- 1
- 0
- 0
True
None
False
None
- 0
- 0
None
None
@@ -43,7 +38,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/coreTools_NewMinimal_nwProject.nwx b/tests/reference/coreTools_NewMinimal_nwProject.nwx
index bed1b6bf..ba168a0e 100644
--- a/tests/reference/coreTools_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreTools_NewMinimal_nwProject.nwx
@@ -1,19 +1,14 @@
-
-
+
+
New Project
New Project
- 1
- 0
- 0
True
None
False
None
- 0
- 0
None
None
@@ -41,7 +36,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index 2c849f63..74bae460 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,20 +1,15 @@
-
-
+
+
New Project
New Novel
Jane Doe
- 4
- 2
- 4
True
None
True
None
- 136
- 27
000000000000f
None
@@ -42,7 +37,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index 84c353e8..b5fd4013 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,20 +1,15 @@
-
-
+
+
New Project
New Novel
Jane Doe
- 2
- 1
- 0
True
None
False
None
- 9
- 0
None
None
@@ -42,7 +37,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx
index b557dc19..b08e3511 100644
--- a/tests/reference/projectXML_ReadLegacy10.nwx
+++ b/tests/reference/projectXML_ReadLegacy10.nwx
@@ -1,21 +1,16 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 0
- 0
- 1000
True
None
True
None
- 0
- 0
None
None
@@ -50,7 +45,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx
index 84df9eef..c8519300 100644
--- a/tests/reference/projectXML_ReadLegacy11.nwx
+++ b/tests/reference/projectXML_ReadLegacy11.nwx
@@ -1,21 +1,16 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 5
- 10
- 1000
True
None
True
None
- 0
- 0
None
None
@@ -50,7 +45,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx
index e87015fe..c28d7e16 100644
--- a/tests/reference/projectXML_ReadLegacy12.nwx
+++ b/tests/reference/projectXML_ReadLegacy12.nwx
@@ -1,21 +1,16 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 5
- 10
- 1000
True
en_GB
True
en_GB
- 840
- 376
None
None
@@ -50,7 +45,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx
index a8be3ecd..8e448959 100644
--- a/tests/reference/projectXML_ReadLegacy13.nwx
+++ b/tests/reference/projectXML_ReadLegacy13.nwx
@@ -1,21 +1,16 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 5
- 10
- 1000
True
en_GB
True
en_GB
- 830
- 376
None
None
@@ -50,7 +45,7 @@
Main
-
+
-
Novel
diff --git a/tests/reference/projectXML_ReadLegacy14.json b/tests/reference/projectXML_ReadLegacy14.json
new file mode 100644
index 00000000..71afa3c9
--- /dev/null
+++ b/tests/reference/projectXML_ReadLegacy14.json
@@ -0,0 +1,677 @@
+[
+ {
+ "name": "Novel",
+ "itemAttr": {
+ "handle": "7031beac91f75",
+ "parent": null,
+ "root": "7031beac91f75",
+ "order": 0,
+ "type": "ROOT",
+ "class": "NOVEL",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sc24b8f",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "Title Page",
+ "itemAttr": {
+ "handle": "53b69b83cdafc",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 93,
+ "wordCount": 19,
+ "paraCount": 2,
+ "cursorPos": 119
+ },
+ "nameAttr": {
+ "status": "sc24b8f",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Page",
+ "itemAttr": {
+ "handle": "974e400180a99",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 251,
+ "wordCount": 50,
+ "paraCount": 2,
+ "cursorPos": 277
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Part One",
+ "itemAttr": {
+ "handle": "edca4be2fcaf8",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 2,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 26,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 36
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Chapter One",
+ "itemAttr": {
+ "handle": "6a2d6d5f4f401",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 3,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 95,
+ "wordCount": 18,
+ "paraCount": 1,
+ "cursorPos": 291
+ },
+ "nameAttr": {
+ "status": "sf24ce6",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Making a Scene",
+ "itemAttr": {
+ "handle": "636b6aa9b697b",
+ "parent": "6a2d6d5f4f401",
+ "root": "7031beac91f75",
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 2687,
+ "wordCount": 479,
+ "paraCount": 14,
+ "cursorPos": 67
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Another Scene",
+ "itemAttr": {
+ "handle": "bc0cbd2a407f3",
+ "parent": "6a2d6d5f4f401",
+ "root": "7031beac91f75",
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 548,
+ "wordCount": 108,
+ "paraCount": 3,
+ "cursorPos": 465
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Interlude",
+ "itemAttr": {
+ "handle": "ba8a28a246524",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 4,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 617,
+ "wordCount": 101,
+ "paraCount": 3,
+ "cursorPos": 310
+ },
+ "nameAttr": {
+ "status": "s78ea90",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "A Note on Structure",
+ "itemAttr": {
+ "handle": "96b68994dfa3d",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 5,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 1909,
+ "wordCount": 346,
+ "paraCount": 7,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf24ce6",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "Chapter Two",
+ "itemAttr": {
+ "handle": "88706ddc78b1b",
+ "parent": "7031beac91f75",
+ "root": "7031beac91f75",
+ "order": 6,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 139,
+ "wordCount": 28,
+ "paraCount": 1,
+ "cursorPos": 188
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "We Found John!",
+ "itemAttr": {
+ "handle": "ae7339df26ded",
+ "parent": "88706ddc78b1b",
+ "root": "7031beac91f75",
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 189,
+ "wordCount": 37,
+ "paraCount": 1,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Sequel",
+ "itemAttr": {
+ "handle": "e5e47ebf63b1c",
+ "parent": null,
+ "root": "e5e47ebf63b1c",
+ "order": 1,
+ "type": "ROOT",
+ "class": "NOVEL",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "Title Page",
+ "itemAttr": {
+ "handle": "bacb7059e3083",
+ "parent": "e5e47ebf63b1c",
+ "root": "e5e47ebf63b1c",
+ "order": 0,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 27,
+ "wordCount": 5,
+ "paraCount": 1,
+ "cursorPos": 100
+ },
+ "nameAttr": {
+ "status": "sc24b8f",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Chapter One",
+ "itemAttr": {
+ "handle": "a520879ca0b45",
+ "parent": "e5e47ebf63b1c",
+ "root": "e5e47ebf63b1c",
+ "order": 1,
+ "type": "FILE",
+ "class": "NOVEL",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 299,
+ "wordCount": 55,
+ "paraCount": 2,
+ "cursorPos": 104
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Characters",
+ "itemAttr": {
+ "handle": "f6622b4617424",
+ "parent": null,
+ "root": "f6622b4617424",
+ "order": 2,
+ "type": "ROOT",
+ "class": "CHARACTER",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "Main Characters",
+ "itemAttr": {
+ "handle": "f7e2d9f330615",
+ "parent": "f6622b4617424",
+ "root": "f6622b4617424",
+ "order": 0,
+ "type": "FOLDER",
+ "class": "CHARACTER",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "John Smith",
+ "itemAttr": {
+ "handle": "14298de4d9524",
+ "parent": "f7e2d9f330615",
+ "root": "f6622b4617424",
+ "order": 0,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 49,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 24
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "icfb3a5",
+ "active": true
+ }
+ },
+ {
+ "name": "Jane Smith",
+ "itemAttr": {
+ "handle": "bb2c23b3c42cc",
+ "parent": "f7e2d9f330615",
+ "root": "f6622b4617424",
+ "order": 1,
+ "type": "FILE",
+ "class": "CHARACTER",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 55,
+ "wordCount": 9,
+ "paraCount": 1,
+ "cursorPos": 25
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "i2d7a54",
+ "active": true
+ }
+ },
+ {
+ "name": "Locations",
+ "itemAttr": {
+ "handle": "15c4492bd5107",
+ "parent": null,
+ "root": "15c4492bd5107",
+ "order": 3,
+ "type": "ROOT",
+ "class": "WORLD",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "Earth",
+ "itemAttr": {
+ "handle": "b3e74dbc1f584",
+ "parent": "15c4492bd5107",
+ "root": "15c4492bd5107",
+ "order": 0,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 76,
+ "wordCount": 15,
+ "paraCount": 1,
+ "cursorPos": 20
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "i56be10",
+ "active": true
+ }
+ },
+ {
+ "name": "Space",
+ "itemAttr": {
+ "handle": "f1471bef9f2ae",
+ "parent": "15c4492bd5107",
+ "root": "15c4492bd5107",
+ "order": 1,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 115,
+ "wordCount": 24,
+ "paraCount": 1,
+ "cursorPos": 133
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "icfb3a5",
+ "active": true
+ }
+ },
+ {
+ "name": "Mars",
+ "itemAttr": {
+ "handle": "5eaea4e8cdee8",
+ "parent": "15c4492bd5107",
+ "root": "15c4492bd5107",
+ "order": 2,
+ "type": "FILE",
+ "class": "WORLD",
+ "layout": "NOTE"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 28,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 45
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "i2d7a54",
+ "active": true
+ }
+ },
+ {
+ "name": "Archive",
+ "itemAttr": {
+ "handle": "6827118336ac1",
+ "parent": null,
+ "root": "6827118336ac1",
+ "order": 4,
+ "type": "ROOT",
+ "class": "ARCHIVE",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "Scenes",
+ "itemAttr": {
+ "handle": "ae9bf3c3ea159",
+ "parent": "6827118336ac1",
+ "root": "6827118336ac1",
+ "order": 0,
+ "type": "FOLDER",
+ "class": "ARCHIVE",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "Old File",
+ "itemAttr": {
+ "handle": "8a5deb88c0e97",
+ "parent": "ae9bf3c3ea159",
+ "root": "6827118336ac1",
+ "order": 0,
+ "type": "FILE",
+ "class": "ARCHIVE",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 232,
+ "wordCount": 42,
+ "paraCount": 1,
+ "cursorPos": 239
+ },
+ "nameAttr": {
+ "status": "s90e6c9",
+ "import": "ia857f0",
+ "active": true
+ }
+ },
+ {
+ "name": "Trash",
+ "itemAttr": {
+ "handle": "98acd8c76c93a",
+ "parent": null,
+ "root": "98acd8c76c93a",
+ "order": 5,
+ "type": "ROOT",
+ "class": "TRASH",
+ "layout": "NO_LAYOUT"
+ },
+ "metaAttr": {
+ "expanded": true,
+ "heading": "H0",
+ "charCount": 0,
+ "wordCount": 0,
+ "paraCount": 0,
+ "cursorPos": 0
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": false
+ }
+ },
+ {
+ "name": "Delete Me!",
+ "itemAttr": {
+ "handle": "b8136a5a774a0",
+ "parent": "98acd8c76c93a",
+ "root": "98acd8c76c93a",
+ "order": 0,
+ "type": "FILE",
+ "class": "TRASH",
+ "layout": "DOCUMENT"
+ },
+ "metaAttr": {
+ "expanded": false,
+ "heading": "H0",
+ "charCount": 30,
+ "wordCount": 6,
+ "paraCount": 1,
+ "cursorPos": 36
+ },
+ "nameAttr": {
+ "status": "sf12341",
+ "import": "ia857f0",
+ "active": true
+ }
+ }
+]
diff --git a/tests/reference/projectXML_ReadLegacy14.nwx b/tests/reference/projectXML_ReadLegacy14.nwx
new file mode 100644
index 00000000..445817a3
--- /dev/null
+++ b/tests/reference/projectXML_ReadLegacy14.nwx
@@ -0,0 +1,158 @@
+
+
+
+ Sample Project
+ Sample Project
+ Jane Smith
+ Jay Doh
+
+
+ True
+ en_GB
+ True
+ en_GB
+
+ None
+ None
+ None
+ None
+
+
+ B
+ E
+ D
+
+
+ %title%
+ Chapter %chw%: %title%
+ %title%
+ Scene %ch%.%sc%: %title%
+
+
+
+ New
+ Notes
+ Started
+ 1st Draft
+ 2nd Draft
+ 3rd Draft
+ Finished
+
+
+ None
+ Minor
+ Major
+ Main
+
+
+
+
-
+
+ Novel
+
+ -
+
+ Title Page
+
+ -
+
+ Page
+
+ -
+
+ Part One
+
+ -
+
+ Chapter One
+
+ -
+
+ Making a Scene
+
+ -
+
+ Another Scene
+
+ -
+
+ Interlude
+
+ -
+
+ A Note on Structure
+
+ -
+
+ Chapter Two
+
+ -
+
+ We Found John!
+
+ -
+
+ Sequel
+
+ -
+
+ Title Page
+
+ -
+
+ Chapter One
+
+ -
+
+ Characters
+
+ -
+
+ Main Characters
+
+ -
+
+ John Smith
+
+ -
+
+ Jane Smith
+
+ -
+
+ Locations
+
+ -
+
+ Earth
+
+ -
+
+ Space
+
+ -
+
+ Mars
+
+ -
+
+ Archive
+
+ -
+
+ Scenes
+
+ -
+
+ Old File
+
+ -
+
+ Trash
+
+ -
+
+ Delete Me!
+
+
+
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index b3af7289..3bbc9e3a 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -42,10 +42,10 @@ class MockProject:
def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
"""Test reading the current XML file format.
"""
- refFile = tstPaths.filesDir / "nwProject-1.4.nwx"
+ refFile = tstPaths.filesDir / "nwProject-1.5.nwx"
tstFile = tstPaths.outDir / "ProjectXML_ReadCurrent.nwx"
- xmlFile = fncPath / "nwProject-1.4.nwx"
- bakFile = fncPath / "nwProject-1.4.bak"
+ xmlFile = fncPath / "nwProject-1.5.nwx"
+ bakFile = fncPath / "nwProject-1.5.bak"
outFile = fncPath / "nwProject.nwx"
xmlReader = ProjectXMLReader(xmlFile)
@@ -81,7 +81,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
# Check parsing of unkown sections
writeFile(xmlFile, (
- ""
+ ""
" "
" "
" "
@@ -129,7 +129,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlReader.read(data, content) is True
assert xmlReader.state == XMLReadState.PARSED_OK
assert xmlReader.xmlRoot == "novelWriterXML"
- assert xmlReader.xmlVersion == 0x0104
+ assert xmlReader.xmlVersion == 0x0105
assert xmlReader.appVersion == "2.0-rc1"
assert xmlReader.hexVersion == "0x020000c1"
@@ -809,3 +809,150 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
assert cmpFiles(testFile, compFile)
# END Test testCoreProjectXML_ReadLegacy13
+
+
+@pytest.mark.core
+def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
+ """Test reading the version 1.4 XML file format.
+ """
+ refFile = tstPaths.filesDir / "nwProject-1.4.nwx"
+ xmlFile = fncPath / "nwProject-1.4.nwx"
+ outFile = fncPath / "nwProject.nwx"
+ copyfile(refFile, xmlFile)
+
+ xmlReader = ProjectXMLReader(xmlFile)
+ assert xmlReader.state == XMLReadState.NO_ACTION
+
+ data = NWProjectData(MockProject())
+ content = []
+
+ assert xmlReader.read(data, content) is True
+ assert xmlReader.state == XMLReadState.WAS_LEGACY
+ assert xmlReader.xmlRoot == "novelWriterXML"
+ assert xmlReader.xmlVersion == 0x0104
+ assert xmlReader.appVersion == "2.0-rc1"
+ assert xmlReader.hexVersion == "0x020000c1"
+
+ # Check loaded data
+ assert data.name == "Sample Project"
+ assert data.title == "Sample Project"
+ assert data.authors == ["Jane Smith", "Jay Doh"]
+ assert data.saveCount == 5
+ assert data.autoCount == 10
+ assert data.editTime == 1000
+
+ assert data.doBackup is True
+ assert data.language == "en_GB"
+ assert data.spellCheck is True
+ assert data.spellLang == "en_GB"
+ assert data.initCounts == (954, 409)
+ assert data.currCounts == (954, 409)
+
+ assert data.getLastHandle("editor") is None # Dropped by conversion
+ assert data.getLastHandle("viewer") is None # Dropped by conversion
+ assert data.getLastHandle("novelTree") is None # Doesn't exist in 1.3
+ assert data.getLastHandle("outline") is None # Doesn't exist in 1.3
+
+ assert data.getTitleFormat("title") == "%title%"
+ assert data.getTitleFormat("chapter") == "Chapter %chw%: %title%"
+ assert data.getTitleFormat("unnumbered") == "%title%"
+ assert data.getTitleFormat("scene") == "Scene %ch%.%sc%: %title%"
+ assert data.getTitleFormat("section") == ""
+
+ assert data.itemStatus.name("sf12341") == "New"
+ assert data.itemStatus.name("sf24ce6") == "Notes"
+ assert data.itemStatus.name("sc24b8f") == "Started"
+ assert data.itemStatus.name("s90e6c9") == "1st Draft"
+ assert data.itemStatus.name("sd51c5b") == "2nd Draft"
+ assert data.itemStatus.name("s8ae72a") == "3rd Draft"
+ assert data.itemStatus.name("s78ea90") == "Finished"
+
+ assert data.itemImport.name("ia857f0") == "None"
+ assert data.itemImport.name("icfb3a5") == "Minor"
+ assert data.itemImport.name("i2d7a54") == "Major"
+ assert data.itemImport.name("i56be10") == "Main"
+
+ assert data.itemStatus.cols("sf12341") == (100, 100, 100)
+ assert data.itemStatus.cols("sf24ce6") == (200, 50, 0)
+ assert data.itemStatus.cols("sc24b8f") == (182, 60, 0)
+ assert data.itemStatus.cols("s90e6c9") == (193, 129, 0)
+ assert data.itemStatus.cols("sd51c5b") == (193, 129, 0)
+ assert data.itemStatus.cols("s8ae72a") == (193, 129, 0)
+ assert data.itemStatus.cols("s78ea90") == (58, 180, 58)
+
+ assert data.itemImport.cols("ia857f0") == (100, 100, 100)
+ assert data.itemImport.cols("icfb3a5") == (0, 122, 188)
+ assert data.itemImport.cols("i2d7a54") == (21, 0, 180)
+ assert data.itemImport.cols("i56be10") == (117, 0, 175)
+
+ assert data.itemStatus.count("sf12341") == 4
+ assert data.itemStatus.count("sf24ce6") == 2
+ assert data.itemStatus.count("sc24b8f") == 3
+ assert data.itemStatus.count("s90e6c9") == 7
+ assert data.itemStatus.count("sd51c5b") == 0
+ assert data.itemStatus.count("s8ae72a") == 0
+ assert data.itemStatus.count("s78ea90") == 1
+
+ assert data.itemImport.count("ia857f0") == 5
+ assert data.itemImport.count("icfb3a5") == 2
+ assert data.itemImport.count("i2d7a54") == 2
+ assert data.itemImport.count("i56be10") == 1
+
+ # Compare content
+ dumpFile = tstPaths.outDir / "projectXML_ReadLegacy14.json"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy14.json"
+ with open(dumpFile, mode="w", encoding="utf-8") as dump:
+ json.dump(content, dump, indent=2)
+ assert cmpFiles(dumpFile, compFile)
+
+ packedContent = []
+ mockProject = MockProject()
+ mockProject.__setattr__("data", data)
+ status = {}
+ for entry in content:
+ item = NWItem(mockProject)
+ item.unpack(entry)
+ status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
+ packedContent.append(item.pack())
+
+ assert status == {
+ "7031beac91f75": "Started",
+ "53b69b83cdafc": "Started",
+ "974e400180a99": "New",
+ "edca4be2fcaf8": "1st Draft",
+ "6a2d6d5f4f401": "Notes",
+ "636b6aa9b697b": "1st Draft",
+ "bc0cbd2a407f3": "1st Draft",
+ "ba8a28a246524": "Finished",
+ "96b68994dfa3d": "Notes",
+ "88706ddc78b1b": "1st Draft",
+ "ae7339df26ded": "1st Draft",
+ "e5e47ebf63b1c": "New",
+ "bacb7059e3083": "Started",
+ "a520879ca0b45": "1st Draft",
+ "f6622b4617424": "None",
+ "f7e2d9f330615": "None",
+ "14298de4d9524": "Minor",
+ "bb2c23b3c42cc": "Major",
+ "15c4492bd5107": "None",
+ "b3e74dbc1f584": "Main",
+ "f1471bef9f2ae": "Minor",
+ "5eaea4e8cdee8": "Major",
+ "6827118336ac1": "New",
+ "ae9bf3c3ea159": "New",
+ "8a5deb88c0e97": "1st Draft",
+ "98acd8c76c93a": "None",
+ "b8136a5a774a0": "None",
+ }
+
+ # Save the project again, which should produce an identical project xml
+ timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp())
+ xmlWriter = ProjectXMLWriter(fncPath)
+ data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
+ assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True
+ testFile = tstPaths.outDir / "projectXML_ReadLegacy14.nwx"
+ compFile = tstPaths.refDir / "projectXML_ReadLegacy14.nwx"
+ copyfile(outFile, testFile)
+ assert cmpFiles(testFile, compFile)
+
+# END Test testCoreProjectXML_ReadLegacy14
diff --git a/tests/tools.py b/tests/tools.py
index 7824268b..e101bcbb 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -25,7 +25,7 @@ import shutil
from PyQt5.QtWidgets import qApp
-XML_IGNORE = ("
Date: Sat, 5 Nov 2022 21:37:10 +0100
Subject: [PATCH 13/26] Make some minor improvements to the xml reader
---
novelwriter/core/projectxml.py | 120 ++++++++++++++++-----------------
1 file changed, 60 insertions(+), 60 deletions(-)
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index e005a3ec..e8dd1621 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -347,8 +347,8 @@ class ProjectXMLReader:
# Deprecated Nodes
if self._version < HEX_VERSION:
for xVal in xItem:
- if xVal.tag == "name":
- name["active"] = checkBool(xVal.attrib.get("exported"), name["active"])
+ if xVal.tag == "name" and "exported" in xVal.attrib:
+ name["active"] = checkBool(xVal.attrib.get("exported"), False)
projContent.append({
"name": itemName,
@@ -365,74 +365,74 @@ class ProjectXMLReader:
logger.debug("Parsing section (legacy format)")
# Create maps to look up name -> key for status and importance
- statusMap = {entry["name"]: key for key, entry in projData.itemStatus.items()}
- importMap = {entry["name"]: key for key, entry in projData.itemImport.items()}
+ statusMap = {entry.get("name"): key for key, entry in projData.itemStatus.items()}
+ importMap = {entry.get("name"): key for key, entry in projData.itemImport.items()}
for xItem in xSection:
+ if xItem.tag != "item":
+ logger.warning("Ignored item in XML", xItem.tag)
+ continue
+
item = {}
meta = {}
name = {}
itemName = ""
- if xItem.tag == "item":
- item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None)
- item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None)
- item["root"] = None # Value was added in 1.4
- item["order"] = checkInt(xItem.attrib.get("order", 0), 0)
- meta["heading"] = "H0" # Value was added in 1.4
+ item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None)
+ item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None)
+ item["root"] = None # Value was added in 1.4
+ item["order"] = checkInt(xItem.attrib.get("order", 0), 0)
+ meta["heading"] = "H0" # Value was added in 1.4
- tmpStatus = ""
- for xVal in xItem:
- if xVal.tag == "name":
- itemName = simplified(checkString(xVal.text, ""))
- elif xVal.tag == "status":
- tmpStatus = checkStringNone(xVal.text, None)
- elif xVal.tag == "type":
- item["type"] = checkString(xVal.text, "")
- elif xVal.tag == "class":
- item["class"] = checkString(xVal.text, "")
- elif xVal.tag == "layout":
- item["layout"] = checkString(xVal.text, "")
- elif xVal.tag == "expanded":
- meta["expanded"] = checkBool(xVal.text, False)
- elif xVal.tag == "exported": # Renamed to active in 1.4
- name["active"] = checkBool(xVal.text, False)
- elif xVal.tag == "charCount":
- meta["charCount"] = checkInt(xVal.text, 0)
- elif xVal.tag == "wordCount":
- meta["wordCount"] = checkInt(xVal.text, 0)
- elif xVal.tag == "paraCount":
- meta["paraCount"] = checkInt(xVal.text, 0)
- elif xVal.tag == "cursorPos":
- meta["cursorPos"] = checkInt(xVal.text, 0)
- else:
- logger.warning("Ignored in XML", xVal.tag)
-
- # Status was split into separate status/import with a key in 1.4
- if item.get("class", "") in ("NOVEL", "ARCHIVE"):
- name["status"] = statusMap.get(tmpStatus, None)
+ tmpStatus = ""
+ for xVal in xItem:
+ if xVal.tag == "name":
+ itemName = simplified(checkString(xVal.text, ""))
+ elif xVal.tag == "status":
+ tmpStatus = checkStringNone(xVal.text, None)
+ elif xVal.tag == "type":
+ item["type"] = checkString(xVal.text, "")
+ elif xVal.tag == "class":
+ item["class"] = checkString(xVal.text, "")
+ elif xVal.tag == "layout":
+ item["layout"] = checkString(xVal.text, "")
+ elif xVal.tag == "expanded":
+ meta["expanded"] = checkBool(xVal.text, False)
+ elif xVal.tag == "exported": # Renamed to active in 1.5
+ name["active"] = checkBool(xVal.text, False)
+ elif xVal.tag == "charCount":
+ meta["charCount"] = checkInt(xVal.text, 0)
+ elif xVal.tag == "wordCount":
+ meta["wordCount"] = checkInt(xVal.text, 0)
+ elif xVal.tag == "paraCount":
+ meta["paraCount"] = checkInt(xVal.text, 0)
+ elif xVal.tag == "cursorPos":
+ meta["cursorPos"] = checkInt(xVal.text, 0)
else:
- name["import"] = importMap.get(tmpStatus, None)
-
- # A number of layouts were removed in 1.3
- if item.get("layout", "") in (
- "TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"
- ):
- item["layout"] = "DOCUMENT"
-
- # The trast type was removed in 1.4
- if item.get("type", "") == "TRASH":
- item["type"] = "ROOT"
-
- projContent.append({
- "name": itemName,
- "itemAttr": item,
- "metaAttr": meta,
- "nameAttr": name,
- })
+ logger.warning("Ignored in XML", xVal.tag)
+ # Status was split into separate status/import with a key in 1.4
+ if item.get("class", "") in ("NOVEL", "ARCHIVE"):
+ name["status"] = statusMap.get(tmpStatus, None)
else:
- logger.warning("Ignored in XML", xItem.tag)
+ name["import"] = importMap.get(tmpStatus, None)
+
+ # A number of layouts were removed in 1.3
+ if item.get("layout", "") in (
+ "TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"
+ ):
+ item["layout"] = "DOCUMENT"
+
+ # The trash type was removed in 1.4
+ if item.get("type", "") == "TRASH":
+ item["type"] = "ROOT"
+
+ projContent.append({
+ "name": itemName,
+ "itemAttr": item,
+ "metaAttr": meta,
+ "nameAttr": name,
+ })
return
@@ -463,7 +463,7 @@ class ProjectXMLReader:
"""Parse a dictionary stored with key as the tag and the value
as the text porperty.
"""
- return {n.tag: checkString(n.text, "") for n in xItem}
+ return {xNode.tag: checkString(xNode.text, "") for xNode in xItem}
# END Class ProjectXMLReader
From 82b34d9d2cdba03fbcbce69290073d03fd5df32f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 5 Nov 2022 22:14:33 +0100
Subject: [PATCH 14/26] Move lock file code to storage class
---
novelwriter/core/project.py | 78 +++-------------------------
novelwriter/core/projectxml.py | 2 +-
novelwriter/core/storage.py | 65 +++++++++++++++++++++--
tests/test_core/test_core_project.py | 66 ++---------------------
4 files changed, 73 insertions(+), 138 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 7892d16e..9d0c78ca 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -286,9 +286,9 @@ class NWProject(QObject):
# ============
if overrideLock:
- self._clearLockFile()
+ self._storage.clearLockFile()
- lockStatus = self._readLockFile()
+ lockStatus = self._storage.readLockFile()
if len(lockStatus) > 0:
if lockStatus[0] == "ERROR":
logger.warning("Failed to check lock file")
@@ -310,7 +310,6 @@ class NWProject(QObject):
self._data = NWProjectData(self)
projContent = []
-
xmlParsed = xmlReader.read(self._data, projContent)
appVersion = xmlReader.appVersion or self.tr("Unknown")
@@ -397,7 +396,7 @@ class NWProject(QObject):
self._projOpened = time()
self._projAltered = False
- self._writeLockFile()
+ self._storage.writeLockFile()
self.setProjectChanged(False)
self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self._data.name))
@@ -458,7 +457,7 @@ class NWProject(QObject):
)
self.mainConf.saveRecentCache()
- self._writeLockFile()
+ self._storage.writeLockFile()
self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name))
self.setProjectChanged(False)
@@ -471,7 +470,7 @@ class NWProject(QObject):
self._options.saveSettings()
self._tree.writeToCFile()
self._appendSessionStats(idleTime)
- self._clearLockFile()
+ self._storage.clearLockFile()
self.clearProject()
self.lockedBy = None
return True
@@ -565,9 +564,9 @@ class NWProject(QObject):
baseName = os.path.join(baseDir, archName)
try:
- self._clearLockFile()
+ self._storage.clearLockFile()
shutil.make_archive(baseName, "zip", self.projPath, ".")
- self._writeLockFile()
+ self._storage.writeLockFile()
logger.info("Backup written to: %s", archName)
if doNotify:
self.mainGui.makeAlert(self.tr(
@@ -819,69 +818,6 @@ class NWProject(QObject):
return True
- def _readLockFile(self):
- """Reads the lock file in the project folder.
- """
- if self.projPath is None:
- return ["ERROR"]
-
- lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK)
- if not os.path.isfile(lockFile):
- return []
-
- theLines = []
- try:
- with open(lockFile, mode="r", encoding="utf-8") as inFile:
- theData = inFile.read()
- theLines = theData.splitlines()
- if len(theLines) != 4:
- return ["ERROR"]
-
- except Exception:
- logger.error("Failed to read project lockfile")
- logException()
- return ["ERROR"]
-
- return theLines
-
- def _writeLockFile(self):
- """Writes a lock file to the project folder.
- """
- if self.projPath is None:
- return False
-
- lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK)
- try:
- with open(lockFile, mode="w+", encoding="utf-8") as outFile:
- outFile.write("%s\n" % self.mainConf.hostName)
- outFile.write("%s\n" % self.mainConf.osType)
- outFile.write("%s\n" % self.mainConf.kernelVer)
- outFile.write("%d\n" % time())
-
- except Exception:
- logger.error("Failed to write project lockfile")
- logException()
- return False
-
- return True
-
- def _clearLockFile(self):
- """Remove the lock file, if it exists.
- """
- if self.projPath is None:
- return False
-
- lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK)
- if os.path.isfile(lockFile):
- try:
- os.unlink(lockFile)
- except Exception:
- logger.error("Failed to remove project lockfile")
- logException()
- return False
-
- return True
-
def _checkFolder(self, thePath):
"""Check if a folder exists, and if it doesn't, create it.
"""
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index e8dd1621..618fb30a 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -276,7 +276,7 @@ class ProjectXMLReader:
projData.setSpellLang(xItem.text)
elif xItem.tag == "status":
self._parseStatusImport(xItem, projData.itemStatus)
- elif xItem.tag in ("import", "importance"):
+ elif xItem.tag == "importance":
self._parseStatusImport(xItem, projData.itemImport)
elif xItem.tag == "lastHandle":
projData.setLastHandle(self._parseDictKeyText(xItem))
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index dfa4a2a2..0813f7cc 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -24,11 +24,14 @@ along with this program. If not, see .
"""
import logging
+import novelwriter
+from time import time
from pathlib import Path
from novelwriter.constants import nwFiles
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
+from novelwriter.error import logException
logger = logging.getLogger(__name__)
@@ -41,10 +44,12 @@ class NWStorage:
def __init__(self, theProject):
+ self.mainConf = novelwriter.CONFIG
self.theProject = theProject
self._storagePath = None
self._runtimePath = None
+ self._lockFilePath = None
self._openMode = self.MODE_INACTIVE
return
@@ -104,6 +109,7 @@ class NWStorage:
self._storagePath = inPath
self._runtimePath = inPath
+ self._lockFilePath = inPath / nwFiles.PROJ_LOCK
self._openMode = self.MODE_INPLACE
if self._prepareStorage(checkLegacy=True) is False:
@@ -162,6 +168,62 @@ class NWStorage:
def getMetaFile(self, kind):
pass
+ def readLockFile(self):
+ """Read the project lock file.
+ """
+ if self._lockFilePath is None:
+ return ["ERROR"]
+
+ if not self._lockFilePath.exists():
+ return []
+
+ try:
+ lines = self._lockFilePath.read_text(encoding="utf-8").split(";")
+ except Exception:
+ logger.error("Failed to read project lockfile")
+ logException()
+ return ["ERROR"]
+
+ if len(lines) != 4:
+ return ["ERROR"]
+
+ return lines
+
+ def writeLockFile(self):
+ """Write the project lock file.
+ """
+ if self._lockFilePath is None:
+ return False
+
+ data = [
+ self.mainConf.hostName, self.mainConf.osType,
+ self.mainConf.kernelVer, str(int(time()))
+ ]
+ try:
+ self._lockFilePath.write_text(";".join(data), encoding="utf-8")
+ except Exception:
+ logger.error("Failed to write project lockfile")
+ logException()
+ return False
+
+ return True
+
+ def clearLockFile(self):
+ """Remove the lock file, if it exists.
+ """
+ if self._lockFilePath is None:
+ return False
+
+ if self._lockFilePath.exists():
+ try:
+ self._lockFilePath.unlink()
+ except Exception:
+ logger.error("Failed to remove project lockfile")
+ logException()
+ return False
+
+ return True
+
##
# Internal Functions
##
@@ -169,9 +231,6 @@ class NWStorage:
def _zipIt(self, target):
pass
- def _readLockFile(self):
- pass
-
def _writeLockFile(self):
pass
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 118c0cb7..752e216b 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -196,12 +196,12 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
# Fail on lock file
theProject.setProjectPath(fncDir)
- assert theProject._writeLockFile()
+ assert theProject._storage.writeLockFile()
assert theProject.openProject(fncDir) is False
# Fail to read lockfile (which still opens the project)
with monkeypatch.context() as mp:
- mp.setattr("builtins.open", causeOSError)
+ mp.setattr("novelwriter.core.storage.NWStorage.readLockFile", lambda *a: ["ERROR"])
caplog.clear()
assert theProject.openProject(fncDir) is True
assert "Failed to check lock file" in caplog.text
@@ -209,7 +209,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
# Force open with lockfile
theProject.setProjectPath(fncDir)
- assert theProject._writeLockFile()
+ assert theProject._storage.writeLockFile()
assert theProject.openProject(fncDir, overrideLock=True) is True
assert theProject.closeProject()
@@ -286,66 +286,6 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
# END Test testCoreProject_Save
-@pytest.mark.core
-def testCoreProject_LockFile(monkeypatch, fncDir, mockGUI):
- """Test lock file functions for the project folder.
- """
- theProject = NWProject(mockGUI)
-
- lockFile = os.path.join(fncDir, nwFiles.PROJ_LOCK)
-
- # No project
- assert theProject._writeLockFile() is False
- assert theProject._readLockFile() == ["ERROR"]
- assert theProject._clearLockFile() is False
-
- theProject.projPath = fncDir
- theProject.mainConf.hostName = "TestHost"
- theProject.mainConf.osType = "TestOS"
- theProject.mainConf.kernelVer = "1.0"
-
- # Block open
- with monkeypatch.context() as mp:
- mp.setattr("builtins.open", causeOSError)
- assert theProject._writeLockFile() is False
-
- # Write lock file
- with monkeypatch.context() as mp:
- mp.setattr("novelwriter.core.project.time", lambda: 123.4)
- assert theProject._writeLockFile() is True
- assert readFile(lockFile) == "TestHost\nTestOS\n1.0\n123\n"
-
- # Block open
- with monkeypatch.context() as mp:
- mp.setattr("builtins.open", causeOSError)
- assert theProject._readLockFile() == ["ERROR"]
-
- # Read lock file
- assert theProject._readLockFile() == ["TestHost", "TestOS", "1.0", "123"]
-
- # Block unlink
- with monkeypatch.context() as mp:
- mp.setattr("os.unlink", causeOSError)
- assert os.path.isfile(lockFile)
- assert theProject._clearLockFile() is False
- assert os.path.isfile(lockFile)
-
- # Clear file
- assert os.path.isfile(lockFile)
- assert theProject._clearLockFile() is True
- assert not os.path.isfile(lockFile)
-
- # Read again, no file
- assert theProject._readLockFile() == []
-
- # Read an invalid lock file
- writeFile(lockFile, "A\nB")
- assert theProject._readLockFile() == ["ERROR"]
- assert theProject._clearLockFile() is True
-
-# END Test testCoreProject_LockFile
-
-
@pytest.mark.core
def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
"""Test helper functions for the project folder.
From 21ab4e58f9f30982ace2744d0550c50dd564a119 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 5 Nov 2022 23:29:25 +0100
Subject: [PATCH 15/26] Remove project meta attribute from project class
---
novelwriter/core/coretools.py | 5 +-
novelwriter/core/index.py | 14 +++--
novelwriter/core/options.py | 14 +++--
novelwriter/core/project.py | 53 ++----------------
novelwriter/core/storage.py | 27 +++++++---
novelwriter/dialogs/wordlist.py | 24 +++++----
novelwriter/tools/writingstats.py | 5 +-
tests/test_core/test_core_options.py | 46 ++++++++++------
tests/test_core/test_core_project.py | 80 +++++++++-------------------
tests/test_gui/test_gui_guimain.py | 4 --
tests/tools.py | 4 +-
11 files changed, 117 insertions(+), 159 deletions(-)
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index 1ff73d2e..f617817d 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -305,11 +305,10 @@ class ProjectBuilder:
return False
project = NWProject(self.mainGui)
- if not project.setProjectPath(projPath, newProject=True):
+ if not project.storage.openProjectInPlace(projPath, newProject=True):
return False
- if not project.storage.openProjectInPlace(projPath):
- return False
+ project.projPath = projPath
lblNewProject = self.tr("New Project")
lblNewChapter = self.tr("New Chapter")
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index ce1a1278..e7c50671 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -26,11 +26,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import json
import logging
from time import time
+from pathlib import Path
from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException
@@ -141,12 +141,15 @@ class NWIndex:
def loadIndex(self):
"""Load index from last session from the project meta folder.
"""
+ indexFile = self.theProject.storage.getMetaFile(nwFiles.INDEX_FILE)
+ if not isinstance(indexFile, Path):
+ return False
+
theData = {}
- indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time()
self._indexBroken = False
- if os.path.isfile(indexFile):
+ if indexFile.exists():
logger.debug("Loading index file")
try:
with open(indexFile, mode="r", encoding="utf-8") as inFile:
@@ -184,8 +187,11 @@ class NWIndex:
"""Save the current index as a json file in the project meta
data folder.
"""
+ indexFile = self.theProject.storage.getMetaFile(nwFiles.INDEX_FILE)
+ if not isinstance(indexFile, Path):
+ return False
+
logger.debug("Saving index file")
- indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time()
try:
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index 9d840e0f..cbf3fe14 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -24,11 +24,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import json
import logging
from enum import Enum
+from pathlib import Path
from novelwriter.error import logException
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
@@ -77,13 +77,12 @@ class OptionState:
def loadSettings(self):
"""Load the options dictionary from the project settings file.
"""
- if self.theProject.projMeta is None:
+ stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
+ if not isinstance(stateFile, Path):
return False
- stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
theState = {}
-
- if os.path.isfile(stateFile):
+ if stateFile.exists():
logger.debug("Loading GUI options file")
try:
with open(stateFile, mode="r", encoding="utf-8") as inFile:
@@ -106,12 +105,11 @@ class OptionState:
def saveSettings(self):
"""Save the options dictionary to the project settings file.
"""
- if self.theProject.projMeta is None:
+ stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
+ if not isinstance(stateFile, Path):
return False
- stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
logger.debug("Saving GUI options file")
-
try:
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
json.dump(self._theState, outFile, indent=2)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 9d0c78ca..891d8dde 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -27,6 +27,7 @@ from __future__ import annotations
import os
import json
+from pathlib import Path
import shutil
import logging
import novelwriter
@@ -84,7 +85,6 @@ class NWProject(QObject):
# Class Settings
self.projPath = None # The full path to where the currently open project is saved
- self.projMeta = None # The full path to the project's meta data folder
self.projCache = None # The full path to the project's cache folder
self.projContent = None # The full path to the project's content folder
self.projDict = None # The spell check dictionary
@@ -254,7 +254,6 @@ class NWProject(QObject):
# Project Settings
self.projPath = None
- self.projMeta = None
self.projCache = None
self.projContent = None
self.projDict = None
@@ -276,11 +275,10 @@ class NWProject(QObject):
self.projPath = str(self._storage.runtimePath)
self.projContent = str(self._storage.contentPath)
self.projCache = str(self._storage.cachePath)
- self.projMeta = str(self._storage.metaPath)
logger.info("Opening project: %s", self.projPath)
- self.projDict = os.path.join(self.projMeta, nwFiles.PROJ_DICT)
+ self.projDict = str(self._storage.getMetaFile(nwFiles.PROJ_DICT))
# Project Lock
# ============
@@ -421,8 +419,6 @@ class NWProject(QObject):
return False
saveTime = time()
- if not self.ensureFolderStructure():
- return False
logger.info("Saving project: %s", self.projPath)
@@ -482,7 +478,6 @@ class NWProject(QObject):
if self.projPath is None or self.projPath == "":
return False
- self.projMeta = os.path.join(self.projPath, "meta")
self.projCache = os.path.join(self.projPath, "cache")
self.projContent = os.path.join(self.projPath, "content")
@@ -490,8 +485,6 @@ class NWProject(QObject):
# Don't make a mess in the user's home folder
return False
- if not self._checkFolder(self.projMeta):
- return False
if not self._checkFolder(self.projCache):
return False
if not self._checkFolder(self.projContent):
@@ -589,41 +582,6 @@ class NWProject(QObject):
# Setters
##
- def setProjectPath(self, projPath, newProject=False):
- """Set the project storage path, and also expand ~ to the user
- directory using the path library.
- """
- if projPath is None or projPath == "":
- self.projPath = None
- else:
- if projPath.startswith("~"):
- projPath = os.path.expanduser(projPath)
- self.projPath = os.path.abspath(projPath)
-
- if newProject:
- if not os.path.isdir(projPath):
- try:
- os.mkdir(projPath)
- logger.debug("Created folder: %s", projPath)
- except Exception as exc:
- self.mainGui.makeAlert(self.tr(
- "Could not create new project folder."
- ), nwAlert.ERROR, exception=exc)
- return False
-
- if os.path.isdir(projPath):
- if os.listdir(self.projPath):
- self.mainGui.makeAlert(self.tr(
- "New project folder is not empty. "
- "Each project requires a dedicated project folder."
- ), nwAlert.ERROR)
- return False
-
- self.ensureFolderStructure()
- self.setProjectChanged(True)
-
- return True
-
def setProjectLang(self, theLang):
"""Set the project-specific language.
"""
@@ -934,12 +892,10 @@ class NWProject(QObject):
def _appendSessionStats(self, idleTime):
"""Append session statistics to the sessions log file.
"""
- if not self.ensureFolderStructure():
+ sessionFile = self._storage.getMetaFile(nwFiles.SESS_STATS)
+ if not isinstance(sessionFile, Path):
return False
- sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
- isFile = os.path.isfile(sessionFile)
-
nowTime = time()
iNovel, iNotes = self._data.initCounts
cNovel, cNotes = self._data.currCounts
@@ -953,6 +909,7 @@ class NWProject(QObject):
return False
try:
+ isFile = sessionFile.exists() # We must save the state before we open
with open(sessionFile, mode="a+", encoding="utf-8") as outFile:
if not isFile:
# It's a new file, so add a header
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index 0813f7cc..485af8f0 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -97,7 +97,7 @@ class NWStorage:
"""
return self._runtimePath is not None
- def openProjectInPlace(self, path):
+ def openProjectInPlace(self, path, newProject=False):
"""Open a novelWriter project in-place. That is, it is opened
directly from a project folder.
"""
@@ -112,7 +112,7 @@ class NWStorage:
self._lockFilePath = inPath / nwFiles.PROJ_LOCK
self._openMode = self.MODE_INPLACE
- if self._prepareStorage(checkLegacy=True) is False:
+ if not self._prepareStorage(checkLegacy=True, newProject=newProject):
self.clear()
return False
@@ -142,7 +142,7 @@ class NWStorage:
##
def getXmlReader(self):
- """
+ """Return a properly configured ProjectXMLReader instance.
"""
if self._runtimePath is None:
return None
@@ -153,7 +153,7 @@ class NWStorage:
return xmlReader
def getXmlWriter(self):
- """
+ """Return a properly configured ProjectXMLWriter instance.
"""
if self._runtimePath is None:
return None
@@ -165,8 +165,12 @@ class NWStorage:
def getDocument(self, tHandle):
pass
- def getMetaFile(self, kind):
- pass
+ def getMetaFile(self, fileName):
+ """Return the path to a file in the project meta folder.
+ """
+ if self._runtimePath is not None:
+ return self._runtimePath / "meta" / fileName
+ return None
def readLockFile(self):
"""Read the project lock file.
@@ -234,7 +238,7 @@ class NWStorage:
def _writeLockFile(self):
pass
- def _prepareStorage(self, checkLegacy=True):
+ def _prepareStorage(self, checkLegacy=True, newProject=False):
"""Prepare the storage area for the project.
"""
path = self._runtimePath
@@ -248,6 +252,15 @@ class NWStorage:
self.clear()
return False
+ if newProject:
+ # If it's a new project, we check that there is no existing
+ # project in the selected path.
+ projFile = path / nwFiles.PROJ_FILE
+ if projFile.exists():
+ logger.error("A project already exists in this path")
+ self.clear()
+ return False
+
# The folder is not required to exist, as it could be a new
# project, so we make sure it does. Then we add subfolders.
try:
diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py
index 01a8afff..baabccdb 100644
--- a/novelwriter/dialogs/wordlist.py
+++ b/novelwriter/dialogs/wordlist.py
@@ -23,10 +23,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import logging
import novelwriter
+from pathlib import Path
+
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
@@ -150,9 +151,11 @@ class GuiWordList(QDialog):
"""
self._saveGuiSettings()
- dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
- tmpFile = dctFile + "~"
+ dctFile = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
+ if not isinstance(dctFile, Path):
+ return False
+ tmpFile = dctFile.with_suffix(".tmp")
try:
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
for i in range(self.listBox.count()):
@@ -160,15 +163,16 @@ class GuiWordList(QDialog):
if item is not None:
outFile.write(item.text() + "\n")
+ if dctFile.exists():
+ dctFile.unlink()
+ tmpFile.rename(dctFile)
+
except Exception:
logger.error("Could not save new word list")
logException()
self.reject()
return False
- if os.path.isfile(dctFile):
- os.unlink(dctFile)
- os.rename(tmpFile, dctFile)
self.accept()
return True
@@ -187,10 +191,12 @@ class GuiWordList(QDialog):
def _loadWordList(self):
"""Load the project's word list, if it exists.
"""
- self.listBox.clear()
+ wordList = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
+ if not isinstance(wordList, Path):
+ return False
- wordList = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
- if not os.path.isfile(wordList):
+ self.listBox.clear()
+ if not wordList.exists():
logger.debug("No project dictionary file found")
return False
diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py
index 2a579837..7015c992 100644
--- a/novelwriter/tools/writingstats.py
+++ b/novelwriter/tools/writingstats.py
@@ -28,6 +28,7 @@ import json
import logging
import novelwriter
+from pathlib import Path
from datetime import datetime
from PyQt5.QtGui import QPixmap, QCursor
@@ -439,8 +440,8 @@ class GuiWritingStats(QDialog):
ttTime = 0
ttIdle = 0
- logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
- if not os.path.isfile(logFile):
+ logFile = self.theProject.storage.getMetaFile(nwFiles.SESS_STATS)
+ if not isinstance(logFile, Path) or not logFile.exists():
logger.info("This project has no writing stats logfile")
return False
diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py
index 0535ca71..8d1b74fb 100644
--- a/tests/test_core/test_core_options.py
+++ b/tests/test_core/test_core_options.py
@@ -19,28 +19,30 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import json
import pytest
from mock import causeOSError
-from tools import writeFile
+from novelwriter.constants import nwFiles
from novelwriter.core.options import OptionState
from novelwriter.core.project import NWProject
-from novelwriter.constants import nwFiles
+from novelwriter.gui.noveltree import NovelTreeColumn
@pytest.mark.core
-def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
+def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
"""Test loading and saving from the OptionState class.
"""
theProject = NWProject(mockGUI)
theOpts = OptionState(theProject)
+ metaDir = fncPath / "meta"
+ metaDir.mkdir()
+
# Write a test file
- optFile = os.path.join(tmpDir, nwFiles.OPTS_FILE)
- writeFile(optFile, json.dumps({
+ optFile = metaDir / nwFiles.OPTS_FILE
+ optFile.write_text(json.dumps({
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
@@ -52,22 +54,22 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
"MockGroup": {
"mockItem": None,
},
- }))
+ }), encoding="utf-8")
# Load and save with no path set
- theProject.projMeta = None
- assert not theOpts.loadSettings()
- assert not theOpts.saveSettings()
+ theProject.storage._runtimePath = None
+ assert theOpts.loadSettings() is False
+ assert theOpts.saveSettings() is False
# Set path
- theProject.projMeta = tmpDir
- assert theProject.projMeta == tmpDir
+ theProject.storage._runtimePath = fncPath
+ assert theProject.storage.getMetaFile(nwFiles.OPTS_FILE) == optFile
# Cause open() to fail
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
- assert not theOpts.loadSettings()
- assert not theOpts.saveSettings()
+ assert theOpts.loadSettings() is False
+ assert theOpts.saveSettings() is False
# Load proper
assert theOpts.loadSettings()
@@ -108,9 +110,11 @@ def testCoreOptions_SetGet(mockGUI):
theProject = NWProject(mockGUI)
theOpts = OptionState(theProject)
+ nwColHidden = NovelTreeColumn.HIDDEN
+
# Set invalid values
- assert not theOpts.setValue("MockGroup", "mockItem", None)
- assert not theOpts.setValue("GuiBuildNovel", "mockItem", None)
+ assert theOpts.setValue("MockGroup", "mockItem", None) is False
+ assert theOpts.setValue("GuiBuildNovel", "mockItem", None) is False
# Set valid value
assert theOpts.setValue("GuiBuildNovel", "winWidth", 100)
@@ -120,6 +124,7 @@ def testCoreOptions_SetGet(mockGUI):
assert theOpts.setValue("GuiBuildNovel", "winHeight", 12.34)
assert theOpts.setValue("GuiBuildNovel", "addNovel", True)
assert theOpts.setValue("GuiBuildNovel", "textFont", "Cantarell")
+ assert theOpts.setValue("GuiNovelView", "lastCol", nwColHidden)
# Generic get, doesn't check type
assert theOpts.getValue("GuiBuildNovel", "winWidth", None) == 100
@@ -139,5 +144,14 @@ def testCoreOptions_SetGet(mockGUI):
assert theOpts.getFloat("GuiBuildNovel", "mockItem", None) is None
assert theOpts.getBool("GuiBuildNovel", "addNovel", None) is True
assert theOpts.getBool("GuiBuildNovel", "mockItem", None) is None
+ assert theOpts.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, None) == nwColHidden
+
+ # Get from non-existent groups
+ assert theOpts.getValue("SomeGroup", "mockItem", None) is None
+ assert theOpts.getString("SomeGroup", "mockItem", None) is None
+ assert theOpts.getInt("SomeGroup", "mockItem", None) is None
+ assert theOpts.getFloat("SomeGroup", "mockItem", None) is None
+ assert theOpts.getBool("SomeGroup", "mockItem", None) is None
+ assert theOpts.getEnum("SomeGroup", "mockItem", NovelTreeColumn, None) is None
# END Test testCoreOptions_SetGet
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 752e216b..4404fdc3 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -23,11 +23,13 @@ import os
import shutil
import pytest
+from time import time
from shutil import copyfile
+from pathlib import Path
from zipfile import ZipFile
from mock import causeOSError
-from tools import C, cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE
+from tools import C, cmpFiles, writeFile, buildTestProject, XML_IGNORE
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.common import formatTimeStamp
@@ -52,11 +54,6 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
mockRnd.reset()
buildTestProject(theProject, fncDir)
- assert theProject.setProjectPath(fncDir) is True
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
- assert theProject.openProject(projFile) is True
-
assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000010"
assert theProject.newRoot(nwItemClass.PLOT) == "0000000000011"
assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000012"
@@ -108,11 +105,6 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI,
mockRnd.reset()
buildTestProject(theProject, fncDir)
- assert theProject.setProjectPath(fncDir) is True
- assert theProject.saveProject() is True
- assert theProject.closeProject() is True
- assert theProject.openProject(projFile) is True
-
# Invalid call
assert theProject.newFolder("New Folder", "1234567890abc") is None
assert theProject.newFile("New File", "1234567890abc") is None
@@ -195,7 +187,6 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
assert theProject.openProject(fncDir) is False
# Fail on lock file
- theProject.setProjectPath(fncDir)
assert theProject._storage.writeLockFile()
assert theProject.openProject(fncDir) is False
@@ -208,7 +199,6 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
assert theProject.closeProject()
# Force open with lockfile
- theProject.setProjectPath(fncDir)
assert theProject._storage.writeLockFile()
assert theProject.openProject(fncDir, overrideLock=True) is True
assert theProject.closeProject()
@@ -267,12 +257,6 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
mockRnd.reset()
buildTestProject(theProject, fncDir)
- # Fail on folder structure check
- with monkeypatch.context() as mp:
- mp.setattr("os.mkdir", causeOSError)
- shutil.rmtree(os.path.join(fncDir, "meta"))
- assert theProject.saveProject() is False
-
# Fail writing
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLWriter, "write", lambda *a: False)
@@ -303,12 +287,6 @@ def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
mp.setattr("os.path.expanduser", lambda *a, **k: fncDir)
assert theProject.ensureFolderStructure() is False
- # Create a file to block meta folder
- metaDir = os.path.join(fncDir, "meta")
- writeFile(metaDir, "stuff")
- assert theProject.ensureFolderStructure() is False
- os.unlink(metaDir)
-
# Create a file to block cache folder
cacheDir = os.path.join(fncDir, "cache")
writeFile(cacheDir, "stuff")
@@ -323,7 +301,7 @@ def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
# Now, do it right
assert theProject.ensureFolderStructure() is True
- assert os.path.isdir(metaDir)
+ # assert os.path.isdir(metaDir)
assert os.path.isdir(cacheDir)
assert os.path.isdir(contentDir)
@@ -506,32 +484,12 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd):
@pytest.mark.core
-def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd):
+def testCoreProject_Methods(monkeypatch, mockGUI, fncDir, mockRnd):
"""Test other project class methods and functions.
"""
theProject = NWProject(mockGUI)
buildTestProject(theProject, fncDir)
- # Setting project path
- assert theProject.setProjectPath(None)
- assert theProject.projPath is None
- assert theProject.setProjectPath("")
- assert theProject.projPath is None
- assert theProject.setProjectPath("~")
- assert theProject.projPath == os.path.expanduser("~")
-
- # Create a new folder and populate it
- projPath = os.path.join(fncDir, "mock1")
- assert theProject.setProjectPath(projPath, newProject=True)
-
- # Make os.mkdir fail
- monkeypatch.setattr("os.mkdir", causeOSError)
- projPath = os.path.join(fncDir, "mock2")
- assert not theProject.setProjectPath(projPath, newProject=True)
-
- # Set back
- assert theProject.setProjectPath(fncDir)
-
# Project Name
theProject.data.setName(" A Name ")
assert theProject.data.name == "A Name"
@@ -639,29 +597,39 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd):
assert theProject.tree.handles() == oldOrder
# Session stats
- theProject._data._initCounts = [50, 50]
- theProject._data._currCounts = [100, 100]
+ theProject.data.setInitCounts(50, 50)
+ theProject.data.setCurrCounts(100, 100)
+
+ # No path for writing
with monkeypatch.context() as mp:
- mp.setattr("os.path.isdir", lambda *a, **k: False)
- assert not theProject._appendSessionStats(idleTime=0)
+ mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
+ assert theProject._appendSessionStats(idleTime=0) is False
# Block open
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
- assert not theProject._appendSessionStats(idleTime=0)
+ assert theProject._appendSessionStats(idleTime=0) is False
+
+ # Session too short
+ theProject._projOpened = time()
+ theProject.data.setInitCounts(50, 50)
+ theProject.data.setCurrCounts(50, 50)
+ assert theProject._appendSessionStats(idleTime=0) is False
# Write entry
- assert theProject.projMeta == os.path.join(fncDir, "meta")
- statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS)
+ statsFile = theProject.storage.getMetaFile(nwFiles.SESS_STATS)
+ assert isinstance(statsFile, Path)
+ statsFile.unlink(missing_ok=True)
theProject._projOpened = 1600002000
- theProject._data._currCounts = [200, 100]
+ theProject.data._initCounts = [50, 50]
+ theProject.data._currCounts = [200, 100]
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.project.time", lambda: 1600005600)
assert theProject._appendSessionStats(idleTime=99)
- assert readFile(statsFile) == (
+ assert statsFile.read_text(encoding="utf-8") == (
"# Offset 100\n"
"# Start Time End Time Novel Notes Idle\n"
"%s %s 200 100 99\n"
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 28582695..4ec1ed5e 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -170,8 +170,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert len(nwGUI.theProject.tree._treeOrder) == 0
assert len(nwGUI.theProject.tree._treeRoots) == 0
assert nwGUI.theProject.tree.trashRoot() is None
- assert nwGUI.theProject.projPath is None
- assert nwGUI.theProject.projMeta is None
assert nwGUI.theProject.data.name == ""
assert nwGUI.theProject.data.title == ""
assert nwGUI.theProject.data.authors == []
@@ -192,8 +190,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert len(nwGUI.theProject.tree._treeOrder) == 8
assert len(nwGUI.theProject.tree._treeRoots) == 4
assert nwGUI.theProject.tree.trashRoot() is None
- assert nwGUI.theProject.projPath == fncProj
- assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta")
assert nwGUI.theProject.data.name == "New Project"
assert nwGUI.theProject.data.title == "New Novel"
assert nwGUI.theProject.data.authors == ["Jane Doe"]
diff --git a/tests/tools.py b/tests/tools.py
index e101bcbb..99f0d258 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -166,8 +166,8 @@ def buildTestProject(theObject, projPath):
theProject = theObject.theProject
theProject.clearProject()
- theProject.setProjectPath(projPath, newProject=True)
- theProject.storage.openProjectInPlace(theProject.projPath)
+ theProject.projPath = projPath
+ theProject.storage.openProjectInPlace(projPath)
theProject.setDefaultStatusImport()
theProject.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")
From 972ed25a8cc8e1e2db4ead40b42253721a244f63 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sat, 5 Nov 2022 23:33:50 +0100
Subject: [PATCH 16/26] Remove project cache attribute from project class
---
novelwriter/core/project.py | 6 ------
novelwriter/core/storage.py | 19 +++++++------------
novelwriter/tools/build.py | 13 ++++++++++---
tests/test_core/test_core_project.py | 8 --------
4 files changed, 17 insertions(+), 29 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 891d8dde..886fe1c2 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -85,7 +85,6 @@ class NWProject(QObject):
# Class Settings
self.projPath = None # The full path to where the currently open project is saved
- self.projCache = None # The full path to the project's cache folder
self.projContent = None # The full path to the project's content folder
self.projDict = None # The spell check dictionary
self.projFiles = [] # A list of all files in the content folder on load
@@ -254,7 +253,6 @@ class NWProject(QObject):
# Project Settings
self.projPath = None
- self.projCache = None
self.projContent = None
self.projDict = None
self.projFiles = []
@@ -274,7 +272,6 @@ class NWProject(QObject):
# ToDo: These should not be set explicitly, and should stay as Path
self.projPath = str(self._storage.runtimePath)
self.projContent = str(self._storage.contentPath)
- self.projCache = str(self._storage.cachePath)
logger.info("Opening project: %s", self.projPath)
@@ -478,15 +475,12 @@ class NWProject(QObject):
if self.projPath is None or self.projPath == "":
return False
- self.projCache = os.path.join(self.projPath, "cache")
self.projContent = os.path.join(self.projPath, "content")
if self.projPath == os.path.expanduser("~"):
# Don't make a mess in the user's home folder
return False
- if not self._checkFolder(self.projCache):
- return False
if not self._checkFolder(self.projContent):
return False
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index 485af8f0..05522469 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -76,18 +76,6 @@ class NWStorage:
return self._runtimePath / "content"
return None
- @property
- def metaPath(self):
- if self._runtimePath is not None:
- return self._runtimePath / "meta"
- return None
-
- @property
- def cachePath(self):
- if self._runtimePath is not None:
- return self._runtimePath / "cache"
- return None
-
##
# Core Methods
##
@@ -172,6 +160,13 @@ class NWStorage:
return self._runtimePath / "meta" / fileName
return None
+ def getCacheFile(self, fileName):
+ """Return the path to a file in the project cache folder.
+ """
+ if self._runtimePath is not None:
+ return self._runtimePath / "cache" / fileName
+ return None
+
def readLockFile(self):
"""Read the project lock file.
"""
diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py
index 8278fb95..78089abf 100644
--- a/novelwriter/tools/build.py
+++ b/novelwriter/tools/build.py
@@ -29,6 +29,7 @@ import logging
import novelwriter
from time import time
+from pathlib import Path
from datetime import datetime
from PyQt5.QtGui import (
@@ -1088,9 +1089,12 @@ class GuiBuildNovel(QDialog):
def _loadCache(self):
"""Save the current data to cache.
"""
- buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
+ buildCache = self.theProject.storage.getCacheFile(nwFiles.BUILD_CACHE)
+ if not isinstance(buildCache, Path):
+ return False
+
dataCount = 0
- if os.path.isfile(buildCache):
+ if buildCache.exists():
logger.debug("Loading build cache")
try:
with open(buildCache, mode="r", encoding="utf-8") as inFile:
@@ -1115,7 +1119,10 @@ class GuiBuildNovel(QDialog):
def _saveCache(self):
"""Save the current data to cache.
"""
- buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
+ buildCache = self.theProject.storage.getCacheFile(nwFiles.BUILD_CACHE)
+ if not isinstance(buildCache, Path):
+ return False
+
logger.debug("Saving build cache")
try:
with open(buildCache, mode="w+", encoding="utf-8") as outFile:
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 4404fdc3..8371a748 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -287,12 +287,6 @@ def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
mp.setattr("os.path.expanduser", lambda *a, **k: fncDir)
assert theProject.ensureFolderStructure() is False
- # Create a file to block cache folder
- cacheDir = os.path.join(fncDir, "cache")
- writeFile(cacheDir, "stuff")
- assert theProject.ensureFolderStructure() is False
- os.unlink(cacheDir)
-
# Create a file to block content folder
contentDir = os.path.join(fncDir, "content")
writeFile(contentDir, "stuff")
@@ -301,8 +295,6 @@ def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
# Now, do it right
assert theProject.ensureFolderStructure() is True
- # assert os.path.isdir(metaDir)
- assert os.path.isdir(cacheDir)
assert os.path.isdir(contentDir)
# END Test testCoreProject_Helpers
From 76a1421fdec6379fb30c2447f396d250f17a6198 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 6 Nov 2022 00:15:24 +0100
Subject: [PATCH 17/26] Remove project content attribute from project class
---
novelwriter/core/document.py | 56 +++++++++++++++-----------
novelwriter/core/project.py | 58 +++++++++------------------
novelwriter/core/storage.py | 2 +
novelwriter/core/tree.py | 15 +++++--
novelwriter/dialogs/wordlist.py | 4 +-
tests/conftest.py | 7 ++++
tests/test_core/test_core_document.py | 4 +-
tests/test_core/test_core_project.py | 32 +--------------
tests/test_core/test_core_tree.py | 26 ++++++------
tests/test_gui/test_gui_noveltree.py | 27 +++++++------
10 files changed, 104 insertions(+), 127 deletions(-)
diff --git a/novelwriter/core/document.py b/novelwriter/core/document.py
index 672fbee5..2d609538 100644
--- a/novelwriter/core/document.py
+++ b/novelwriter/core/document.py
@@ -23,9 +23,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import logging
+from pathlib import Path
+
from novelwriter.enum import nwItemLayout, nwItemClass
from novelwriter.error import formatException
from novelwriter.common import isHandle, sha256sum
@@ -73,7 +74,7 @@ class NWDoc:
empty string. If something went wrong, return None.
"""
self._docError = ""
- if self._docHandle is None:
+ if not isinstance(self._docHandle, str):
logger.error("No document handle set")
return None
@@ -81,17 +82,22 @@ class NWDoc:
logger.error("Unknown novelWriter document")
return None
+ contentPath = self.theProject.storage.contentPath
+ if not isinstance(contentPath, Path):
+ logger.error("No content path set")
+ return None
+
docFile = self._docHandle+".nwd"
logger.debug("Opening document: %s", docFile)
- docPath = os.path.join(self.theProject.projContent, docFile)
+ docPath = contentPath / docFile
self._fileLoc = docPath
theText = ""
self._docMeta = {}
self._prevHash = None
- if os.path.isfile(docPath):
+ if docPath.exists():
self._prevHash = sha256sum(docPath)
try:
with open(docPath, mode="r", encoding="utf-8") as inFile:
@@ -125,17 +131,20 @@ class NWDoc:
if not.
"""
self._docError = ""
- if self._docHandle is None:
+ if not isinstance(self._docHandle, str):
logger.error("No document handle set")
return False
- self.theProject.ensureFolderStructure()
+ contentPath = self.theProject.storage.contentPath
+ if not isinstance(contentPath, Path):
+ logger.error("No content path set")
+ return None
docFile = self._docHandle+".nwd"
logger.debug("Saving document: %s", docFile)
- docPath = os.path.join(self.theProject.projContent, docFile)
- docTemp = os.path.join(self.theProject.projContent, docFile+"~")
+ docPath = contentPath / docFile
+ docTemp = docPath.with_suffix(".tmp")
if self._prevHash is not None and not forceWrite:
self._currHash = sha256sum(docPath)
@@ -164,7 +173,7 @@ class NWDoc:
# If we're here, the file was successfully saved, so we can
# replace the temp file with the actual file
try:
- os.replace(docTemp, docPath)
+ docTemp.replace(docPath)
except OSError as exc:
self._docError = formatException(exc)
return False
@@ -179,23 +188,24 @@ class NWDoc:
from the project data folder.
"""
self._docError = ""
- if self._docHandle is None:
+ if not isinstance(self._docHandle, str):
logger.error("No document handle set")
return False
- chkList = [
- os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd"),
- os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd~"),
- ]
+ contentPath = self.theProject.storage.contentPath
+ if not isinstance(contentPath, Path):
+ logger.error("No content path set")
+ return None
- for chkFile in chkList:
- if os.path.isfile(chkFile):
- try:
- os.unlink(chkFile)
- logger.debug("Deleted: %s", chkFile)
- except Exception as exc:
- self._docError = formatException(exc)
- return False
+ docPath = contentPath / f"{self._docHandle}.nwd"
+ docTemp = docPath.with_suffix(".tmp")
+
+ try:
+ docPath.unlink(missing_ok=True)
+ docTemp.unlink(missing_ok=True)
+ except Exception as exc:
+ self._docError = formatException(exc)
+ return False
return True
@@ -206,7 +216,7 @@ class NWDoc:
def getFileLocation(self):
"""Return the file location of the current document.
"""
- return self._fileLoc
+ return str(self._fileLoc)
def getCurrentItem(self):
"""Return a pointer to the currently open NWItem.
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 886fe1c2..66ae755a 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -27,12 +27,12 @@ from __future__ import annotations
import os
import json
-from pathlib import Path
import shutil
import logging
import novelwriter
from time import time
+from pathlib import Path
from functools import partial
from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal
@@ -84,10 +84,9 @@ class NWProject(QObject):
self.lockedBy = None # Data on which computer has the project open
# Class Settings
- self.projPath = None # The full path to where the currently open project is saved
- self.projContent = None # The full path to the project's content folder
- self.projDict = None # The spell check dictionary
- self.projFiles = [] # A list of all files in the content folder on load
+ self.projPath = None # The full path to where the currently open project is saved
+ self.projDict = None # The spell check dictionary
+ self.projFiles = [] # A list of all files in the content folder on load
# Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject")
@@ -252,10 +251,9 @@ class NWProject(QObject):
self._data = NWProjectData(self)
# Project Settings
- self.projPath = None
- self.projContent = None
- self.projDict = None
- self.projFiles = []
+ self.projPath = None
+ self.projDict = None
+ self.projFiles = []
return
@@ -271,7 +269,6 @@ class NWProject(QObject):
# ToDo: These should not be set explicitly, and should stay as Path
self.projPath = str(self._storage.runtimePath)
- self.projContent = str(self._storage.contentPath)
logger.info("Opening project: %s", self.projPath)
@@ -468,24 +465,6 @@ class NWProject(QObject):
self.lockedBy = None
return True
- def ensureFolderStructure(self):
- """Ensure that all necessary folders exist in the project
- folder.
- """
- if self.projPath is None or self.projPath == "":
- return False
-
- self.projContent = os.path.join(self.projPath, "content")
-
- if self.projPath == os.path.expanduser("~"):
- # Don't make a mess in the user's home folder
- return False
-
- if not self._checkFolder(self.projContent):
- return False
-
- return True
-
def setDefaultStatusImport(self):
"""Set the default status and importance values.
"""
@@ -790,31 +769,34 @@ class NWProject(QObject):
orphaned files so the user can either delete them, or put them
back into the project tree.
"""
- if self.projPath is None:
+ contentPath = self._storage.contentPath
+ if not isinstance(contentPath, Path):
return False
# Then check the files in the data folder
logger.debug("Checking files in project content folder")
orphanFiles = []
self.projFiles = []
- for fileItem in os.listdir(self.projContent):
- if not fileItem.endswith(".nwd"):
- logger.warning("Skipping file: %s", fileItem)
+
+ for item in contentPath.iterdir():
+ itemName = item.name
+ if not itemName.endswith(".nwd"):
+ logger.warning("Skipping file: %s", itemName)
continue
- if len(fileItem) != 17:
- logger.warning("Skipping file: %s", fileItem)
+ if len(itemName) != 17:
+ logger.warning("Skipping file: %s", itemName)
continue
- fHandle = fileItem[:13]
+ fHandle = itemName[:13]
if not isHandle(fHandle):
- logger.warning("Skipping file: %s", fileItem)
+ logger.warning("Skipping file: %s", itemName)
continue
if fHandle in self._tree:
self.projFiles.append(fHandle)
- logger.debug("Checking file %s, handle '%s': OK", fileItem, fHandle)
+ logger.debug("Checking file %s, handle '%s': OK", itemName, fHandle)
else:
- logger.warning("Checking file %s, handle '%s': Orphaned", fileItem, fHandle)
+ logger.warning("Checking file %s, handle '%s': Orphaned", itemName, fHandle)
orphanFiles.append(fHandle)
# Report status
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index 05522469..b82421c6 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -151,6 +151,8 @@ class NWStorage:
return xmlWriter
def getDocument(self, tHandle):
+ """Return a document wrapper object.
+ """
pass
def getMetaFile(self, fileName):
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index e6d66f21..9d499aaf 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -23,10 +23,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import random
import logging
+from pathlib import Path
+
from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkHandle
@@ -140,16 +141,22 @@ class NWTree:
"""Write the convenience table of contents file in the root of
the project directory.
"""
+ runtimePath = self.theProject.storage.runtimePath
+ contentPath = self.theProject.storage.contentPath
+ if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
+ return False
+
tocList = []
tocLen = 0
for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
+
tFile = tHandle+".nwd"
- if os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
+ if (contentPath / tFile).is_file():
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
- os.path.join("content", tFile),
+ str(Path("content") / tFile),
tItem.itemClass.name,
tItem.itemLayout.name,
tItem.itemName,
@@ -159,7 +166,7 @@ class NWTree:
try:
# Dump the text
- tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT)
+ tocText = runtimePath / nwFiles.TOC_TXT
with open(tocText, mode="w", encoding="utf-8") as outFile:
outFile.write("\n")
outFile.write("Table of Contents\n")
diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py
index baabccdb..9281df0a 100644
--- a/novelwriter/dialogs/wordlist.py
+++ b/novelwriter/dialogs/wordlist.py
@@ -163,9 +163,7 @@ class GuiWordList(QDialog):
if item is not None:
outFile.write(item.text() + "\n")
- if dctFile.exists():
- dctFile.unlink()
- tmpFile.rename(dctFile)
+ tmpFile.replace(dctFile)
except Exception:
logger.error("Could not save new word list")
diff --git a/tests/conftest.py b/tests/conftest.py
index 758d46da..bcaa2ea3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -64,6 +64,13 @@ def tmpDir():
return theDir
+@pytest.fixture(scope="function")
+def tmpPath(tmpDir):
+ """A temporary folder for a single test function.
+ """
+ return Path(tmpDir)
+
+
@pytest.fixture(scope="session")
def tstPaths(tmpDir):
"""Returns an object that can provide the various paths needed for
diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py
index 761859ef..1923bc34 100644
--- a/tests/test_core/test_core_document.py
+++ b/tests/test_core/test_core_document.py
@@ -114,7 +114,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Cause os.replace() to fail while saving
with monkeypatch.context() as mp:
- mp.setattr("os.replace", causeOSError)
+ mp.setattr("pathlib.Path.replace", causeOSError)
assert theDoc.writeDocument(theText) is False
assert theDoc.getError() == "OSError: Mock OSError"
@@ -135,7 +135,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Cause the delete to fail
with monkeypatch.context() as mp:
- mp.setattr("os.unlink", causeOSError)
+ mp.setattr("pathlib.Path.unlink", causeOSError)
theDoc = NWDoc(theProject, xHandle)
assert theDoc.deleteDocument() is False
assert theDoc.getError() == "OSError: Mock OSError"
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 8371a748..76f420d6 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -141,7 +141,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI,
# Delete new file, but block access
with monkeypatch.context() as mp:
- mp.setattr("os.unlink", causeOSError)
+ mp.setattr("pathlib.Path.unlink", causeOSError)
assert theProject.removeItem("0000000000011") is False
assert "0000000000011" in theProject.tree
@@ -270,36 +270,6 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
# END Test testCoreProject_Save
-@pytest.mark.core
-def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
- """Test helper functions for the project folder.
- """
- theProject = NWProject(mockGUI)
-
- # No path
- assert theProject.ensureFolderStructure() is False
-
- # Set the correct dir
- theProject.projPath = fncDir
-
- # Block user's home folder
- with monkeypatch.context() as mp:
- mp.setattr("os.path.expanduser", lambda *a, **k: fncDir)
- assert theProject.ensureFolderStructure() is False
-
- # Create a file to block content folder
- contentDir = os.path.join(fncDir, "content")
- writeFile(contentDir, "stuff")
- assert theProject.ensureFolderStructure() is False
- os.unlink(contentDir)
-
- # Now, do it right
- assert theProject.ensureFolderStructure() is True
- assert os.path.isdir(contentDir)
-
-# END Test testCoreProject_Helpers
-
-
@pytest.mark.core
def testCoreProject_AccessItems(mockGUI, fncDir, mockRnd):
"""Test helper functions for the project folder.
diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py
index 518122e7..9c3d0961 100644
--- a/tests/test_core/test_core_tree.py
+++ b/tests/test_core/test_core_tree.py
@@ -19,10 +19,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import pytest
import random
+from pathlib import Path
+
from tools import readFile
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -394,7 +395,7 @@ def testCoreTree_Reorder(mockGUI, mockItems):
@pytest.mark.core
-def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
+def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpPath):
"""Test writing the ToC.txt file.
"""
theProject = NWProject(mockGUI)
@@ -411,24 +412,23 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
"""Return True for items that are files in novelWriter and
should thus also be files in the project folder structure.
"""
- dItem = theTree[fileName[8:21]]
+ dItem = theTree[fileName.name[:13]]
assert dItem is not None
return dItem.itemType == nwItemType.FILE
- monkeypatch.setattr("os.path.isfile", mockIsFile)
+ monkeypatch.setattr("pathlib.Path.is_file", mockIsFile)
- theProject.projContent = "content"
- theProject.projPath = None
- assert not theTree.writeToCFile()
+ theProject._storage._runtimePath = None
+ assert theTree.writeToCFile() is False
- theProject.projPath = tmpDir
- assert theTree.writeToCFile()
+ theProject._storage._runtimePath = tmpPath
+ assert theTree.writeToCFile() is True
- pathA = os.path.join("content", "c000000000001.nwd")
- pathB = os.path.join("content", "c000000000002.nwd")
- pathC = os.path.join("content", "b000000000002.nwd")
+ pathA = str(Path("content") / "c000000000001.nwd")
+ pathB = str(Path("content") / "c000000000002.nwd")
+ pathC = str(Path("content") / "b000000000002.nwd")
- assert readFile(os.path.join(tmpDir, nwFiles.TOC_TXT)) == (
+ assert readFile(tmpPath / nwFiles.TOC_TXT) == (
"\n"
"Table of Contents\n"
"=================\n"
diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py
index 4ec79000..cfaeecc4 100644
--- a/tests/test_gui/test_gui_noveltree.py
+++ b/tests/test_gui/test_gui_noveltree.py
@@ -19,10 +19,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import pytest
-from tools import C, buildTestProject, writeFile
+from pathlib import Path
+
+from tools import C, buildTestProject
from PyQt5.QtGui import QFocusEvent
from PyQt5.QtCore import Qt, QEvent
@@ -46,18 +47,18 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
- writeFile(
- os.path.join(nwGUI.theProject.projContent, "0000000000010.nwd"),
- "# Jane Doe\n\n@tag: Jane\n\n"
- )
- writeFile(
- os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), (
- "### Scene One\n\n"
- "@pov: Jane\n"
- "@focus: Jane\n\n"
- "% Synopsis: This is a scene."
- )
+ contentPath = nwGUI.theProject.storage.contentPath
+ assert isinstance(contentPath, Path)
+
+ (contentPath / "0000000000010.nwd").write_text(
+ "# Jane Doe\n\n@tag: Jane\n\n", encoding="utf-8"
)
+ (contentPath / "000000000000f.nwd").write_text((
+ "### Scene One\n\n"
+ "@pov: Jane\n"
+ "@focus: Jane\n\n"
+ "% Synopsis: This is a scene."
+ ), encoding="utf-8")
novelView = nwGUI.novelView
novelTree = novelView.novelTree
From a75424e44bb596f409c5909bf608647e35f5f02a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 6 Nov 2022 00:29:43 +0100
Subject: [PATCH 18/26] Use storage class to return document objects
---
novelwriter/core/__init__.py | 2 --
novelwriter/core/coretools.py | 21 ++++++++++-----------
novelwriter/core/index.py | 3 +--
novelwriter/core/project.py | 7 +++----
novelwriter/core/storage.py | 8 ++++----
novelwriter/core/tokenizer.py | 3 +--
novelwriter/dialogs/docsplit.py | 3 +--
novelwriter/gui/doceditor.py | 4 ++--
tests/test_core/test_core_coretools.py | 3 +--
tests/test_core/test_core_project.py | 7 ++++---
tests/test_core/test_core_tokenizer.py | 3 +--
tests/test_gui/test_gui_projtree.py | 7 +++----
tests/test_gui/test_gui_statusbar.py | 3 +--
tests/tools.py | 8 ++++----
14 files changed, 36 insertions(+), 46 deletions(-)
diff --git a/novelwriter/core/__init__.py b/novelwriter/core/__init__.py
index b535785b..07d6cc94 100644
--- a/novelwriter/core/__init__.py
+++ b/novelwriter/core/__init__.py
@@ -20,7 +20,6 @@ along with this program. If not, see .
"""
from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
-from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
from novelwriter.core.spellcheck import NWSpellEnchant
@@ -33,7 +32,6 @@ __all__ = [
"DocSplitter",
"ProjectBuilder",
"countWords",
- "NWDoc",
"NWProject",
"NWSpellEnchant",
"ToHtml",
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index f617817d..d283669c 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -38,7 +38,6 @@ from novelwriter.enum import nwAlert
from novelwriter.common import minmax, simplified
from novelwriter.constants import nwItemClass
from novelwriter.core.project import NWProject
-from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
@@ -102,7 +101,7 @@ class DocMerger:
if srcItem is None:
return False
- inDoc = NWDoc(self.theProject, srcHandle)
+ inDoc = self.theProject.storage.getDocument(srcHandle)
docText = (inDoc.readDocument() or "").rstrip("\n")
if addComment:
@@ -122,7 +121,7 @@ class DocMerger:
if self._targetDoc is None:
return False
- outDoc = NWDoc(self.theProject, self._targetDoc)
+ outDoc = self.theProject.storage.getDocument(self._targetDoc)
docText = (outDoc.readDocument() or "").rstrip("\n")
if docText:
self._targetText.insert(0, docText)
@@ -247,7 +246,7 @@ class DocSplitter:
newItem.setStatus(self._srcItem.itemStatus)
newItem.setImport(self._srcItem.itemImport)
- outDoc = NWDoc(self.theProject, dHandle)
+ outDoc = self.theProject.storage.getDocument(dHandle)
status = outDoc.writeDocument("\n".join(docText))
if not status:
self._error = outDoc.getError()
@@ -337,18 +336,18 @@ class ProjectBuilder:
if project.data.authors:
titlePage += f">> {lblByAuthors} {project.getFormattedAuthors()} <<\n\n"
- aDoc = NWDoc(project, hTitlePage)
+ aDoc = project.storage.getDocument(hTitlePage)
aDoc.writeDocument(titlePage)
if popMinimal:
# Creating a minimal project with a few root folders and a
# single chapter with a single scene.
hChapter = project.newFile(lblNewChapter, hNovelRoot)
- aDoc = NWDoc(project, hChapter)
+ aDoc = project.storage.getDocument(hChapter)
aDoc.writeDocument(f"## {lblNewChapter}\n\n")
hScene = project.newFile(lblNewScene, hChapter)
- aDoc = NWDoc(project, hScene)
+ aDoc = project.storage.getDocument(hScene)
aDoc.writeDocument(f"### {lblNewScene}\n\n")
project.newRoot(nwItemClass.PLOT)
@@ -376,7 +375,7 @@ class ProjectBuilder:
for ch in range(numChapters):
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
cHandle = project.newFile(chTitle, hNovelRoot)
- aDoc = NWDoc(project, cHandle)
+ aDoc = project.storage.getDocument(cHandle)
aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
# Create chapter scenes
@@ -384,7 +383,7 @@ class ProjectBuilder:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = project.newFile(scTitle, cHandle)
- aDoc = NWDoc(project, sHandle)
+ aDoc = project.storage.getDocument(sHandle)
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
# Create scenes (no chapters)
@@ -392,7 +391,7 @@ class ProjectBuilder:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = project.newFile(scTitle, hNovelRoot)
- aDoc = NWDoc(project, sHandle)
+ aDoc = project.storage.getDocument(sHandle)
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
# Create notes folders
@@ -409,7 +408,7 @@ class ProjectBuilder:
if addNotes:
aHandle = project.newFile(noteTitles[newRoot], rHandle)
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
- aDoc = NWDoc(project, aHandle)
+ aDoc = project.storage.getDocument(aHandle)
aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
# Also add the archive and trash folders
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index e7c50671..f07c0ee0 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -35,7 +35,6 @@ from pathlib import Path
from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
-from novelwriter.core.document import NWDoc
from novelwriter.common import (
checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
)
@@ -118,7 +117,7 @@ class NWIndex:
return False
logger.debug("Re-indexing item '%s'", tHandle)
- theDoc = NWDoc(self.theProject, tHandle)
+ theDoc = self.theProject.storage.getDocument(tHandle)
self.scanText(tHandle, theDoc.readDocument() or "")
return True
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 66ae755a..aef749a7 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -45,7 +45,6 @@ from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex
from novelwriter.core.options import OptionState
from novelwriter.core.storage import NWStorage
-from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.core.projectdata import NWProjectData
from novelwriter.common import (
@@ -183,7 +182,7 @@ class NWProject(QObject):
if not tItem.isFileType():
return False
- newDoc = NWDoc(self, tHandle)
+ newDoc = self._storage.getDocument(tHandle)
if (newDoc.readDocument() or "").strip():
return False
@@ -204,7 +203,7 @@ class NWProject(QObject):
project entry and a document file if it exists.
"""
if self._tree.checkType(tHandle, nwItemType.FILE):
- delDoc = NWDoc(self, tHandle)
+ delDoc = self._storage.getDocument(tHandle)
if not delDoc.deleteDocument():
self.mainGui.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
@@ -820,7 +819,7 @@ class NWProject(QObject):
oClass = None
oLayout = None
- aDoc = NWDoc(self, oHandle)
+ aDoc = self._storage.getDocument(oHandle)
if aDoc.readDocument(isOrphan=True) is not None:
oName, oParent, oClass, oLayout = aDoc.getMeta()
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index b82421c6..b79e98cb 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -30,6 +30,7 @@ from time import time
from pathlib import Path
from novelwriter.constants import nwFiles
+from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
from novelwriter.error import logException
@@ -153,7 +154,9 @@ class NWStorage:
def getDocument(self, tHandle):
"""Return a document wrapper object.
"""
- pass
+ if self._runtimePath is not None:
+ return NWDoc(self.theProject, tHandle)
+ return NWDoc(self.theProject, None)
def getMetaFile(self, fileName):
"""Return the path to a file in the project meta folder.
@@ -232,9 +235,6 @@ class NWStorage:
def _zipIt(self, target):
pass
- def _writeLockFile(self):
- pass
-
def _prepareStorage(self, checkLegacy=True, newProject=False):
"""Prepare the storage area for the project.
"""
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index e81fe2a6..0b2ab376 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -36,7 +36,6 @@ from PyQt5.QtCore import QCoreApplication, QRegularExpression
from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.common import numberToRoman, checkInt
from novelwriter.constants import nwConst, nwRegEx, nwUnicode
-from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
@@ -305,7 +304,7 @@ class Tokenizer(ABC):
return False
if theText is None:
- theText = NWDoc(self.theProject, theHandle).readDocument() or ""
+ theText = self.theProject.storage.getDocument(theHandle).readDocument() or ""
self._theText = theText
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index a8904b46..314ef8d8 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -33,7 +33,6 @@ from PyQt5.QtWidgets import (
QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
)
-from novelwriter.core import NWDoc
from novelwriter.custom import QHelpLabel, QSwitch
logger = logging.getLogger(__name__)
@@ -204,7 +203,7 @@ class GuiDocSplit(QDialog):
spLevel = self.splitLevel.currentData()
if not self._text:
- inDoc = NWDoc(self.theProject, sHandle)
+ inDoc = self.theProject.storage.getDocument(sHandle)
self._text = (inDoc.readDocument() or "").splitlines()
for lineNo, aLine in enumerate(self._text):
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index b59829bc..27048fe9 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -50,7 +50,7 @@ from PyQt5.QtWidgets import (
QFrame
)
-from novelwriter.core import NWDoc, NWSpellEnchant, countWords
+from novelwriter.core import NWSpellEnchant, countWords
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode
from novelwriter.common import transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
@@ -339,7 +339,7 @@ class GuiDocEditor(QTextEdit):
document is new (empty string), we set up the editor for editing
the file.
"""
- self._nwDocument = NWDoc(self.theProject, tHandle)
+ self._nwDocument = self.theProject.storage.getDocument(tHandle)
self._nwItem = self._nwDocument.getCurrentItem()
theDoc = self._nwDocument.readDocument()
diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py
index afb4d07b..4fc6c9ec 100644
--- a/tests/test_core/test_core_coretools.py
+++ b/tests/test_core/test_core_coretools.py
@@ -31,7 +31,6 @@ from tools import C, buildTestProject, cmpFiles, XML_IGNORE
from novelwriter.constants import nwItemClass
from novelwriter.core.project import NWProject
-from novelwriter.core.document import NWDoc
from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
@@ -164,7 +163,7 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mock
docText = "\n\n".join(docData)
docRaw = docText.splitlines()
- assert NWDoc(theProject, hSplitDoc).writeDocument(docText) is True
+ assert theProject.storage.getDocument(hSplitDoc).writeDocument(docText) is True
theProject.tree[hSplitDoc].setStatus(C.sFinished)
theProject.tree[hSplitDoc].setImport(C.iMain)
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 76f420d6..8b3896d6 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -38,7 +38,6 @@ from novelwriter.core.tree import NWTree
from novelwriter.core.index import NWIndex
from novelwriter.core.project import NWProject
from novelwriter.core.options import OptionState
-from novelwriter.core.document import NWDoc
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
@@ -125,11 +124,13 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI,
# Write to file, success
assert theProject.writeNewFile("0000000000011", 2, True) is True
- assert NWDoc(theProject, "0000000000011").readDocument() == "## Hello\n\n"
+ assert theProject.storage.getDocument("0000000000011").readDocument() == "## Hello\n\n"
# Write to file with additional text, success
assert theProject.writeNewFile("0000000000012", 1, False, "Hi Jane\n\n") is True
- assert NWDoc(theProject, "0000000000012").readDocument() == "# Jane\n\nHi Jane\n\n"
+ assert theProject.storage.getDocument("0000000000012").readDocument() == (
+ "# Jane\n\nHi Jane\n\n"
+ )
# Save, close and check
assert theProject.projChanged is True
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index a1cc796f..dfe9cf25 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -25,7 +25,6 @@ import pytest
from tools import C, buildTestProject, readFile
from novelwriter.core.project import NWProject
-from novelwriter.core.document import NWDoc
from novelwriter.core.tokenizer import Tokenizer
@@ -156,7 +155,7 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncDir):
)
docTextR = docText.replace("", "this").replace("", "that")
- nDoc = NWDoc(theProject, C.hSceneDoc)
+ nDoc = theProject.storage.getDocument(C.hSceneDoc)
assert nDoc.writeDocument(docText)
theProject.data.setAutoReplace({"A": "this", "B": "that"})
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index 31228e1e..13d27382 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -29,7 +29,6 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidgetItem, QDialog
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass
-from novelwriter.core import NWDoc
from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
@@ -714,7 +713,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, i
# The merge goes through
assert projTree._mergeDocuments(hChapter1, True) is True
- assert len(NWDoc(theProject, mergedDoc1).readDocument()) > lenAll
+ assert len(theProject.storage.getDocument(mergedDoc1).readDocument()) > lenAll
# Merge to Existing Doc
# =====================
@@ -733,9 +732,9 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, i
# Successful merge, and move to trash
mergeData["moveToTrash"] = True
- assert len(NWDoc(theProject, hChapter1).readDocument()) < lenAll
+ assert len(theProject.storage.getDocument(hChapter1).readDocument()) < lenAll
assert projTree._mergeDocuments(hChapter1, False) is True
- assert len(NWDoc(theProject, hChapter1).readDocument()) > lenAll
+ assert len(theProject.storage.getDocument(hChapter1).readDocument()) > lenAll
assert theProject.tree.isTrash(hSceneOne11)
assert theProject.tree.isTrash(hSceneOne12)
diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py
index 30d8d861..3f04acda 100644
--- a/tests/test_gui/test_gui_statusbar.py
+++ b/tests/test_gui/test_gui_statusbar.py
@@ -25,7 +25,6 @@ import pytest
from tools import C, buildTestProject
from novelwriter.enum import nwState
-from novelwriter.core.document import NWDoc
@pytest.mark.gui
@@ -34,7 +33,7 @@ def testGuiStatusBar_Main(qtbot, nwGUI, fncProj, mockRnd):
"""
buildTestProject(nwGUI, fncProj)
cHandle = nwGUI.theProject.newFile("A Note", C.hCharRoot)
- newDoc = NWDoc(nwGUI.theProject, cHandle)
+ newDoc = nwGUI.theProject.storage.getDocument(cHandle)
newDoc.writeDocument("# A Note\n\n")
nwGUI.projView.projTree.revealNewTreeItem(cHandle)
nwGUI.rebuildIndex(beQuiet=True)
diff --git a/tests/tools.py b/tests/tools.py
index 99f0d258..07fa6aab 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -156,7 +156,7 @@ def buildTestProject(theObject, projPath):
object as the parent.
"""
from novelwriter.enum import nwItemClass
- from novelwriter.core import NWProject, NWDoc
+ from novelwriter.core import NWProject
if isinstance(theObject, NWProject):
theGUI = None
@@ -187,15 +187,15 @@ def buildTestProject(theObject, projPath):
xHandle[7] = theProject.newFile("New Chapter", xHandle[6])
xHandle[8] = theProject.newFile("New Scene", xHandle[6])
- aDoc = NWDoc(theProject, xHandle[5])
+ aDoc = theProject.storage.getDocument(xHandle[5])
aDoc.writeDocument("#! New Novel\n\n>> By Jane Doe <<\n")
theProject.index.reIndexHandle(xHandle[5])
- aDoc = NWDoc(theProject, xHandle[7])
+ aDoc = theProject.storage.getDocument(xHandle[7])
aDoc.writeDocument("## %s\n\n" % theProject.tr("New Chapter"))
theProject.index.reIndexHandle(xHandle[7])
- aDoc = NWDoc(theProject, xHandle[8])
+ aDoc = theProject.storage.getDocument(xHandle[8])
aDoc.writeDocument("### %s\n\n" % theProject.tr("New Scene"))
theProject.index.reIndexHandle(xHandle[8])
From 615ee2958a5b529f92d17e86355bcc144dac6d19 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 6 Nov 2022 00:39:30 +0100
Subject: [PATCH 19/26] Remove project path attribute from project class
---
novelwriter/core/coretools.py | 2 --
novelwriter/core/project.py | 25 +++++++------------------
novelwriter/core/storage.py | 4 ++++
novelwriter/dialogs/projdetails.py | 2 +-
novelwriter/guimain.py | 2 +-
tests/test_core/test_core_project.py | 3 ++-
tests/tools.py | 1 -
7 files changed, 15 insertions(+), 24 deletions(-)
diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py
index d283669c..4f9d4954 100644
--- a/novelwriter/core/coretools.py
+++ b/novelwriter/core/coretools.py
@@ -307,8 +307,6 @@ class ProjectBuilder:
if not project.storage.openProjectInPlace(projPath, newProject=True):
return False
- project.projPath = projPath
-
lblNewProject = self.tr("New Project")
lblNewChapter = self.tr("New Chapter")
lblNewScene = self.tr("New Scene")
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index aef749a7..12ffcfac 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -83,7 +83,6 @@ class NWProject(QObject):
self.lockedBy = None # Data on which computer has the project open
# Class Settings
- self.projPath = None # The full path to where the currently open project is saved
self.projDict = None # The spell check dictionary
self.projFiles = [] # A list of all files in the content folder on load
@@ -250,7 +249,6 @@ class NWProject(QObject):
self._data = NWProjectData(self)
# Project Settings
- self.projPath = None
self.projDict = None
self.projFiles = []
@@ -266,10 +264,7 @@ class NWProject(QObject):
if not self._storage.openProjectInPlace(projPath):
return False
- # ToDo: These should not be set explicitly, and should stay as Path
- self.projPath = str(self._storage.runtimePath)
-
- logger.info("Opening project: %s", self.projPath)
+ logger.info("Opening project: %s", projPath)
self.projDict = str(self._storage.getMetaFile(nwFiles.PROJ_DICT))
@@ -368,7 +363,7 @@ class NWProject(QObject):
# Update recent projects
self.mainConf.updateRecentCache(
- self.projPath, self._data.name, sum(self._data.initCounts), time()
+ self._storage.storagePath, self._data.name, sum(self._data.initCounts), time()
)
self.mainConf.saveRecentCache()
@@ -399,12 +394,6 @@ class NWProject(QObject):
to make sure if the save fails, we're not left with a truncated
file.
"""
- if self.projPath is None:
- self.mainGui.makeAlert(self.tr(
- "Project path not set, cannot save project."
- ), nwAlert.ERROR)
- return False
-
if not self._storage.isOpen():
self.mainGui.makeAlert(self.tr(
"There is no project open."
@@ -413,7 +402,7 @@ class NWProject(QObject):
saveTime = time()
- logger.info("Saving project: %s", self.projPath)
+ logger.info("Saving project: %s", self._storage.storagePath)
if autoSave:
self._data.incAutoCount()
@@ -442,7 +431,7 @@ class NWProject(QObject):
# Update recent projects
self.mainConf.updateRecentCache(
- self.projPath, self._data.name, sum(self._data.currCounts), saveTime
+ self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime
)
self.mainConf.saveRecentCache()
@@ -455,7 +444,7 @@ class NWProject(QObject):
def closeProject(self, idleTime=0.0):
"""Close the current project and clear all meta data.
"""
- logger.info("Closing project: %s", self.projPath)
+ logger.info("Closing project")
self._options.saveSettings()
self._tree.writeToCFile()
self._appendSessionStats(idleTime)
@@ -517,7 +506,7 @@ class NWProject(QObject):
), nwAlert.ERROR, exception=exc)
return False
- if baseDir and baseDir.startswith(self.projPath):
+ if baseDir and baseDir.startswith(str(self._storage.runtimePath)):
self.mainGui.makeAlert(self.tr(
"Cannot backup project because the backup path is within the "
"project folder to be backed up. Please choose a different "
@@ -530,7 +519,7 @@ class NWProject(QObject):
try:
self._storage.clearLockFile()
- shutil.make_archive(baseName, "zip", self.projPath, ".")
+ shutil.make_archive(baseName, "zip", self._storage.runtimePath, ".")
self._storage.writeLockFile()
logger.info("Backup written to: %s", archName)
if doNotify:
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index b79e98cb..ad90126e 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -67,6 +67,10 @@ class NWStorage:
# Properties
##
+ @property
+ def storagePath(self):
+ return self._storagePath
+
@property
def runtimePath(self):
return self._runtimePath
diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py
index 0f185e89..9f3999a4 100644
--- a/novelwriter/dialogs/projdetails.py
+++ b/novelwriter/dialogs/projdetails.py
@@ -260,7 +260,7 @@ class GuiProjectDetailsMain(QWidget):
self.revCountVal.setText(f"{self.theProject.data.saveCount:n}")
self.editTimeVal.setText(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
- self.projPathVal.setText(self.theProject.projPath)
+ self.projPathVal.setText(str(self.theProject.storage.storagePath))
return
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index c0745460..6537e7a0 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -1382,7 +1382,7 @@ class GuiMain(QMainWindow):
"""
doSave = self.hasProject
doSave &= self.theProject.projChanged
- doSave &= self.theProject.projPath is not None
+ doSave &= self.theProject.storage.isOpen()
if doSave:
logger.debug("Autosaving project")
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 8b3896d6..62e631c3 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -649,7 +649,8 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
writeFile(tstPath, "\n")
assert theProject.openProject(nwLipsum)
- assert theProject.projPath is not None
+ assert theProject.storage.storagePath is not None
+ assert theProject.storage.runtimePath is not None
assert theProject.tree["636b6aa9b697bb"] is None
assert theProject.tree["abcdefghijklm"] is None
diff --git a/tests/tools.py b/tests/tools.py
index 07fa6aab..c2511b28 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -166,7 +166,6 @@ def buildTestProject(theObject, projPath):
theProject = theObject.theProject
theProject.clearProject()
- theProject.projPath = projPath
theProject.storage.openProjectInPlace(projPath)
theProject.setDefaultStatusImport()
From 6f6fe876e25a41cc8b5eba1d9db5a609457cb937 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 7 Nov 2022 10:45:53 +0100
Subject: [PATCH 20/26] Fix parameter missing in Python 3.7
---
novelwriter/core/document.py | 8 ++++++--
tests/test_core/test_core_project.py | 3 ++-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/novelwriter/core/document.py b/novelwriter/core/document.py
index 2d609538..2f04c128 100644
--- a/novelwriter/core/document.py
+++ b/novelwriter/core/document.py
@@ -201,8 +201,12 @@ class NWDoc:
docTemp = docPath.with_suffix(".tmp")
try:
- docPath.unlink(missing_ok=True)
- docTemp.unlink(missing_ok=True)
+ # ToDo: When Python 3.7 is dropped, these can be changed to
+ # path.unlink(missing_ok=True)
+ if docPath.exists():
+ docPath.unlink()
+ if docTemp.exists():
+ docTemp.unlink()
except Exception as exc:
self._docError = formatException(exc)
return False
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 62e631c3..368a7ddb 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -582,7 +582,8 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncDir, mockRnd):
# Write entry
statsFile = theProject.storage.getMetaFile(nwFiles.SESS_STATS)
assert isinstance(statsFile, Path)
- statsFile.unlink(missing_ok=True)
+ if statsFile.exists():
+ statsFile.unlink()
theProject._projOpened = 1600002000
theProject.data._initCounts = [50, 50]
From 26f7bb4b324b2916ea0d3bd73f37deb9cef45d29 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 7 Nov 2022 22:46:30 +0100
Subject: [PATCH 21/26] Change how spel checking and booleans in general are
saved in the XML
---
novelwriter/common.py | 11 +-
novelwriter/core/item.py | 6 +-
novelwriter/core/projectxml.py | 23 ++--
sample/nwProject.nwx | 103 +++++++++---------
tests/files/nwProject-1.5.nwx | 101 +++++++++--------
tests/lipsum/nwProject.nwx | 83 +++++++-------
.../coreProject_NewFileFolder_nwProject.nwx | 43 ++++----
.../coreProject_NewRoot_nwProject.nwx | 49 ++++-----
.../coreTools_NewCustomA_nwProject.nwx | 85 +++++++--------
.../coreTools_NewCustomB_nwProject.nwx | 61 +++++------
.../coreTools_NewMinimal_nwProject.nwx | 31 +++---
.../guiEditor_Main_Final_nwProject.nwx | 45 ++++----
.../guiEditor_Main_Initial_nwProject.nwx | 31 +++---
tests/reference/projectXML_ReadLegacy10.nwx | 83 +++++++-------
tests/reference/projectXML_ReadLegacy11.nwx | 83 +++++++-------
tests/reference/projectXML_ReadLegacy12.nwx | 91 ++++++++--------
tests/reference/projectXML_ReadLegacy13.nwx | 91 ++++++++--------
tests/reference/projectXML_ReadLegacy14.nwx | 99 +++++++++--------
tests/test_base/test_base_common.py | 67 ++++++++++--
tests/test_core/test_core_item.py | 8 +-
20 files changed, 622 insertions(+), 572 deletions(-)
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 5a84302d..7eb1ec59 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -88,9 +88,10 @@ def checkBool(value, default):
if isinstance(value, bool):
return value
elif isinstance(value, str):
- if value == "True":
+ check = value.lower()
+ if check in ("true", "yes", "on"):
return True
- elif value == "False":
+ elif check in ("false", "no", "off"):
return False
else:
return default
@@ -259,6 +260,12 @@ def simplified(string):
return " ".join(str(string).strip().split())
+def yesNo(value):
+ """Convert a boolean evaluated variable to a yes or no.
+ """
+ return "yes" if value else "no"
+
+
def splitVersionNumber(value):
"""Split a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc.
diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py
index 7b6530e0..9f472dab 100644
--- a/novelwriter/core/item.py
+++ b/novelwriter/core/item.py
@@ -27,7 +27,7 @@ import logging
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
- checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
+ checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, yesNo
)
from novelwriter.constants import nwHeaders, nwLabels, trConst
@@ -162,7 +162,7 @@ class NWItem:
item["order"] = str(self._order)
item["type"] = str(self._type.name)
item["class"] = str(self._class.name)
- meta["expanded"] = str(self._expanded)
+ meta["expanded"] = yesNo(self._expanded)
name["status"] = str(self._status)
name["import"] = str(self._import)
@@ -173,7 +173,7 @@ class NWItem:
meta["wordCount"] = str(self._wordCount)
meta["paraCount"] = str(self._paraCount)
meta["cursorPos"] = str(self._cursorPos)
- name["active"] = str(self._active)
+ name["active"] = yesNo(self._active)
data = {
"name": str(self._name),
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index 618fb30a..cf78a587 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -33,7 +33,8 @@ from time import time
from pathlib import Path
from novelwriter.common import (
- checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString
+ checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
+ simplified, yesNo
)
from novelwriter.constants import nwFiles
@@ -270,10 +271,9 @@ class ProjectXMLReader:
projData.setDoBackup(xItem.text)
elif xItem.tag == "language":
projData.setLanguage(xItem.text)
- elif xItem.tag == "spellCheck":
- projData.setSpellCheck(xItem.text)
- elif xItem.tag == "spellLang":
+ elif xItem.tag == "spellChecking":
projData.setSpellLang(xItem.text)
+ projData.setSpellCheck(xItem.attrib.get("auto", False))
elif xItem.tag == "status":
self._parseStatusImport(xItem, projData.itemStatus)
elif xItem.tag == "importance":
@@ -296,7 +296,11 @@ class ProjectXMLReader:
# Deprecated Nodes
if self._version < HEX_VERSION:
for xItem in xSection:
- if xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
+ if xItem.tag == "spellCheck": # Changed to spellChecking in 1.5
+ projData.setSpellCheck(xItem.text)
+ elif xItem.tag == "spellLang": # Changed to spellChecking in 1.5
+ projData.setSpellLang(xItem.text)
+ elif xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
projData.setInitCounts(novel=xItem.text)
elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
projData.setInitCounts(notes=xItem.text)
@@ -517,10 +521,11 @@ class ProjectXMLWriter:
# Save Project Settings
xSettings = etree.SubElement(xRoot, "settings")
- self._packSingleValue(xSettings, "doBackup", projData.doBackup)
+ self._packSingleValue(xSettings, "doBackup", yesNo(projData.doBackup))
self._packSingleValue(xSettings, "language", projData.language)
- self._packSingleValue(xSettings, "spellCheck", projData.spellCheck)
- self._packSingleValue(xSettings, "spellLang", projData.spellLang)
+ self._packSingleValue(xSettings, "spellChecking", projData.spellLang, attrib={
+ "auto": yesNo(projData.spellCheck)
+ })
self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle)
self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace)
self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat)
@@ -536,7 +541,7 @@ class ProjectXMLWriter:
# Save Tree Content
contAttr = {
- "itemCount": str(len(projContent)),
+ "items": str(len(projContent)),
"novelWords": str(projData.currCounts[0]),
"notesWords": str(projData.currCounts[1]),
}
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index b1f151ac..de890f9b 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,16 +1,15 @@
-
-
+
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- False
+ no
en_GB
- True
- None
+ None
636b6aa9b697b
636b6aa9b697b
@@ -45,114 +44,114 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Page
+
+ Page
-
-
- Part One
+
+ Part One
-
-
- Chapter One
+
+ Chapter One
-
-
- Making a Scene
+
+ Making a Scene
-
-
- Another Scene
+
+ Another Scene
-
-
- Interlude
+
+ Interlude
-
-
- A Note on Structure
+
+ A Note on Structure
-
-
- Chapter Two
+
+ Chapter Two
-
-
- We Found John!
+
+ We Found John!
-
-
+
Sequel
-
-
- Title Page
+
+ Title Page
-
-
- Chapter One
+
+ Chapter One
-
-
+
Characters
-
-
+
Main Characters
-
-
- John Smith
+
+ John Smith
-
-
- Jane Smith
+
+ Jane Smith
-
-
+
Locations
-
-
- Earth
+
+ Earth
-
-
- Space
+
+ Space
-
-
- Mars
+
+ Mars
-
-
+
Archive
-
-
+
Scenes
-
-
- Old File
+
+ Old File
-
-
+
Trash
-
-
- Delete Me!
+
+ Delete Me!
diff --git a/tests/files/nwProject-1.5.nwx b/tests/files/nwProject-1.5.nwx
index 28f03cef..ec8668f4 100644
--- a/tests/files/nwProject-1.5.nwx
+++ b/tests/files/nwProject-1.5.nwx
@@ -1,5 +1,5 @@
-
+
Sample Project
Sample Project
@@ -7,10 +7,9 @@
Jay Doh
- True
+ yes
en_GB
- True
- en_GB
+ en_GB
636b6aa9b697b
636b6aa9b697b
@@ -45,114 +44,114 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Page
+
+ Page
-
-
- Part One
+
+ Part One
-
-
- Chapter One
+
+ Chapter One
-
-
- Making a Scene
+
+ Making a Scene
-
-
- Another Scene
+
+ Another Scene
-
-
- Interlude
+
+ Interlude
-
-
- A Note on Structure
+
+ A Note on Structure
-
-
- Chapter Two
+
+ Chapter Two
-
-
- We Found John!
+
+ We Found John!
-
-
+
Sequel
-
-
- Title Page
+
+ Title Page
-
-
- Chapter One
+
+ Chapter One
-
-
+
Characters
-
-
+
Main Characters
-
-
- John Smith
+
+ John Smith
-
-
- Jane Smith
+
+ Jane Smith
-
-
+
Locations
-
-
- Earth
+
+ Earth
-
-
- Space
+
+ Space
-
-
- Mars
+
+ Mars
-
-
+
Archive
-
-
+
Scenes
-
-
- Old File
+
+ Old File
-
-
+
Trash
-
-
- Delete Me!
+
+ Delete Me!
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx
index d5b2b2c7..086183c6 100644
--- a/tests/lipsum/nwProject.nwx
+++ b/tests/lipsum/nwProject.nwx
@@ -1,15 +1,14 @@
-
-
+
+
Lorem Ipsum
Lorem Ipsum
lipsum.com
- False
+ no
en_GB
- False
- None
+ None
7a992350f3eb6
None
@@ -40,90 +39,90 @@
Main
-
+
-
-
+
Novel
-
-
- Lorem Ipsum
+
+ Lorem Ipsum
-
-
- Front Matter
+
+ Front Matter
-
-
- Prologue
+
+ Prologue
-
-
- Act One
+
+ Act One
-
-
+
Chapter One
-
-
- Chapter One
+
+ Chapter One
-
-
- Scene One
+
+ Scene One
-
-
- Scene Two
+
+ Scene Two
-
-
- Interlude
+
+ Interlude
-
-
+
Chapter Two
-
-
- Chapter Two
+
+ Chapter Two
-
-
- Scene Three
+
+ Scene Three
-
-
- Scene Four
+
+ Scene Four
-
-
- Scene Five
+
+ Scene Five
-
-
+
Characters
-
-
- Mr. Nobody
+
+ Mr. Nobody
-
-
+
Plot
-
-
- Main
+
+ Main
-
-
+
World
-
-
- Ancient Europe
+
+ Ancient Europe
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index 520883e5..5000c7d9 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,15 +1,14 @@
-
-
+
+
New Project
New Novel
Jane Doe
- True
+ yes
None
- False
- None
+ None
None
None
@@ -37,50 +36,50 @@
Main
-
+
-
-
+
Novel
-
-
+
Plot
-
-
+
Characters
-
-
+
World
-
-
- Title Page
+
+ Title Page
-
-
+
New Chapter
-
-
- New Chapter
+
+ New Chapter
-
-
- New Scene
+
+ New Scene
-
-
+
Stuff
-
-
- Hello
+
+ Hello
-
-
- Jane
+
+ Jane
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 6e2c26af..06a0ca07 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,15 +1,14 @@
-
-
+
+
New Project
New Novel
Jane Doe
- True
+ yes
None
- False
- None
+ None
None
None
@@ -37,69 +36,69 @@
Main
-
+
-
-
+
Novel
-
-
+
Plot
-
-
+
Characters
-
-
+
World
-
-
- Title Page
+
+ Title Page
-
-
+
New Chapter
-
-
- New Chapter
+
+ New Chapter
-
-
- New Scene
+
+ New Scene
-
-
+
Novel
-
-
+
Plot
-
-
+
Characters
-
-
+
Locations
-
-
+
Timeline
-
-
+
Objects
-
-
+
Custom
-
-
+
Custom
diff --git a/tests/reference/coreTools_NewCustomA_nwProject.nwx b/tests/reference/coreTools_NewCustomA_nwProject.nwx
index e7775cd5..48330e5f 100644
--- a/tests/reference/coreTools_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomA_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -7,10 +7,9 @@
John Doh
- True
+ yes
None
- False
- None
+ None
None
None
@@ -38,93 +37,93 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Chapter 1
+
+ Chapter 1
-
-
- Scene 1.1
+
+ Scene 1.1
-
-
- Scene 1.2
+
+ Scene 1.2
-
-
- Scene 1.3
+
+ Scene 1.3
-
-
- Chapter 2
+
+ Chapter 2
-
-
- Scene 2.1
+
+ Scene 2.1
-
-
- Scene 2.2
+
+ Scene 2.2
-
-
- Scene 2.3
+
+ Scene 2.3
-
-
- Chapter 3
+
+ Chapter 3
-
-
- Scene 3.1
+
+ Scene 3.1
-
-
- Scene 3.2
+
+ Scene 3.2
-
-
- Scene 3.3
+
+ Scene 3.3
-
-
+
Plot
-
-
- Main Plot
+
+ Main Plot
-
-
+
Characters
-
-
- Protagonist
+
+ Protagonist
-
-
+
Locations
-
-
- Main Location
+
+ Main Location
-
-
+
Archive
-
-
+
Trash
diff --git a/tests/reference/coreTools_NewCustomB_nwProject.nwx b/tests/reference/coreTools_NewCustomB_nwProject.nwx
index 6937cb0a..161d1157 100644
--- a/tests/reference/coreTools_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreTools_NewCustomB_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -7,10 +7,9 @@
John Doh
- True
+ yes
None
- False
- None
+ None
None
None
@@ -38,69 +37,69 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Scene 1
+
+ Scene 1
-
-
- Scene 2
+
+ Scene 2
-
-
- Scene 3
+
+ Scene 3
-
-
- Scene 4
+
+ Scene 4
-
-
- Scene 5
+
+ Scene 5
-
-
- Scene 6
+
+ Scene 6
-
-
+
Plot
-
-
- Main Plot
+
+ Main Plot
-
-
+
Characters
-
-
- Protagonist
+
+ Protagonist
-
-
+
Locations
-
-
- Main Location
+
+ Main Location
-
-
+
Archive
-
-
+
Trash
diff --git a/tests/reference/coreTools_NewMinimal_nwProject.nwx b/tests/reference/coreTools_NewMinimal_nwProject.nwx
index ba168a0e..1ac190ee 100644
--- a/tests/reference/coreTools_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreTools_NewMinimal_nwProject.nwx
@@ -1,14 +1,13 @@
-
+
New Project
New Project
- True
+ yes
None
- False
- None
+ None
None
None
@@ -36,37 +35,37 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- New Chapter
+
+ New Chapter
-
-
- New Scene
+
+ New Scene
-
-
+
Plot
-
-
+
Characters
-
-
+
Locations
-
-
+
Archive
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index 74bae460..e384ef9e 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,15 +1,14 @@
-
+
New Project
New Novel
Jane Doe
- True
+ yes
None
- True
- None
+ None
000000000000f
None
@@ -37,53 +36,53 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
+
New Chapter
-
-
- New Chapter
+
+ New Chapter
-
-
- New Scene
+
+ New Scene
-
-
+
Plot
-
-
- New Note
+
+ New Note
-
-
+
Characters
-
-
- New Note
+
+ New Note
-
-
+
World
-
-
- New Note
+
+ New Note
-
-
+
Trash
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index b5fd4013..977f03c1 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,15 +1,14 @@
-
+
New Project
New Novel
Jane Doe
- True
+ yes
None
- False
- None
+ None
None
None
@@ -37,37 +36,37 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
+
New Chapter
-
-
- New Chapter
+
+ New Chapter
-
-
- New Scene
+
+ New Scene
-
-
+
Plot
-
-
+
Characters
-
-
+
World
diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx
index b08e3511..d4ef1259 100644
--- a/tests/reference/projectXML_ReadLegacy10.nwx
+++ b/tests/reference/projectXML_ReadLegacy10.nwx
@@ -7,10 +7,9 @@
Jay Doh
- True
+ yes
None
- True
- None
+ None
None
None
@@ -45,94 +44,94 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Page
+
+ Page
-
-
- Part One
+
+ Part One
-
-
+
A Folder
-
-
- Chapter One
+
+ Chapter One
-
-
- Making a Scene
+
+ Making a Scene
-
-
- Another Scene
+
+ Another Scene
-
-
- Interlude
+
+ Interlude
-
-
- A Note on Structure
+
+ A Note on Structure
-
-
- Chapter Two
+
+ Chapter Two
-
-
- We Found John!
+
+ We Found John!
-
-
+
Characters
-
-
+
Main Characters
-
-
- John Smith
+
+ John Smith
-
-
- Jane Smith
+
+ Jane Smith
-
-
+
Locations
-
-
- Earth
+
+ Earth
-
-
- Space
+
+ Space
-
-
- Mars
+
+ Mars
-
-
+
Trash
-
-
- Delete Me!
+
+ Delete Me!
diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx
index c8519300..c2bb4cf4 100644
--- a/tests/reference/projectXML_ReadLegacy11.nwx
+++ b/tests/reference/projectXML_ReadLegacy11.nwx
@@ -7,10 +7,9 @@
Jay Doh
- True
+ yes
None
- True
- None
+ None
None
None
@@ -45,94 +44,94 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Page
+
+ Page
-
-
- Part One
+
+ Part One
-
-
+
A Folder
-
-
- Chapter One
+
+ Chapter One
-
-
- Making a Scene
+
+ Making a Scene
-
-
- Another Scene
+
+ Another Scene
-
-
- Interlude
+
+ Interlude
-
-
- A Note on Structure
+
+ A Note on Structure
-
-
- Chapter Two
+
+ Chapter Two
-
-
- We Found John!
+
+ We Found John!
-
-
+
Characters
-
-
+
Main Characters
-
-
- John Smith
+
+ John Smith
-
-
- Jane Smith
+
+ Jane Smith
-
-
+
Locations
-
-
- Earth
+
+ Earth
-
-
- Space
+
+ Space
-
-
- Mars
+
+ Mars
-
-
+
Trash
-
-
- Delete Me!
+
+ Delete Me!
diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx
index c28d7e16..90408b3f 100644
--- a/tests/reference/projectXML_ReadLegacy12.nwx
+++ b/tests/reference/projectXML_ReadLegacy12.nwx
@@ -7,10 +7,9 @@
Jay Doh
- True
+ yes
en_GB
- True
- en_GB
+ en_GB
None
None
@@ -45,106 +44,106 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Page
+
+ Page
-
-
- Part One
+
+ Part One
-
-
+
A Folder
-
-
- Chapter One
+
+ Chapter One
-
-
- Making a Scene
+
+ Making a Scene
-
-
- Another Scene
+
+ Another Scene
-
-
- Interlude
+
+ Interlude
-
-
- A Note on Structure
+
+ A Note on Structure
-
-
- Chapter Two
+
+ Chapter Two
-
-
- We Found John!
+
+ We Found John!
-
-
+
Characters
-
-
+
Main Characters
-
-
- John Smith
+
+ John Smith
-
-
- Jane Smith
+
+ Jane Smith
-
-
+
Locations
-
-
- Earth
+
+ Earth
-
-
- Space
+
+ Space
-
-
- Mars
+
+ Mars
-
-
+
Outtakes
-
-
+
Scenes
-
-
- Old File
+
+ Old File
-
-
+
Trash
-
-
- Delete Me!
+
+ Delete Me!
diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx
index 8e448959..226affe4 100644
--- a/tests/reference/projectXML_ReadLegacy13.nwx
+++ b/tests/reference/projectXML_ReadLegacy13.nwx
@@ -7,10 +7,9 @@
Jay Doh
- True
+ yes
en_GB
- True
- en_GB
+ en_GB
None
None
@@ -45,106 +44,106 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Page
+
+ Page
-
-
- Part One
+
+ Part One
-
-
+
A Folder
-
-
- Chapter One
+
+ Chapter One
-
-
- Making a Scene
+
+ Making a Scene
-
-
- Another Scene
+
+ Another Scene
-
-
- Interlude
+
+ Interlude
-
-
- A Note on Structure
+
+ A Note on Structure
-
-
- Chapter Two
+
+ Chapter Two
-
-
- We Found John!
+
+ We Found John!
-
-
+
Characters
-
-
+
Main Characters
-
-
- John Smith
+
+ John Smith
-
-
- Jane Smith
+
+ Jane Smith
-
-
+
Locations
-
-
- Earth
+
+ Earth
-
-
- Space
+
+ Space
-
-
- Mars
+
+ Mars
-
-
+
Archive
-
-
+
Scenes
-
-
- Old File
+
+ Old File
-
-
+
Trash
-
-
- Delete Me!
+
+ Delete Me!
diff --git a/tests/reference/projectXML_ReadLegacy14.nwx b/tests/reference/projectXML_ReadLegacy14.nwx
index 445817a3..8cd18728 100644
--- a/tests/reference/projectXML_ReadLegacy14.nwx
+++ b/tests/reference/projectXML_ReadLegacy14.nwx
@@ -7,10 +7,9 @@
Jay Doh
- True
+ yes
en_GB
- True
- en_GB
+ en_GB
None
None
@@ -45,114 +44,114 @@
Main
-
+
-
-
+
Novel
-
-
- Title Page
+
+ Title Page
-
-
- Page
+
+ Page
-
-
- Part One
+
+ Part One
-
-
- Chapter One
+
+ Chapter One
-
-
- Making a Scene
+
+ Making a Scene
-
-
- Another Scene
+
+ Another Scene
-
-
- Interlude
+
+ Interlude
-
-
- A Note on Structure
+
+ A Note on Structure
-
-
- Chapter Two
+
+ Chapter Two
-
-
- We Found John!
+
+ We Found John!
-
-
+
Sequel
-
-
- Title Page
+
+ Title Page
-
-
- Chapter One
+
+ Chapter One
-
-
+
Characters
-
-
+
Main Characters
-
-
- John Smith
+
+ John Smith
-
-
- Jane Smith
+
+ Jane Smith
-
-
+
Locations
-
-
- Earth
+
+ Earth
-
-
- Space
+
+ Space
-
-
- Mars
+
+ Mars
-
-
+
Archive
-
-
+
Scenes
-
-
- Old File
+
+ Old File
-
-
+
Trash
-
-
- Delete Me!
+
+ Delete Me!
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index ca487aa2..f6d60a1c 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -32,9 +32,9 @@ from novelwriter.common import (
checkStringNone, checkString, checkInt, checkFloat, checkBool, checkHandle,
checkUuid, isHandle, isTitleTag, isItemClass, isItemType, isItemLayout,
hexToInt, minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime,
- simplified, splitVersionNumber, transferCase, fuzzyTime, numberToRoman,
- jsonEncode, readTextFile, makeFileNameSafe, ensureFolder, sha256sum,
- getGuiItem, NWConfigParser
+ simplified, yesNo, splitVersionNumber, transferCase, fuzzyTime,
+ numberToRoman, jsonEncode, readTextFile, makeFileNameSafe, ensureFolder,
+ sha256sum, getGuiItem, NWConfigParser
)
@@ -105,16 +105,41 @@ def testBaseCommon_CheckBool():
bool, or integer 1 or 0, are returned as bool. Otherwise, the
default is returned.
"""
+ # Bools
+ assert checkBool(True, False) is True
+ assert checkBool(False, True) is False
+
+ # Valid Strings
assert checkBool("True", False) is True
assert checkBool("False", True) is False
- assert checkBool("Boo", False) is False
- assert checkBool("Boo", True) is True
- assert checkBool(None, True) is True
- assert checkBool(None, False) is False
+ assert checkBool("true", False) is True
+ assert checkBool("false", True) is False
+ assert checkBool("Yes", False) is True
+ assert checkBool("No", True) is False
+ assert checkBool("yes", False) is True
+ assert checkBool("no", True) is False
+ assert checkBool("On", False) is True
+ assert checkBool("Off", True) is False
+ assert checkBool("on", False) is True
+ assert checkBool("off", True) is False
+
+ # Invalid Strings
+ assert checkBool("Foo", False) is False
+ assert checkBool("Foo", True) is True
+ assert checkBool("bar", False) is False
+ assert checkBool("bar", True) is True
+
+ # Valid Integers
assert checkBool(0, True) is False
assert checkBool(1, False) is True
+
+ # Inalid Integers
assert checkBool(2, True) is True
assert checkBool(2, False) is False
+
+ # Other Types
+ assert checkBool(None, True) is True
+ assert checkBool(None, False) is False
assert checkBool(0.0, True) is True
assert checkBool(1.0, False) is False
assert checkBool(2.0, True) is True
@@ -331,6 +356,34 @@ def testBaseCommon_Simplified():
# END Test testBaseCommon_Simplified
+@pytest.mark.base
+def testBaseCommon_YesNo():
+ """Test the yesNo function.
+ """
+ # Bool
+ assert yesNo(True) == "yes"
+ assert yesNo(False) == "no"
+
+ # None
+ assert yesNo(None) == "no"
+
+ # String
+ assert yesNo("foo") == "yes"
+ assert yesNo("") == "no"
+
+ # Integer
+ assert yesNo(0) == "no"
+ assert yesNo(1) == "yes"
+ assert yesNo(2) == "yes"
+
+ # Float
+ assert yesNo(0.0) == "no"
+ assert yesNo(1.0) == "yes"
+ assert yesNo(2.0) == "yes"
+
+# END Test testBaseCommon_YesNo
+
+
@pytest.mark.base
def testBaseCommon_SplitVersionNumber():
"""Test the splitVersionNumber function.
diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py
index f02f21f3..16d2ffe2 100644
--- a/tests/test_core/test_core_item.py
+++ b/tests/test_core/test_core_item.py
@@ -564,7 +564,7 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
"layout": "DOCUMENT",
},
"metaAttr": {
- "expanded": "True",
+ "expanded": "yes",
"heading": "H1",
"charCount": "100",
"wordCount": "20",
@@ -574,7 +574,7 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
"nameAttr": {
"status": "s000000",
"import": "i000001",
- "active": "False",
+ "active": "no",
}
}
@@ -635,7 +635,7 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
"class": "NOVEL",
},
"metaAttr": {
- "expanded": "True",
+ "expanded": "yes",
},
"nameAttr": {
"status": "s000000",
@@ -700,7 +700,7 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
"class": "NOVEL",
},
"metaAttr": {
- "expanded": "True",
+ "expanded": "yes",
},
"nameAttr": {
"status": "s000000",
From 262a6c36b70599d9e18b94dc906812009cb3b419 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 7 Nov 2022 22:51:09 +0100
Subject: [PATCH 22/26] Rename NWDoc class to NWDocument
---
novelwriter/core/document.py | 6 ++---
novelwriter/core/storage.py | 6 ++---
novelwriter/gui/doceditor.py | 4 ++--
tests/test_core/test_core_document.py | 32 +++++++++++++--------------
4 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/novelwriter/core/document.py b/novelwriter/core/document.py
index 2f04c128..7fe7f8e5 100644
--- a/novelwriter/core/document.py
+++ b/novelwriter/core/document.py
@@ -34,7 +34,7 @@ from novelwriter.common import isHandle, sha256sum
logger = logging.getLogger(__name__)
-class NWDoc:
+class NWDocument:
def __init__(self, theProject, theHandle):
@@ -58,7 +58,7 @@ class NWDoc:
return
def __repr__(self):
- return f""
+ return f""
def __bool__(self):
return self._docHandle is not None and bool(self._theItem)
@@ -277,4 +277,4 @@ class NWDoc:
return
-# END Class NWDoc
+# END Class NWDocument
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index ad90126e..6cfa9ffa 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -30,7 +30,7 @@ from time import time
from pathlib import Path
from novelwriter.constants import nwFiles
-from novelwriter.core.document import NWDoc
+from novelwriter.core.document import NWDocument
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
from novelwriter.error import logException
@@ -159,8 +159,8 @@ class NWStorage:
"""Return a document wrapper object.
"""
if self._runtimePath is not None:
- return NWDoc(self.theProject, tHandle)
- return NWDoc(self.theProject, None)
+ return NWDocument(self.theProject, tHandle)
+ return NWDocument(self.theProject, None)
def getMetaFile(self, fileName):
"""Return the path to a file in the project meta folder.
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 27048fe9..3e53b897 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -470,8 +470,8 @@ class GuiDocEditor(QTextEdit):
return True
def saveText(self):
- """Save the text currently in the editor to the NWDoc object,
- and update the NWItem meta data.
+ """Save the text currently in the editor to the NWDocument
+ object, and update the NWItem meta data.
"""
if self._nwItem is None or self._nwDocument is None:
logger.error("Cannot save text as no document is open")
diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py
index 1923bc34..ba74afbd 100644
--- a/tests/test_core/test_core_document.py
+++ b/tests/test_core/test_core_document.py
@@ -1,6 +1,6 @@
"""
-novelWriter – NWDoc Class Tester
-================================
+novelWriter – NWDocument Class Tester
+=====================================
This file is a part of novelWriter
Copyright 2018–2022, Veronica Berglyd Olsen
@@ -27,12 +27,12 @@ from tools import C, buildTestProject, readFile, writeFile
from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.core.project import NWProject
-from novelwriter.core.document import NWDoc
+from novelwriter.core.document import NWDocument
@pytest.mark.core
def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
- """Test loading and saving a document with the NWDoc class.
+ """Test loading and saving a document with the NWDocument class.
"""
theProject = NWProject(mockGUI)
mockRnd.reset()
@@ -42,31 +42,31 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# =============
# Not a valid handle
- theDoc = NWDoc(theProject, "stuff")
+ theDoc = NWDocument(theProject, "stuff")
assert bool(theDoc) is False
assert theDoc.readDocument() is None
# Non-existent handle
- theDoc = NWDoc(theProject, C.hInvalid)
+ theDoc = NWDocument(theProject, C.hInvalid)
assert theDoc.readDocument() is None
assert theDoc._currHash is None
# Cause open() to fail while loading
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
- theDoc = NWDoc(theProject, C.hSceneDoc)
+ theDoc = NWDocument(theProject, C.hSceneDoc)
assert theDoc.readDocument() is None
assert theDoc.getError() == "OSError: Mock OSError"
# Load the text
- theDoc = NWDoc(theProject, C.hSceneDoc)
+ theDoc = NWDocument(theProject, C.hSceneDoc)
assert theDoc.readDocument() == "### New Scene\n\n"
# Try to open a new (non-existent) file
xHandle = theProject.newFile("New File", C.hNovelRoot)
- theDoc = NWDoc(theProject, xHandle)
+ theDoc = NWDocument(theProject, xHandle)
assert bool(theDoc) is True
- assert repr(theDoc) == f""
+ assert repr(theDoc) == f""
assert theDoc.readDocument() == ""
# Write Document
@@ -74,7 +74,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Set handle and save again
theText = "### Test File\n\nText ...\n\n"
- theDoc = NWDoc(theProject, xHandle)
+ theDoc = NWDocument(theProject, xHandle)
assert theDoc.readDocument(xHandle) == ""
assert theDoc.writeDocument(theText) is True
@@ -129,19 +129,19 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# ===============
# Delete the last document
- theDoc = NWDoc(theProject, "stuff")
+ theDoc = NWDocument(theProject, "stuff")
assert theDoc.deleteDocument() is False
assert os.path.isfile(docPath)
# Cause the delete to fail
with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.unlink", causeOSError)
- theDoc = NWDoc(theProject, xHandle)
+ theDoc = NWDocument(theProject, xHandle)
assert theDoc.deleteDocument() is False
assert theDoc.getError() == "OSError: Mock OSError"
# Make the delete pass
- theDoc = NWDoc(theProject, xHandle)
+ theDoc = NWDocument(theProject, xHandle)
assert theDoc.deleteDocument() is True
assert not os.path.isfile(docPath)
@@ -150,13 +150,13 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
@pytest.mark.core
def testCoreDocument_Methods(mockGUI, fncDir, mockRnd):
- """Test other methods of the NWDoc class.
+ """Test other methods of the NWDocument class.
"""
theProject = NWProject(mockGUI)
mockRnd.reset()
buildTestProject(theProject, fncDir)
- theDoc = NWDoc(theProject, C.hSceneDoc)
+ theDoc = NWDocument(theProject, C.hSceneDoc)
docPath = os.path.join(fncDir, "content", C.hSceneDoc+".nwd")
assert theDoc.readDocument() == "### New Scene\n\n"
From b024c0996d24eebf14341e5e81e7bf171a4d4aa6 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 8 Nov 2022 19:25:56 +0100
Subject: [PATCH 23/26] Move zipping of project to storage class
---
novelwriter/core/project.py | 77 +++++++------------------
novelwriter/core/projectxml.py | 8 +--
novelwriter/core/storage.py | 61 ++++++++++++++++----
novelwriter/gui/mainmenu.py | 2 +-
novelwriter/guimain.py | 2 +-
tests/test_core/test_core_index.py | 3 +-
tests/test_core/test_core_project.py | 26 ++++-----
tests/test_core/test_core_projectxml.py | 12 ++--
8 files changed, 96 insertions(+), 95 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 12ffcfac..d117f109 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -27,7 +27,6 @@ from __future__ import annotations
import os
import json
-import shutil
import logging
import novelwriter
@@ -299,7 +298,6 @@ class NWProject(QObject):
xmlParsed = xmlReader.read(self._data, projContent)
appVersion = xmlReader.appVersion or self.tr("Unknown")
- hexVersion = xmlReader.hexVersion or "0x0"
if not xmlParsed:
if xmlReader.state == XMLReadState.NOT_NWX_FILE:
@@ -339,7 +337,7 @@ class NWProject(QObject):
# Check novelWriter Version
# =========================
- if hexToInt(hexVersion) > hexToInt(novelwriter.__hexversion__):
+ if xmlReader.hexVersion > hexToInt(novelwriter.__hexversion__):
msgYes = self.mainGui.askQuestion(
self.tr("Version Conflict"),
self.tr(
@@ -359,7 +357,7 @@ class NWProject(QObject):
self._tree.unpack(projContent)
self._options.loadSettings()
- self._index.loadIndex()
+ self._loadProjectLocalisation()
# Update recent projects
self.mainConf.updateRecentCache(
@@ -376,7 +374,7 @@ class NWProject(QObject):
del self._tree[tHandle] # The file will be re-added as orphaned
self._scanProjectFolder()
- self._loadProjectLocalisation()
+ self._index.loadIndex()
self.updateWordCounts()
self._projOpened = time()
@@ -466,21 +464,17 @@ class NWProject(QObject):
self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
return
- ##
- # Zip/Unzip Project
- ##
-
- def zipIt(self, doNotify):
+ def backupProject(self, doNotify):
"""Create a zip file of the entire project.
"""
- if not self.mainGui.hasProject:
+ if not self._storage.isOpen():
logger.error("No project open")
return False
logger.info("Backing up project")
self.mainGui.setStatus(self.tr("Backing up project ..."))
- if not (self.mainConf.backupPath and os.path.isdir(self.mainConf.backupPath)):
+ if not self.mainConf.backupPath:
self.mainGui.makeAlert(self.tr(
"Cannot backup project because no valid backup path is set. "
"Please set a valid backup location in Preferences."
@@ -490,52 +484,37 @@ class NWProject(QObject):
if not self._data.name:
self.mainGui.makeAlert(self.tr(
"Cannot backup project because no project name is set. "
- "Please set a Working Title in Project Settings."
+ "Please set a Project Name in Project Settings."
), nwAlert.ERROR)
return False
cleanName = makeFileNameSafe(self._data.name)
- baseDir = os.path.abspath(os.path.join(self.mainConf.backupPath, cleanName))
- if not os.path.isdir(baseDir):
- try:
- os.mkdir(baseDir)
- logger.debug("Created folder: %s", baseDir)
- except Exception as exc:
- self.mainGui.makeAlert(self.tr(
- "Could not create backup folder."
- ), nwAlert.ERROR, exception=exc)
- return False
-
- if baseDir and baseDir.startswith(str(self._storage.runtimePath)):
+ baseDir = Path(self.mainConf.backupPath) / cleanName
+ try:
+ baseDir.mkdir(exist_ok=True)
+ except Exception as exc:
self.mainGui.makeAlert(self.tr(
- "Cannot backup project because the backup path is within the "
- "project folder to be backed up. Please choose a different "
- "backup path in Preferences."
- ), nwAlert.ERROR)
+ "Could not create backup folder."
+ ), nwAlert.ERROR, exception=exc)
return False
- archName = self.tr("Backup from {0}").format(formatTimeStamp(time(), fileSafe=True))
- baseName = os.path.join(baseDir, archName)
-
- try:
- self._storage.clearLockFile()
- shutil.make_archive(baseName, "zip", self._storage.runtimePath, ".")
- self._storage.writeLockFile()
- logger.info("Backup written to: %s", archName)
+ archName = baseDir / self.tr(
+ "Backup from {0}.zip"
+ ).format(formatTimeStamp(time(), fileSafe=True))
+ if self._storage.zipIt(archName, compression=2):
if doNotify:
self.mainGui.makeAlert(self.tr(
"Backup archive file written to: {0}"
- ).format(f"{os.path.join(cleanName, archName)}.zip"), nwAlert.INFO)
-
- except Exception as exc:
+ ).format(str(archName), nwAlert.INFO))
+ else:
self.mainGui.makeAlert(self.tr(
"Could not write backup archive."
- ), nwAlert.ERROR, exception=exc)
+ ), nwAlert.ERROR)
return False
self.mainGui.setStatus(self.tr(
"Project backed up to '{0}'"
- ).format(f"{baseName}.zip"))
+ ).format(str(archName)))
return True
@@ -737,20 +716,6 @@ class NWProject(QObject):
return True
- def _checkFolder(self, thePath):
- """Check if a folder exists, and if it doesn't, create it.
- """
- if not os.path.isdir(thePath):
- try:
- os.mkdir(thePath)
- logger.debug("Created folder: %s", thePath)
- except Exception as exc:
- self.mainGui.makeAlert(self.tr(
- "Could not create folder."
- ), nwAlert.ERROR, exception=exc)
- return False
- return True
-
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
diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py
index cf78a587..5b1b72f4 100644
--- a/novelwriter/core/projectxml.py
+++ b/novelwriter/core/projectxml.py
@@ -34,7 +34,7 @@ from pathlib import Path
from novelwriter.common import (
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
- simplified, yesNo
+ hexToInt, simplified, yesNo
)
from novelwriter.constants import nwFiles
@@ -104,9 +104,9 @@ class ProjectXMLReader:
self._state = XMLReadState.NO_ACTION
self._root = ""
- self._version = 0x0000
+ self._version = 0x0
self._appVersion = ""
- self._hexVersion = ""
+ self._hexVersion = 0x0
self._timeStamp = ""
return
@@ -200,7 +200,7 @@ class ProjectXMLReader:
logger.debug("XML is '%s' version '%s'", self._root, fileVersion)
self._appVersion = str(xRoot.attrib.get("appVersion", ""))
- self._hexVersion = str(xRoot.attrib.get("hexVersion", ""))
+ self._hexVersion = hexToInt(xRoot.attrib.get("hexVersion", ""))
self._timeStamp = str(xRoot.attrib.get("timeStamp", ""))
for xSection in xRoot:
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index 6cfa9ffa..40751909 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -28,7 +28,9 @@ import novelwriter
from time import time
from pathlib import Path
+from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
+from novelwriter.common import minmax
from novelwriter.constants import nwFiles
from novelwriter.core.document import NWDocument
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
@@ -232,18 +234,55 @@ class NWStorage:
return True
+ def zipIt(self, target, compression=None):
+ """Zip the content of the project at its runtime location into a
+ zip file. This process will only grab files that are supposed to
+ be in the project. All non-project files will be left out.
+ """
+ basePath = self._runtimePath
+ if not isinstance(basePath, Path):
+ logger.error("No path set")
+ return False
+
+ baseMeta = basePath / "meta"
+ baseCont = basePath / "content"
+ files = [
+ (basePath / nwFiles.PROJ_FILE, nwFiles.PROJ_FILE),
+ (baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
+ (baseMeta / nwFiles.SESS_STATS, f"meta/{nwFiles.SESS_STATS}"),
+ (baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
+ (baseMeta / nwFiles.PROJ_DICT, f"meta/{nwFiles.PROJ_DICT}"),
+ ]
+ for contItem in baseCont.iterdir():
+ name = contItem.name
+ if contItem.is_file() and len(name) == 17 and name.endswith(".nwd"):
+ files.append((contItem, f"content/{name}"))
+
+ comp = ZIP_STORED if compression is None else ZIP_DEFLATED
+ level = minmax(compression, 0, 9) if isinstance(compression, int) else None
+ try:
+ with ZipFile(target, mode="w", compression=comp, compresslevel=level) as zipObj:
+ logger.info("Creating archive: %s", target)
+ for srcPath, zipPath in files:
+ if srcPath.is_file():
+ zipObj.write(srcPath, zipPath)
+ logger.debug("Added: %s", zipPath)
+ except Exception:
+ logger.error("Failed to create acrhive")
+ logException()
+ return False
+
+ return True
+
##
# Internal Functions
##
- def _zipIt(self, target):
- pass
-
def _prepareStorage(self, checkLegacy=True, newProject=False):
"""Prepare the storage area for the project.
"""
path = self._runtimePath
- if path is None:
+ if not isinstance(path, Path):
logger.error("No path set")
self.clear()
return False
@@ -336,13 +375,13 @@ class NWStorage:
"""Delete files that are no longer used by novelWriter.
"""
remove = [
- path / "meta" / "mainOptions.json",
- path / "meta" / "exportOptions.json",
- path / "meta" / "outlineOptions.json",
- path / "meta" / "timelineOptions.json",
- path / "meta" / "docMergeOptions.json",
- path / "meta" / "sessionLogOptions.json",
- path / "ToC.json",
+ path / "meta" / "mainOptions.json", # Replaced in 0.5
+ path / "meta" / "exportOptions.json", # Replaced in 0.5
+ path / "meta" / "outlineOptions.json", # Replaced in 0.5
+ path / "meta" / "timelineOptions.json", # Replaced in 0.5
+ path / "meta" / "docMergeOptions.json", # Replaced in 0.5
+ path / "meta" / "sessionLogOptions.json", # Replaced in 0.5
+ path / "ToC.json", # Dropped in 1.0 RC 1
]
for item in remove:
if item.is_file():
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 51a74bae..a3b7eca0 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -824,7 +824,7 @@ class GuiMainMenu(QMenuBar):
# Tools > Backup
self.aBackupProject = QAction(self.tr("Backup Project"), self)
- self.aBackupProject.triggered.connect(lambda: self.theProject.zipIt(True))
+ self.aBackupProject.triggered.connect(lambda: self.theProject.backupProject(doNoify=True))
self.toolsMenu.addAction(self.aBackupProject)
# Tools > Export Project
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 6537e7a0..961a9458 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -409,7 +409,7 @@ class GuiMain(QMainWindow):
if not msgYes:
doBackup = False
if doBackup:
- self.theProject.zipIt(False)
+ self.theProject.backupProject(doNotify=False)
else:
saveOK = True
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 624f6ec8..678f00e5 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -29,6 +29,7 @@ from mock import causeException
from tools import C, buildTestProject, cmpFiles, writeFile
from novelwriter.enum import nwItemClass, nwItemLayout
+from novelwriter.constants import nwFiles
from novelwriter.core.index import NWIndex, countWords, TagsIndex
from novelwriter.core.project import NWProject
@@ -38,7 +39,7 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir):
"""Test core functionality of scaning, saving, loading and checking
the index cache file.
"""
- projFile = os.path.join(nwLipsum, "meta", "tagsIndex.json")
+ projFile = os.path.join(nwLipsum, "meta", nwFiles.INDEX_FILE)
testFile = os.path.join(outDir, "coreIndex_LoadSave_tagsIndex.json")
compFile = os.path.join(refDir, "coreIndex_LoadSave_tagsIndex.json")
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index 368a7ddb..e2e6aad2 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -235,7 +235,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
# Won't convert legacy file
with monkeypatch.context() as mp:
- mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: "0x99999999"))
+ mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999))
mockGUI.askResponse = False
assert theProject.openProject(fncDir) is False
assert "This project was saved by a newer version" in mockGUI.lastQuestion[1]
@@ -699,46 +699,42 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncDir, tmpDir):
# No project
mockGUI.hasProject = False
- assert theProject.zipIt(doNotify=False) is False
+ assert theProject.backupProject(doNotify=False) is False
mockGUI.hasProject = True
# Invalid path
theProject.mainConf.backupPath = None
- assert theProject.zipIt(doNotify=False) is False
+ assert theProject.backupProject(doNotify=False) is False
# Missing project name
theProject.mainConf.backupPath = tmpDir
theProject.data.setName("")
- assert theProject.zipIt(doNotify=False) is False
+ assert theProject.backupProject(doNotify=False) is False
# Non-existent folder
theProject.mainConf.backupPath = os.path.join(tmpDir, "nonexistent")
theProject.data.setName("Test Minimal")
- assert theProject.zipIt(doNotify=False) is False
-
- # Same folder as project (causes infinite loop in zipping)
- theProject.mainConf.backupPath = fncDir
- assert theProject.zipIt(doNotify=False) is False
+ assert theProject.backupProject(doNotify=False) is False
# Subfolder of project (causes infinite loop in zipping)
theProject.mainConf.backupPath = os.path.join(fncDir, "subdir")
- assert theProject.zipIt(doNotify=False) is False
+ assert theProject.backupProject(doNotify=False) is False
# Set a valid folder
theProject.mainConf.backupPath = tmpDir
# Can't make folder
with monkeypatch.context() as mp:
- mp.setattr("os.mkdir", causeOSError)
- assert theProject.zipIt(doNotify=False) is False
+ mp.setattr("pathlib.Path.mkdir", causeOSError)
+ assert theProject.backupProject(doNotify=False) is False
# Can't write archive
with monkeypatch.context() as mp:
- mp.setattr("shutil.make_archive", causeOSError)
- assert theProject.zipIt(doNotify=False) is False
+ mp.setattr("zipfile.ZipFile.write", causeOSError)
+ assert theProject.backupProject(doNotify=False) is False
# Test correct settings
- assert theProject.zipIt(doNotify=True) is True
+ assert theProject.backupProject(doNotify=True) is True
theFiles = os.listdir(os.path.join(tmpDir, "Test Minimal"))
assert len(theFiles) == 1
diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py
index 3bbc9e3a..b9dce90c 100644
--- a/tests/test_core/test_core_projectxml.py
+++ b/tests/test_core/test_core_projectxml.py
@@ -131,7 +131,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0105
assert xmlReader.appVersion == "2.0-rc1"
- assert xmlReader.hexVersion == "0x020000c1"
+ assert xmlReader.hexVersion == 0x020000c1
# Check loaded data
assert data.name == "Sample Project"
@@ -257,7 +257,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0100
assert xmlReader.appVersion == "0.6.1"
- assert xmlReader.hexVersion == "0x000601f0"
+ assert xmlReader.hexVersion == 0x000601f0
# Check loaded data
assert data.name == "Sample Project"
@@ -399,7 +399,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0101
assert xmlReader.appVersion == "0.9.2"
- assert xmlReader.hexVersion == "0x000902f0"
+ assert xmlReader.hexVersion == 0x000902f0
# Check loaded data
assert data.name == "Sample Project"
@@ -541,7 +541,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0102
assert xmlReader.appVersion == "1.4.2"
- assert xmlReader.hexVersion == "0x010402f0"
+ assert xmlReader.hexVersion == 0x010402f0
# Check loaded data
assert data.name == "Sample Project"
@@ -686,7 +686,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0103
assert xmlReader.appVersion == "1.6.6"
- assert xmlReader.hexVersion == "0x010606f0"
+ assert xmlReader.hexVersion == 0x010606f0
# Check loaded data
assert data.name == "Sample Project"
@@ -831,7 +831,7 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0104
assert xmlReader.appVersion == "2.0-rc1"
- assert xmlReader.hexVersion == "0x020000c1"
+ assert xmlReader.hexVersion == 0x020000c1
# Check loaded data
assert data.name == "Sample Project"
From ee79d3d6d6ae63cecb301c5aa7385fa5dd8a59d4 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 8 Nov 2022 19:48:06 +0100
Subject: [PATCH 24/26] Fix tests for core classes
---
novelwriter/core/document.py | 4 +-
novelwriter/core/index.py | 2 -
novelwriter/core/project.py | 30 ++---
novelwriter/core/spellcheck.py | 6 +-
novelwriter/gui/doceditor.py | 5 +-
tests/test_core/test_core_document.py | 43 ++++--
tests/test_core/test_core_index.py | 20 ++-
tests/test_core/test_core_project.py | 166 +++++++++++++-----------
tests/test_core/test_core_spellcheck.py | 11 +-
9 files changed, 163 insertions(+), 124 deletions(-)
diff --git a/novelwriter/core/document.py b/novelwriter/core/document.py
index 7fe7f8e5..ab9b8ac9 100644
--- a/novelwriter/core/document.py
+++ b/novelwriter/core/document.py
@@ -138,7 +138,7 @@ class NWDocument:
contentPath = self.theProject.storage.contentPath
if not isinstance(contentPath, Path):
logger.error("No content path set")
- return None
+ return False
docFile = self._docHandle+".nwd"
logger.debug("Saving document: %s", docFile)
@@ -195,7 +195,7 @@ class NWDocument:
contentPath = self.theProject.storage.contentPath
if not isinstance(contentPath, Path):
logger.error("No content path set")
- return None
+ return False
docPath = contentPath / f"{self._docHandle}.nwd"
docTemp = docPath.with_suffix(".tmp")
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index f07c0ee0..249fd19d 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -820,8 +820,6 @@ class ItemIndex:
elif tItem.itemRoot == rootHandle:
for sTitle in self._items[tHandle].headings():
yield tHandle, sTitle, self._items[tHandle][sTitle]
- else:
- continue
return
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index d117f109..65d292c8 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -25,7 +25,6 @@ along with this program. If not, see .
from __future__ import annotations
-import os
import json
import logging
import novelwriter
@@ -82,8 +81,7 @@ class NWProject(QObject):
self.lockedBy = None # Data on which computer has the project open
# Class Settings
- self.projDict = None # The spell check dictionary
- self.projFiles = [] # A list of all files in the content folder on load
+ self.projFiles = [] # A list of all files in the content folder on load
# Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject")
@@ -248,7 +246,6 @@ class NWProject(QObject):
self._data = NWProjectData(self)
# Project Settings
- self.projDict = None
self.projFiles = []
return
@@ -265,8 +262,6 @@ class NWProject(QObject):
logger.info("Opening project: %s", projPath)
- self.projDict = str(self._storage.getMetaFile(nwFiles.PROJ_DICT))
-
# Project Lock
# ============
@@ -367,11 +362,12 @@ class NWProject(QObject):
# Check the project tree consistency
for tItem in self._tree:
- tHandle = tItem.itemHandle
- logger.debug("Checking item '%s'", tHandle)
- if not self._tree.updateItemData(tHandle):
- logger.error("There was a problem item '%s', and it has been removed", tHandle)
- del self._tree[tHandle] # The file will be re-added as orphaned
+ if tItem:
+ tHandle = tItem.itemHandle
+ logger.debug("Checking item '%s'", tHandle)
+ if not self._tree.updateItemData(tHandle):
+ logger.error("There was a problem the item, and it has been removed")
+ del self._tree[tHandle] # The file will be re-added as orphaned
self._scanProjectFolder()
self._index.loadIndex()
@@ -694,20 +690,18 @@ class NWProject(QObject):
def _loadProjectLocalisation(self):
"""Load the language data for the current project language.
"""
- if self._data.language is None:
+ if self._data.language is None or self.mainConf.nwLangPath is None:
self._langData = {}
return False
- langFile = os.path.join(
- self.mainConf.nwLangPath, "project_%s.json" % self._data.language
- )
- if not os.path.isfile(langFile):
- langFile = os.path.join(self.mainConf.nwLangPath, "project_en_GB.json")
+ langFile = Path(self.mainConf.nwLangPath) / f"project_{self._data.language}.json"
+ if not langFile.is_file():
+ langFile = Path(self.mainConf.nwLangPath) / "project_en_GB.json"
try:
with open(langFile, mode="r", encoding="utf-8") as inFile:
self._langData = json.load(inFile)
- logger.debug("Loaded project language file: %s", os.path.basename(langFile))
+ logger.debug("Loaded project language file: %s", langFile.name)
except Exception:
logger.error("Failed to project language file")
diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py
index 4a63bf39..30396d0d 100644
--- a/novelwriter/core/spellcheck.py
+++ b/novelwriter/core/spellcheck.py
@@ -23,10 +23,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import logging
from collections import namedtuple
+from pathlib import Path
from novelwriter.error import logException
@@ -173,10 +173,10 @@ class NWSpellEnchant:
self._projDict = set()
self._projectDict = projectDict
- if projectDict is None:
+ if not isinstance(projectDict, Path):
return False
- if not os.path.isfile(projectDict):
+ if not projectDict.exists():
return False
try:
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 3e53b897..39c5edb3 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -53,7 +53,7 @@ from PyQt5.QtWidgets import (
from novelwriter.core import NWSpellEnchant, countWords
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode
from novelwriter.common import transferCase
-from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
+from novelwriter.constants import nwConst, nwFiles, nwKeyWords, nwUnicode
from novelwriter.gui.dochighlight import GuiDocHighlighter
logger = logging.getLogger(__name__)
@@ -689,7 +689,8 @@ class GuiDocEditor(QTextEdit):
else:
theLang = self.theProject.data.spellLang
- self.spEnchant.setLanguage(theLang, self.theProject.projDict)
+ projDict = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
+ self.spEnchant.setLanguage(theLang, projDict)
_, theProvider = self.spEnchant.describeDict()
self.spellDictionaryChanged.emit(str(theLang), str(theProvider))
diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py
index ba74afbd..50dd28bf 100644
--- a/tests/test_core/test_core_document.py
+++ b/tests/test_core/test_core_document.py
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import pytest
from mock import causeOSError
@@ -31,12 +30,12 @@ from novelwriter.core.document import NWDocument
@pytest.mark.core
-def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
+def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
"""Test loading and saving a document with the NWDocument class.
"""
theProject = NWProject(mockGUI)
mockRnd.reset()
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
# Read Document
# =============
@@ -51,6 +50,12 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
assert theDoc.readDocument() is None
assert theDoc._currHash is None
+ # No content path
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.storage.NWStorage.contentPath", property(lambda *a: None))
+ theDoc = NWDocument(theProject, C.hSceneDoc)
+ assert theDoc.readDocument() is None
+
# Cause open() to fail while loading
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
@@ -72,17 +77,23 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Write Document
# ==============
- # Set handle and save again
+ # No content path
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.storage.NWStorage.contentPath", property(lambda *a: None))
+ theDoc = NWDocument(theProject, xHandle)
+ assert theDoc.writeDocument("") is False
+
+ # Set handle and save
theText = "### Test File\n\nText ...\n\n"
theDoc = NWDocument(theProject, xHandle)
assert theDoc.readDocument(xHandle) == ""
assert theDoc.writeDocument(theText) is True
# Save again to ensure temp file and previous file is handled
- assert theDoc.writeDocument(theText)
+ assert theDoc.writeDocument(theText) is True
# Check file content
- docPath = os.path.join(fncDir, "content", xHandle+".nwd")
+ docPath = fncPath / "content" / f"{xHandle}.nwd"
assert readFile(docPath) == (
"%%~name: New File\n"
f"%%~path: {C.hNovelRoot}/{xHandle}\n"
@@ -128,10 +139,16 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Delete Document
# ===============
- # Delete the last document
+ # Delete a non-existing document
theDoc = NWDocument(theProject, "stuff")
assert theDoc.deleteDocument() is False
- assert os.path.isfile(docPath)
+ assert docPath.exists()
+
+ # No content path
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.storage.NWStorage.contentPath", property(lambda *a: None))
+ theDoc = NWDocument(theProject, xHandle)
+ assert theDoc.deleteDocument() is False
# Cause the delete to fail
with monkeypatch.context() as mp:
@@ -143,26 +160,26 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Make the delete pass
theDoc = NWDocument(theProject, xHandle)
assert theDoc.deleteDocument() is True
- assert not os.path.isfile(docPath)
+ assert not docPath.exists()
# END Test testCoreDocument_Load
@pytest.mark.core
-def testCoreDocument_Methods(mockGUI, fncDir, mockRnd):
+def testCoreDocument_Methods(mockGUI, fncPath, mockRnd):
"""Test other methods of the NWDocument class.
"""
theProject = NWProject(mockGUI)
mockRnd.reset()
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
theDoc = NWDocument(theProject, C.hSceneDoc)
- docPath = os.path.join(fncDir, "content", C.hSceneDoc+".nwd")
+ docPath = fncPath / "content" / f"{C.hSceneDoc}.nwd"
assert theDoc.readDocument() == "### New Scene\n\n"
# Check location
- assert theDoc.getFileLocation() == docPath
+ assert theDoc.getFileLocation() == str(docPath)
# Check the item
assert theDoc.getCurrentItem() is not None
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 678f00e5..d08577d7 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -19,11 +19,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import json
import pytest
from shutil import copyfile
+from pathlib import Path
from mock import causeException
from tools import C, buildTestProject, cmpFiles, writeFile
@@ -35,13 +35,13 @@ from novelwriter.core.project import NWProject
@pytest.mark.core
-def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir):
+def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, tstPaths):
"""Test core functionality of scaning, saving, loading and checking
the index cache file.
"""
- projFile = os.path.join(nwLipsum, "meta", nwFiles.INDEX_FILE)
- testFile = os.path.join(outDir, "coreIndex_LoadSave_tagsIndex.json")
- compFile = os.path.join(refDir, "coreIndex_LoadSave_tagsIndex.json")
+ projFile = Path(nwLipsum) / "meta" / nwFiles.INDEX_FILE
+ testFile = tstPaths.outDir / "coreIndex_LoadSave_tagsIndex.json"
+ compFile = tstPaths.refDir / "coreIndex_LoadSave_tagsIndex.json"
theProject = NWProject(mockGUI)
assert theProject.openProject(nwLipsum)
@@ -62,6 +62,11 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir):
assert theIndex.reIndexHandle(None) is False
+ # No folder for saving
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
+ assert theIndex.saveIndex() is False
+
# Make the save fail
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeException)
@@ -86,6 +91,11 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, outDir, refDir):
assert theIndex._tagsIndex._tags == {}
assert theIndex._itemIndex._items == {}
+ # No folder for sloading
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
+ assert theIndex.loadIndex() is False
+
# Make the load fail
with monkeypatch.context() as mp:
mp.setattr(json, "load", causeException)
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index e2e6aad2..377273da 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -19,8 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
-import shutil
import pytest
from time import time
@@ -42,16 +40,16 @@ from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLR
@pytest.mark.core
-def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
+def testCoreProject_NewRoot(fncPath, tstPaths, mockGUI, mockRnd):
"""Check that new root folders can be added to the project.
"""
- projFile = os.path.join(fncDir, "nwProject.nwx")
- testFile = os.path.join(outDir, "coreProject_NewRoot_nwProject.nwx")
- compFile = os.path.join(refDir, "coreProject_NewRoot_nwProject.nwx")
+ projFile = fncPath / "nwProject.nwx"
+ testFile = tstPaths.outDir / "coreProject_NewRoot_nwProject.nwx"
+ compFile = tstPaths.refDir / "coreProject_NewRoot_nwProject.nwx"
theProject = NWProject(mockGUI)
mockRnd.reset()
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000010"
assert theProject.newRoot(nwItemClass.PLOT) == "0000000000011"
@@ -93,16 +91,16 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd):
@pytest.mark.core
-def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd):
+def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
"""Check that new files can be added to the project.
"""
- projFile = os.path.join(fncDir, "nwProject.nwx")
- testFile = os.path.join(outDir, "coreProject_NewFileFolder_nwProject.nwx")
- compFile = os.path.join(refDir, "coreProject_NewFileFolder_nwProject.nwx")
+ projFile = fncPath / "nwProject.nwx"
+ testFile = tstPaths.outDir / "coreProject_NewFileFolder_nwProject.nwx"
+ compFile = tstPaths.refDir / "coreProject_NewFileFolder_nwProject.nwx"
theProject = NWProject(mockGUI)
mockRnd.reset()
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
# Invalid call
assert theProject.newFolder("New Folder", "1234567890abc") is None
@@ -147,15 +145,15 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI,
assert "0000000000011" in theProject.tree
# Delete new files and folders
- assert os.path.isfile(os.path.join(fncDir, "content", "0000000000012.nwd"))
- assert os.path.isfile(os.path.join(fncDir, "content", "0000000000011.nwd"))
+ assert (fncPath / "content" / "0000000000012.nwd").exists()
+ assert (fncPath / "content" / "0000000000011.nwd").exists()
assert theProject.removeItem("0000000000012") is True
assert theProject.removeItem("0000000000011") is True
assert theProject.removeItem("0000000000010") is True
- assert not os.path.isfile(os.path.join(fncDir, "content", "0000000000012.nwd"))
- assert not os.path.isfile(os.path.join(fncDir, "content", "0000000000011.nwd"))
+ assert not (fncPath / "content" / "0000000000012.nwd").exists()
+ assert not (fncPath / "content" / "0000000000011.nwd").exists()
assert "0000000000010" not in theProject.tree
assert "0000000000011" not in theProject.tree
@@ -167,69 +165,66 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI,
@pytest.mark.core
-def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
+def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
"""Test opening a project.
"""
theProject = NWProject(mockGUI)
mockRnd.reset()
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
- # Rename the project file to check handling
- rName = os.path.join(fncDir, nwFiles.PROJ_FILE)
- wName = os.path.join(fncDir, nwFiles.PROJ_FILE+"_sdfghj")
- os.rename(rName, wName)
- assert theProject.openProject(fncDir) is False
- os.rename(wName, rName)
-
- # Fail on folder structure check
+ # Initialising the storage class fails
with monkeypatch.context() as mp:
- mp.setattr("os.mkdir", causeOSError)
- shutil.rmtree(os.path.join(fncDir, "meta"))
- assert theProject.openProject(fncDir) is False
+ mp.setattr("novelwriter.core.storage.NWStorage.openProjectInPlace", lambda *a, **k: False)
+ assert theProject.openProject(fncPath) is False
# Fail on lock file
assert theProject._storage.writeLockFile()
- assert theProject.openProject(fncDir) is False
+ assert theProject.openProject(fncPath) is False
# Fail to read lockfile (which still opens the project)
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.storage.NWStorage.readLockFile", lambda *a: ["ERROR"])
caplog.clear()
- assert theProject.openProject(fncDir) is True
+ assert theProject.openProject(fncPath) is True
assert "Failed to check lock file" in caplog.text
- assert theProject.closeProject()
+ assert theProject.closeProject()
# Force open with lockfile
assert theProject._storage.writeLockFile()
- assert theProject.openProject(fncDir, overrideLock=True) is True
+ assert theProject.openProject(fncPath, overrideLock=True) is True
assert theProject.closeProject()
+ # Fail getting xml reader
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.storage.NWStorage.getXmlReader", lambda *a: None)
+ assert theProject.openProject(fncPath) is False
+
# Not a novelwriter XML file
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.NOT_NWX_FILE))
- assert theProject.openProject(fncDir) is False
+ assert theProject.openProject(fncPath) is False
assert "Project file does not appear" in mockGUI.lastAlert
# Unknown project file version
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.UNKNOWN_VERSION))
- assert theProject.openProject(fncDir) is False
+ assert theProject.openProject(fncPath) is False
assert "Unknown or unsupported novelWriter project file" in mockGUI.lastAlert
# Other parse error
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.CANNOT_PARSE))
- assert theProject.openProject(fncDir) is False
+ assert theProject.openProject(fncPath) is False
assert "Failed to parse project xml" in mockGUI.lastAlert
# Won't convert legacy file
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY))
mockGUI.askResponse = False
- assert theProject.openProject(fncDir) is False
+ assert theProject.openProject(fncPath) is False
assert "The file format of your project is about to be" in mockGUI.lastQuestion[1]
mockGUI.askResponse = True
@@ -237,17 +232,22 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd):
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999))
mockGUI.askResponse = False
- assert theProject.openProject(fncDir) is False
+ assert theProject.openProject(fncPath) is False
assert "This project was saved by a newer version" in mockGUI.lastQuestion[1]
mockGUI.askResponse = True
+ # Fail checking items should still pass
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.tree.NWTree.updateItemData", lambda *a: False)
+ assert theProject.openProject(fncPath) is True
+
assert theProject.closeProject()
# END Test testCoreProject_Open
@pytest.mark.core
-def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
+def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath):
"""Test saving a project.
"""
theProject = NWProject(mockGUI)
@@ -256,7 +256,12 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
assert theProject.saveProject() is False
mockRnd.reset()
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
+
+ # Fail getting xml writer
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.storage.NWStorage.getXmlWriter", lambda *a: None)
+ assert theProject.saveProject() is False
# Fail writing
with monkeypatch.context() as mp:
@@ -272,11 +277,11 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
@pytest.mark.core
-def testCoreProject_AccessItems(mockGUI, fncDir, mockRnd):
+def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
"""Test helper functions for the project folder.
"""
theProject = NWProject(mockGUI)
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
# Storage Objects
assert isinstance(theProject.index, NWIndex)
@@ -337,12 +342,12 @@ def testCoreProject_AccessItems(mockGUI, fncDir, mockRnd):
@pytest.mark.core
-def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd):
+def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
"""Test the status and importance flag handling.
"""
theProject = NWProject(mockGUI)
mockRnd.reset()
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
statusKeys = [C.sNew, C.sNote, C.sDraft, C.sFinished]
importKeys = [C.iNew, C.iMinor, C.iMajor, C.iMain]
@@ -447,11 +452,11 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd):
@pytest.mark.core
-def testCoreProject_Methods(monkeypatch, mockGUI, fncDir, mockRnd):
+def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
"""Test other project class methods and functions.
"""
theProject = NWProject(mockGUI)
- buildTestProject(theProject, fncDir)
+ buildTestProject(theProject, fncPath)
# Project Name
theProject.data.setName(" A Name ")
@@ -497,9 +502,12 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncDir, mockRnd):
# Spell check
theProject.setProjectChanged(False)
+ theProject._projAltered = False
theProject.data.setSpellCheck(True)
theProject.data.setSpellCheck(False)
- assert theProject.projChanged
+ assert theProject.projChanged is True
+ assert theProject.projAltered is True
+ assert theProject.projOpened > 0
# Spell language
theProject.setProjectChanged(False)
@@ -510,7 +518,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncDir, mockRnd):
assert theProject.data.spellLang is None
theProject.data.setSpellLang("en_GB")
assert theProject.data.spellLang == "en_GB"
- assert theProject.projChanged
+ assert theProject.projChanged is True
# Project Language
theProject.setProjectChanged(False)
@@ -524,6 +532,20 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncDir, mockRnd):
assert theProject.localLookup(1) == "One"
assert theProject.localLookup(10) == "Ten"
+ # Set invalid language
+ theProject.data.setLanguage("foo")
+ theProject._loadProjectLocalisation()
+ assert theProject.localLookup(1) == "One"
+ assert theProject.localLookup(10) == "Ten"
+
+ # Block reading language data
+ theProject.data.setLanguage("en")
+ with monkeypatch.context() as mp:
+ mp.setattr("builtins.open", causeOSError)
+ theProject._loadProjectLocalisation()
+ assert theProject.localLookup(1) == "One"
+ assert theProject.localLookup(10) == "Ten"
+
# Last edited
theProject.setProjectChanged(False)
theProject._data.setLastHandle("0123456789abc", "editor")
@@ -624,7 +646,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
assert theProject.closeProject() is True
# First Item with Meta Data
- orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
+ orphPath = Path(nwLipsum) / "content" / "636b6aa9b697b.nwd"
writeFile(orphPath, (
"%%~name:[Recovered] Mars\n"
"%%~path:5eaea4e8cdee8/636b6aa9b697b\n"
@@ -634,19 +656,19 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
))
# Second Item without Meta Data
- orphPath = os.path.join(nwLipsum, "content", "736b6aa9b697b.nwd")
+ orphPath = Path(nwLipsum) / "content" / "736b6aa9b697b.nwd"
writeFile(orphPath, "\n")
# Invalid File Name
- tstPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.txt")
+ tstPath = Path(nwLipsum) / "content" / "636b6aa9b697b.txt"
writeFile(tstPath, "\n")
# Invalid File Name
- tstPath = os.path.join(nwLipsum, "content", "636b6aa9b697bb.nwd")
+ tstPath = Path(nwLipsum) / "content" / "636b6aa9b697bb.nwd"
writeFile(tstPath, "\n")
# Invalid File Name
- tstPath = os.path.join(nwLipsum, "content", "abcdefghijklm.nwd")
+ tstPath = Path(nwLipsum) / "content" / "abcdefghijklm.nwd"
writeFile(tstPath, "\n")
assert theProject.openProject(nwLipsum)
@@ -686,16 +708,21 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
@pytest.mark.core
-def testCoreProject_Backup(monkeypatch, mockGUI, fncDir, tmpDir):
+def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tmpPath):
"""Test the automated backup feature of the project class. The test
creates a backup of the Minimal test project, and then unzips the
backupd file and checks that the project XML file is identical to
the original file.
"""
theProject = NWProject(mockGUI)
- buildTestProject(theProject, fncDir)
- # Test faulty settings
+ # No Project
+ assert theProject.backupProject(doNotify=False) is False
+
+ buildTestProject(theProject, fncPath)
+
+ # Invalid Settings
+ # ================
# No project
mockGUI.hasProject = False
@@ -707,21 +734,14 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncDir, tmpDir):
assert theProject.backupProject(doNotify=False) is False
# Missing project name
- theProject.mainConf.backupPath = tmpDir
+ theProject.mainConf.backupPath = str(tmpPath)
theProject.data.setName("")
assert theProject.backupProject(doNotify=False) is False
- # Non-existent folder
- theProject.mainConf.backupPath = os.path.join(tmpDir, "nonexistent")
+ # Valid Settings
+ # ==============
+ theProject.mainConf.backupPath = str(tmpPath)
theProject.data.setName("Test Minimal")
- assert theProject.backupProject(doNotify=False) is False
-
- # Subfolder of project (causes infinite loop in zipping)
- theProject.mainConf.backupPath = os.path.join(fncDir, "subdir")
- assert theProject.backupProject(doNotify=False) is False
-
- # Set a valid folder
- theProject.mainConf.backupPath = tmpDir
# Can't make folder
with monkeypatch.context() as mp:
@@ -736,21 +756,21 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncDir, tmpDir):
# Test correct settings
assert theProject.backupProject(doNotify=True) is True
- theFiles = os.listdir(os.path.join(tmpDir, "Test Minimal"))
+ theFiles = list((tmpPath / "Test Minimal").iterdir())
assert len(theFiles) == 1
- theZip = theFiles[0]
+ theZip = theFiles[0].name
assert theZip[:12] == "Backup from "
assert theZip[-4:] == ".zip"
# Extract the archive
- with ZipFile(os.path.join(tmpDir, "Test Minimal", theZip), "r") as inZip:
- inZip.extractall(os.path.join(tmpDir, "extract"))
+ with ZipFile(tmpPath / "Test Minimal" / theZip, mode="r") as inZip:
+ inZip.extractall(tmpPath / "extract")
# Check that the main project file was restored
assert cmpFiles(
- os.path.join(fncDir, "nwProject.nwx"),
- os.path.join(tmpDir, "extract", "nwProject.nwx")
+ fncPath / "nwProject.nwx",
+ tmpPath / "extract" / "nwProject.nwx"
)
# END Test testCoreProject_Backup
diff --git a/tests/test_core/test_core_spellcheck.py b/tests/test_core/test_core_spellcheck.py
index 66dd33b2..b3b00bf4 100644
--- a/tests/test_core/test_core_spellcheck.py
+++ b/tests/test_core/test_core_spellcheck.py
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
-import os
import sys
import pytest
@@ -63,10 +62,10 @@ def testCoreSpell_FakeEnchant(monkeypatch):
@pytest.mark.core
-def testCoreSpell_Enchant(monkeypatch, fncDir):
+def testCoreSpell_Enchant(monkeypatch, fncPath):
"""Test the pyenchant spell checker.
"""
- wList = os.path.join(fncDir, "wordlist.txt")
+ wList = fncPath / "wordlist.txt"
writeFile(wList, "a_word\nb_word\nc_word\n")
# Break the enchant package, and check error handling
@@ -134,13 +133,13 @@ def testCoreSpell_Enchant(monkeypatch, fncDir):
@pytest.mark.core
-def testCoreSpell_SessionWords(fncDir):
+def testCoreSpell_SessionWords(fncPath):
"""Test the handling of the custom word list in the spell checker.
New project sessions should not inherit the project word list from
other sessions, so this test checks that they don't bleed through.
"""
- wList1 = os.path.join(fncDir, "wordlist1.txt")
- wList2 = os.path.join(fncDir, "wordlist2.txt")
+ wList1 = fncPath / "wordlist1.txt"
+ wList2 = fncPath / "wordlist2.txt"
writeFile(wList1, "a_word\nb_word\nc_word\n")
writeFile(wList2, "d_word\ne_word\nf_word\n")
From f5a3280565799b53439f0615283eb625b0809f4d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 8 Nov 2022 22:19:22 +0100
Subject: [PATCH 25/26] Add test coverage of storage class
---
novelwriter/core/project.py | 2 +
novelwriter/core/storage.py | 10 +-
tests/conftest.py | 21 +-
tests/test_core/test_core_storage.py | 332 +++++++++++++++++++++++++++
4 files changed, 349 insertions(+), 16 deletions(-)
create mode 100644 tests/test_core/test_core_storage.py
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 65d292c8..86c14761 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -422,6 +422,7 @@ class NWProject(QObject):
# Save other project data
self._options.saveSettings()
self._index.saveIndex()
+ self._storage.runPostSaveTasks(autoSave=autoSave)
# Update recent projects
self.mainConf.updateRecentCache(
@@ -443,6 +444,7 @@ class NWProject(QObject):
self._tree.writeToCFile()
self._appendSessionStats(idleTime)
self._storage.clearLockFile()
+ self._storage.closeSession()
self.clearProject()
self.lockedBy = None
return True
diff --git a/novelwriter/core/storage.py b/novelwriter/core/storage.py
index 40751909..9b28f31a 100644
--- a/novelwriter/core/storage.py
+++ b/novelwriter/core/storage.py
@@ -113,10 +113,10 @@ class NWStorage:
return True
- def openProjectArchive(self, path):
+ def openProjectArchive(self, path): # pragma: no cover
pass
- def runPostSaveTasks(self, autoSave=False):
+ def runPostSaveTasks(self, autoSave=False): # pragma: no cover
"""Run tasks after the project has been saved.
"""
if self._openMode == self.MODE_INPLACE:
@@ -295,9 +295,8 @@ class NWStorage:
if newProject:
# If it's a new project, we check that there is no existing
# project in the selected path.
- projFile = path / nwFiles.PROJ_FILE
- if projFile.exists():
- logger.error("A project already exists in this path")
+ if path.exists() and len(list(path.iterdir())) > 0:
+ logger.error("The new project folder is not empty")
self.clear()
return False
@@ -310,6 +309,7 @@ class NWStorage:
(path / "meta").mkdir(exist_ok=True)
except Exception as exc:
logger.error("Failed to create required project folders", exc_info=exc)
+ self.clear()
return False
if not checkLegacy:
diff --git a/tests/conftest.py b/tests/conftest.py
index bcaa2ea3..605393e6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -29,7 +29,7 @@ from pathlib import Path
from mock import MockGuiMain
from tools import cleanProject
-sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)))
+sys.path.insert(1, str(Path(__file__).parent.parent.absolute()))
import novelwriter # noqa: E402
@@ -64,15 +64,15 @@ def tmpDir():
return theDir
-@pytest.fixture(scope="function")
+@pytest.fixture(scope="session")
def tmpPath(tmpDir):
- """A temporary folder for a single test function.
+ """A temporary folder for the test session. Path version.
"""
return Path(tmpDir)
@pytest.fixture(scope="session")
-def tstPaths(tmpDir):
+def tstPaths(tmpPath):
"""Returns an object that can provide the various paths needed for
running tests.
"""
@@ -80,7 +80,7 @@ def tstPaths(tmpDir):
testDir = Path(__file__).parent
filesDir = testDir / "files"
refDir = testDir / "reference"
- outDir = testDir / tmpDir / "results"
+ outDir = tmpPath / "results"
store = _Store()
store.outDir.mkdir(exist_ok=True)
@@ -89,14 +89,13 @@ def tstPaths(tmpDir):
@pytest.fixture(scope="function")
-def fncPath(tmpDir):
- """A temporary folder for a single test function.
+def fncPath(tmpPath):
+ """A temporary folder for a single test function. Path version.
"""
- fncPath = Path(tmpDir) / "f_temp"
+ fncPath = tmpPath / "function"
if fncPath.is_dir():
shutil.rmtree(fncPath)
- if not fncPath.is_dir():
- fncPath.mkdir()
+ fncPath.mkdir(exist_ok=True)
return fncPath
@@ -133,7 +132,7 @@ def outDir(tmpDir):
def fncDir(tmpDir):
"""A temporary folder for a single test function.
"""
- fncDir = os.path.join(tmpDir, "f_temp")
+ fncDir = os.path.join(tmpDir, "function")
if os.path.isdir(fncDir):
shutil.rmtree(fncDir)
if not os.path.isdir(fncDir):
diff --git a/tests/test_core/test_core_storage.py b/tests/test_core/test_core_storage.py
new file mode 100644
index 00000000..7f986185
--- /dev/null
+++ b/tests/test_core/test_core_storage.py
@@ -0,0 +1,332 @@
+"""
+novelWriter – NWStorage Class Tester
+====================================
+
+This file is a part of novelWriter
+Copyright 2018–2022, 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 .
+"""
+
+from zipfile import ZipFile
+import pytest
+
+from mock import causeOSError
+from tools import C, buildTestProject, writeFile
+
+from novelwriter.constants import nwFiles
+from novelwriter.core.project import NWProject
+from novelwriter.core.storage import NWStorage
+from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
+
+
+class MockProject:
+ pass
+
+
+@pytest.mark.core
+def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
+ """Test opening a project in a folder.
+ """
+ theProject = NWProject(mockGUI)
+ mockRnd.reset()
+ buildTestProject(theProject, fncPath)
+ theProject.closeProject()
+
+ # Create instance
+ storage = NWStorage(theProject)
+
+ # Check defaults
+ assert storage.storagePath is None
+ assert storage.runtimePath is None
+ assert storage.contentPath is None
+ assert storage._openMode == NWStorage.MODE_INACTIVE
+
+ # Check closed project return values
+ assert storage.isOpen() is False
+ assert storage.getXmlReader() is None
+ assert storage.getXmlWriter() is None
+ assert bool(storage.getDocument(C.hSceneDoc)) is False
+ assert storage.getMetaFile("file") is None
+ assert storage.getCacheFile("file") is None
+
+ # Open project as a new project should fail
+ assert storage.openProjectInPlace(fncPath, newProject=True) is False
+
+ # Opening as a no-new project is fine
+ assert storage.openProjectInPlace(fncPath, newProject=False) is True
+
+ # Opening the project file is also fine
+ assert storage.openProjectInPlace(fncPath / nwFiles.PROJ_FILE, newProject=False) is True
+
+ # Check settings
+ assert storage.storagePath == fncPath
+ assert storage.runtimePath == fncPath
+ assert storage.contentPath == fncPath / "content"
+ assert storage._openMode == NWStorage.MODE_INPLACE
+
+ # Open the project itself
+ theProject.openProject(fncPath)
+ storage = theProject.storage
+
+ # Get XML components
+ assert isinstance(storage.getXmlReader(), ProjectXMLReader)
+ assert isinstance(storage.getXmlWriter(), ProjectXMLWriter)
+
+ # Get document
+ assert storage.getDocument(C.hSceneDoc).readDocument() == "### New Scene\n\n"
+
+ # Get paths
+ assert storage.getMetaFile("stuff") == fncPath / "meta" / "stuff"
+ assert storage.getCacheFile("stuff") == fncPath / "cache" / "stuff"
+
+ # Clean up
+ assert theProject.closeProject() is True
+
+ # Check closed project return values (again)
+ assert storage.isOpen() is False
+ assert storage.getXmlReader() is None
+ assert storage.getXmlWriter() is None
+ assert bool(storage.getDocument(C.hSceneDoc)) is False
+ assert storage.getMetaFile("file") is None
+ assert storage.getCacheFile("file") is None
+
+# END Test testCoreStorage_ProjectInPlace
+
+
+@pytest.mark.core
+def testCoreStorage_LockFile(monkeypatch, fncPath):
+ """Test the project lock file.
+ """
+ monkeypatch.setattr("novelwriter.core.storage.time", lambda: 1000.0)
+
+ storage = NWStorage(MockProject())
+ assert storage.isOpen() is False
+
+ # Project not open, so cannot read/write lock file
+ assert storage.readLockFile() == ["ERROR"]
+ assert storage.writeLockFile() is False
+ assert storage.clearLockFile() is False
+
+ # Set a path to work with
+ lockFilePath = fncPath / nwFiles.PROJ_LOCK
+ storage._lockFilePath = lockFilePath
+
+ # Path is set, but there is no lockfile
+ assert storage.readLockFile() == []
+
+ # Write lockfile fails
+ with monkeypatch.context() as mp:
+ mp.setattr("pathlib.Path.write_text", causeOSError)
+ assert storage.writeLockFile() is False
+ assert not lockFilePath.exists()
+
+ # Successful write
+ assert storage.writeLockFile() is True
+ assert lockFilePath.exists()
+ assert lockFilePath.read_text().split(";")[3] == "1000"
+
+ # Read lockfile fails
+ with monkeypatch.context() as mp:
+ mp.setattr("pathlib.Path.read_text", causeOSError)
+ assert storage.readLockFile() == ["ERROR"]
+ assert lockFilePath.exists()
+
+ # Successful read
+ assert storage.readLockFile() == [
+ storage.mainConf.hostName,
+ storage.mainConf.osType,
+ storage.mainConf.kernelVer,
+ "1000",
+ ]
+
+ # Write an invalid lockfile
+ writeFile(lockFilePath, "a;b;c")
+ assert storage.readLockFile() == ["ERROR"]
+
+ # Fail to remove lockfile
+ with monkeypatch.context() as mp:
+ mp.setattr("pathlib.Path.unlink", causeOSError)
+ assert storage.clearLockFile() is False
+ assert lockFilePath.exists()
+
+ # Successful remove
+ assert storage.clearLockFile() is True
+ assert not lockFilePath.exists()
+
+# END Test testCoreStorage_LockFile
+
+
+@pytest.mark.core
+def testCoreStorage_PrepareStorage(monkeypatch, fncPath):
+ """Test the project path preparation functions.
+ """
+ storage = NWStorage(MockProject())
+ assert storage.isOpen() is False
+
+ # No path set
+ assert storage._prepareStorage() is False
+
+ # Set path to home
+ storage._runtimePath = fncPath
+ with monkeypatch.context() as mp:
+ mp.setattr("pathlib.Path.home", lambda: fncPath)
+ assert storage._prepareStorage() is False
+
+ # Fail on mkdir
+ storage._runtimePath = fncPath
+ with monkeypatch.context() as mp:
+ mp.setattr("pathlib.Path.mkdir", causeOSError)
+ assert storage._prepareStorage() is False
+
+ # Set up the folder
+ storage._runtimePath = fncPath
+ assert storage._prepareStorage(checkLegacy=False) is True
+ assert (fncPath / "content").exists()
+ assert (fncPath / "cache").exists()
+ assert (fncPath / "meta").exists()
+
+ # Add a legacy folder
+ storage._runtimePath = fncPath
+ dataDir = fncPath / "data_0"
+ dataDir.mkdir()
+ assert storage._prepareStorage(checkLegacy=True) is True
+ assert not dataDir.exists()
+
+ # We cannot add a new project here
+ storage._runtimePath = fncPath
+ assert storage._prepareStorage(checkLegacy=False, newProject=True) is False
+
+ # Legacy Data Folder
+ # ==================
+ storage._runtimePath = fncPath
+
+ data = []
+ files = []
+ for c in "0123456789abcdefX":
+ dataDir = fncPath / f"data_{c}"
+ dataDir.mkdir()
+ data.append(dataDir)
+
+ nwdFile = dataDir / f"00000000000{c}_main.nwd"
+ bakFile = dataDir / f"00000000000{c}_main.bak"
+ nwdFile.write_text("#")
+ bakFile.write_text("#")
+ files.append(nwdFile)
+ files.append(bakFile)
+
+ for item in files:
+ assert item.exists()
+
+ # Pollute folder 7 and 8
+ (data[7] / "stuff.txt").write_text("foo")
+ (data[8] / "bar").mkdir()
+
+ # Process folders
+ for i in range(9):
+ storage._legacyDataFolder(fncPath, data[i])
+
+ # Files form 0 to 8 should now be in content
+ for c in "012345678":
+ assert (fncPath / "content" / f"{c}00000000000{c}.nwd").exists()
+
+ # Folders 0 to 6 should be deleted
+ for i in range(7):
+ assert not data[i].exists()
+
+ # While 7 and 8 remain
+ assert data[7].exists()
+ assert data[8].exists()
+
+ # So does folder X, which is invalid
+ storage._legacyDataFolder(fncPath, data[16])
+ assert data[16].exists()
+
+ # Fail cleanup of folder 9
+ with monkeypatch.context() as mp:
+ mp.setattr("pathlib.Path.rename", causeOSError)
+ mp.setattr("pathlib.Path.unlink", causeOSError)
+ storage._legacyDataFolder(fncPath, data[9])
+ assert data[9].exists()
+ assert not (fncPath / "content" / "9000000000009.nwd").exists()
+
+ # Run the remaining through the prepare storage call
+ assert storage._prepareStorage(checkLegacy=True) is True
+ for c in "0123456789abcdef":
+ assert (fncPath / "content" / f"{c}00000000000{c}.nwd").exists()
+
+ # Deprecated Files
+ # ================
+
+ remove = [
+ fncPath / "meta" / "mainOptions.json",
+ fncPath / "meta" / "exportOptions.json",
+ fncPath / "meta" / "outlineOptions.json",
+ fncPath / "meta" / "timelineOptions.json",
+ fncPath / "meta" / "docMergeOptions.json",
+ fncPath / "meta" / "sessionLogOptions.json",
+ fncPath / "ToC.json",
+ ]
+ for depFile in remove:
+ depFile.write_text("foo")
+ assert depFile.exists()
+
+ with monkeypatch.context() as mp:
+ mp.setattr("pathlib.Path.unlink", causeOSError)
+ storage._deleteDeprecatedFiles(fncPath)
+ for depFile in remove:
+ assert depFile.exists()
+
+ storage._deleteDeprecatedFiles(fncPath)
+ for depFile in remove:
+ assert not depFile.exists()
+
+# END Test testCoreStorage_PrepareStorage
+
+
+@pytest.mark.core
+def testCoreStorage_ZipIt(monkeypatch, mockGUI, fncPath, tmpPath, mockRnd):
+ """Test making a zip archive of a project.
+ """
+ zipFile = tmpPath / "project.zip"
+
+ theProject = NWProject(mockGUI)
+ storage = theProject.storage
+ assert storage.zipIt(zipFile) is False
+
+ # Make a project
+ mockRnd.reset()
+ buildTestProject(theProject, fncPath)
+
+ # Fail to create archive
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.storage.ZipFile.write", causeOSError)
+ assert storage.zipIt(zipFile) is False
+
+ # Create archive
+ assert storage.zipIt(zipFile) is True
+
+ # Check content
+ with ZipFile(zipFile, mode="r") as archive:
+ names = archive.namelist()
+ assert names[0] == nwFiles.PROJ_FILE
+ assert names[1] == f"meta/{nwFiles.OPTS_FILE}"
+ assert names[2] == f"meta/{nwFiles.INDEX_FILE}"
+ assert names[3] == f"content/{C.hTitlePage}.nwd"
+ assert names[4] == f"content/{C.hChapterDoc}.nwd"
+ assert names[5] == f"content/{C.hSceneDoc}.nwd"
+
+ theProject.closeProject()
+
+# END Test testCoreStorage_ZipIt
From 0b44cf9c0a2e4eb80f2c67e84db061e761cd72f2 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Tue, 8 Nov 2022 22:29:54 +0100
Subject: [PATCH 26/26] Fix issues with variying file order in zip archive
---
tests/test_core/test_core_storage.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/tests/test_core/test_core_storage.py b/tests/test_core/test_core_storage.py
index 7f986185..7545a870 100644
--- a/tests/test_core/test_core_storage.py
+++ b/tests/test_core/test_core_storage.py
@@ -320,12 +320,12 @@ def testCoreStorage_ZipIt(monkeypatch, mockGUI, fncPath, tmpPath, mockRnd):
# Check content
with ZipFile(zipFile, mode="r") as archive:
names = archive.namelist()
- assert names[0] == nwFiles.PROJ_FILE
- assert names[1] == f"meta/{nwFiles.OPTS_FILE}"
- assert names[2] == f"meta/{nwFiles.INDEX_FILE}"
- assert names[3] == f"content/{C.hTitlePage}.nwd"
- assert names[4] == f"content/{C.hChapterDoc}.nwd"
- assert names[5] == f"content/{C.hSceneDoc}.nwd"
+ assert nwFiles.PROJ_FILE in names
+ assert f"meta/{nwFiles.OPTS_FILE}" in names
+ assert f"meta/{nwFiles.INDEX_FILE}" in names
+ assert f"content/{C.hTitlePage}.nwd" in names
+ assert f"content/{C.hChapterDoc}.nwd" in names
+ assert f"content/{C.hSceneDoc}.nwd" in names
theProject.closeProject()