From ba88160debe563ec0e0f9cb4e7bc1ca3966fe607 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 29 Sep 2022 20:07:52 +0200 Subject: [PATCH 01/18] Rough framework for XML reader/writer classes --- novelwriter/core/projectxml.py | 104 +++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 novelwriter/core/projectxml.py diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py new file mode 100644 index 00000000..b47a6181 --- /dev/null +++ b/novelwriter/core/projectxml.py @@ -0,0 +1,104 @@ +""" +novelWriter – Project XML Read/Write +==================================== +Classes for reading and writing the project XML file + +File History: +Created: 2022-09-28 [1.7.b1] + +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 os +import logging + +from enum import Enum +from lxml import etree + +logger = logging.getLogger(__name__) + + +class XMLReadState(Enum): + + NO_ACTION = 0 + NO_ERROR = 1 + PARSED_BACKUP = 2 + CANNOT_PARSE = 3 + +# END Class XMLReadState + + +class ProjectXMLReader: + + def __init__(self, path): + + self._path = path + self._state = XMLReadState.NO_ACTION + + return + + def read(self): + """ + """ + try: + xml = etree.parse(self._path) + self._state = XMLReadState.NO_ERROR + + except Exception as exc: + # Trying to open backup file instead + logger.error("Failed to parse project xml", exc_info=exc) + self._state = XMLReadState.CANNOT_PARSE + + backFile = self._path[:-3]+"bak" + if os.path.isfile(backFile): + try: + xml = etree.parse(backFile) + 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) + self._state = XMLReadState.CANNOT_PARSE + return False + else: + return False + + return True + + ## + # Internal Functions + ## + +# END Class ProjectXMLReader + + +class ProjectXMLWriter: + + def __init__(self, path): + + self._path = path + self._error = None + + return + + def write(self): + return + + ## + # Internal Functions + ## + +# END Class ProjectXMLWriter From ea80fd3c719ce6ec6b4eacc2d7f0bccdca1fbd26 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 30 Oct 2022 22:51:31 +0100 Subject: [PATCH 02/18] Complete the XML parsing --- novelwriter/core/item.py | 107 ++++------- novelwriter/core/project.py | 211 +++++++-------------- novelwriter/core/projectxml.py | 331 ++++++++++++++++++++++++++++++++- novelwriter/core/status.py | 25 +-- novelwriter/core/tree.py | 14 +- 5 files changed, 447 insertions(+), 241 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 40426d67..66cb7fbe 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -148,7 +148,7 @@ class NWItem: return self._cursorPos ## - # XML Pack/Unpack + # Pack/Unpack Data ## def packXML(self, xParent): @@ -167,7 +167,7 @@ class NWItem: metaAttrib = {} metaAttrib["expanded"] = str(self._expanded) if self._type == nwItemType.FILE: - metaAttrib["mainHeading"] = str(self._heading) + metaAttrib["heading"] = str(self._heading) metaAttrib["charCount"] = str(self._charCount) metaAttrib["wordCount"] = str(self._wordCount) metaAttrib["paraCount"] = str(self._paraCount) @@ -185,70 +185,31 @@ class NWItem: return - def unpackXML(self, xItem): - """Set the values from an XML entry of type 'item'. + def unpack(self, data): + """Set the values from a data dictionary. """ - if xItem.tag != "item": - logger.error("XML entry is not an NWItem") - return False - - if "handle" in xItem.attrib: - self.setHandle(xItem.attrib["handle"]) + if "handle" in data: + self.setHandle(data["handle"]) else: logger.error("XML item entry does not have a handle") return False - self.setParent(xItem.attrib.get("parent", None)) - self.setRoot(xItem.attrib.get("root", None)) - self.setOrder(xItem.attrib.get("order", 0)) - self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE)) - self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS)) - self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT)) - - for xValue in xItem: - if xValue.tag == "meta": - self.setExpanded(xValue.attrib.get("expanded", False)) - self.setMainHeading(xValue.attrib.get("mainHeading", "H0")) - self.setCharCount(xValue.attrib.get("charCount", 0)) - self.setWordCount(xValue.attrib.get("wordCount", 0)) - self.setParaCount(xValue.attrib.get("paraCount", 0)) - self.setCursorPos(xValue.attrib.get("cursorPos", 0)) - elif xValue.tag == "name": - self.setName(xValue.text) - self.setStatus(xValue.attrib.get("status", None)) - self.setImport(xValue.attrib.get("import", None)) - self.setActive(xValue.attrib.get("active", True)) - - # ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases. - if "exported" in xValue.attrib: - self.setActive(xValue.attrib.get("exported", True)) - - # Legacy Format (1.3 and earlier) - elif xValue.tag == "status": - self.setImportStatus(xValue.text) - elif xValue.tag == "type": - self.setType(xValue.text) - elif xValue.tag == "class": - self.setClass(xValue.text) - elif xValue.tag == "layout": - self.setLayout(xValue.text) - elif xValue.tag == "expanded": - self.setExpanded(xValue.text) - elif xValue.tag == "exported": - self.setActive(xValue.text) - elif xValue.tag == "charCount": - self.setCharCount(xValue.text) - elif xValue.tag == "wordCount": - self.setWordCount(xValue.text) - elif xValue.tag == "paraCount": - self.setParaCount(xValue.text) - elif xValue.tag == "cursorPos": - self.setCursorPos(xValue.text) - else: - # Sliently skip as we may otherwise cause orphaned - # items if an otherwise valid file is opened by a - # version of novelWriter that doesn't know the tag - logger.error("Unknown tag '%s'", xValue.tag) + 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.setExpanded(data.get("expanded", False)) + self.setMainHeading(data.get("mainHeading", "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.setName(data.get("label", "")) + self.setStatus(data.get("status", None)) + self.setImport(data.get("import", None)) + self.setActive(data.get("active", True)) # Make some checks to ensure consistency if self._type == nwItemType.ROOT: @@ -451,8 +412,6 @@ class NWItem: self._type = value elif isItemType(value): self._type = nwItemType[value] - elif value == "TRASH": - self._type = nwItemType.ROOT else: logger.error("Unrecognised item type '%s'", value) self._type = nwItemType.NO_TYPE @@ -479,8 +438,6 @@ class NWItem: self._layout = value elif isItemLayout(value): self._layout = nwItemLayout[value] - elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"): - self._layout = nwItemLayout.DOCUMENT else: logger.error("Unrecognised item layout '%s'", value) self._layout = nwItemLayout.NO_LAYOUT @@ -532,25 +489,37 @@ class NWItem: def setCharCount(self, count): """Set the character count, and ensure that it is an integer. """ - self._charCount = max(0, checkInt(count, 0)) + if isinstance(count, int): + self._charCount = max(0, count) + else: + self._charCount = 0 return def setWordCount(self, count): """Set the word count, and ensure that it is an integer. """ - self._wordCount = max(0, checkInt(count, 0)) + if isinstance(count, int): + self._wordCount = max(0, count) + else: + self._wordCount = 0 return def setParaCount(self, count): """Set the paragraph count, and ensure that it is an integer. """ - self._paraCount = max(0, checkInt(count, 0)) + if isinstance(count, int): + self._paraCount = max(0, count) + else: + self._paraCount = 0 return def setCursorPos(self, position): """Set the cursor position, and ensure that it is an integer. """ - self._cursorPos = max(0, checkInt(position, 0)) + if isinstance(position, int): + self._cursorPos = max(0, position) + else: + self._cursorPos = 0 return def saveInitialCount(self): diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 66f9617b..76294a08 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -35,18 +35,19 @@ from functools import partial from PyQt5.QtCore import QCoreApplication +from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert +from novelwriter.error import logException +from novelwriter.common import ( + checkString, checkStringNone, isHandle, formatTimeStamp, + makeFileNameSafe, hexToInt, minmax, simplified +) 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.document import NWDoc -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.error import logException -from novelwriter.common import ( - checkString, checkBool, checkInt, checkStringNone, isHandle, formatTimeStamp, - makeFileNameSafe, hexToInt, minmax, simplified -) +from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState from novelwriter.constants import trConst, nwFiles, nwLabels logger = logging.getLogger(__name__) @@ -62,6 +63,8 @@ class NWProject: self.mainConf = novelwriter.CONFIG self.mainGui = mainGui + self._data = {} + # Core Elements self._optState = OptionState(self) # Project-specific GUI options self._projTree = NWTree(self) # The project tree @@ -477,80 +480,45 @@ class NWProject: # Open The Project XML File # ========================= - try: - nwXML = etree.parse(fileName) - except Exception as exc: - self.mainGui.makeAlert(self.tr( - "Failed to parse project xml." - ), nwAlert.ERROR, exception=exc) + xmlReader = ProjectXMLReader(fileName) + xmlParsed = xmlReader.read() + xmlData = xmlReader.data - # Trying to open backup file instead - backFile = fileName[:-3]+"bak" - if os.path.isfile(backFile): + print(json.dumps(xmlData, indent=2)) + + nwxRoot = xmlData.get("xmlRoot", "") + appVersion = xmlData.get("appVersion", self.tr("Unknown")) + hexVersion = xmlData.get("hexVersion", 0x0000) + xmlVersion = xmlData.get("xmlVersion", self.tr("Unknown")) + + if not xmlParsed: + if xmlReader.state == XMLReadState.NOT_NWX_FILE: self.mainGui.makeAlert(self.tr( - "Attempting to open backup project file instead." - ), nwAlert.INFO) - try: - nwXML = etree.parse(backFile) - except Exception as exc: - self.mainGui.makeAlert(self.tr( - "Failed to parse project xml." - ), nwAlert.ERROR, exception=exc) - self.clearProject() - return False + "Project file does not appear to be a novelWriterXML file." + ), nwAlert.ERROR) + elif xmlReader.state == XMLReadState.UNKNOWN_VERSION: + self.mainGui.makeAlert(self.tr( + "Unknown or unsupported novelWriter project file format. " + "The project cannot be opened by this version of novelWriter. " + "The file was saved with novelWriter version {0}." + ).format(appVersion), nwAlert.ERROR) else: - self.clearProject() - return False + self.mainGui.makeAlert(self.tr( + "Failed to parse project xml." + ), nwAlert.ERROR) - xRoot = nwXML.getroot() - nwxRoot = xRoot.tag + self.clearProject() + return False - appVersion = xRoot.attrib.get("appVersion", self.tr("Unknown")) - hexVersion = xRoot.attrib.get("hexVersion", "0x0") - fileVersion = xRoot.attrib.get("fileVersion", self.tr("Unknown")) + self._data = xmlData logger.debug("XML root is '%s'", nwxRoot) - logger.debug("File version is '%s'", fileVersion) + logger.debug("File version is '%s'", xmlVersion) - # Check File Type - # =============== + # Check Legacy Upgrade + # ==================== - if nwxRoot != "novelWriterXML": - self.mainGui.makeAlert(self.tr( - "Project file does not appear to be a novelWriterXML file." - ), nwAlert.ERROR) - self.clearProject() - return False - - # Check Project Storage Version - # ============================= - - # Changes: - # 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. - # 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.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. Introduced in version 1.7. - - if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"): - self.mainGui.makeAlert(self.tr( - "Unknown or unsupported novelWriter project file format. " - "The project cannot be opened by this version of novelWriter. " - "The file was saved with novelWriter version {0}." - ).format(appVersion), nwAlert.ERROR) - self.clearProject() - return False - - if fileVersion != self.FILE_VERSION: + if xmlReader.state == XMLReadState.WAS_LEGACY: msgYes = self.mainGui.askQuestion( self.tr("File Version"), self.tr( @@ -581,79 +549,40 @@ class NWProject: self.clearProject() return False - # Start Parsing the XML - # ===================== + # Extract Data + # ============ - for xChild in xRoot: - if xChild.tag == "project": - logger.debug("Found project meta") - for xItem in xChild: - if xItem.text is None: - continue - if xItem.tag == "name": - self.projName = simplified(checkString(xItem.text, "")) - logger.info("Project Name: '%s'", self.projName) - elif xItem.tag == "title": - self.bookTitle = simplified(checkString(xItem.text, "")) - logger.info("Project Title: '%s'", self.bookTitle) - elif xItem.tag == "author": - author = simplified(checkString(xItem.text, "")) - if author: - self.bookAuthors.append(author) - logger.debug("Author: '%s'", author) - elif xItem.tag == "saveCount": - self.saveCount = checkInt(xItem.text, 0) - elif xItem.tag == "autoCount": - self.autoCount = checkInt(xItem.text, 0) - elif xItem.tag == "editTime": - self.editTime = checkInt(xItem.text, 0) + xmlProject = xmlData.get("project", {}) - elif xChild.tag == "settings": - logger.debug("Found project settings") - for xItem in xChild: - if xItem.text is None: - continue - if xItem.tag == "doBackup": - self.doBackup = checkBool(xItem.text, False) - elif xItem.tag == "language": - self.projLang = checkStringNone(xItem.text, None) - elif xItem.tag == "spellCheck": - self.spellCheck = checkBool(xItem.text, False) - elif xItem.tag == "spellLang": - self.projSpell = checkStringNone(xItem.text, None) - elif xItem.tag == "lastEdited": - self.lastEdited = checkStringNone(xItem.text, None) - elif xItem.tag == "lastViewed": - self.lastViewed = checkStringNone(xItem.text, None) - elif xItem.tag == "lastNovel": - self.lastNovel = checkStringNone(xItem.text, None) - elif xItem.tag == "lastOutline": - self.lastOutline = checkStringNone(xItem.text, None) - elif xItem.tag == "lastWordCount": - self.lastWCount = checkInt(xItem.text, 0) - elif xItem.tag == "novelWordCount": - self.lastNovelWC = checkInt(xItem.text, 0) - elif xItem.tag == "notesWordCount": - self.lastNotesWC = checkInt(xItem.text, 0) - elif xItem.tag == "status": - self.statusItems.unpackXML(xItem) - elif xItem.tag == "importance": - self.importItems.unpackXML(xItem) - elif xItem.tag == "autoReplace": - for xEntry in xItem: - if xEntry.tag == "entry" and "key" in xEntry.attrib: - self.autoReplace[xEntry.attrib["key"]] = checkString( - xEntry.text, "ERROR" - ) - elif xItem.tag == "titleFormat": - titleFormat = self.titleFormat.copy() - for xEntry in xItem: - titleFormat[xEntry.tag] = checkString(xEntry.text, "") - self.setTitleFormat(titleFormat) + self.projName = xmlProject.get("name", "") + self.bookTitle = xmlProject.get("title", "") + self.bookAuthors = xmlProject.get("authors", []) + self.saveCount = xmlProject.get("saveCount", 0) + self.autoCount = xmlProject.get("autoCount", 0) + self.editTime = xmlProject.get("editTime", 0) - elif xChild.tag == "content": - logger.debug("Found project content") - self._projTree.unpackXML(xChild) + logger.info("Project Name: '%s'", self.projName) + logger.info("Project Title: '%s'", self.bookTitle) + + xmlSettings = xmlData.get("settings", {}) + + self.doBackup = xmlSettings.get("doBackup", False) + self.projLang = xmlSettings.get("language", None) + self.spellCheck = xmlSettings.get("spellCheck", False) + self.projSpell = xmlSettings.get("spellLang", None) + self.lastEdited = xmlSettings.get("lastEdited", None) + self.lastViewed = xmlSettings.get("lastViewed", None) + self.lastNovel = xmlSettings.get("lastNovel", None) + self.lastOutline = xmlSettings.get("lastOutline", None) + self.lastWCount = xmlSettings.get("lastWordCount", 0) + self.lastNovelWC = xmlSettings.get("novelWordCount", 0) + self.lastNotesWC = xmlSettings.get("notesWordCount", 0) + self.statusItems.unpack(xmlSettings.get("status", {})) + self.importItems.unpack(xmlSettings.get("import", {})) + self.autoReplace = xmlSettings.get("autoReplace", {}) + self.titleFormat.update(xmlSettings.get("titleFormat", {})) + + self._projTree.unpack(xmlData.get("content", [])) self._optState.loadSettings() diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index b47a6181..2aa2d248 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -29,15 +29,33 @@ import logging from enum import Enum from lxml import etree +from novelwriter.common import ( + checkBool, checkInt, checkStringNone, minmax, simplified, checkString +) + logger = logging.getLogger(__name__) +NUM_VERSION = { + "1.0": 0x0100, + "1.1": 0x0101, + "1.2": 0x0102, + "1.3": 0x0103, + "1.4": 0x0104, +} + + class XMLReadState(Enum): - NO_ACTION = 0 - NO_ERROR = 1 - PARSED_BACKUP = 2 - CANNOT_PARSE = 3 + NO_ACTION = 0 + NO_ERROR = 1 + PARSED_BACKUP = 2 + CANNOT_PARSE = 3 + NOT_NWX_FILE = 4 + UNKNOWN_VERSION = 5 + PARSING_ERROR = 6 + PARSED_OK = 7 + WAS_LEGACY = 8 # END Class XMLReadState @@ -49,11 +67,34 @@ class ProjectXMLReader: self._path = path self._state = XMLReadState.NO_ACTION + self._data = {} + self._version = 0x0000 + self._statusData = {} + self._statusMap = {} + return + ## + # Properties + ## + + @property + def data(self): + return self._data + + @property + def state(self): + return self._state + + ## + # Methods + ## + def read(self): + """Read and parse the project XML file. """ - """ + self._data = {} + try: xml = etree.parse(self._path) self._state = XMLReadState.NO_ERROR @@ -76,12 +117,292 @@ class ProjectXMLReader: else: return False + xRoot = xml.getroot() + self._data["xmlRoot"] = str(xRoot.tag) + if xRoot.tag != "novelWriterXML": + self._state = XMLReadState.NOT_NWX_FILE + return False + + # Changes: + # 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. + # 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.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. Introduced in version 1.7. + + fileVersion = str(xRoot.attrib.get("fileVersion", "")) + if fileVersion in NUM_VERSION: + self._version = NUM_VERSION[fileVersion] + else: + self._state = XMLReadState.UNKNOWN_VERSION + return False + + self._data["xmlVersion"] = self._version + self._data["appVersion"] = str(xRoot.attrib.get("appVersion", "")) + self._data["hexVersion"] = str(xRoot.attrib.get("appVersion", "")) + self._data["timeStamp"] = str(xRoot.attrib.get("timeStamp", "")) + + status = True + for xSection in xRoot: + if xSection.tag == "project": + status &= self._parseProjectMeta(xSection) + elif xSection.tag == "settings": + status &= self._parseProjectSettings(xSection) + elif xSection.tag == "content": + if self._version >= 0x0104: + status &= self._parseProjectContent(xSection) + else: + status &= self._parseProjectContentLegacy(xSection) + else: + logger.warning("Ignored in xml", xSection.tag) + + if not status: + self._state = XMLReadState.PARSING_ERROR + return False + + if self._version == 0x0104: + self._state = XMLReadState.PARSED_OK + else: + self._state = XMLReadState.WAS_LEGACY + return True ## # Internal Functions ## + def _parseProjectMeta(self, xSection): + """Parse the project section of the XML file. + """ + logger.debug("Parsing xml ") + data = {} + authors = [] + for xItem in xSection: + if xItem.tag == "name": + data["name"] = simplified(checkString(xItem.text, "")) + elif xItem.tag == "title": + data["title"] = simplified(checkString(xItem.text, "")) + elif xItem.tag == "author": + authors.append(simplified(checkString(xItem.text, ""))) + elif xItem.tag == "saveCount": + data["saveCount"] = checkInt(xItem.text, 0) + elif xItem.tag == "autoCount": + data["autoCount"] = checkInt(xItem.text, 0) + elif xItem.tag == "editTime": + data["editTime"] = checkInt(xItem.text, 0) + else: + logger.warning("Ignored in xml", xItem.tag) + + data["authors"] = authors + self._data["project"] = data + + return True + + def _parseProjectSettings(self, xSection): + """Parse the settings section of the XML file. + """ + logger.debug("Parsing xml ") + + data = {} + autoReplace = {} + titleFormat = {} + for xItem in xSection: + if xItem.tag == "doBackup": + data["doBackup"] = checkBool(xItem.text, False) + elif xItem.tag == "language": + data["language"] = checkStringNone(xItem.text, None) + elif xItem.tag == "spellCheck": + data["spellCheck"] = checkBool(xItem.text, False) + elif xItem.tag == "spellLang": + data["spellLang"] = checkStringNone(xItem.text, None) + elif xItem.tag == "lastEdited": + data["lastEdited"] = checkStringNone(xItem.text, None) + elif xItem.tag == "lastViewed": + data["lastViewed"] = checkStringNone(xItem.text, None) + elif xItem.tag == "lastNovel": + data["lastNovel"] = checkStringNone(xItem.text, None) + elif xItem.tag == "lastOutline": + data["lastOutline"] = checkStringNone(xItem.text, None) + elif xItem.tag == "lastWordCount": + data["lastWordCount"] = checkInt(xItem.text, 0) + elif xItem.tag == "novelWordCount": + data["novelWordCount"] = checkInt(xItem.text, 0) + elif xItem.tag == "notesWordCount": + data["notesWordCount"] = checkInt(xItem.text, 0) + elif xItem.tag == "status": + data["status"] = self._parseStatusImport(xItem, "status") + elif xItem.tag in ("import", "importance"): + data["import"] = self._parseStatusImport(xItem, "import") + elif xItem.tag == "autoReplace": + if self._version >= 0x0102: + for xEntry in xItem: + if xEntry.tag == "entry" and "key" in xEntry.attrib: + autoReplace[xEntry.attrib["key"]] = checkString(xEntry.text, "ERROR") + else: # Pre 1.2 format + for xEntry in xItem: + autoReplace[xEntry.tag] = checkString(xEntry.text, "ERROR") + elif xItem.tag == "titleFormat": + for xEntry in xItem: + titleFormat[xEntry.tag] = checkString(xEntry.text, "") + else: + logger.warning("Ignored in xml", xItem.tag) + + data["autoReplace"] = autoReplace + data["titleFormat"] = titleFormat + self._data["settings"] = data + + return True + + def _parseProjectContent(self, xSection): + """Parse the content section of the XML file. + """ + logger.debug("Parsing xml ") + + data = [] + for xItem in xSection: + if xItem.tag == "item": + item = {} + item["handle"] = xItem.attrib.get("handle", None) + item["parent"] = xItem.attrib.get("parent", None) + item["root"] = xItem.attrib.get("root", None) + item["order"] = checkInt(xItem.attrib.get("order", 0), 0) + item["type"] = checkString(xItem.attrib.get("type", ""), "") + item["class"] = checkString(xItem.attrib.get("class", ""), "") + item["layout"] = checkString(xItem.attrib.get("layout", ""), "") + for xVal in xItem: + if xVal.tag == "meta": + item["expanded"] = checkBool(xVal.attrib.get("expanded", False), False) + item["heading"] = checkString(xVal.attrib.get("heading", "H0"), "H0") + item["charCount"] = checkInt(xVal.attrib.get("charCount", 0), 0) + item["wordCount"] = checkInt(xVal.attrib.get("wordCount", 0), 0) + item["paraCount"] = checkInt(xVal.attrib.get("paraCount", 0), 0) + item["cursorPos"] = checkInt(xVal.attrib.get("cursorPos", 0), 0) + elif xVal.tag == "name": + item["label"] = simplified(checkString(xVal.text, "")) + item["status"] = checkStringNone(xVal.attrib.get("status", None), None) + item["import"] = checkStringNone(xVal.attrib.get("import", None), None) + item["active"] = checkBool(xVal.attrib.get("active", False), 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), False) + else: + logger.warning("Ignored in xml", xVal.tag) + data.append(item) + else: + logger.warning("Ignored item in xml", xItem.tag) + + self._data["content"] = data + + return True + + def _parseProjectContentLegacy(self, xSection): + """Parse the content section of the XML file for version before 1.4. + """ + logger.debug("Parsing xml (legacy format)") + depLayout = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") + data = [] + for xItem in xSection: + item = {} + if xItem.tag == "item": + item["handle"] = xItem.attrib.get("handle", None) + item["parent"] = xItem.attrib.get("parent", 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 + + tmpStatus = "" + for xVal in xItem: + if xVal.tag == "name": + item["label"] = 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": + item["expanded"] = checkBool(xVal.text, False) + elif xVal.tag == "exported": # Renamed to active in 1.4 + item["active"] = checkBool(xVal.text, False) + elif xVal.tag == "charCount": + item["charCount"] = checkInt(xVal.text, 0) + elif xVal.tag == "wordCount": + item["wordCount"] = checkInt(xVal.text, 0) + elif xVal.tag == "paraCount": + item["paraCount"] = checkInt(xVal.text, 0) + elif xVal.tag == "cursorPos": + item["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"): + item["status"] = self._getLegacyUnportStatus(tmpStatus, "status") + else: + item["import"] = self._getLegacyUnportStatus(tmpStatus, "import") + + # A number of layouts were removed in 1.3 + if item.get("layout", "") in depLayout: + item["layout"] = "DOCUMENT" + + # The trast type was removed in 1.4 + if item.get("type", "") == "TRASH": + item["type"] = "ROOT" + + data.append(item) + else: + logger.warning("Ignored in xml", xItem.tag) + + self._data["content"] = data + + return True + + def _parseStatusImport(self, xItem, type): + """Parse a status or importance entry. + """ + data = self._statusData.get(type, {}) + for xEntry in xItem: + if xEntry.tag == "entry": + key = xEntry.attrib.get("key", f"{type[0]}{len(data):06x}") + data[key] = { + "label": xEntry.text, + "count": checkInt(xEntry.attrib.get("count", 0), 0), + "colour": ( + minmax(checkInt(xEntry.attrib.get("red", 0), 0), 0, 255), + minmax(checkInt(xEntry.attrib.get("green", 0), 0), 0, 255), + minmax(checkInt(xEntry.attrib.get("blue", 0), 0), 0, 255), + ), + } + self._statusData[type] = data + + return data + + def _getLegacyUnportStatus(self, label, type): + """Look up the label in defined status or importance values. + This is needed for file formats prior to 1.4 where the status + was saved as the label, not the key. + """ + if not self._statusMap.get(type): + lookup = {} + for key, entry in self._statusData.get(type, {}).items(): + lookup[entry.get("label", "")] = key + self._statusMap[type] = lookup + print(lookup) + + return self._statusMap.get(type, {}).get(label, None) + # END Class ProjectXMLReader diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index c466461e..1d19a08e 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -33,7 +33,7 @@ from lxml import etree from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor from PyQt5.QtCore import QRectF, Qt -from novelwriter.common import checkInt, minmax, simplified +from novelwriter.common import simplified logger = logging.getLogger(__name__) @@ -47,7 +47,6 @@ class NWStatus: self._type = type self._store = {} - self._reverse = {} self._default = None self._iPX = novelwriter.CONFIG.pxInt(24) @@ -90,7 +89,6 @@ class NWStatus: "cols": col, "count": count, } - self._reverse[name] = key if self._default is None: self._default = key @@ -106,7 +104,6 @@ class NWStatus: if self._store[key]["count"] > 0: return False - del self._reverse[self._store[key]["name"]] del self._store[key] keys = list(self._store.keys()) @@ -123,8 +120,6 @@ class NWStatus: """ if self._isKey(value) and value in self._store: return value - elif value in self._reverse: - return self._reverse[value] elif self._default is not None: return self._default else: @@ -222,21 +217,17 @@ class NWStatus: return True - def unpackXML(self, xParent): - """Unpack an XML tree and set the class values. + def unpack(self, data): + """Unpack a data dictionary and set the class values. """ self._store = {} - self._reverse = {} self._default = None - for xChild in xParent: - key = xChild.attrib.get("key", None) - name = xChild.text.strip() - count = max(checkInt(xChild.attrib.get("count", 0), 0), 0) - red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255) - green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255) - blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255) - self.write(key, name, (red, green, blue), count) + for key, entry in data.items(): + label = entry.get("label", "") + colour = entry.get("colour", (100, 100, 100)) + count = entry.get("count", 0) + self.write(key, label, colour, count) return True diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 506556cd..1db11a27 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -126,18 +126,14 @@ class NWTree: tItem.packXML(xContent) return - def unpackXML(self, xContent): - """Iterate through all items of a content XML object and add - them to the project tree. + def unpack(self, data): + """Iterate through all items of a list and add them to the + project tree. """ - if xContent.tag != "content": - logger.error("XML entry is not a NWTree") - return False - self.clear() - for xItem in xContent: + for item in data: nwItem = NWItem(self.theProject) - if nwItem.unpackXML(xItem): + if nwItem.unpack(item): self.append(nwItem.itemHandle, nwItem.itemParent, nwItem) nwItem.saveInitialCount() From e887ec6991113c27c7e5c350eee7f62e11eab416 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 30 Oct 2022 23:18:18 +0100 Subject: [PATCH 03/18] Move project name to project data class --- novelwriter/core/index.py | 2 +- novelwriter/core/project.py | 64 +++++------ novelwriter/core/projectdata.py | 112 ++++++++++++++++++++ novelwriter/core/projectxml.py | 27 ++--- novelwriter/core/tohtml.py | 2 +- novelwriter/dialogs/projdetails.py | 2 +- novelwriter/dialogs/projsettings.py | 4 +- novelwriter/guimain.py | 6 +- novelwriter/tools/build.py | 4 +- tests/test_core/test_core_project.py | 16 +-- tests/test_dialogs/test_dlg_projsettings.py | 2 +- tests/test_gui/test_gui_guimain.py | 4 +- tests/tools.py | 2 +- 13 files changed, 174 insertions(+), 73 deletions(-) create mode 100644 novelwriter/core/projectdata.py diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 1058628b..ce1a1278 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -75,7 +75,7 @@ class NWIndex: return def __repr__(self): - return f"" + return f"" ## # Properties diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 76294a08..50fe0cef 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -38,9 +38,10 @@ from PyQt5.QtCore import QCoreApplication from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException from novelwriter.common import ( - checkString, checkStringNone, isHandle, formatTimeStamp, - makeFileNameSafe, hexToInt, minmax, simplified + checkString, checkStringNone, isHandle, formatTimeStamp, makeFileNameSafe, + hexToInt, minmax, simplified ) +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 @@ -48,7 +49,7 @@ from novelwriter.core.status import NWStatus from novelwriter.core.options import OptionState from novelwriter.core.document import NWDoc from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState -from novelwriter.constants import trConst, nwFiles, nwLabels +from novelwriter.core.projectdata import NWProjectData logger = logging.getLogger(__name__) @@ -63,7 +64,8 @@ class NWProject: self.mainConf = novelwriter.CONFIG self.mainGui = mainGui - self._data = {} + self._data = NWProjectData() + self._raw = {} # Core Elements self._optState = OptionState(self) # Project-specific GUI options @@ -91,7 +93,6 @@ class NWProject: self.projFiles = [] # A list of all files in the content folder on load # Project Meta - self.projName = "" # Project name self.bookTitle = "" # The final title; should only be used for exports self.bookAuthors = [] # A list of book authors @@ -125,6 +126,10 @@ class NWProject: # Properties ## + @property + def data(self): + return self._data + @property def index(self): return self._projIndex @@ -263,7 +268,6 @@ class NWProject: self.projSpell = None self.projLang = None self.projFiles = [] - self.projName = "" self.bookTitle = "" self.bookAuthors = [] self.autoReplace = {} @@ -328,14 +332,14 @@ class NWProject: if not self.setProjectPath(projPath, newProject=True): return False - self.setProjectName(projName) + self.data.setName(projName) self.setBookTitle(projTitle) self.setBookAuthors(projAuthors) hNovelRoot = self.newRoot(nwItemClass.NOVEL) hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot) - titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self.projName) + titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self._data.name) if self.bookAuthors: titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors()) @@ -480,8 +484,9 @@ class NWProject: # Open The Project XML File # ========================= + self._data = NWProjectData() xmlReader = ProjectXMLReader(fileName) - xmlParsed = xmlReader.read() + xmlParsed = xmlReader.read(self._data) xmlData = xmlReader.data print(json.dumps(xmlData, indent=2)) @@ -510,7 +515,7 @@ class NWProject: self.clearProject() return False - self._data = xmlData + self._raw = xmlData logger.debug("XML root is '%s'", nwxRoot) logger.debug("File version is '%s'", xmlVersion) @@ -552,16 +557,13 @@ class NWProject: # Extract Data # ============ - xmlProject = xmlData.get("project", {}) + self.bookTitle = self._data.title + self.bookAuthors = self._data.autors + self.saveCount = self._data.saveCount + self.autoCount = self._data.autoCount + self.editTime = self._data.editTime - self.projName = xmlProject.get("name", "") - self.bookTitle = xmlProject.get("title", "") - self.bookAuthors = xmlProject.get("authors", []) - self.saveCount = xmlProject.get("saveCount", 0) - self.autoCount = xmlProject.get("autoCount", 0) - self.editTime = xmlProject.get("editTime", 0) - - logger.info("Project Name: '%s'", self.projName) + logger.info("Project Name: '%s'", self._data.name) logger.info("Project Title: '%s'", self.bookTitle) xmlSettings = xmlData.get("settings", {}) @@ -601,7 +603,7 @@ class NWProject: self._deprecatedFiles() # Update recent projects - self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) + self.mainConf.updateRecentCache(self.projPath, self._data.name, self.lastWCount, time()) self.mainConf.saveRecentCache() # Check the project tree consistency @@ -621,7 +623,7 @@ class NWProject: self._writeLockFile() self.setProjectChanged(False) - self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self.projName)) + self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self._data.name)) return True @@ -662,7 +664,7 @@ class NWProject: # Save Project Meta xProject = etree.SubElement(nwXML, "project") - self._packProjectValue(xProject, "name", self.projName) + self._packProjectValue(xProject, "name", self._data.name) self._packProjectValue(xProject, "title", self.bookTitle) self._packProjectValue(xProject, "author", self.bookAuthors) self._packProjectValue(xProject, "saveCount", str(self.saveCount)) @@ -734,11 +736,11 @@ class NWProject: self._optState.saveSettings() # Update recent projects - self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime) + self.mainConf.updateRecentCache(self.projPath, self._data.name, self.currWCount, saveTime) self.mainConf.saveRecentCache() self._writeLockFile() - self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self.projName)) + self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name)) self.setProjectChanged(False) return True @@ -800,14 +802,14 @@ class NWProject: ), nwAlert.ERROR) return False - if not self.projName: + 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." ), nwAlert.ERROR) return False - cleanName = makeFileNameSafe(self.projName) + cleanName = makeFileNameSafe(self._data.name) baseDir = os.path.abspath(os.path.join(self.mainConf.backupPath, cleanName)) if not os.path.isdir(baseDir): try: @@ -953,14 +955,6 @@ class NWProject: return True - def setProjectName(self, projName): - """Set the project name, This is the the name used for backup - files etc. - """ - self.projName = simplified(projName) - self.setProjectChanged(True) - return True - def setBookTitle(self, bookTitle): """Set the book title, that is, the title to include in exports. """ @@ -998,7 +992,7 @@ class NWProject: ), nwAlert.WARN) return False - if self.projName == "": + if self._data.name == "": self.mainGui.makeAlert(self.tr( "You must set a valid project name in Project Settings to " "use the automatic project backup feature." diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py new file mode 100644 index 00000000..f5719949 --- /dev/null +++ b/novelwriter/core/projectdata.py @@ -0,0 +1,112 @@ +""" +novelWriter – Project Data Class +================================ +Class for holding the project settings + +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 . +""" + +import logging + +from novelwriter.common import checkInt, simplified + +logger = logging.getLogger(__name__) + + +class NWProjectData: + + def __init__(self): + + # Project Meta + self._name = "" + self._title = "" + self._authors = [] + self._saveCount = 0 + self._autoCount = 0 + self._editTime = 0 + + # Internal + self._changed = False + + return + + ## + # Properties + ## + + @property + def name(self): + return self._name + + @property + def title(self): + return self._title + + @property + def autors(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 + + ## + # Setters + ## + + def setName(self, value): + self._name = simplified(str(value)) + self._changed = True + return + + def setTitle(self, value): + self._title = simplified(str(value)) + self._changed = True + return + + def addAuthor(self, value): + self._authors.append(simplified(str(value))) + self._changed = True + return + + def setSaveCount(self, value): + self._saveCount = checkInt(value, 0) + self._changed = True + return + + def setAutoCount(self, value): + self._autoCount = checkInt(value, 0) + self._changed = True + return + + def setEditTime(self, value): + self._editTime = checkInt(value, 0) + self._changed = True + return + +# END Class NWProjectData diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 2aa2d248..7b78dd2c 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -4,7 +4,8 @@ novelWriter – Project XML Read/Write Classes for reading and writing the project XML file File History: -Created: 2022-09-28 [1.7.b1] +Created: 2022-09-28 [2.0rc1] ProjectXMLReader +Created: 2022-09-28 [2.0rc1] XMLReadState This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -90,7 +91,7 @@ class ProjectXMLReader: # Methods ## - def read(self): + def read(self, projData): """Read and parse the project XML file. """ self._data = {} @@ -154,7 +155,7 @@ class ProjectXMLReader: status = True for xSection in xRoot: if xSection.tag == "project": - status &= self._parseProjectMeta(xSection) + status &= self._parseProjectMeta(xSection, projData) elif xSection.tag == "settings": status &= self._parseProjectSettings(xSection) elif xSection.tag == "content": @@ -180,31 +181,25 @@ class ProjectXMLReader: # Internal Functions ## - def _parseProjectMeta(self, xSection): + def _parseProjectMeta(self, xSection, projData): """Parse the project section of the XML file. """ logger.debug("Parsing xml ") - data = {} - authors = [] for xItem in xSection: if xItem.tag == "name": - data["name"] = simplified(checkString(xItem.text, "")) + projData.setName(xItem.text) elif xItem.tag == "title": - data["title"] = simplified(checkString(xItem.text, "")) + projData.setTitle(xItem.text) elif xItem.tag == "author": - authors.append(simplified(checkString(xItem.text, ""))) + projData.addAuthor(xItem.text) elif xItem.tag == "saveCount": - data["saveCount"] = checkInt(xItem.text, 0) + projData.setSaveCount(xItem.text) elif xItem.tag == "autoCount": - data["autoCount"] = checkInt(xItem.text, 0) + projData.setAutoCount(xItem.text) elif xItem.tag == "editTime": - data["editTime"] = checkInt(xItem.text, 0) + projData.setEditTime(xItem.text) else: logger.warning("Ignored in xml", xItem.tag) - - data["authors"] = authors - self._data["project"] = data - return True def _parseProjectSettings(self, xSection): diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index 2110300c..88647d63 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -315,7 +315,7 @@ class ToHtml(Tokenizer): "\n" "\n" ).format( - projTitle=self.theProject.projName, + projTitle=self.theProject.data.name, htmlStyle="\n".join(theStyle), bodyText=bodyText, ) diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 7f2259c3..368c25ee 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -166,7 +166,7 @@ class GuiProjectDetailsMain(QWidget): self.bookTitle.setWordWrap(True) self.projName = QLabel( - self.tr("Working Title: {0}").format(self.theProject.projName) + self.tr("Working Title: {0}").format(self.theProject.data.name) ) workFont = self.projName.font() workFont.setPointSizeF(0.8*fPt) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index eb96ffb2..78165613 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -114,7 +114,7 @@ class GuiProjectSettings(PagedDialog): spellLang = self.tabMain.spellLang.currentData() doBackup = not self.tabMain.doBackup.isChecked() - self.theProject.setProjectName(projName) + self.theProject.data.setName(projName) self.theProject.setBookTitle(bookTitle) self.theProject.setBookAuthors(bookAuthors) self.theProject.setProjBackup(doBackup) @@ -209,7 +209,7 @@ class GuiProjectEditMain(QWidget): self.editName = QLineEdit() self.editName.setMaxLength(200) self.editName.setMaximumWidth(xW) - self.editName.setText(self.theProject.projName) + self.editName.setText(self.theProject.data.name) self.mainForm.addRow( self.tr("Project name"), self.editName, diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 89e4ca44..af76edf8 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -383,7 +383,7 @@ class GuiMain(QMainWindow): self.mainStatus.setDocumentStatus(nwState.NONE) self.mainStatus.setStatus(self.tr("New project created ...")) - self._updateWindowTitle(self.theProject.projName) + self._updateWindowTitle(self.theProject.data.name) else: self.theProject.clearProject() @@ -521,7 +521,7 @@ class GuiMain(QMainWindow): self.theProject.index.loadIndex() # Update GUI - self._updateWindowTitle(self.theProject.projName) + self._updateWindowTitle(self.theProject.data.name) self.rebuildTrees() self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(self.theProject.spellCheck) @@ -960,7 +960,7 @@ class GuiMain(QMainWindow): if dlgProj.spellChanged: self.docEditor.setDictionaries() self.itemDetails.refreshDetails() - self._updateWindowTitle(self.theProject.projName) + self._updateWindowTitle(self.theProject.data.name) return True diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 683f8e99..3529d577 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -888,7 +888,7 @@ class GuiBuildNovel(QDialog): # Generate File Name # ================== - cleanName = makeFileNameSafe(self.theProject.projName) + cleanName = makeFileNameSafe(self.theProject.data.name) fileName = "%s.%s" % (cleanName, fileExt) saveDir = self.mainConf.lastPath if not os.path.isdir(saveDir): @@ -972,7 +972,7 @@ class GuiBuildNovel(QDialog): elif theFmt == self.FMT_JSON_H or theFmt == self.FMT_JSON_M: jsonData = { "meta": { - "workingTitle": self.theProject.projName, + "workingTitle": self.theProject.data.name, "novelTitle": self.theProject.bookTitle, "authors": self.theProject.bookAuthors, "buildTime": self.buildTime, diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 5a95295b..62d1f830 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -202,7 +202,7 @@ def testCoreProject_NewSampleA(fncDir, tmpConf, mockGUI, tmpDir): assert theProject.newProject(projData) is True assert theProject.openProject(fncDir) is True - assert theProject.projName == "Sample Project" + assert theProject.data.name == "Sample Project" assert theProject.saveProject() is True assert theProject.closeProject() is True os.unlink(dstSample) @@ -236,7 +236,7 @@ def testCoreProject_NewSampleB(monkeypatch, fncDir, tmpConf, mockGUI, tmpDir): monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx") assert theProject.newProject(projData) is True assert theProject.openProject(fncDir) is True - assert theProject.projName == "Sample Project" + assert theProject.data.name == "Sample Project" assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -895,8 +895,8 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.setProjectPath(fncDir) # Project Name - assert theProject.setProjectName(" A Name ") - assert theProject.projName == "A Name" + assert theProject.data.setName(" A Name ") + assert theProject.data.name == "A Name" # Project Title assert theProject.setBookTitle(" A Title ") @@ -944,9 +944,9 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): theProject.mainConf.backupPath = tmpDir assert theProject.setProjBackup(True) - assert theProject.setProjectName("") + assert theProject.data.setName("") assert not theProject.setProjBackup(True) - assert theProject.setProjectName("A Name") + assert theProject.data.setName("A Name") assert theProject.setProjBackup(True) # Spell check @@ -1327,12 +1327,12 @@ def testCoreProject_Backup(monkeypatch, mockGUI, nwMinimal, tmpDir): # Missing project name theProject.mainConf.backupPath = tmpDir - theProject.projName = "" + theProject.data.name = "" assert theProject.zipIt(doNotify=False) is False # Non-existent folder theProject.mainConf.backupPath = os.path.join(tmpDir, "nonexistent") - theProject.projName = "Test Minimal" + theProject.data.name = "Test Minimal" assert theProject.zipIt(doNotify=False) is False # Same folder as project (causes infinite loop in zipping) diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index bb38f317..7e8b4e1d 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -136,7 +136,7 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd assert projSettings.spellChanged is False projSettings._doSave() - assert theProject.projName == "Project Name" + assert theProject.data.name == "Project Name" assert theProject.bookTitle == "Project Title" assert theProject.bookAuthors == ["Jane Doe", "John Doh"] diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 79fcbe52..497f521b 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -172,7 +172,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath is None assert nwGUI.theProject.projMeta is None - assert nwGUI.theProject.projName == "" + assert nwGUI.theProject.data.name == "" assert nwGUI.theProject.bookTitle == "" assert len(nwGUI.theProject.bookAuthors) == 0 assert not nwGUI.theProject.spellCheck @@ -194,7 +194,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath == fncProj assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta") - assert nwGUI.theProject.projName == "New Project" + assert nwGUI.theProject.data.name == "New Project" assert nwGUI.theProject.bookTitle == "New Novel" assert len(nwGUI.theProject.bookAuthors) == 1 assert nwGUI.theProject.spellCheck is False diff --git a/tests/tools.py b/tests/tools.py index 4a97c930..bf8d2c40 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -167,7 +167,7 @@ def buildTestProject(theObject, projPath): theProject.clearProject() theProject.setProjectPath(projPath, newProject=True) - theProject.setProjectName("New Project") + theProject.data.setName("New Project") theProject.setBookTitle("New Novel") theProject.setBookAuthors("Jane Doe") From 192806a46d47f169a3a5d2955ddc0eb229e0850c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 30 Oct 2022 23:55:58 +0100 Subject: [PATCH 04/18] Fix or disable broken tests --- novelwriter/core/project.py | 2 +- tests/files/nwProject-1.0.nwx | 299 +++++++++++++++++ tests/files/nwProject-1.1.nwx | 283 ++++++++++++++++ tests/files/nwProject-1.2.nwx | 313 ++++++++++++++++++ tests/files/nwProject-1.3.nwx | 313 ++++++++++++++++++ tests/lipsum/nwProject.nwx | 30 +- tests/minimal/nwProject.nwx | 12 +- .../coreProject_NewCustomA_nwProject.nwx | 34 +- .../coreProject_NewCustomB_nwProject.nwx | 22 +- .../coreProject_NewFileFolder_nwProject.nwx | 12 +- .../coreProject_NewMinimal_nwProject.nwx | 10 +- .../coreProject_NewRoot_nwProject.nwx | 8 +- .../guiEditor_Main_Final_nwProject.nwx | 16 +- .../guiEditor_Main_Initial_nwProject.nwx | 8 +- tests/test_core/test_core_item.py | 24 +- tests/test_core/test_core_project.py | 29 +- tests/test_core/test_core_status.py | 17 +- tests/test_core/test_core_tree.py | 1 + tests/test_gui/test_gui_guimain.py | 2 +- 19 files changed, 1320 insertions(+), 115 deletions(-) create mode 100644 tests/files/nwProject-1.0.nwx create mode 100644 tests/files/nwProject-1.1.nwx create mode 100644 tests/files/nwProject-1.2.nwx create mode 100644 tests/files/nwProject-1.3.nwx diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 50fe0cef..05bebca0 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -489,7 +489,7 @@ class NWProject: xmlParsed = xmlReader.read(self._data) xmlData = xmlReader.data - print(json.dumps(xmlData, indent=2)) + # print(json.dumps(xmlData, indent=2)) nwxRoot = xmlData.get("xmlRoot", "") appVersion = xmlData.get("appVersion", self.tr("Unknown")) diff --git a/tests/files/nwProject-1.0.nwx b/tests/files/nwProject-1.0.nwx new file mode 100644 index 00000000..80465ec6 --- /dev/null +++ b/tests/files/nwProject-1.0.nwx @@ -0,0 +1,299 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + False + + + True + True + 636b6aa9b697b + ba8a28a246524 + 914 + + B + E + D + + + %title% + Chapter %ch%: %title% + %title% + Scene %ch%.%sc%: %title% +
+ True + True + False +
+ + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished + + + None + Minor + Major + Main + +
+ + + Novel + ROOT + NOVEL + Started + True + + + Title Page + FILE + NOVEL + Started + False + True + TITLE + 72 + 15 + 2 + 78 + + + Page + FILE + NOVEL + New + False + True + PAGE + 208 + 40 + 2 + 213 + + + Part One + FILE + NOVEL + New + False + True + PARTITION + 23 + 5 + 1 + 0 + + + A Folder + FOLDER + NOVEL + 1st Draft + True + + + Chapter One + FILE + NOVEL + Notes + False + True + CHAPTER + 12 + 3 + 0 + 215 + + + Making a Scene + FILE + NOVEL + 1st Draft + False + True + SCENE + 1199 + 216 + 7 + 527 + + + Another Scene + FILE + NOVEL + 1st Draft + False + True + SCENE + 476 + 93 + 3 + 551 + + + Interlude + FILE + NOVEL + Finished + False + True + UNNUMBERED + 633 + 101 + 3 + 1238 + + + A Note on Structure + FILE + NOVEL + 2nd Draft + False + False + NOTE + 1692 + 313 + 6 + 1721 + + + Chapter Two + FILE + NOVEL + 1st Draft + False + True + CHAPTER + 139 + 28 + 1 + 343 + + + We Found John! + FILE + NOVEL + 1st Draft + False + True + SCENE + 189 + 37 + 1 + 224 + + + Characters + ROOT + CHARACTER + None + True + + + Main Characters + FOLDER + CHARACTER + None + True + + + John Smith + FILE + CHARACTER + Minor + False + True + NOTE + 49 + 9 + 1 + 24 + + + Jane Smith + FILE + CHARACTER + Major + False + True + NOTE + 55 + 9 + 1 + 25 + + + Locations + ROOT + WORLD + None + True + + + Earth + FILE + WORLD + Main + False + True + NOTE + 76 + 15 + 1 + 20 + + + Space + FILE + WORLD + Minor + False + True + NOTE + 115 + 24 + 1 + 133 + + + Mars + FILE + WORLD + Major + False + True + NOTE + 28 + 6 + 1 + 45 + + + Trash + TRASH + TRASH + None + True + + + Delete Me! + FILE + NOVEL + New + False + True + SCENE + 0 + 0 + 0 + 36 + + +
diff --git a/tests/files/nwProject-1.1.nwx b/tests/files/nwProject-1.1.nwx new file mode 100644 index 00000000..9e244c8b --- /dev/null +++ b/tests/files/nwProject-1.1.nwx @@ -0,0 +1,283 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + 408 + 71 + 15120 + + + False + True + True + 636b6aa9b697b + bb2c23b3c42cc + 967 + + B + E + D + + + %title% + Chapter %ch%: %title% + %title% + Scene %ch%.%sc%: %title% +
+
+ + New + Notes + Started + 1st Draft + 2nd Draft + 3rd Draft + Finished + + + None + Minor + Major + Main + +
+ + + Novel + ROOT + NOVEL + Started + True + + + Title Page + FILE + NOVEL + Started + True + TITLE + 72 + 15 + 2 + 78 + + + Page + FILE + NOVEL + New + True + PAGE + 210 + 40 + 2 + 213 + + + Part One + FILE + NOVEL + New + True + PARTITION + 23 + 5 + 1 + 0 + + + A Folder + FOLDER + NOVEL + 1st Draft + True + + + Chapter One + FILE + NOVEL + Notes + True + CHAPTER + 12 + 3 + 0 + 215 + + + Making a Scene + FILE + NOVEL + 1st Draft + True + SCENE + 1483 + 263 + 8 + 1086 + + + Another Scene + FILE + NOVEL + 1st Draft + True + SCENE + 476 + 93 + 3 + 428 + + + Interlude + FILE + NOVEL + Finished + True + UNNUMBERED + 633 + 101 + 3 + 1238 + + + A Note on Structure + FILE + NOVEL + 2nd Draft + False + NOTE + 1692 + 313 + 6 + 1721 + + + Chapter Two + FILE + NOVEL + 1st Draft + True + CHAPTER + 139 + 28 + 1 + 343 + + + We Found John! + FILE + NOVEL + 1st Draft + True + SCENE + 189 + 37 + 1 + 224 + + + Characters + ROOT + CHARACTER + None + True + + + Main Characters + FOLDER + CHARACTER + None + True + + + John Smith + FILE + CHARACTER + Minor + True + NOTE + 49 + 9 + 1 + 24 + + + Jane Smith + FILE + CHARACTER + Major + True + NOTE + 55 + 9 + 1 + 25 + + + Locations + ROOT + WORLD + None + True + + + Earth + FILE + WORLD + Main + True + NOTE + 76 + 15 + 1 + 20 + + + Space + FILE + WORLD + Minor + True + NOTE + 115 + 24 + 1 + 133 + + + Mars + FILE + WORLD + Major + True + NOTE + 28 + 6 + 1 + 45 + + + Trash + TRASH + TRASH + None + True + + + Delete Me! + FILE + NOVEL + New + True + SCENE + 30 + 6 + 1 + 36 + + +
diff --git a/tests/files/nwProject-1.2.nwx b/tests/files/nwProject-1.2.nwx new file mode 100644 index 00000000..af9892e8 --- /dev/null +++ b/tests/files/nwProject-1.2.nwx @@ -0,0 +1,313 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + 1122 + 191 + 53749 + + + False + en + True + None + True + 636b6aa9b697b + 636b6aa9b697b + 1216 + 840 + 376 + + 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 + ROOT + NOVEL + Started + True + + + Title Page + FILE + NOVEL + Started + True + TITLE + 241 + 42 + 3 + 252 + + + Page + FILE + NOVEL + New + True + PAGE + 125 + 26 + 2 + 127 + + + Part One + FILE + NOVEL + New + True + PARTITION + 26 + 6 + 1 + 30 + + + A Folder + FOLDER + NOVEL + 1st Draft + True + + + Chapter One + FILE + NOVEL + Notes + True + CHAPTER + 75 + 14 + 1 + 279 + + + Making a Scene + FILE + NOVEL + 1st Draft + True + SCENE + 2429 + 432 + 14 + 61 + + + Another Scene + FILE + NOVEL + 1st Draft + True + SCENE + 476 + 93 + 3 + 577 + + + Interlude + FILE + NOVEL + New + True + UNNUMBERED + 617 + 101 + 3 + 1137 + + + A Note on Structure + FILE + NOVEL + 2nd Draft + False + NOTE + 1692 + 313 + 6 + 1110 + + + Chapter Two + FILE + NOVEL + 1st Draft + True + CHAPTER + 139 + 28 + 1 + 343 + + + We Found John! + FILE + NOVEL + 1st Draft + True + SCENE + 189 + 37 + 1 + 224 + + + Characters + ROOT + CHARACTER + None + True + + + Main Characters + FOLDER + CHARACTER + None + True + + + John Smith + FILE + CHARACTER + Minor + True + NOTE + 49 + 9 + 1 + 24 + + + Jane Smith + FILE + CHARACTER + Major + True + NOTE + 55 + 9 + 1 + 25 + + + Locations + ROOT + WORLD + None + True + + + Earth + FILE + WORLD + Main + True + NOTE + 76 + 15 + 1 + 20 + + + Space + FILE + WORLD + Minor + True + NOTE + 115 + 24 + 1 + 133 + + + Mars + FILE + WORLD + Major + True + NOTE + 28 + 6 + 1 + 45 + + + Outtakes + ROOT + ARCHIVE + None + True + + + Scenes + FOLDER + ARCHIVE + None + True + + + Old File + FILE + NOVEL + 1st Draft + True + SCENE + 315 + 55 + 1 + 322 + + + Trash + TRASH + TRASH + None + True + + + Delete Me! + FILE + NOVEL + New + True + SCENE + 30 + 6 + 1 + 36 + + +
diff --git a/tests/files/nwProject-1.3.nwx b/tests/files/nwProject-1.3.nwx new file mode 100644 index 00000000..74b42a6c --- /dev/null +++ b/tests/files/nwProject-1.3.nwx @@ -0,0 +1,313 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + 1312 + 199 + 65207 + + + False + en_GB + True + None + True + 636b6aa9b697b + 636b6aa9b697b + 1206 + 830 + 376 + + 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 + ROOT + NOVEL + Started + True + + + Title Page + FILE + NOVEL + Started + True + DOCUMENT + 93 + 19 + 2 + 2 + + + Page + FILE + NOVEL + New + True + DOCUMENT + 186 + 39 + 2 + 212 + + + Part One + FILE + NOVEL + New + True + DOCUMENT + 26 + 6 + 1 + 33 + + + A Folder + FOLDER + NOVEL + 1st Draft + True + + + Chapter One + FILE + NOVEL + Notes + True + DOCUMENT + 75 + 14 + 1 + 279 + + + Making a Scene + FILE + NOVEL + 1st Draft + True + DOCUMENT + 2429 + 432 + 14 + 62 + + + Another Scene + FILE + NOVEL + 1st Draft + True + DOCUMENT + 476 + 93 + 3 + 577 + + + Interlude + FILE + NOVEL + New + True + DOCUMENT + 617 + 101 + 3 + 4 + + + A Note on Structure + FILE + NOVEL + 2nd Draft + False + NOTE + 1692 + 313 + 6 + 1110 + + + Chapter Two + FILE + NOVEL + 1st Draft + True + DOCUMENT + 139 + 28 + 1 + 343 + + + We Found John! + FILE + NOVEL + 1st Draft + True + DOCUMENT + 189 + 37 + 1 + 224 + + + Characters + ROOT + CHARACTER + None + True + + + Main Characters + FOLDER + CHARACTER + None + True + + + John Smith + FILE + CHARACTER + Minor + True + NOTE + 49 + 9 + 1 + 24 + + + Jane Smith + FILE + CHARACTER + Major + True + NOTE + 55 + 9 + 1 + 25 + + + Locations + ROOT + WORLD + None + True + + + Earth + FILE + WORLD + Main + True + NOTE + 76 + 15 + 1 + 20 + + + Space + FILE + WORLD + Minor + True + NOTE + 115 + 24 + 1 + 133 + + + Mars + FILE + WORLD + Major + True + NOTE + 28 + 6 + 1 + 45 + + + Archive + ROOT + ARCHIVE + New + True + + + Scenes + FOLDER + ARCHIVE + New + True + + + Old File + FILE + NOVEL + 1st Draft + True + DOCUMENT + 314 + 55 + 1 + 322 + + + Trash + TRASH + TRASH + None + True + + + Delete Me! + FILE + NOVEL + New + True + DOCUMENT + 30 + 6 + 1 + 36 + + +
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index bfaedd84..bcc0b0ce 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -50,19 +50,19 @@ Novel - + Lorem Ipsum - + Front Matter - + Prologue - + Act One @@ -70,19 +70,19 @@ Chapter One - + Chapter One - + Scene One - + Scene Two - + Interlude @@ -90,19 +90,19 @@ Chapter Two - + Chapter Two - + Scene Three - + Scene Four - + Scene Five @@ -110,7 +110,7 @@ Characters - + Mr. Nobody @@ -118,7 +118,7 @@ Plot - + Main @@ -126,7 +126,7 @@ World - + Ancient Europe diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index b9180838..3b9d81c6 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 19 + 21 2 - 167 + 177 True @@ -48,7 +48,7 @@ Novel - + Title Page @@ -56,11 +56,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index ef2056f7..a42b8290 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -48,55 +48,55 @@ Novel - + Title Page - + Chapter 1 - + Scene 1.1 - + Scene 1.2 - + Scene 1.3 - + Chapter 2 - + Scene 2.1 - + Scene 2.2 - + Scene 2.3 - + Chapter 3 - + Scene 3.1 - + Scene 3.2 - + Scene 3.3 @@ -104,7 +104,7 @@ Plot - + Main Plot @@ -112,7 +112,7 @@ Characters - + Protagonist @@ -120,7 +120,7 @@ Locations - + Main Location diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 30d6c2a9..63deaff4 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -48,31 +48,31 @@ Novel - + Title Page - + Scene 1 - + Scene 2 - + Scene 3 - + Scene 4 - + Scene 5 - + Scene 6 @@ -80,7 +80,7 @@ Plot - + Main Plot @@ -88,7 +88,7 @@ Characters - + Protagonist @@ -96,7 +96,7 @@ Locations - + Main Location diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index e1faf5a7..dd242336 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -59,7 +59,7 @@ World - + Title Page @@ -67,11 +67,11 @@ New Chapter - + New Chapter - + New Scene @@ -79,11 +79,11 @@ Stuff - + Hello - + Jane diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 282aa1f3..2bc604a6 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,8 +1,8 @@ - + New Project - + None 2 1 0 @@ -46,15 +46,15 @@ Novel - + Title Page - + New Chapter - + New Scene diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index af7e0b73..213106e6 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -59,7 +59,7 @@ World - + Title Page @@ -67,11 +67,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index fb58330e..ad38f479 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 - 4 + 3 True @@ -47,7 +47,7 @@ Novel - + Title Page @@ -55,11 +55,11 @@ New Chapter - + New Chapter - + New Scene @@ -67,7 +67,7 @@ Plot - + New Note @@ -75,7 +75,7 @@ Characters - + New Note @@ -83,7 +83,7 @@ World - + New Note diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index aa3b3057..2e36ef35 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -47,7 +47,7 @@ Novel - + Title Page @@ -55,11 +55,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index e7a72336..df841aa2 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -25,6 +25,8 @@ from lxml import etree from PyQt5.QtGui import QIcon +from tools import C + from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout @@ -153,7 +155,7 @@ def testCoreItem_Setters(mockGUI, mockRnd): theItem.setCharCount(None) assert theItem.charCount == 0 theItem.setCharCount("1") - assert theItem.charCount == 1 + assert theItem.charCount == 0 theItem.setCharCount(1) assert theItem.charCount == 1 @@ -161,7 +163,7 @@ def testCoreItem_Setters(mockGUI, mockRnd): theItem.setWordCount(None) assert theItem.wordCount == 0 theItem.setWordCount("1") - assert theItem.wordCount == 1 + assert theItem.wordCount == 0 theItem.setWordCount(1) assert theItem.wordCount == 1 @@ -169,7 +171,7 @@ def testCoreItem_Setters(mockGUI, mockRnd): theItem.setParaCount(None) assert theItem.paraCount == 0 theItem.setParaCount("1") - assert theItem.paraCount == 1 + assert theItem.paraCount == 0 theItem.setParaCount(1) assert theItem.paraCount == 1 @@ -177,7 +179,7 @@ def testCoreItem_Setters(mockGUI, mockRnd): theItem.setCursorPos(None) assert theItem.cursorPos == 0 theItem.setCursorPos("1") - assert theItem.cursorPos == 1 + assert theItem.cursorPos == 0 theItem.setCursorPos(1) assert theItem.cursorPos == 1 @@ -190,9 +192,10 @@ def testCoreItem_Setters(mockGUI, mockRnd): @pytest.mark.core -def testCoreItem_Methods(mockGUI): +def testCoreItem_Methods(mockGUI, mockRnd): """Test the simple methods of the NWItem class. """ + mockRnd.reset() theProject = NWProject(mockGUI) theItem = NWItem(theProject) @@ -250,15 +253,15 @@ def testCoreItem_Methods(mockGUI): # ============= theItem.setType("FILE") - theItem.setStatus("Note") - theItem.setImport("Minor") + theItem.setStatus(C.sNote) + theItem.setImport(C.iMinor) theItem.setClass("NOVEL") stT, stI = theItem.getImportStatus() assert stT == "Note" assert isinstance(stI, QIcon) - theItem.setImportStatus("Draft") + theItem.setImportStatus(C.sDraft) stT, stI = theItem.getImportStatus() assert stT == "Draft" @@ -267,7 +270,7 @@ def testCoreItem_Methods(mockGUI): assert stT == "Minor" assert isinstance(stI, QIcon) - theItem.setImportStatus("Major") + theItem.setImportStatus(C.iMajor) stT, stI = theItem.getImportStatus() assert stT == "Major" @@ -491,6 +494,7 @@ def testCoreItem_ClassDefaults(mockGUI): @pytest.mark.core +@pytest.mark.skip def testCoreItem_XMLPackUnpack(mockGUI, caplog, mockRnd): """Test packing and unpacking XML objects for the NWItem class. """ @@ -637,6 +641,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, mockRnd): @pytest.mark.core +@pytest.mark.skip def testCoreItem_ConvertFromFmt12(mockGUI): """Test the setter for all the nwItemLayout values for the NWItem class using the class names that were present in file format 1.2. @@ -666,6 +671,7 @@ def testCoreItem_ConvertFromFmt12(mockGUI): @pytest.mark.core +@pytest.mark.skip def testCoreItem_ConvertFromFmt13(mockGUI): """Test packing and unpacking XML objects for the NWItem class from format version 1.3 diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 62d1f830..9ac7271e 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -381,6 +381,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI, @pytest.mark.core +@pytest.mark.skip def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI): """Test opening a project. """ @@ -522,6 +523,7 @@ def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI): @pytest.mark.core +@pytest.mark.skip def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir): """Test saving a project. """ @@ -747,6 +749,7 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): @pytest.mark.core +@pytest.mark.skip def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): """Test the status and importance flag handling. """ @@ -759,10 +762,10 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): # Change Status # ============= - theProject.tree["0000000000014"].setStatus("Finished") - theProject.tree["0000000000015"].setStatus("Draft") - theProject.tree["0000000000016"].setStatus("Note") - theProject.tree["0000000000017"].setStatus("Finished") + theProject.tree["0000000000014"].setStatus(statusKeys[3]) + theProject.tree["0000000000015"].setStatus(statusKeys[2]) + theProject.tree["0000000000016"].setStatus(statusKeys[1]) + theProject.tree["0000000000017"].setStatus(statusKeys[3]) assert theProject.tree["0000000000014"].itemStatus == statusKeys[3] assert theProject.tree["0000000000015"].itemStatus == statusKeys[2] @@ -790,7 +793,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): assert theProject.statusItems.cols(statusKeys[3]) == (4, 4, 4) # Check the new entry - lastKey = theProject.statusItems.check("Finished") + lastKey = theProject.statusItems.check("s000018") assert lastKey == "s000018" assert theProject.statusItems.name(lastKey) == "Finished" assert theProject.statusItems.cols(lastKey) == (5, 5, 5) @@ -803,7 +806,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): # ================= fHandle = theProject.newFile("Jane Doe", "0000000000012") - theProject.tree[fHandle].setImport("Main") + theProject.tree[fHandle].setImport(importKeys[3]) assert theProject.tree[fHandle].itemImport == importKeys[3] newList = [ @@ -827,7 +830,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): assert theProject.importItems.cols(importKeys[3]) == (4, 4, 4) # Check the new entry - lastKey = theProject.importItems.check("Max") + lastKey = theProject.importItems.check("i00001a") assert lastKey == "i00001a" assert theProject.importItems.name(lastKey) == "Max" assert theProject.importItems.cols(lastKey) == (5, 5, 5) @@ -895,11 +898,11 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.setProjectPath(fncDir) # Project Name - assert theProject.data.setName(" A Name ") + theProject.data.setName(" A Name ") assert theProject.data.name == "A Name" # Project Title - assert theProject.setBookTitle(" A Title ") + theProject.setBookTitle(" A Title ") assert theProject.bookTitle == "A Title" # Project Authors @@ -944,9 +947,9 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): theProject.mainConf.backupPath = tmpDir assert theProject.setProjBackup(True) - assert theProject.data.setName("") + theProject.data.setName("") assert not theProject.setProjBackup(True) - assert theProject.data.setName("A Name") + theProject.data.setName("A Name") assert theProject.setProjBackup(True) # Spell check @@ -1327,12 +1330,12 @@ def testCoreProject_Backup(monkeypatch, mockGUI, nwMinimal, tmpDir): # Missing project name theProject.mainConf.backupPath = tmpDir - theProject.data.name = "" + theProject.data.setName("") assert theProject.zipIt(doNotify=False) is False # Non-existent folder theProject.mainConf.backupPath = os.path.join(tmpDir, "nonexistent") - theProject.data.name = "Test Minimal" + theProject.data.setName("Test Minimal") assert theProject.zipIt(doNotify=False) is False # Same folder as project (causes infinite loop in zipping) diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index 868a3034..9f01136c 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -161,14 +161,6 @@ def testCoreStatus_Entries(): assert theStatus[statusKeys[3]]["name"] == "Entry 4" assert theStatus[statusKeys[3]]["cols"] == (100, 100, 100) - # Check reverse map - assert theStatus._reverse == { - "Entry 1": statusKeys[0], - "Entry 2": statusKeys[1], - "Entry 3": statusKeys[2], - "Entry 4": statusKeys[3], - } - # Check # ===== @@ -176,14 +168,8 @@ def testCoreStatus_Entries(): for key in statusKeys: assert theStatus.check(key) == key - # Reverse map lookup - assert theStatus.check("Entry 1") == statusKeys[0] - assert theStatus.check("Entry 2") == statusKeys[1] - assert theStatus.check("Entry 3") == statusKeys[2] - assert theStatus.check("Entry 4") == statusKeys[3] - # Non-existing name - assert theStatus.check("Entry 5") == statusKeys[0] + assert theStatus.check("s987654") == statusKeys[0] # Name Access # =========== @@ -314,6 +300,7 @@ def testCoreStatus_Entries(): @pytest.mark.core +@pytest.mark.skip def testCoreStatus_XMLPackUnpack(): """Test all the XML pack/unpack of the NWStatus class. """ diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 588d23f5..cef9fa58 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -396,6 +396,7 @@ def testCoreTree_Reorder(mockGUI, mockItems): @pytest.mark.core +@pytest.mark.skip def testCoreTree_XMLPackUnpack(mockGUI, mockItems): """Test packing and unpacking the tree to and from XML. """ diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 497f521b..4ea895e6 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -172,7 +172,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock 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.name == "New Project" assert nwGUI.theProject.bookTitle == "" assert len(nwGUI.theProject.bookAuthors) == 0 assert not nwGUI.theProject.spellCheck From aeef8734b89a1370a55a88b47740a7fbdd201e17 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 00:28:10 +0100 Subject: [PATCH 05/18] Move the remaining project meta settings to project data class --- novelwriter/core/project.py | 90 +++++---------------- novelwriter/core/projectdata.py | 52 +++++++++++- novelwriter/core/toodt.py | 4 +- novelwriter/dialogs/projdetails.py | 8 +- novelwriter/dialogs/projsettings.py | 8 +- novelwriter/tools/build.py | 4 +- tests/test_core/test_core_project.py | 45 ++++++----- tests/test_dialogs/test_dlg_projdetails.py | 2 +- tests/test_dialogs/test_dlg_projsettings.py | 6 +- tests/test_gui/test_gui_guimain.py | 10 +-- tests/tools.py | 4 +- 11 files changed, 115 insertions(+), 118 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 05bebca0..cecb6092 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -78,9 +78,6 @@ class NWProject: self.projChanged = False # The project has unsaved changes self.projAltered = False # The project has been altered this session self.lockedBy = None # Data on which computer has the project open - self.saveCount = 0 # Meta data: number of saves - self.autoCount = 0 # Meta data: number of automatic saves - self.editTime = 0 # The accumulated edit time read from the project file # Class Settings self.projPath = None # The full path to where the currently open project is saved @@ -92,10 +89,6 @@ class NWProject: self.projLang = None # The project language, used for builds self.projFiles = [] # A list of all files in the content folder on load - # Project Meta - self.bookTitle = "" # The final title; should only be used for exports - self.bookAuthors = [] # A list of book authors - # Project Settings self.autoReplace = {} # Text to auto-replace on exports self.titleFormat = {} # The formatting of titles for exports @@ -253,12 +246,12 @@ class NWProject: self.projOpened = 0 self.projChanged = False self.projAltered = False - self.saveCount = 0 - self.autoCount = 0 # Project Tree self._projTree.clear() + self._data = NWProjectData() + # Project Settings self.projPath = None self.projMeta = None @@ -268,8 +261,6 @@ class NWProject: self.projSpell = None self.projLang = None self.projFiles = [] - self.bookTitle = "" - self.bookAuthors = [] self.autoReplace = {} self.titleFormat = { "title": "%title%", @@ -332,16 +323,18 @@ class NWProject: if not self.setProjectPath(projPath, newProject=True): return False - self.data.setName(projName) - self.setBookTitle(projTitle) - self.setBookAuthors(projAuthors) + 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.bookTitle if self.bookTitle else self._data.name) - if self.bookAuthors: - titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors()) + 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._data.getAuthors(self.tr("and")) + ) aDoc = NWDoc(self, hTitlePage) aDoc.writeDocument(titlePage) @@ -557,14 +550,8 @@ class NWProject: # Extract Data # ============ - self.bookTitle = self._data.title - self.bookAuthors = self._data.autors - self.saveCount = self._data.saveCount - self.autoCount = self._data.autoCount - self.editTime = self._data.editTime - logger.info("Project Name: '%s'", self._data.name) - logger.info("Project Title: '%s'", self.bookTitle) + logger.info("Project Title: '%s'", self._data.title) xmlSettings = xmlData.get("settings", {}) @@ -646,9 +633,9 @@ class NWProject: logger.info("Saving project: %s", self.projPath) if autoSave: - self.autoCount += 1 + self._data.incAutoCount() else: - self.saveCount += 1 + self._data.incSaveCount() # Root element and project details logger.debug("Writing project meta") @@ -660,15 +647,15 @@ class NWProject: }) self.updateWordCounts() - editTime = int(self.editTime + saveTime - self.projOpened) + editTime = int(self._data.editTime + saveTime - self.projOpened) # Save Project Meta xProject = etree.SubElement(nwXML, "project") self._packProjectValue(xProject, "name", self._data.name) - self._packProjectValue(xProject, "title", self.bookTitle) - self._packProjectValue(xProject, "author", self.bookAuthors) - self._packProjectValue(xProject, "saveCount", str(self.saveCount)) - self._packProjectValue(xProject, "autoCount", str(self.autoCount)) + self._packProjectValue(xProject, "title", self._data.title) + self._packProjectValue(xProject, "author", self._data.authors) + self._packProjectValue(xProject, "saveCount", str(self._data.saveCount)) + self._packProjectValue(xProject, "autoCount", str(self._data.autoCount)) self._packProjectValue(xProject, "editTime", str(editTime)) # Save Project Settings @@ -955,30 +942,6 @@ class NWProject: return True - def setBookTitle(self, bookTitle): - """Set the book title, that is, the title to include in exports. - """ - self.bookTitle = simplified(bookTitle) - self.setProjectChanged(True) - return True - - def setBookAuthors(self, bookAuthors): - """A line-separated list of authors, parsed into an array. - """ - if not isinstance(bookAuthors, str): - return False - - self.bookAuthors = [] - for bookAuthor in bookAuthors.splitlines(): - bookAuthor = simplified(bookAuthor) - if bookAuthor == "": - continue - self.bookAuthors.append(bookAuthor) - - self.setProjectChanged(True) - - return True - def setProjBackup(self, doBackup): """Set whether projects should be backed up or not. The user will be notified in case required settings are missing. @@ -1116,26 +1079,11 @@ class NWProject: # Getters ## - def getAuthors(self): - """Return a formatted string of authors. - """ - nAuth = len(self.bookAuthors) - authString = "" - - if nAuth == 1: - authString = self.bookAuthors[0] - elif nAuth > 1: - authString = "%s %s %s" % ( - ", ".join(self.bookAuthors[0:-1]), self.tr("and"), self.bookAuthors[-1] - ) - - return authString - def getCurrentEditTime(self): """Get the total project edit time, including the time spent in the current session. """ - return round(self.editTime + time() - self.projOpened) + return round(self._data.editTime + time() - self.projOpened) def getProjectItems(self): """This function ensures that the item tree loaded is sent to diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index f5719949..49ac64af 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -60,7 +60,7 @@ class NWProjectData: return self._title @property - def autors(self): + def authors(self): return self._authors @property @@ -75,6 +75,44 @@ class NWProjectData: def editTime(self): return self._editTime + ## + # Methods + ## + + def addAuthor(self, value): + self._authors.append(simplified(str(value))) + self._changed = True + return + + def incSaveCount(self): + self._saveCount += 1 + self._changed = True + return + + def incAutoCount(self): + self._autoCount += 1 + self._changed = True + return + + ## + # Getters + ## + + def getAuthors(self, trAnd="and"): + """Return a formatted string of authors. + """ + nAuth = len(self._authors) + authors = "" + + if nAuth == 1: + authors = self._authors[0] + elif nAuth > 1: + authors = "%s %s %s" % ( + ", ".join(self._authors[0:-1]), trAnd, self._authors[-1] + ) + + return authors + ## # Setters ## @@ -89,9 +127,17 @@ class NWProjectData: self._changed = True return - def addAuthor(self, value): - self._authors.append(simplified(str(value))) + def setAuthors(self, value): + self._authors = [] self._changed = True + if isinstance(value, str): + for author in value.splitlines(): + author = simplified(author) + if author: + self.addAuthor(author) + self._changed = True + elif isinstance(value, list): + self._authors = value return def setSaveCount(self, value): diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py index 022bda4c..b4306f30 100644 --- a/novelwriter/core/toodt.py +++ b/novelwriter/core/toodt.py @@ -261,8 +261,8 @@ class ToOdt(Tokenizer): # =============== if self._headerText == "": - theTitle = self.theProject.bookTitle - theAuth = self.theProject.getAuthors() + theTitle = self.theProject.data.title + theAuth = self.theProject.data.getAuthors(self.tr("and")) self._headerText = f"{theTitle} / {theAuth} /" # Create Roots diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 368c25ee..0d916205 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -157,7 +157,7 @@ class GuiProjectDetailsMain(QWidget): # Header # ====== - self.bookTitle = QLabel(self.theProject.bookTitle) + self.bookTitle = QLabel(self.theProject.data.title) bookFont = self.bookTitle.font() bookFont.setPointSizeF(2.2*fPt) bookFont.setWeight(QFont.Bold) @@ -175,7 +175,9 @@ class GuiProjectDetailsMain(QWidget): self.projName.setAlignment(Qt.AlignHCenter) self.projName.setWordWrap(True) - self.bookAuthors = QLabel(self.tr("By {0}").format(self.theProject.getAuthors())) + self.bookAuthors = QLabel(self.tr("By {0}").format( + self.theProject.data.getAuthors(self.tr("and")) + )) authFont = self.bookAuthors.font() authFont.setPointSizeF(1.2*fPt) self.bookAuthors.setFont(authFont) @@ -255,7 +257,7 @@ class GuiProjectDetailsMain(QWidget): self.wordCountVal.setText(f"{nwCount:n}") self.chapCountVal.setText(f"{hCounts[2]:n}") self.sceneCountVal.setText(f"{hCounts[3]:n}") - self.revCountVal.setText(f"{self.theProject.saveCount:n}") + 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) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 78165613..988b520b 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -115,8 +115,8 @@ class GuiProjectSettings(PagedDialog): doBackup = not self.tabMain.doBackup.isChecked() self.theProject.data.setName(projName) - self.theProject.setBookTitle(bookTitle) - self.theProject.setBookAuthors(bookAuthors) + self.theProject.data.setTitle(bookTitle) + self.theProject.data.setAuthors(bookAuthors) self.theProject.setProjBackup(doBackup) # Remember this as updating spell dictionary can be expensive @@ -219,7 +219,7 @@ class GuiProjectEditMain(QWidget): self.editTitle = QLineEdit() self.editTitle.setMaxLength(200) self.editTitle.setMaximumWidth(xW) - self.editTitle.setText(self.theProject.bookTitle) + self.editTitle.setText(self.theProject.data.title) self.mainForm.addRow( self.tr("Novel title"), self.editTitle, @@ -229,7 +229,7 @@ class GuiProjectEditMain(QWidget): self.editAuthors = QPlainTextEdit() self.editAuthors.setMaximumHeight(xH) self.editAuthors.setMaximumWidth(xW) - self.editAuthors.setPlainText("\n".join(self.theProject.bookAuthors)) + self.editAuthors.setPlainText("\n".join(self.theProject.data.authors)) self.mainForm.addRow( self.tr("Author(s)"), self.editAuthors, diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 3529d577..e9aa37cd 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -973,8 +973,8 @@ class GuiBuildNovel(QDialog): jsonData = { "meta": { "workingTitle": self.theProject.data.name, - "novelTitle": self.theProject.bookTitle, - "authors": self.theProject.bookAuthors, + "novelTitle": self.theProject.data.title, + "authors": self.theProject.data.authors, "buildTime": self.buildTime, } } diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 9ac7271e..16ba37d4 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -555,22 +555,22 @@ def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir): assert os.path.isfile(backFile) is False # Successful save - saveCount = theProject.saveCount - autoCount = theProject.autoCount + saveCount = theProject.data.saveCount + autoCount = theProject.data.autoCount assert theProject.saveProject() is True - assert theProject.saveCount == saveCount + 1 - assert theProject.autoCount == autoCount + assert theProject.data.saveCount == saveCount + 1 + assert theProject.data.autoCount == autoCount assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # Check that a second save creates a .bak file assert os.path.isfile(backFile) is True # Successful autosave - saveCount = theProject.saveCount - autoCount = theProject.autoCount + saveCount = theProject.data.saveCount + autoCount = theProject.data.autoCount assert theProject.saveProject(autoSave=True) is True - assert theProject.saveCount == saveCount - assert theProject.autoCount == autoCount + 1 + assert theProject.data.saveCount == saveCount + assert theProject.data.autoCount == autoCount + 1 assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # Close test project @@ -902,30 +902,31 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.data.name == "A Name" # Project Title - theProject.setBookTitle(" A Title ") - assert theProject.bookTitle == "A Title" + theProject.data.setTitle(" A Title ") + assert theProject.data.title == "A Title" # Project Authors # Check that the list is cleaned up and that it can be extracted as # a properly formatted string, depending on number of names - assert not theProject.setBookAuthors([]) - assert theProject.setBookAuthors(" Jane Doe \n John Doh \n ") - assert theProject.bookAuthors == ["Jane Doe", "John Doh"] + theProject.data.setAuthors([]) + assert theProject.data.authors == [] + theProject.data.setAuthors(" Jane Doe \n John Doh \n ") + assert theProject.data.authors == ["Jane Doe", "John Doh"] - assert theProject.setBookAuthors("") - assert theProject.getAuthors() == "" + theProject.data.setAuthors("") + assert theProject.data.getAuthors() == "" - assert theProject.setBookAuthors("Jane Doe") - assert theProject.getAuthors() == "Jane Doe" + theProject.data.setAuthors("Jane Doe") + assert theProject.data.getAuthors() == "Jane Doe" - assert theProject.setBookAuthors("Jane Doe\nJohn Doh") - assert theProject.getAuthors() == "Jane Doe and John Doh" + theProject.data.setAuthors("Jane Doe\nJohn Doh") + assert theProject.data.getAuthors() == "Jane Doe and John Doh" - assert theProject.setBookAuthors("Jane Doe\nJohn Doh\nBod Owens") - assert theProject.getAuthors() == "Jane Doe, John Doh and Bod Owens" + theProject.data.setAuthors("Jane Doe\nJohn Doh\nBod Owens") + assert theProject.data.getAuthors() == "Jane Doe, John Doh and Bod Owens" # Edit Time - theProject.editTime = 1234 + theProject.data.setEditTime(1234) theProject.projOpened = 1600000000 with monkeypatch.context() as mp: mp.setattr("novelwriter.core.project.time", lambda: 1600005600) diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py index 84360d11..c7d86670 100644 --- a/tests/test_dialogs/test_dlg_projdetails.py +++ b/tests/test_dialogs/test_dlg_projdetails.py @@ -55,7 +55,7 @@ def testDlgProjDetails_Dialog(qtbot, nwGUI, nwLipsum): assert projDet.tabMain.wordCountVal.text() == f"{3000:n}" assert projDet.tabMain.chapCountVal.text() == f"{3:n}" assert projDet.tabMain.sceneCountVal.text() == f"{5:n}" - assert projDet.tabMain.revCountVal.text() == f"{nwGUI.theProject.saveCount:n}" + assert projDet.tabMain.revCountVal.text() == f"{nwGUI.theProject.data.saveCount:n}" assert projDet.tabMain.projPathVal.text() == nwLipsum diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index 7e8b4e1d..71c90ba8 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -96,7 +96,7 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd # Set some values theProject = nwGUI.theProject theProject.setSpellLang("en") - theProject.setBookAuthors("Jane Smith\nJohn Smith") + theProject.data.setAuthors("Jane Smith\nJohn Smith") theProject.setAutoReplace({"A": "B", "C": "D"}) # Create Dialog @@ -137,8 +137,8 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd projSettings._doSave() assert theProject.data.name == "Project Name" - assert theProject.bookTitle == "Project Title" - assert theProject.bookAuthors == ["Jane Doe", "John Doh"] + assert theProject.data.title == "Project Title" + assert theProject.data.authors == ["Jane Doe", "John Doh"] # Clean up projSettings._doClose() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 4ea895e6..fda4edea 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -172,9 +172,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.tree.trashRoot() is None assert nwGUI.theProject.projPath is None assert nwGUI.theProject.projMeta is None - assert nwGUI.theProject.data.name == "New Project" - assert nwGUI.theProject.bookTitle == "" - assert len(nwGUI.theProject.bookAuthors) == 0 + assert nwGUI.theProject.data.name == "" + assert nwGUI.theProject.data.title == "" + assert nwGUI.theProject.data.authors == [] assert not nwGUI.theProject.spellCheck # Check the files @@ -195,8 +195,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.projPath == fncProj assert nwGUI.theProject.projMeta == os.path.join(fncProj, "meta") assert nwGUI.theProject.data.name == "New Project" - assert nwGUI.theProject.bookTitle == "New Novel" - assert len(nwGUI.theProject.bookAuthors) == 1 + assert nwGUI.theProject.data.title == "New Novel" + assert nwGUI.theProject.data.authors == ["Jane Doe"] assert nwGUI.theProject.spellCheck is False # Check that tree items have been created diff --git a/tests/tools.py b/tests/tools.py index bf8d2c40..82e528fe 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -168,8 +168,8 @@ def buildTestProject(theObject, projPath): theProject.clearProject() theProject.setProjectPath(projPath, newProject=True) theProject.data.setName("New Project") - theProject.setBookTitle("New Novel") - theProject.setBookAuthors("Jane Doe") + theProject.data.setTitle("New Novel") + theProject.data.setAuthors("Jane Doe") # Creating a minimal project with a few root folders and a # single chapter folder with a single file. From 9d283d2eae11c0cacf4527f2d8b694861340df7c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:55:33 +0100 Subject: [PATCH 06/18] Move all remaining trivial project settings to the data class --- novelwriter/core/project.py | 176 ++++++++----------------- novelwriter/core/projectdata.py | 71 +++++++++- novelwriter/core/projectxml.py | 72 +++++++--- novelwriter/dialogs/projsettings.py | 4 +- novelwriter/gui/docviewer.py | 2 +- novelwriter/gui/noveltree.py | 11 +- novelwriter/gui/outline.py | 6 +- novelwriter/guimain.py | 24 ++-- novelwriter/tools/build.py | 2 +- tests/test_core/test_core_project.py | 46 +++---- tests/test_core/test_core_tokenizer.py | 4 +- tests/tools.py | 2 +- 12 files changed, 225 insertions(+), 195 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index cecb6092..e4153709 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -74,10 +74,10 @@ class NWProject: self._langData = {} # Localisation data # Project Status - self.projOpened = 0 # The time stamp of when the project file was opened - self.projChanged = False # The project has unsaved changes - self.projAltered = False # The project has been altered this session - self.lockedBy = None # Data on which computer has the project open + self._projOpened = 0 # The time stamp of when the project file was opened + self._projChanged = False # The project has unsaved changes + self._projAltered = False # The project has been altered this session + 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 @@ -86,7 +86,6 @@ class NWProject: self.projContent = None # The full path to the project's content folder self.projDict = None # The spell check dictionary self.projSpell = None # The spell check language, if different than default - self.projLang = None # The project language, used for builds self.projFiles = [] # A list of all files in the content folder on load # Project Settings @@ -95,17 +94,9 @@ class NWProject: self.spellCheck = False # Controls the spellcheck-as-you-type feature self.statusItems = None # Novel file progress status values self.importItems = None # Note file importance values - self.lastEdited = None # The handle of the last file to be edited - self.lastViewed = None # The handle of the last file to be viewed - self.lastNovel = None # The handle of the last novel root viewed - self.lastOutline = None # The handle of the last outline root viewed - self.lastWCount = 0 # The project word count from last session - self.lastNovelWC = 0 # The novel files word count from last session - self.lastNotesWC = 0 # The note files word count from last session self.currWCount = 0 # The project word count in current session self.currNovelWC = 0 # The novel files word count in cutrent session self.currNotesWC = 0 # The note files word count in cutrent session - self.doBackup = True # Run project backup on exit # Internal Mapping self.tr = partial(QCoreApplication.translate, "NWProject") @@ -135,6 +126,18 @@ class NWProject: def options(self): return self._optState + @property + def projOpened(self): + return self._projOpened + + @property + def projChanged(self): + return self._projChanged or self._data.changed + + @property + def projAltered(self): + return self._projAltered + ## # Item Methods ## @@ -243,9 +246,9 @@ class NWProject: default values. """ # Project Status - self.projOpened = 0 - self.projChanged = False - self.projAltered = False + self._projOpened = 0 + self._projChanged = False + self._projAltered = False # Project Tree self._projTree.clear() @@ -259,7 +262,6 @@ class NWProject: self.projContent = None self.projDict = None self.projSpell = None - self.projLang = None self.projFiles = [] self.autoReplace = {} self.titleFormat = { @@ -280,11 +282,6 @@ class NWProject: self.importItems.write(None, self.tr("Minor"), (200, 50, 0)) self.importItems.write(None, self.tr("Major"), (200, 150, 0)) self.importItems.write(None, self.tr("Main"), (50, 200, 0)) - self.lastEdited = None - self.lastViewed = None - self.lastWCount = 0 - self.lastNovelWC = 0 - self.lastNotesWC = 0 self.currWCount = 0 self.currNovelWC = 0 self.currNotesWC = 0 @@ -414,7 +411,7 @@ class NWProject: # Finalise if popCustom or popMinimal: - self.projOpened = time() + self._projOpened = time() self.setProjectChanged(True) self.saveProject(autoSave=True) @@ -484,10 +481,10 @@ class NWProject: # print(json.dumps(xmlData, indent=2)) - nwxRoot = xmlData.get("xmlRoot", "") - appVersion = xmlData.get("appVersion", self.tr("Unknown")) - hexVersion = xmlData.get("hexVersion", 0x0000) - xmlVersion = xmlData.get("xmlVersion", self.tr("Unknown")) + nwxRoot = xmlReader.xmlRoot + appVersion = xmlReader.appVersion or self.tr("Unknown") + hexVersion = xmlReader.hexVersion or "0x0" + xmlVersion = xmlReader.xmlVersion or self.tr("Unknown") if not xmlParsed: if xmlReader.state == XMLReadState.NOT_NWX_FILE: @@ -555,17 +552,8 @@ class NWProject: xmlSettings = xmlData.get("settings", {}) - self.doBackup = xmlSettings.get("doBackup", False) - self.projLang = xmlSettings.get("language", None) - self.spellCheck = xmlSettings.get("spellCheck", False) - self.projSpell = xmlSettings.get("spellLang", None) - self.lastEdited = xmlSettings.get("lastEdited", None) - self.lastViewed = xmlSettings.get("lastViewed", None) - self.lastNovel = xmlSettings.get("lastNovel", None) - self.lastOutline = xmlSettings.get("lastOutline", None) - self.lastWCount = xmlSettings.get("lastWordCount", 0) - self.lastNovelWC = xmlSettings.get("novelWordCount", 0) - self.lastNotesWC = xmlSettings.get("notesWordCount", 0) + self.spellCheck = self._data.spellCheck + self.projSpell = self._data.spellLang self.statusItems.unpack(xmlSettings.get("status", {})) self.importItems.unpack(xmlSettings.get("import", {})) self.autoReplace = xmlSettings.get("autoReplace", {}) @@ -590,7 +578,9 @@ class NWProject: self._deprecatedFiles() # Update recent projects - self.mainConf.updateRecentCache(self.projPath, self._data.name, self.lastWCount, time()) + self.mainConf.updateRecentCache( + self.projPath, self._data.name, self._data.getLastCount("total"), time() + ) self.mainConf.saveRecentCache() # Check the project tree consistency @@ -605,8 +595,8 @@ class NWProject: self._loadProjectLocalisation() self.updateWordCounts() - self.projOpened = time() - self.projAltered = False + self._projOpened = time() + self._projAltered = False self._writeLockFile() self.setProjectChanged(False) @@ -647,7 +637,7 @@ class NWProject: }) self.updateWordCounts() - editTime = int(self._data.editTime + saveTime - self.projOpened) + editTime = int(self._data.editTime + saveTime - self._projOpened) # Save Project Meta xProject = etree.SubElement(nwXML, "project") @@ -660,14 +650,14 @@ class NWProject: # Save Project Settings xSettings = etree.SubElement(nwXML, "settings") - self._packProjectValue(xSettings, "doBackup", self.doBackup) - self._packProjectValue(xSettings, "language", self.projLang) - self._packProjectValue(xSettings, "spellCheck", self.spellCheck) - self._packProjectValue(xSettings, "spellLang", self.projSpell) - self._packProjectValue(xSettings, "lastEdited", self.lastEdited) - self._packProjectValue(xSettings, "lastViewed", self.lastViewed) - self._packProjectValue(xSettings, "lastNovel", self.lastNovel) - self._packProjectValue(xSettings, "lastOutline", self.lastOutline) + self._packProjectValue(xSettings, "doBackup", self._data.doBackup) + self._packProjectValue(xSettings, "language", self._data.language) + self._packProjectValue(xSettings, "spellCheck", self._data.spellCheck) + self._packProjectValue(xSettings, "spellLang", self._data.spellLang) + self._packProjectValue(xSettings, "lastEdited", self._data.getLastHandle("editor")) + self._packProjectValue(xSettings, "lastViewed", self._data.getLastHandle("viewer")) + self._packProjectValue(xSettings, "lastNovel", self._data.getLastHandle("noveltree")) + self._packProjectValue(xSettings, "lastOutline", self._data.getLastHandle("outline")) self._packProjectValue(xSettings, "lastWordCount", self.currWCount) self._packProjectValue(xSettings, "novelWordCount", self.currNovelWC) self._packProjectValue(xSettings, "notesWordCount", self.currNotesWC) @@ -942,28 +932,6 @@ class NWProject: return True - def setProjBackup(self, doBackup): - """Set whether projects should be backed up or not. The user - will be notified in case required settings are missing. - """ - self.doBackup = doBackup - if doBackup: - if not os.path.isdir(self.mainConf.backupPath): - self.mainGui.makeAlert(self.tr( - "You must set a valid backup path in Preferences to use " - "the automatic project backup feature." - ), nwAlert.WARN) - return False - - if self._data.name == "": - self.mainGui.makeAlert(self.tr( - "You must set a valid project name in Project Settings to " - "use the automatic project backup feature." - ), nwAlert.WARN) - return False - - return True - def setSpellCheck(self, theMode): """Enable/disable spell checking. """ @@ -986,8 +954,8 @@ class NWProject: """Set the project-specific language. """ theLang = checkStringNone(theLang, None) - if self.projLang != theLang: - self.projLang = theLang + if self._data.language != theLang: + self._data.setLanguage(theLang) self._loadProjectLocalisation() self.setProjectChanged(True) return True @@ -1003,38 +971,6 @@ class NWProject: self.setProjectChanged(True) return True - def setLastEdited(self, tHandle): - """Set last edited project item. - """ - if self.lastEdited != tHandle: - self.lastEdited = tHandle - self.setProjectChanged(True) - return True - - def setLastViewed(self, tHandle): - """Set last viewed project item. - """ - if self.lastViewed != tHandle: - self.lastViewed = tHandle - self.setProjectChanged(True) - return True - - def setLastNovelViewed(self, tHandle): - """Set last viewed novel root in the novel tree. - """ - if self.lastNovel != tHandle: - self.lastNovel = tHandle - self.setProjectChanged(True) - return True - - def setLastOutlineViewed(self, tHandle): - """Set last viewed novel root in the outline view. - """ - if self.lastOutline != tHandle: - self.lastOutline = tHandle - self.setProjectChanged(True) - return True - def setStatusColours(self, newCols, delCols): """Update the list of novel file status flags. """ @@ -1068,12 +1004,15 @@ class NWProject: """Toggle the project changed flag, and propagate the information to the GUI statusbar. """ - self.projChanged = bValue + self._projChanged = bValue self.mainGui.mainStatus.doUpdateProjectStatus(bValue) - if bValue: + if bValue is True: # If we've changed the project at all, this should be True - self.projAltered = True - return self.projChanged + self._projAltered = True + else: + # If we're resetting the status, also reset for data class + self._data.resetProjectChanged() + return self._projChanged ## # Getters @@ -1083,7 +1022,7 @@ class NWProject: """Get the total project edit time, including the time spent in the current session. """ - return round(self._data.editTime + time() - self.projOpened) + return round(self._data.editTime + time() - self._projOpened) def getProjectItems(self): """This function ensures that the item tree loaded is sent to @@ -1193,11 +1132,11 @@ class NWProject: def _loadProjectLocalisation(self): """Load the language data for the current project language. """ - if self.projLang is None: + if self._data.language is None: self._langData = {} return False - langFile = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang) + 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") @@ -1421,8 +1360,9 @@ class NWProject: isFile = os.path.isfile(sessionFile) nowTime = time() - sessDiff = self.currWCount - self.lastWCount - sessTime = nowTime - self.projOpened + lastCount = self._data.getLastCount("total") + sessDiff = self.currWCount - lastCount + sessTime = nowTime - self._projOpened logger.info("The session lasted %d sec and added %d words", int(sessTime), sessDiff) if sessTime < 300 and sessDiff == 0: @@ -1433,14 +1373,14 @@ class NWProject: with open(sessionFile, mode="a+", encoding="utf-8") as outFile: if not isFile: # It's a new file, so add a header - if self.lastWCount > 0: - outFile.write("# Offset %d\n" % self.lastWCount) + if lastCount > 0: + outFile.write("# Offset %d\n" % lastCount) outFile.write("# %-17s %-19s %8s %8s %8s\n" % ( "Start Time", "End Time", "Novel", "Notes", "Idle" )) outFile.write("%-19s %-19s %8d %8d %8d\n" % ( - formatTimeStamp(self.projOpened), + formatTimeStamp(self._projOpened), formatTimeStamp(nowTime), self.currNovelWC, self.currNotesWC, diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 49ac64af..d87c5660 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -25,7 +25,9 @@ along with this program. If not, see . import logging -from novelwriter.common import checkInt, simplified +from novelwriter.common import ( + checkBool, checkInt, checkStringNone, simplified +) logger = logging.getLogger(__name__) @@ -42,6 +44,14 @@ class NWProjectData: self._autoCount = 0 self._editTime = 0 + # Project Settings + self._doBackup = True + self._language = None + self._spellCheck = False + self._spellLang = None + self._lastHandle = {} + self._lastCount = {} + # Internal self._changed = False @@ -75,6 +85,26 @@ class NWProjectData: 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 changed(self): + return self._changed + ## # Methods ## @@ -94,10 +124,19 @@ class NWProjectData: self._changed = True return + def resetProjectChanged(self): + self._changed = False + ## # Getters ## + def getLastHandle(self, component): + return self._lastHandle.get(component, None) + + def getLastCount(self, type): + return self._lastCount.get(type, 0) + def getAuthors(self, trAnd="and"): """Return a formatted string of authors. """ @@ -155,4 +194,34 @@ class NWProjectData: self._changed = True return + def setDoBackup(self, value): + self._doBackup = checkBool(value, False) + self._changed = True + return + + def setLanguage(self, value): + self._language = checkStringNone(value, None) + self._changed = True + return + + def setSpellCheck(self, value): + self._spellCheck = checkBool(value, False) + self._changed = True + return + + def setSpellLang(self, value): + self._spellLang = checkStringNone(value, None) + self._changed = True + return + + def setLastHandle(self, value, component): + self._lastHandle[component] = checkStringNone(value, None) + self._changed = True + return + + def setLastCount(self, value, type): + self._lastCount[type] = checkInt(value, 0) + self._changed = True + return + # END Class NWProjectData diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 7b78dd2c..8760f6b1 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -69,10 +69,16 @@ class ProjectXMLReader: self._state = XMLReadState.NO_ACTION self._data = {} - self._version = 0x0000 + self._content = [] self._statusData = {} self._statusMap = {} + self._root = "" + self._version = 0x0000 + self._appVersion = "" + self._hexVersion = "" + self._timeStamp = "" + return ## @@ -83,10 +89,34 @@ class ProjectXMLReader: def data(self): return self._data + @property + def content(self): + return self._content + @property def state(self): return self._state + @property + def xmlRoot(self): + return self._root + + @property + def xmlVersion(self): + return self._version + + @property + def appVersion(self): + return self._appVersion + + @property + def hexVersion(self): + return self._hexVersion + + @property + def timeStamp(self): + return self._timeStamp + ## # Methods ## @@ -95,6 +125,7 @@ class ProjectXMLReader: """Read and parse the project XML file. """ self._data = {} + self._content = [] try: xml = etree.parse(self._path) @@ -119,8 +150,8 @@ class ProjectXMLReader: return False xRoot = xml.getroot() - self._data["xmlRoot"] = str(xRoot.tag) - if xRoot.tag != "novelWriterXML": + self._root = str(xRoot.tag) + if self._root != "novelWriterXML": self._state = XMLReadState.NOT_NWX_FILE return False @@ -147,17 +178,16 @@ class ProjectXMLReader: self._state = XMLReadState.UNKNOWN_VERSION return False - self._data["xmlVersion"] = self._version - self._data["appVersion"] = str(xRoot.attrib.get("appVersion", "")) - self._data["hexVersion"] = str(xRoot.attrib.get("appVersion", "")) - self._data["timeStamp"] = str(xRoot.attrib.get("timeStamp", "")) + self._appVersion = str(xRoot.attrib.get("appVersion", "")) + self._hexVersion = str(xRoot.attrib.get("appVersion", "")) + self._timeStamp = str(xRoot.attrib.get("timeStamp", "")) status = True for xSection in xRoot: if xSection.tag == "project": status &= self._parseProjectMeta(xSection, projData) elif xSection.tag == "settings": - status &= self._parseProjectSettings(xSection) + status &= self._parseProjectSettings(xSection, projData) elif xSection.tag == "content": if self._version >= 0x0104: status &= self._parseProjectContent(xSection) @@ -202,7 +232,7 @@ class ProjectXMLReader: logger.warning("Ignored in xml", xItem.tag) return True - def _parseProjectSettings(self, xSection): + def _parseProjectSettings(self, xSection, projData): """Parse the settings section of the XML file. """ logger.debug("Parsing xml ") @@ -212,27 +242,27 @@ class ProjectXMLReader: titleFormat = {} for xItem in xSection: if xItem.tag == "doBackup": - data["doBackup"] = checkBool(xItem.text, False) + projData.setDoBackup(xItem.text) elif xItem.tag == "language": - data["language"] = checkStringNone(xItem.text, None) + projData.setLanguage(xItem.text) elif xItem.tag == "spellCheck": - data["spellCheck"] = checkBool(xItem.text, False) + projData.setSpellCheck(xItem.text) elif xItem.tag == "spellLang": - data["spellLang"] = checkStringNone(xItem.text, None) + projData.setSpellLang(xItem.text) elif xItem.tag == "lastEdited": - data["lastEdited"] = checkStringNone(xItem.text, None) + projData.setLastHandle(xItem.text, "editor") elif xItem.tag == "lastViewed": - data["lastViewed"] = checkStringNone(xItem.text, None) + projData.setLastHandle(xItem.text, "viewer") elif xItem.tag == "lastNovel": - data["lastNovel"] = checkStringNone(xItem.text, None) + projData.setLastHandle(xItem.text, "noveltree") elif xItem.tag == "lastOutline": - data["lastOutline"] = checkStringNone(xItem.text, None) + projData.setLastHandle(xItem.text, "outline") elif xItem.tag == "lastWordCount": - data["lastWordCount"] = checkInt(xItem.text, 0) + projData.setLastCount(xItem.text, "total") elif xItem.tag == "novelWordCount": - data["novelWordCount"] = checkInt(xItem.text, 0) + projData.setLastCount(xItem.text, "novel") elif xItem.tag == "notesWordCount": - data["notesWordCount"] = checkInt(xItem.text, 0) + projData.setLastCount(xItem.text, "notes") elif xItem.tag == "status": data["status"] = self._parseStatusImport(xItem, "status") elif xItem.tag in ("import", "importance"): @@ -293,6 +323,7 @@ class ProjectXMLReader: else: logger.warning("Ignored in xml", xVal.tag) data.append(item) + self._content.append(item) else: logger.warning("Ignored item in xml", xItem.tag) @@ -357,6 +388,7 @@ class ProjectXMLReader: item["type"] = "ROOT" data.append(item) + self._content.append(item) else: logger.warning("Ignored in xml", xItem.tag) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 988b520b..a9ee7188 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -117,7 +117,7 @@ class GuiProjectSettings(PagedDialog): self.theProject.data.setName(projName) self.theProject.data.setTitle(bookTitle) self.theProject.data.setAuthors(bookAuthors) - self.theProject.setProjBackup(doBackup) + self.theProject.data.setDoBackup(doBackup) # Remember this as updating spell dictionary can be expensive self._spellChanged = self.theProject.setSpellLang(spellLang) @@ -258,7 +258,7 @@ class GuiProjectEditMain(QWidget): self.spellLang.setCurrentIndex(spellIdx) self.doBackup = QSwitch(self) - self.doBackup.setChecked(not self.theProject.doBackup) + self.doBackup.setChecked(not self.theProject.data.doBackup) self.mainForm.addRow( self.tr("No backup on close"), self.doBackup, diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 0437363e..808749df 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -217,7 +217,7 @@ class GuiDocViewer(QTextBrowser): self.verticalScrollBar().setValue(sPos) self._docHandle = tHandle - self.theProject.setLastViewed(tHandle) + self.theProject._data.setLastHandle(tHandle, "viewer") self.docHeader.setTitleFromHandle(self._docHandle) self.updateDocMargins() diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 210626b5..bfa1da1c 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -109,7 +109,7 @@ class GuiNovelView(QWidget): def refreshTree(self): """Refresh the current tree. """ - self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel) + self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("noveltree")) return def clearProject(self): @@ -122,7 +122,7 @@ class GuiNovelView(QWidget): def openProjectTasks(self): """Run open project tasks. """ - lastNovel = self.theProject.lastNovel + lastNovel = self.theProject.data.getLastHandle("noveltree") if lastNovel not in self.theProject.tree: lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) @@ -319,7 +319,7 @@ class GuiNovelToolBar(QWidget): def _refreshNovelTree(self): """Rebuild the current tree. """ - rootHandle = self.theProject.lastNovel + rootHandle = self.theProject.data.getLastHandle("noveltree") self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return @@ -485,7 +485,7 @@ class GuiNovelTree(QTreeWidget): titleKey = selItem[0].data(self.C_TITLE, self.D_KEY) self._populateTree(rootHandle) - self.theProject.setLastNovelViewed(rootHandle) + self.theProject.data.setLastHandle(rootHandle, "noveltree") if titleKey is not None and titleKey in self._treeMap: self._treeMap[titleKey].setSelected(True) @@ -523,7 +523,8 @@ class GuiNovelTree(QTreeWidget): self._lastCol = colType self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN) if doRefresh: - self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) + lastNovel = self.theProject.data.getLastHandle("noveltree") + self.refreshTree(rootHandle=lastNovel, overRide=True) return def setActiveHandle(self, tHandle): diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index b5a1060a..5d66d139 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -114,7 +114,7 @@ class GuiOutlineView(QWidget): def refreshTree(self): """Refresh the current tree. """ - self.outlineTree.refreshTree(rootHandle=self.theProject.lastOutline) + self.outlineTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("outline")) return def clearProject(self): @@ -126,7 +126,7 @@ class GuiOutlineView(QWidget): def openProjectTasks(self): """Run open project tasks. """ - lastOutline = self.theProject.lastOutline + lastOutline = self.theProject.data.getLastHandle("outline") if not (lastOutline in self.theProject.tree or lastOutline is None): lastOutline = self.theProject.tree.findRoot(nwItemClass.NOVEL) @@ -504,7 +504,7 @@ class GuiOutlineTree(QTreeWidget): return self._populateTree(rootHandle) - self.theProject.setLastOutlineViewed(rootHandle or None) + self.theProject.data.setLastHandle(rootHandle or None, "outline") return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index af76edf8..d3eb4f2b 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -417,7 +417,7 @@ class GuiMain(QMainWindow): if self.theProject.projAltered: saveOK = self.saveProject() doBackup = False - if self.theProject.doBackup and self.mainConf.backupOnClose: + if self.theProject.data.doBackup and self.mainConf.backupOnClose: doBackup = True if self.mainConf.askBeforeBackup: msgYes = self.askQuestion( @@ -532,11 +532,13 @@ class GuiMain(QMainWindow): self._updateStatusWordCount() # Restore previously open documents, if any - if self.theProject.lastEdited is not None: - self.openDocument(self.theProject.lastEdited, doScroll=True) + lastEdited = self.theProject.data.getLastHandle("editor") + if lastEdited is not None: + self.openDocument(lastEdited, doScroll=True) - if self.theProject.lastViewed is not None: - self.viewDocument(self.theProject.lastViewed) + lastViewed = self.theProject.data.getLastHandle("viewer") + if lastViewed is not None: + self.viewDocument(lastViewed) # Check if we need to rebuild the index if self.theProject.index.indexBroken: @@ -607,7 +609,7 @@ class GuiMain(QMainWindow): if self.docEditor.loadText(tHandle, tLine): if changeFocus: self.docEditor.setFocus() - self.theProject.setLastEdited(tHandle) + self.theProject.data.setLastHandle(tHandle, "editor") self.projView.setSelectedHandle(tHandle, doScroll=doScroll) self.novelView.setActiveHandle(tHandle) else: @@ -676,7 +678,7 @@ class GuiMain(QMainWindow): tHandle = self.projView.getSelectedHandle() if tHandle is None: - tHandle = self.theProject.lastViewed + tHandle = self.theProject.data.getLastHandle("viewer") if tHandle is None: logger.debug("No document to view, giving up") @@ -1223,14 +1225,14 @@ class GuiMain(QMainWindow): """Close the document edit panel. This does not hide the editor. """ self.closeDocument() - self.theProject.setLastEdited(None) + self.theProject.data.setLastHandle(None, "editor") return def closeDocViewer(self): """Close the document view panel. """ self.docViewer.clearViewer() - self.theProject.setLastViewed(None) + self.theProject.data.setLastHandle(None, "viewer") bPos = self.splitMain.sizes() self.splitView.setVisible(False) self.splitDocs.setSizes([bPos[1], 0]) @@ -1556,10 +1558,10 @@ class GuiMain(QMainWindow): self.theProject.updateWordCounts() if self.mainConf.incNotesWCount: currWords = self.theProject.currWCount - diffWords = currWords - self.theProject.lastWCount + diffWords = currWords - self.theProject.data.getLastCount("total") else: currWords = self.theProject.currNovelWC - diffWords = currWords - self.theProject.lastNovelWC + diffWords = currWords - self.theProject.data.getLastCount("novel") self.mainStatus.setProjectStats(currWords, diffWords) diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index e9aa37cd..979e3e98 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -168,7 +168,7 @@ class GuiBuildNovel(QDialog): for langID, langName in theLangs: self.buildLang.addItem(langName, langID) - langIdx = self.buildLang.findData(self.theProject.projLang) + langIdx = self.buildLang.findData(self.theProject.data.language) if langIdx != -1: self.buildLang.setCurrentIndex(langIdx) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 16ba37d4..7d1edf17 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -927,7 +927,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): # Edit Time theProject.data.setEditTime(1234) - theProject.projOpened = 1600000000 + theProject._projOpened = 1600000000 with monkeypatch.context() as mp: mp.setattr("novelwriter.core.project.time", lambda: 1600005600) assert theProject.getCurrentEditTime() == 6834 @@ -939,28 +939,14 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.trashFolder() == hTrash assert theProject.trashFolder() == hTrash - # Project backup - assert theProject.doBackup is True - assert theProject.setProjBackup(False) - assert theProject.doBackup is False - - assert not theProject.setProjBackup(True) - theProject.mainConf.backupPath = tmpDir - assert theProject.setProjBackup(True) - - theProject.data.setName("") - assert not theProject.setProjBackup(True) - theProject.data.setName("A Name") - assert theProject.setProjBackup(True) - # Spell check - theProject.projChanged = False + theProject.setProjectChanged(False) assert theProject.setSpellCheck(True) assert not theProject.setSpellCheck(False) assert theProject.projChanged # Spell language - theProject.projChanged = False + theProject.setProjectChanged(False) assert theProject.projSpell is None assert theProject.setSpellLang(None) is False assert theProject.projSpell is None @@ -971,31 +957,31 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.projChanged # Project Language - theProject.projChanged = False - theProject.projLang = "en" + theProject.setProjectChanged(False) + theProject.data.setLanguage("en") assert theProject.setProjectLang(None) is True - assert theProject.projLang is None + assert theProject.data.language is None assert theProject.setProjectLang("en_GB") is True - assert theProject.projLang == "en_GB" + assert theProject.data.language == "en_GB" # Language Lookup assert theProject.localLookup(1) == "One" assert theProject.localLookup(10) == "Ten" # Last edited - theProject.projChanged = False - assert theProject.setLastEdited("0123456789abc") - assert theProject.lastEdited == "0123456789abc" + theProject.setProjectChanged(False) + theProject._data.setLastHandle("0123456789abc", "editor") + assert theProject._data.getLastHandle("editor") == "0123456789abc" assert theProject.projChanged # Last viewed - theProject.projChanged = False - assert theProject.setLastViewed("0123456789abc") - assert theProject.lastViewed == "0123456789abc" + theProject.setProjectChanged(False) + theProject._data.setLastHandle("0123456789abc", "viewer") + assert theProject._data.getLastHandle("viewer") == "0123456789abc" assert theProject.projChanged # Autoreplace - theProject.projChanged = False + theProject.setProjectChanged(False) assert theProject.setAutoReplace({"A": "B", "C": "D"}) assert theProject.autoReplace == {"A": "B", "C": "D"} assert theProject.projChanged @@ -1019,7 +1005,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): # Session stats theProject.currWCount = 200 - theProject.lastWCount = 100 + theProject.data.setLastCount(100, "total") with monkeypatch.context() as mp: mp.setattr("os.path.isdir", lambda *a, **k: False) assert not theProject._appendSessionStats(idleTime=0) @@ -1033,7 +1019,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.projMeta == os.path.join(fncDir, "meta") statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS) - theProject.projOpened = 1600002000 + theProject._projOpened = 1600002000 theProject.currNovelWC = 200 theProject.currNotesWC = 100 diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index 38121268..23f47565 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -137,7 +137,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): """Test handling files and text in the Tokenizer class. """ theProject = NWProject(mockGUI) - theProject.projLang = "en" + theProject.data.setLanguage("en") theProject._loadProjectLocalisation() theToken = BareTokenizer(theProject) @@ -884,7 +884,7 @@ def testCoreToken_ProcessHeaders(mockGUI): """Test the header and page parser of the Tokenizer class. """ theProject = NWProject(mockGUI) - theProject.projLang = "en" + theProject.data.setLanguage("en") theProject._loadProjectLocalisation() theToken = BareTokenizer(theProject) diff --git a/tests/tools.py b/tests/tools.py index 82e528fe..1cbc0359 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -195,7 +195,7 @@ def buildTestProject(theObject, projPath): aDoc.writeDocument("### %s\n\n" % theProject.tr("New Scene")) theProject.index.reIndexHandle(xHandle[8]) - theProject.projOpened = time.time() + theProject._projOpened = time.time() theProject.setProjectChanged(True) theProject.saveProject(autoSave=True) From cc2ef85afd1831ec773f50d7aac418be44ad71ef Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 16:19:31 +0100 Subject: [PATCH 07/18] CHange how current word counts are handled --- novelwriter/core/project.py | 50 +++++++++++++++++----------- novelwriter/core/projectdata.py | 25 ++++++-------- novelwriter/core/toodt.py | 2 +- novelwriter/dialogs/projdetails.py | 2 +- novelwriter/guimain.py | 4 +-- tests/test_core/test_core_project.py | 14 ++++---- 6 files changed, 51 insertions(+), 46 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index e4153709..9d005bf4 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -94,9 +94,6 @@ class NWProject: self.spellCheck = False # Controls the spellcheck-as-you-type feature self.statusItems = None # Novel file progress status values self.importItems = None # Note file importance values - self.currWCount = 0 # The project word count in current session - self.currNovelWC = 0 # The novel files word count in cutrent session - self.currNotesWC = 0 # The note files word count in cutrent session # Internal Mapping self.tr = partial(QCoreApplication.translate, "NWProject") @@ -282,9 +279,6 @@ class NWProject: self.importItems.write(None, self.tr("Minor"), (200, 50, 0)) self.importItems.write(None, self.tr("Major"), (200, 150, 0)) self.importItems.write(None, self.tr("Main"), (50, 200, 0)) - self.currWCount = 0 - self.currNovelWC = 0 - self.currNotesWC = 0 return @@ -330,7 +324,7 @@ class NWProject: 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._data.getAuthors(self.tr("and")) + titlePage, self.tr("By"), self.getFormattedAuthors() ) aDoc = NWDoc(self, hTitlePage) @@ -658,9 +652,9 @@ class NWProject: self._packProjectValue(xSettings, "lastViewed", self._data.getLastHandle("viewer")) self._packProjectValue(xSettings, "lastNovel", self._data.getLastHandle("noveltree")) self._packProjectValue(xSettings, "lastOutline", self._data.getLastHandle("outline")) - self._packProjectValue(xSettings, "lastWordCount", self.currWCount) - self._packProjectValue(xSettings, "novelWordCount", self.currNovelWC) - self._packProjectValue(xSettings, "notesWordCount", self.currNotesWC) + self._packProjectValue(xSettings, "lastWordCount", self._data.getCurrCount("total")) + self._packProjectValue(xSettings, "novelWordCount", self._data.getCurrCount("novel")) + self._packProjectValue(xSettings, "notesWordCount", self._data.getCurrCount("notes")) self._packProjectKeyValue(xSettings, "autoReplace", self.autoReplace) xTitleFmt = etree.SubElement(xSettings, "titleFormat") @@ -713,7 +707,9 @@ class NWProject: self._optState.saveSettings() # Update recent projects - self.mainConf.updateRecentCache(self.projPath, self._data.name, self.currWCount, saveTime) + self.mainConf.updateRecentCache( + self.projPath, self._data.name, self._data.getCurrCount("total"), saveTime + ) self.mainConf.saveRecentCache() self._writeLockFile() @@ -1018,6 +1014,22 @@ class NWProject: # Getters ## + def getFormattedAuthors(self): + """Return a formatted string of authors. + """ + authors = self._data.authors + nAuth = len(authors) + + result = "" + if nAuth == 1: + result = authors[0] + elif nAuth > 1: + result = "%s %s %s" % ( + ", ".join(authors[0:-1]), self.tr("and"), authors[-1] + ) + + return result + def getCurrentEditTime(self): """Get the total project edit time, including the time spent in the current session. @@ -1074,12 +1086,9 @@ class NWProject: """Update the total word count values. """ wcNovel, wcNotes = self._projTree.sumWords() - wcTotal = wcNovel + wcNotes - if wcTotal != self.currWCount: - self.currNovelWC = wcNovel - self.currNotesWC = wcNotes - self.currWCount = wcTotal - self.setProjectChanged(True) + self._data.setCurrCount(wcNovel, "novel") + self._data.setCurrCount(wcNotes, "notes") + self._data.setCurrCount(wcNovel + wcNotes, "total") return def countStatus(self): @@ -1361,7 +1370,8 @@ class NWProject: nowTime = time() lastCount = self._data.getLastCount("total") - sessDiff = self.currWCount - lastCount + currCount = self._data.getCurrCount("total") + sessDiff = currCount - lastCount sessTime = nowTime - self._projOpened logger.info("The session lasted %d sec and added %d words", int(sessTime), sessDiff) @@ -1382,8 +1392,8 @@ class NWProject: outFile.write("%-19s %-19s %8d %8d %8d\n" % ( formatTimeStamp(self._projOpened), formatTimeStamp(nowTime), - self.currNovelWC, - self.currNotesWC, + self._data.getCurrCount("novel"), + self._data.getCurrCount("notes"), int(idleTime), )) diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index d87c5660..b22c74c9 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -51,6 +51,7 @@ class NWProjectData: self._spellLang = None self._lastHandle = {} self._lastCount = {} + self._currCount = {} # Internal self._changed = False @@ -137,20 +138,8 @@ class NWProjectData: def getLastCount(self, type): return self._lastCount.get(type, 0) - def getAuthors(self, trAnd="and"): - """Return a formatted string of authors. - """ - nAuth = len(self._authors) - authors = "" - - if nAuth == 1: - authors = self._authors[0] - elif nAuth > 1: - authors = "%s %s %s" % ( - ", ".join(self._authors[0:-1]), trAnd, self._authors[-1] - ) - - return authors + def getCurrCount(self, type): + return self._currCount.get(type, 0) ## # Setters @@ -173,7 +162,7 @@ class NWProjectData: for author in value.splitlines(): author = simplified(author) if author: - self.addAuthor(author) + self._authors.append(author) self._changed = True elif isinstance(value, list): self._authors = value @@ -224,4 +213,10 @@ class NWProjectData: self._changed = True return + def setCurrCount(self, value, type): + if value != self._currCount.get(type, 0): + self._currCount[type] = checkInt(value, 0) + self._changed = True + return + # END Class NWProjectData diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py index b4306f30..594074ff 100644 --- a/novelwriter/core/toodt.py +++ b/novelwriter/core/toodt.py @@ -262,7 +262,7 @@ class ToOdt(Tokenizer): if self._headerText == "": theTitle = self.theProject.data.title - theAuth = self.theProject.data.getAuthors(self.tr("and")) + theAuth = self.theProject.getFormattedAuthors() self._headerText = f"{theTitle} / {theAuth} /" # Create Roots diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 0d916205..0f185e89 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -176,7 +176,7 @@ class GuiProjectDetailsMain(QWidget): self.projName.setWordWrap(True) self.bookAuthors = QLabel(self.tr("By {0}").format( - self.theProject.data.getAuthors(self.tr("and")) + self.theProject.getFormattedAuthors() )) authFont = self.bookAuthors.font() authFont.setPointSizeF(1.2*fPt) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index d3eb4f2b..a3e4d295 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1557,10 +1557,10 @@ class GuiMain(QMainWindow): self.theProject.updateWordCounts() if self.mainConf.incNotesWCount: - currWords = self.theProject.currWCount + currWords = self.theProject.data.getCurrCount("total") diffWords = currWords - self.theProject.data.getLastCount("total") else: - currWords = self.theProject.currNovelWC + currWords = self.theProject.data.getCurrCount("novel") diffWords = currWords - self.theProject.data.getLastCount("novel") self.mainStatus.setProjectStats(currWords, diffWords) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 7d1edf17..aafa1e52 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -914,16 +914,16 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.data.authors == ["Jane Doe", "John Doh"] theProject.data.setAuthors("") - assert theProject.data.getAuthors() == "" + assert theProject.getFormattedAuthors() == "" theProject.data.setAuthors("Jane Doe") - assert theProject.data.getAuthors() == "Jane Doe" + assert theProject.getFormattedAuthors() == "Jane Doe" theProject.data.setAuthors("Jane Doe\nJohn Doh") - assert theProject.data.getAuthors() == "Jane Doe and John Doh" + assert theProject.getFormattedAuthors() == "Jane Doe and John Doh" theProject.data.setAuthors("Jane Doe\nJohn Doh\nBod Owens") - assert theProject.data.getAuthors() == "Jane Doe, John Doh and Bod Owens" + assert theProject.getFormattedAuthors() == "Jane Doe, John Doh and Bod Owens" # Edit Time theProject.data.setEditTime(1234) @@ -1004,7 +1004,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.tree.handles() == oldOrder # Session stats - theProject.currWCount = 200 + theProject.data.setCurrCount(200, "total") theProject.data.setLastCount(100, "total") with monkeypatch.context() as mp: mp.setattr("os.path.isdir", lambda *a, **k: False) @@ -1020,8 +1020,8 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS) theProject._projOpened = 1600002000 - theProject.currNovelWC = 200 - theProject.currNotesWC = 100 + theProject._data.setCurrCount(200, "novel") + theProject._data.setCurrCount(100, "notes") with monkeypatch.context() as mp: mp.setattr("novelwriter.core.project.time", lambda: 1600005600) From 522acc479b374c60c0ef4f63d1ec65a218fbd276 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:01:50 +0100 Subject: [PATCH 08/18] Move status and importance to data class --- novelwriter/core/item.py | 12 ++-- novelwriter/core/project.py | 41 +++++------ novelwriter/core/projectdata.py | 12 ++++ novelwriter/core/projectxml.py | 51 +++++-------- novelwriter/core/status.py | 17 +++-- novelwriter/dialogs/projsettings.py | 4 +- novelwriter/gui/projtree.py | 4 +- tests/test_core/test_core_project.py | 80 ++++++++++----------- tests/test_dialogs/test_dlg_projsettings.py | 4 +- 9 files changed, 110 insertions(+), 115 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 66cb7fbe..e71d0643 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -271,11 +271,11 @@ class NWItem: the current item based on its class. """ if self.isNovelLike(): - stName = self.theProject.statusItems.name(self._status) - stIcon = self.theProject.statusItems.icon(self._status) if incIcon else None + stName = self.theProject.data.itemStatus.name(self._status) + stIcon = self.theProject.data.itemStatus.icon(self._status) if incIcon else None else: - stName = self.theProject.importItems.name(self._import) - stIcon = self.theProject.importItems.icon(self._import) if incIcon else None + stName = self.theProject.data.itemImport.name(self._import) + stIcon = self.theProject.data.itemImport.icon(self._import) if incIcon else None return stName, stIcon ## @@ -447,14 +447,14 @@ class NWItem: """Set the item status by looking it up in the valid status items of the current project. """ - self._status = self.theProject.statusItems.check(value) + self._status = self.theProject.data.itemStatus.check(value) return def setImport(self, value): """Set the item importance by looking it up in the valid import items of the current project. """ - self._import = self.theProject.importItems.check(value) + self._import = self.theProject.data.itemImport.check(value) return def setActive(self, state): diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 9d005bf4..a7741a13 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -45,7 +45,6 @@ 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.document import NWDoc from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState @@ -92,8 +91,6 @@ class NWProject: self.autoReplace = {} # Text to auto-replace on exports self.titleFormat = {} # The formatting of titles for exports self.spellCheck = False # Controls the spellcheck-as-you-type feature - self.statusItems = None # Novel file progress status values - self.importItems = None # Note file importance values # Internal Mapping self.tr = partial(QCoreApplication.translate, "NWProject") @@ -268,17 +265,15 @@ class NWProject: "scene": "* * *", "section": "", } - self.spellCheck = False - self.statusItems = NWStatus(NWStatus.STATUS) - self.statusItems.write(None, self.tr("New"), (100, 100, 100)) - self.statusItems.write(None, self.tr("Note"), (200, 50, 0)) - self.statusItems.write(None, self.tr("Draft"), (200, 150, 0)) - self.statusItems.write(None, self.tr("Finished"), (50, 200, 0)) - self.importItems = NWStatus(NWStatus.IMPORT) - self.importItems.write(None, self.tr("New"), (100, 100, 100)) - self.importItems.write(None, self.tr("Minor"), (200, 50, 0)) - self.importItems.write(None, self.tr("Major"), (200, 150, 0)) - self.importItems.write(None, self.tr("Main"), (50, 200, 0)) + self.spellCheck = False + 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 @@ -548,8 +543,6 @@ class NWProject: self.spellCheck = self._data.spellCheck self.projSpell = self._data.spellLang - self.statusItems.unpack(xmlSettings.get("status", {})) - self.importItems.unpack(xmlSettings.get("import", {})) self.autoReplace = xmlSettings.get("autoReplace", {}) self.titleFormat.update(xmlSettings.get("titleFormat", {})) @@ -665,9 +658,9 @@ class NWProject: # Save Status/Importance self.countStatus() xStatus = etree.SubElement(xSettings, "status") - self.statusItems.packXML(xStatus) + self._data.itemStatus.packXML(xStatus) xStatus = etree.SubElement(xSettings, "importance") - self.importItems.packXML(xStatus) + self._data.itemImport.packXML(xStatus) # Save Tree Content logger.debug("Writing project content") @@ -970,12 +963,12 @@ class NWProject: def setStatusColours(self, newCols, delCols): """Update the list of novel file status flags. """ - return self._setStatusImport(newCols, delCols, self.statusItems) + return self._setStatusImport(newCols, delCols, self._data.itemStatus) def setImportColours(self, newCols, delCols): """Update the list of note file importance flags. """ - return self._setStatusImport(newCols, delCols, self.importItems) + return self._setStatusImport(newCols, delCols, self._data.itemImport) def setAutoReplace(self, autoReplace): """Update the auto-replace dictionary. @@ -1096,13 +1089,13 @@ class NWProject: project tree. The counts themselves are kept in the NWStatus objects. This is essentially a refresh. """ - self.statusItems.resetCounts() - self.importItems.resetCounts() + self._data.itemStatus.resetCounts() + self._data.itemImport.resetCounts() for nwItem in self._projTree: if nwItem.isNovelLike(): - self.statusItems.increment(nwItem.itemStatus) + self._data.itemStatus.increment(nwItem.itemStatus) else: - self.importItems.increment(nwItem.itemImport) + self._data.itemImport.increment(nwItem.itemImport) return def localLookup(self, theWord): diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index b22c74c9..10988104 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -28,6 +28,7 @@ import logging from novelwriter.common import ( checkBool, checkInt, checkStringNone, simplified ) +from novelwriter.core.status import NWStatus logger = logging.getLogger(__name__) @@ -53,6 +54,9 @@ class NWProjectData: self._lastCount = {} self._currCount = {} + self._status = NWStatus(NWStatus.STATUS) + self._import = NWStatus(NWStatus.IMPORT) + # Internal self._changed = False @@ -102,6 +106,14 @@ class NWProjectData: def spellLang(self): return self._spellLang + @property + def itemStatus(self): + return self._status + + @property + def itemImport(self): + return self._import + @property def changed(self): return self._changed diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 8760f6b1..eb8506b2 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -31,7 +31,7 @@ from enum import Enum from lxml import etree from novelwriter.common import ( - checkBool, checkInt, checkStringNone, minmax, simplified, checkString + checkBool, checkInt, checkStringNone, simplified, checkString ) logger = logging.getLogger(__name__) @@ -192,6 +192,7 @@ class ProjectXMLReader: if self._version >= 0x0104: status &= self._parseProjectContent(xSection) else: + self._genLegacyImportStatysMap(projData) status &= self._parseProjectContentLegacy(xSection) else: logger.warning("Ignored in xml", xSection.tag) @@ -264,9 +265,9 @@ class ProjectXMLReader: elif xItem.tag == "notesWordCount": projData.setLastCount(xItem.text, "notes") elif xItem.tag == "status": - data["status"] = self._parseStatusImport(xItem, "status") + self._parseStatusImport(xItem, projData.itemStatus) elif xItem.tag in ("import", "importance"): - data["import"] = self._parseStatusImport(xItem, "import") + self._parseStatusImport(xItem, projData.itemImport) elif xItem.tag == "autoReplace": if self._version >= 0x0102: for xEntry in xItem: @@ -375,9 +376,9 @@ class ProjectXMLReader: # Status was split into separate status/import with a key in 1.4 if item.get("class", "") in ("NOVEL", "ARCHIVE"): - item["status"] = self._getLegacyUnportStatus(tmpStatus, "status") + item["status"] = self._statusMap.get(tmpStatus, None) else: - item["import"] = self._getLegacyUnportStatus(tmpStatus, "import") + item["import"] = self._importMap.get(tmpStatus, None) # A number of layouts were removed in 1.3 if item.get("layout", "") in depLayout: @@ -396,39 +397,25 @@ class ProjectXMLReader: return True - def _parseStatusImport(self, xItem, type): + def _parseStatusImport(self, xItem, sObject): """Parse a status or importance entry. """ - data = self._statusData.get(type, {}) for xEntry in xItem: if xEntry.tag == "entry": - key = xEntry.attrib.get("key", f"{type[0]}{len(data):06x}") - data[key] = { - "label": xEntry.text, - "count": checkInt(xEntry.attrib.get("count", 0), 0), - "colour": ( - minmax(checkInt(xEntry.attrib.get("red", 0), 0), 0, 255), - minmax(checkInt(xEntry.attrib.get("green", 0), 0), 0, 255), - minmax(checkInt(xEntry.attrib.get("blue", 0), 0), 0, 255), - ), - } - self._statusData[type] = data + key = xEntry.attrib.get("key", None) + red = checkInt(xEntry.attrib.get("red", 0), 0) + green = checkInt(xEntry.attrib.get("green", 0), 0) + blue = checkInt(xEntry.attrib.get("blue", 0), 0) + count = checkInt(xEntry.attrib.get("count", 0), 0) + sObject.write(key, xEntry.text, (red, green, blue), count) + return - return data - - def _getLegacyUnportStatus(self, label, type): - """Look up the label in defined status or importance values. - This is needed for file formats prior to 1.4 where the status - was saved as the label, not the key. + def _genLegacyImportStatysMap(self, projData): + """Generate a map of legacy import/status values. """ - if not self._statusMap.get(type): - lookup = {} - for key, entry in self._statusData.get(type, {}).items(): - lookup[entry.get("label", "")] = key - self._statusMap[type] = lookup - print(lookup) - - return self._statusMap.get(type, {}).get(label, None) + self._statusMap = {entry["name"]: key for key, entry in projData.itemStatus.items()} + self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()} + return # END Class ProjectXMLReader diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 1d19a08e..63268a4c 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -33,7 +33,7 @@ from lxml import etree from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor from PyQt5.QtCore import QRectF, Qt -from novelwriter.common import simplified +from novelwriter.common import minmax, simplified logger = logging.getLogger(__name__) @@ -57,7 +57,7 @@ class NWStatus: self._iconPath = QPainterPath() self._iconPath.addRoundedRect(QRectF(pA, pA, pB, pB), pR, pR) - self._defaultIcon = self._createIcon([100, 100, 100]) + self._defaultIcon = self._createIcon(100, 100, 100) if self._type == self.STATUS: self._prefix = "s" @@ -79,14 +79,17 @@ class NWStatus: if len(col) != 3: col = (100, 100, 100) + cR = minmax(col[0], 0, 255) + cG = minmax(col[1], 0, 255) + cB = minmax(col[2], 0, 255) name = simplified(name) if count is None: - count = self._store[key]["count"] if key in self._store else 0 + count = self._store.get(key, {}).get("count", 0) self._store[key] = { "name": name, - "icon": self._createIcon(col), - "cols": col, + "icon": self._createIcon(cR, cG, cB), + "cols": (cR, cG, cB), "count": count, } @@ -261,7 +264,7 @@ class NWStatus: return False return True - def _createIcon(self, col): + def _createIcon(self, red, green, blue): """Generate an icon for a status label. """ pixmap = QPixmap(self._iPX, self._iPX) @@ -269,7 +272,7 @@ class NWStatus: painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) - painter.fillPath(self._iconPath, QColor(*col)) + painter.fillPath(self._iconPath, QColor(red, green, blue)) painter.end() return QIcon(pixmap) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index a9ee7188..18e905a0 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -288,11 +288,11 @@ class GuiProjectEditStatus(QWidget): self.mainTheme = projGui.mainGui.mainTheme if isStatus: - self.theStatus = self.theProject.statusItems + self.theStatus = self.theProject.data.itemStatus pageLabel = self.tr("Novel File Status Levels") colSetting = "statusColW" else: - self.theStatus = self.theProject.importItems + self.theStatus = self.theProject.data.itemImport pageLabel = self.tr("Note File Importance Levels") colSetting = "importColW" diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 02e1352a..b41e7ed7 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1208,7 +1208,7 @@ class GuiProjectTree(QTreeWidget): checkMark = f" ({nwUnicode.U_CHECK})" if tItem.isNovelLike(): mStatus = ctxMenu.addMenu(self.tr("Set Status to ...")) - for n, (key, entry) in enumerate(self.theProject.statusItems.items()): + for n, (key, entry) in enumerate(self.theProject.data.itemStatus.items()): entryName = entry["name"] + (checkMark if tItem.itemStatus == key else "") aStatus = mStatus.addAction(entry["icon"], entryName) aStatus.triggered.connect( @@ -1221,7 +1221,7 @@ class GuiProjectTree(QTreeWidget): ) else: mImport = ctxMenu.addMenu(self.tr("Set Importance to ...")) - for n, (key, entry) in enumerate(self.theProject.importItems.items()): + for n, (key, entry) in enumerate(self.theProject.data.itemImport.items()): entryName = entry["name"] + (checkMark if tItem.itemImport == key else "") aImport = mImport.addAction(entry["icon"], entryName) aImport.triggered.connect( diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index aafa1e52..23598f8b 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -783,24 +783,24 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): assert theProject.setStatusColours([], []) is False assert theProject.setStatusColours(newList, []) is True - assert theProject.statusItems.name(statusKeys[0]) == "New" - assert theProject.statusItems.name(statusKeys[1]) == "Draft" - assert theProject.statusItems.name(statusKeys[2]) == "Note" - assert theProject.statusItems.name(statusKeys[3]) == "Edited" - assert theProject.statusItems.cols(statusKeys[0]) == (1, 1, 1) - assert theProject.statusItems.cols(statusKeys[1]) == (2, 2, 2) - assert theProject.statusItems.cols(statusKeys[2]) == (3, 3, 3) - assert theProject.statusItems.cols(statusKeys[3]) == (4, 4, 4) + assert theProject.data.itemStatus.name(statusKeys[0]) == "New" + assert theProject.data.itemStatus.name(statusKeys[1]) == "Draft" + assert theProject.data.itemStatus.name(statusKeys[2]) == "Note" + assert theProject.data.itemStatus.name(statusKeys[3]) == "Edited" + assert theProject.data.itemStatus.cols(statusKeys[0]) == (1, 1, 1) + assert theProject.data.itemStatus.cols(statusKeys[1]) == (2, 2, 2) + assert theProject.data.itemStatus.cols(statusKeys[2]) == (3, 3, 3) + assert theProject.data.itemStatus.cols(statusKeys[3]) == (4, 4, 4) # Check the new entry - lastKey = theProject.statusItems.check("s000018") + lastKey = theProject.data.itemStatus.check("s000018") assert lastKey == "s000018" - assert theProject.statusItems.name(lastKey) == "Finished" - assert theProject.statusItems.cols(lastKey) == (5, 5, 5) + assert theProject.data.itemStatus.name(lastKey) == "Finished" + assert theProject.data.itemStatus.cols(lastKey) == (5, 5, 5) # Delete last entry assert theProject.setStatusColours([], [lastKey]) is True - assert theProject.statusItems.name(lastKey) == "New" + assert theProject.data.itemStatus.name(lastKey) == "New" # Change Importance # ================= @@ -820,52 +820,52 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): assert theProject.setImportColours([], []) is False assert theProject.setImportColours(newList, []) is True - assert theProject.importItems.name(importKeys[0]) == "New" - assert theProject.importItems.name(importKeys[1]) == "Minor" - assert theProject.importItems.name(importKeys[2]) == "Major" - assert theProject.importItems.name(importKeys[3]) == "Min" - assert theProject.importItems.cols(importKeys[0]) == (1, 1, 1) - assert theProject.importItems.cols(importKeys[1]) == (2, 2, 2) - assert theProject.importItems.cols(importKeys[2]) == (3, 3, 3) - assert theProject.importItems.cols(importKeys[3]) == (4, 4, 4) + assert theProject.data.itemImport.name(importKeys[0]) == "New" + assert theProject.data.itemImport.name(importKeys[1]) == "Minor" + assert theProject.data.itemImport.name(importKeys[2]) == "Major" + assert theProject.data.itemImport.name(importKeys[3]) == "Min" + assert theProject.data.itemImport.cols(importKeys[0]) == (1, 1, 1) + assert theProject.data.itemImport.cols(importKeys[1]) == (2, 2, 2) + assert theProject.data.itemImport.cols(importKeys[2]) == (3, 3, 3) + assert theProject.data.itemImport.cols(importKeys[3]) == (4, 4, 4) # Check the new entry - lastKey = theProject.importItems.check("i00001a") + lastKey = theProject.data.itemImport.check("i00001a") assert lastKey == "i00001a" - assert theProject.importItems.name(lastKey) == "Max" - assert theProject.importItems.cols(lastKey) == (5, 5, 5) + assert theProject.data.itemImport.name(lastKey) == "Max" + assert theProject.data.itemImport.cols(lastKey) == (5, 5, 5) # Delete last entry assert theProject.setImportColours([], [lastKey]) is True - assert theProject.importItems.name(lastKey) == "New" + assert theProject.data.itemImport.name(lastKey) == "New" # Delete Status/Import # ==================== - theProject.statusItems.resetCounts() - for key in list(theProject.statusItems.keys()): - assert theProject.statusItems.remove(key) is True + theProject.data.itemStatus.resetCounts() + for key in list(theProject.data.itemStatus.keys()): + assert theProject.data.itemStatus.remove(key) is True - theProject.importItems.resetCounts() - for key in list(theProject.importItems.keys()): - assert theProject.importItems.remove(key) is True + theProject.data.itemImport.resetCounts() + for key in list(theProject.data.itemImport.keys()): + assert theProject.data.itemImport.remove(key) is True - assert len(theProject.statusItems) == 0 - assert len(theProject.importItems) == 0 + assert len(theProject.data.itemStatus) == 0 + assert len(theProject.data.itemImport) == 0 assert theProject.saveProject() is True assert theProject.closeProject() is True # This should restore the default status/import labels assert theProject.openProject(fncDir) is True assert theProject.saveProject() is True - assert theProject.statusItems.name("s000023") == "New" - assert theProject.statusItems.name("s000024") == "Note" - assert theProject.statusItems.name("s000025") == "Draft" - assert theProject.statusItems.name("s000026") == "Finished" - assert theProject.importItems.name("i000027") == "New" - assert theProject.importItems.name("i000028") == "Minor" - assert theProject.importItems.name("i000029") == "Major" - assert theProject.importItems.name("i00002a") == "Main" + assert theProject.data.itemStatus.name("s000023") == "New" + assert theProject.data.itemStatus.name("s000024") == "Note" + assert theProject.data.itemStatus.name("s000025") == "Draft" + assert theProject.data.itemStatus.name("s000026") == "Finished" + assert theProject.data.itemImport.name("i000027") == "New" + assert theProject.data.itemImport.name("i000028") == "Minor" + assert theProject.data.itemImport.name("i000029") == "Major" + assert theProject.data.itemImport.name("i00002a") == "Main" # END Test testCoreProject_StatusImport diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index 71c90ba8..08121c6b 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -330,13 +330,13 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncDir, fncProj, # Check Project projSettings._doSave() - statusItems = dict(theProject.statusItems.items()) + statusItems = dict(theProject.data.itemStatus.items()) assert statusItems[C.sNew]["name"] == "New" assert statusItems[C.sDraft]["name"] == "Draft" assert statusItems[C.sFinished]["name"] == "Finished" assert statusItems["s000013"]["name"] == "Final" - importItems = dict(theProject.importItems.items()) + importItems = dict(theProject.data.itemImport.items()) assert importItems[C.iNew]["name"] == "New" assert importItems[C.iMajor]["name"] == "Major" assert importItems[C.iMain]["name"] == "Main" From 3a161e90f90eb9d674fa29a3602d643582d00529 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 18:11:46 +0100 Subject: [PATCH 09/18] Complete loading project xml via data class --- novelwriter/core/project.py | 78 ++------------------- novelwriter/core/projectdata.py | 37 ++++++++++ novelwriter/core/projectxml.py | 54 +++++++------- novelwriter/core/tokenizer.py | 5 +- novelwriter/dialogs/projsettings.py | 10 +-- novelwriter/gui/doceditor.py | 6 +- novelwriter/gui/mainmenu.py | 2 +- novelwriter/guimain.py | 2 +- novelwriter/tools/build.py | 12 ++-- sample/nwProject.nwx | 44 ++++++------ tests/test_core/test_core_project.py | 22 +++--- tests/test_core/test_core_tokenizer.py | 2 +- tests/test_dialogs/test_dlg_projsettings.py | 12 ++-- tests/test_gui/test_gui_guimain.py | 4 +- 14 files changed, 129 insertions(+), 161 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index a7741a13..df300ae4 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -38,8 +38,8 @@ from PyQt5.QtCore import QCoreApplication from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException from novelwriter.common import ( - checkString, checkStringNone, isHandle, formatTimeStamp, makeFileNameSafe, - hexToInt, minmax, simplified + checkStringNone, isHandle, formatTimeStamp, makeFileNameSafe, hexToInt, + minmax, simplified ) from novelwriter.constants import trConst, nwFiles, nwLabels from novelwriter.core.tree import NWTree @@ -63,8 +63,8 @@ class NWProject: self.mainConf = novelwriter.CONFIG self.mainGui = mainGui + # Project Data self._data = NWProjectData() - self._raw = {} # Core Elements self._optState = OptionState(self) # Project-specific GUI options @@ -84,14 +84,8 @@ class NWProject: 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.projSpell = None # The spell check language, if different than default self.projFiles = [] # A list of all files in the content folder on load - # Project Settings - self.autoReplace = {} # Text to auto-replace on exports - self.titleFormat = {} # The formatting of titles for exports - self.spellCheck = False # Controls the spellcheck-as-you-type feature - # Internal Mapping self.tr = partial(QCoreApplication.translate, "NWProject") @@ -255,17 +249,7 @@ class NWProject: self.projCache = None self.projContent = None self.projDict = None - self.projSpell = None self.projFiles = [] - self.autoReplace = {} - self.titleFormat = { - "title": "%title%", - "chapter": "%title%", - "unnumbered": "%title%", - "scene": "* * *", - "section": "", - } - self.spellCheck = False 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)) @@ -466,9 +450,6 @@ class NWProject: self._data = NWProjectData() xmlReader = ProjectXMLReader(fileName) xmlParsed = xmlReader.read(self._data) - xmlData = xmlReader.data - - # print(json.dumps(xmlData, indent=2)) nwxRoot = xmlReader.xmlRoot appVersion = xmlReader.appVersion or self.tr("Unknown") @@ -494,8 +475,6 @@ class NWProject: self.clearProject() return False - self._raw = xmlData - logger.debug("XML root is '%s'", nwxRoot) logger.debug("File version is '%s'", xmlVersion) @@ -539,15 +518,7 @@ class NWProject: logger.info("Project Name: '%s'", self._data.name) logger.info("Project Title: '%s'", self._data.title) - xmlSettings = xmlData.get("settings", {}) - - self.spellCheck = self._data.spellCheck - self.projSpell = self._data.spellLang - self.autoReplace = xmlSettings.get("autoReplace", {}) - self.titleFormat.update(xmlSettings.get("titleFormat", {})) - - self._projTree.unpack(xmlData.get("content", [])) - + self._projTree.unpack(xmlReader.content) self._optState.loadSettings() # Sort out old file locations @@ -648,10 +619,10 @@ class NWProject: self._packProjectValue(xSettings, "lastWordCount", self._data.getCurrCount("total")) self._packProjectValue(xSettings, "novelWordCount", self._data.getCurrCount("novel")) self._packProjectValue(xSettings, "notesWordCount", self._data.getCurrCount("notes")) - self._packProjectKeyValue(xSettings, "autoReplace", self.autoReplace) + self._packProjectKeyValue(xSettings, "autoReplace", self._data.autoReplace) xTitleFmt = etree.SubElement(xSettings, "titleFormat") - for aKey, aValue in self.titleFormat.items(): + for aKey, aValue in self._data.titleFormat.items(): if len(aKey) > 0: self._packProjectValue(xTitleFmt, aKey, aValue) @@ -921,24 +892,6 @@ class NWProject: return True - def setSpellCheck(self, theMode): - """Enable/disable spell checking. - """ - if self.spellCheck != theMode: - self.spellCheck = theMode - self.setProjectChanged(True) - return self.spellCheck - - def setSpellLang(self, theLang): - """Set the project-specific spell check language. - """ - theLang = checkStringNone(theLang, None) - if self.projSpell != theLang: - self.projSpell = theLang - self.setProjectChanged(True) - return True - return False - def setProjectLang(self, theLang): """Set the project-specific language. """ @@ -970,25 +923,6 @@ class NWProject: """ return self._setStatusImport(newCols, delCols, self._data.itemImport) - def setAutoReplace(self, autoReplace): - """Update the auto-replace dictionary. - """ - self.autoReplace = {} - for key, entry in autoReplace.items(): - self.autoReplace[key] = simplified(entry) - self.setProjectChanged(True) - return True - - def setTitleFormat(self, titleFormat): - """Set the formatting of titles in the project. - """ - for valKey, valEntry in titleFormat.items(): - if valKey in self.titleFormat: - self.titleFormat[valKey] = checkString( - simplified(valEntry), self.titleFormat[valKey] - ) - return True - def setProjectChanged(self, bValue): """Toggle the project changed flag, and propagate the information to the GUI statusbar. diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 10988104..d91c39c7 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -54,6 +54,15 @@ class NWProjectData: self._lastCount = {} self._currCount = {} + self._autoReplace = {} + self._titleFormat = { + "title": "%title%", + "chapter": "%title%", + "unnumbered": "%title%", + "scene": "* * *", + "section": "", + } + self._status = NWStatus(NWStatus.STATUS) self._import = NWStatus(NWStatus.IMPORT) @@ -106,6 +115,14 @@ class NWProjectData: def spellLang(self): return self._spellLang + @property + def autoReplace(self): + return self._autoReplace + + @property + def titleFormat(self): + return self._titleFormat + @property def itemStatus(self): return self._status @@ -153,6 +170,9 @@ class NWProjectData: def getCurrCount(self, type): return self._currCount.get(type, 0) + def getTitleFormat(self, kind): + return self._titleFormat.get(kind, "%title%") + ## # Setters ## @@ -231,4 +251,21 @@ class NWProjectData: self._changed = True return + def setAutoReplace(self, value): + if isinstance(value, dict): + self._autoReplace = {} + for key, entry in value.items(): + if isinstance(entry, str): + self._autoReplace[key] = simplified(entry) + self._changed = True + return + + def setTitleFormat(self, value): + 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._changed = True + return + # END Class NWProjectData diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index eb8506b2..fe241efa 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -68,7 +68,6 @@ class ProjectXMLReader: self._path = path self._state = XMLReadState.NO_ACTION - self._data = {} self._content = [] self._statusData = {} self._statusMap = {} @@ -85,10 +84,6 @@ class ProjectXMLReader: # Properties ## - @property - def data(self): - return self._data - @property def content(self): return self._content @@ -124,7 +119,6 @@ class ProjectXMLReader: def read(self, projData): """Read and parse the project XML file. """ - self._data = {} self._content = [] try: @@ -231,6 +225,7 @@ class ProjectXMLReader: projData.setEditTime(xItem.text) else: logger.warning("Ignored in xml", xItem.tag) + return True def _parseProjectSettings(self, xSection, projData): @@ -238,9 +233,6 @@ class ProjectXMLReader: """ logger.debug("Parsing xml ") - data = {} - autoReplace = {} - titleFormat = {} for xItem in xSection: if xItem.tag == "doBackup": projData.setDoBackup(xItem.text) @@ -270,22 +262,14 @@ class ProjectXMLReader: self._parseStatusImport(xItem, projData.itemImport) elif xItem.tag == "autoReplace": if self._version >= 0x0102: - for xEntry in xItem: - if xEntry.tag == "entry" and "key" in xEntry.attrib: - autoReplace[xEntry.attrib["key"]] = checkString(xEntry.text, "ERROR") + projData.setAutoReplace(self._parseDictKeyText(xItem)) else: # Pre 1.2 format - for xEntry in xItem: - autoReplace[xEntry.tag] = checkString(xEntry.text, "ERROR") + projData.setAutoReplace(self._parseDictTagText(xItem)) elif xItem.tag == "titleFormat": - for xEntry in xItem: - titleFormat[xEntry.tag] = checkString(xEntry.text, "") + projData.setTitleFormat(self._parseDictTagText(xItem)) else: logger.warning("Ignored in xml", xItem.tag) - data["autoReplace"] = autoReplace - data["titleFormat"] = titleFormat - self._data["settings"] = data - return True def _parseProjectContent(self, xSection): @@ -293,7 +277,6 @@ class ProjectXMLReader: """ logger.debug("Parsing xml ") - data = [] for xItem in xSection: if xItem.tag == "item": item = {} @@ -323,21 +306,20 @@ class ProjectXMLReader: item["active"] = checkBool(xVal.attrib.get("exported", False), False) else: logger.warning("Ignored in xml", xVal.tag) - data.append(item) + self._content.append(item) + else: logger.warning("Ignored item in xml", xItem.tag) - self._data["content"] = data - return True def _parseProjectContentLegacy(self, xSection): - """Parse the content section of the XML file for version before 1.4. + """Parse the content section of the XML file for older version. """ logger.debug("Parsing xml (legacy format)") depLayout = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") - data = [] + for xItem in xSection: item = {} if xItem.tag == "item": @@ -388,13 +370,11 @@ class ProjectXMLReader: if item.get("type", "") == "TRASH": item["type"] = "ROOT" - data.append(item) self._content.append(item) + else: logger.warning("Ignored in xml", xItem.tag) - self._data["content"] = data - return True def _parseStatusImport(self, xItem, sObject): @@ -417,6 +397,22 @@ class ProjectXMLReader: self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()} return + def _parseDictKeyText(self, xItem): + """Parse a dictionary stored with key as an attribute and the + value as the text porperty. + """ + result = {} + for xEntry in xItem: + if xEntry.tag == "entry" and "key" in xEntry.attrib: + result[xEntry.attrib["key"]] = checkString(xEntry.text, "") + return result + + def _parseDictTagText(self, xItem): + """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} + # END Class ProjectXMLReader diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index c55e3659..e81fe2a6 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -327,9 +327,10 @@ class Tokenizer(ABC): """Run trough the various replace doctionaries. """ # Process the user's auto-replace dictionary - if len(self.theProject.autoReplace) > 0: + autoReplace = self.theProject.data.autoReplace + if len(autoReplace) > 0: repDict = {} - for aKey, aVal in self.theProject.autoReplace.items(): + for aKey, aVal in autoReplace.items(): repDict[f"<{aKey}>"] = aVal xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) self._theText = xRep.sub(lambda x: repDict[x.group(0)], self._theText) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 18e905a0..77da0860 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -120,7 +120,7 @@ class GuiProjectSettings(PagedDialog): self.theProject.data.setDoBackup(doBackup) # Remember this as updating spell dictionary can be expensive - self._spellChanged = self.theProject.setSpellLang(spellLang) + self._spellChanged = self.theProject.data.setSpellLang(spellLang) if self.tabStatus.colChanged: newList, delList = self.tabStatus.getNewList() @@ -135,7 +135,7 @@ class GuiProjectSettings(PagedDialog): if self.tabReplace.arChanged: newList = self.tabReplace.getNewList() - self.theProject.setAutoReplace(newList) + self.theProject.data.setAutoReplace(newList) self._saveGuiSettings() self.accept() @@ -252,8 +252,8 @@ class GuiProjectEditMain(QWidget): ) spellIdx = 0 - if self.theProject.projSpell is not None: - spellIdx = self.spellLang.findData(self.theProject.projSpell) + if self.theProject.data.spellLang is not None: + spellIdx = self.spellLang.findData(self.theProject.data.spellLang) if spellIdx != -1: self.spellLang.setCurrentIndex(spellIdx) @@ -578,7 +578,7 @@ class GuiProjectEditReplace(QWidget): self.listBox.setColumnWidth(self.COL_KEY, wCol0) self.listBox.setIndentation(0) - for aKey, aVal in self.theProject.autoReplace.items(): + for aKey, aVal in self.theProject.data.autoReplace.items(): newItem = QTreeWidgetItem(["<%s>" % aKey, aVal]) self.listBox.addTopLevelItem(newItem) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 0bde6d8c..b59829bc 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -684,10 +684,10 @@ class GuiDocEditor(QTextEdit): """Set the spell checker dictionary language, and emit the dictionary changed signal. """ - if self.theProject.projSpell is None: + if self.theProject.data.spellLang is None: theLang = self.mainConf.spellLanguage else: - theLang = self.theProject.projSpell + theLang = self.theProject.data.spellLang self.spEnchant.setLanguage(theLang, self.theProject.projDict) _, theProvider = self.spEnchant.describeDict() @@ -721,7 +721,7 @@ class GuiDocEditor(QTextEdit): self._spellCheck = theMode self.mainGui.mainMenu.setSpellCheck(theMode) - self.theProject.setSpellCheck(theMode) + self.theProject.data.setSpellCheck(theMode) self.highLight.setSpellCheck(theMode) if not self._bigDoc: self.spellCheckDocument() diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 8a74035e..51a74bae 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -794,7 +794,7 @@ class GuiMainMenu(QMenuBar): # Tools > Check Spelling self.aSpellCheck = QAction(self.tr("Check Spelling"), self) self.aSpellCheck.setCheckable(True) - self.aSpellCheck.setChecked(self.theProject.spellCheck) + self.aSpellCheck.setChecked(self.theProject.data.spellCheck) self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled! self.aSpellCheck.setShortcut("Ctrl+F7") self.toolsMenu.addAction(self.aSpellCheck) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index a3e4d295..553667f6 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -524,7 +524,7 @@ class GuiMain(QMainWindow): self._updateWindowTitle(self.theProject.data.name) self.rebuildTrees() self.docEditor.setDictionaries() - self.docEditor.toggleSpellCheck(self.theProject.spellCheck) + self.docEditor.toggleSpellCheck(self.theProject.data.spellCheck) self.mainStatus.setRefTime(self.theProject.projOpened) self.projView.openProjectTasks() self.novelView.openProjectTasks() diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 979e3e98..8278fb95 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -126,7 +126,7 @@ class GuiBuildNovel(QDialog): self.fmtTitle.setMinimumWidth(xFmt) self.fmtTitle.setToolTip(fmtHelp) self.fmtTitle.setText( - self._reFmtCodes(self.theProject.titleFormat["title"]) + self._reFmtCodes(self.theProject.data.getTitleFormat("title")) ) self.fmtChapter = QLineEdit() @@ -134,7 +134,7 @@ class GuiBuildNovel(QDialog): self.fmtChapter.setMinimumWidth(xFmt) self.fmtChapter.setToolTip(fmtHelp) self.fmtChapter.setText( - self._reFmtCodes(self.theProject.titleFormat["chapter"]) + self._reFmtCodes(self.theProject.data.getTitleFormat("chapter")) ) self.fmtUnnumbered = QLineEdit() @@ -142,7 +142,7 @@ class GuiBuildNovel(QDialog): self.fmtUnnumbered.setMinimumWidth(xFmt) self.fmtUnnumbered.setToolTip(fmtHelp) self.fmtUnnumbered.setText( - self._reFmtCodes(self.theProject.titleFormat["unnumbered"]) + self._reFmtCodes(self.theProject.data.getTitleFormat("unnumbered")) ) self.fmtScene = QLineEdit() @@ -150,7 +150,7 @@ class GuiBuildNovel(QDialog): self.fmtScene.setMinimumWidth(xFmt) self.fmtScene.setToolTip(fmtHelp + fmtScHelp) self.fmtScene.setText( - self._reFmtCodes(self.theProject.titleFormat["scene"]) + self._reFmtCodes(self.theProject.data.getTitleFormat("scene")) ) self.fmtSection = QLineEdit() @@ -158,7 +158,7 @@ class GuiBuildNovel(QDialog): self.fmtSection.setMinimumWidth(xFmt) self.fmtSection.setToolTip(fmtHelp + fmtScHelp) self.fmtSection.setText( - self._reFmtCodes(self.theProject.titleFormat["section"]) + self._reFmtCodes(self.theProject.data.getTitleFormat("section")) ) self.buildLang = QComboBox() @@ -1159,7 +1159,7 @@ class GuiBuildNovel(QDialog): logger.debug("Saving GuiBuildNovel settings") # Formatting - self.theProject.setTitleFormat({ + self.theProject.data.setTitleFormat({ "title": self.fmtTitle.text().strip(), "chapter": self.fmtChapter.text().strip(), "unnumbered": self.fmtUnnumbered.text().strip(), diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 535b6d18..6b8d929d 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1386 + 1387 236 - 69352 + 69358 False @@ -55,43 +55,43 @@ Novel - + Title Page - + Page - + Part One - + Chapter One - + Making a Scene - + Another Scene - + Interlude - + A Note on Structure - + Chapter Two - + We Found John! @@ -99,11 +99,11 @@ Sequel - + Title Page - + Chapter One @@ -115,11 +115,11 @@ Main Characters - + John Smith - + Jane Smith @@ -127,15 +127,15 @@ Locations - + Earth - + Space - + Mars @@ -147,7 +147,7 @@ Scenes - + Old File @@ -155,7 +155,7 @@ Trash - + Delete Me! diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 23598f8b..8acf751c 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -941,19 +941,19 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): # Spell check theProject.setProjectChanged(False) - assert theProject.setSpellCheck(True) - assert not theProject.setSpellCheck(False) + theProject.data.setSpellCheck(True) + theProject.data.setSpellCheck(False) assert theProject.projChanged # Spell language theProject.setProjectChanged(False) - assert theProject.projSpell is None - assert theProject.setSpellLang(None) is False - assert theProject.projSpell is None - assert theProject.setSpellLang("None") is False # Should be interpreded as None - assert theProject.projSpell is None - assert theProject.setSpellLang("en_GB") - assert theProject.projSpell == "en_GB" + assert theProject.data.spellLang is None + theProject.data.setSpellLang(None) + assert theProject.data.spellLang is None + theProject.data.setSpellLang("None") # Should be interpreded as None + assert theProject.data.spellLang is None + theProject.data.setSpellLang("en_GB") + assert theProject.data.spellLang == "en_GB" assert theProject.projChanged # Project Language @@ -982,8 +982,8 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): # Autoreplace theProject.setProjectChanged(False) - assert theProject.setAutoReplace({"A": "B", "C": "D"}) - assert theProject.autoReplace == {"A": "B", "C": "D"} + theProject.data.setAutoReplace({"A": "B", "C": "D"}) + assert theProject.data.autoReplace == {"A": "B", "C": "D"} assert theProject.projChanged # Change project tree order diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index 23f47565..fa3035d2 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -160,7 +160,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): nDoc = NWDoc(theProject, sHandle) assert nDoc.writeDocument(docText) - theProject.setAutoReplace({"A": "this", "B": "that"}) + theProject.data.setAutoReplace({"A": "this", "B": "that"}) assert theProject.saveProject() diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index 08121c6b..085e5398 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -50,7 +50,7 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): # Pretend we have a project nwGUI.hasProject = True - nwGUI.theProject.setSpellLang("en") + nwGUI.theProject.data.setSpellLang("en") # Get the dialog object nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) @@ -95,9 +95,9 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd # Set some values theProject = nwGUI.theProject - theProject.setSpellLang("en") + theProject.data.setSpellLang("en") theProject.data.setAuthors("Jane Smith\nJohn Smith") - theProject.setAutoReplace({"A": "B", "C": "D"}) + theProject.data.setAutoReplace({"A": "B", "C": "D"}) # Create Dialog projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_MAIN) @@ -365,9 +365,9 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mock # Set some values theProject = nwGUI.theProject - theProject.autoReplace = { + theProject.data.setAutoReplace({ "A": "B", "C": "D" - } + }) # Create Dialog projSettings = GuiProjectSettings(nwGUI, GuiProjectSettings.TAB_REPLACE) @@ -429,7 +429,7 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mock # Check Project projSettings._doSave() - assert theProject.autoReplace == { + assert theProject.data.autoReplace == { "A": "B", "C": "D", "This": "With This Stuff" } diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index fda4edea..28b5b729 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -175,7 +175,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.data.name == "" assert nwGUI.theProject.data.title == "" assert nwGUI.theProject.data.authors == [] - assert not nwGUI.theProject.spellCheck + assert nwGUI.theProject.data.spellCheck is False # Check the files projFile = os.path.join(fncProj, "nwProject.nwx") @@ -197,7 +197,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.data.name == "New Project" assert nwGUI.theProject.data.title == "New Novel" assert nwGUI.theProject.data.authors == ["Jane Doe"] - assert nwGUI.theProject.spellCheck is False + assert nwGUI.theProject.data.spellCheck is False # Check that tree items have been created assert nwGUI.projView.projTree._getTreeItem(C.hNovelRoot) is not None From a760aff826567bd607b8a818e32aa9c27d52602f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 20:05:20 +0100 Subject: [PATCH 10/18] Complete the xml writer class --- novelwriter/core/item.py | 76 ++++---- novelwriter/core/project.py | 109 +---------- novelwriter/core/projectxml.py | 174 ++++++++++++++++-- novelwriter/core/status.py | 28 ++- novelwriter/core/tree.py | 13 +- .../coreProject_NewFileFolder_nwProject.nwx | 8 +- .../coreProject_NewRoot_nwProject.nwx | 8 +- tests/test_core/test_core_project.py | 26 --- 8 files changed, 220 insertions(+), 222 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index e71d0643..eae4813a 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -25,8 +25,6 @@ along with this program. If not, see . import logging -from lxml import etree - from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.common import ( checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified @@ -151,39 +149,40 @@ class NWItem: # Pack/Unpack Data ## - def packXML(self, xParent): - """Pack all the data in the class instance into an XML object. + def pack(self): + """Pack all the data in the class instance into a dictionary. """ - itemAttrib = {} - itemAttrib["handle"] = str(self._handle) - itemAttrib["parent"] = str(self._parent) - itemAttrib["root"] = str(self._root) - itemAttrib["order"] = str(self._order) - itemAttrib["type"] = str(self._type.name) - itemAttrib["class"] = str(self._class.name) + item = {} + meta = {} + name = {} + + item["handle"] = str(self._handle) + item["parent"] = str(self._parent) + item["root"] = str(self._root) + item["order"] = str(self._order) + item["type"] = str(self._type.name) + item["class"] = str(self._class.name) + meta["expanded"] = str(self._expanded) + name["status"] = str(self._status) + name["import"] = str(self._import) + if self._type == nwItemType.FILE: - itemAttrib["layout"] = str(self._layout.name) + item["layout"] = str(self._layout.name) + meta["heading"] = str(self._heading) + meta["charCount"] = str(self._charCount) + meta["wordCount"] = str(self._wordCount) + meta["paraCount"] = str(self._paraCount) + meta["cursorPos"] = str(self._cursorPos) + name["active"] = str(self._active) - metaAttrib = {} - metaAttrib["expanded"] = str(self._expanded) - if self._type == nwItemType.FILE: - metaAttrib["heading"] = str(self._heading) - metaAttrib["charCount"] = str(self._charCount) - metaAttrib["wordCount"] = str(self._wordCount) - metaAttrib["paraCount"] = str(self._paraCount) - metaAttrib["cursorPos"] = str(self._cursorPos) + data = { + "name": str(self._name), + "itemAttr": item, + "metaAttr": meta, + "nameAttr": name, + } - nameAttrib = {} - nameAttrib["status"] = str(self._status) - nameAttrib["import"] = str(self._import) - if self._type == nwItemType.FILE: - nameAttrib["active"] = str(self._active) - - xPack = etree.SubElement(xParent, "item", attrib=itemAttrib) - self._subPack(xPack, "meta", attrib=metaAttrib) - self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib) - - return + return data def unpack(self, data): """Set the values from a data dictionary. @@ -201,7 +200,7 @@ class NWItem: self.setClass(data.get("class", nwItemClass.NO_CLASS)) self.setLayout(data.get("layout", nwItemLayout.NO_LAYOUT)) self.setExpanded(data.get("expanded", False)) - self.setMainHeading(data.get("mainHeading", "H0")) + self.setMainHeading(data.get("heading", "H0")) self.setCharCount(data.get("charCount", 0)) self.setWordCount(data.get("wordCount", 0)) self.setParaCount(data.get("paraCount", 0)) @@ -224,19 +223,6 @@ class NWItem: return True - @staticmethod - def _subPack(xParent, name, attrib=None, text=None, none=True): - """Pack the values into an XML element. - """ - if not none and (text is None or text == "None"): - return None - xAttr = {} if attrib is None else attrib - xSub = etree.SubElement(xParent, name, attrib=xAttr) - if text is not None: - xSub.text = text - - return - ## # Lookup Methods ## diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index df300ae4..c0230758 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -30,7 +30,6 @@ import logging import novelwriter from time import time -from lxml import etree from functools import partial from PyQt5.QtCore import QCoreApplication @@ -47,7 +46,7 @@ from novelwriter.core.item import NWItem from novelwriter.core.index import NWIndex from novelwriter.core.options import OptionState from novelwriter.core.document import NWDoc -from novelwriter.core.projectxml import ProjectXMLReader, XMLReadState +from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.core.projectdata import NWProjectData logger = logging.getLogger(__name__) @@ -585,86 +584,16 @@ class NWProject: else: self._data.incSaveCount() - # Root element and project details - logger.debug("Writing project meta") - nwXML = etree.Element("novelWriterXML", attrib={ - "appVersion": str(novelwriter.__version__), - "hexVersion": str(novelwriter.__hexversion__), - "fileVersion": self.FILE_VERSION, - "timeStamp": formatTimeStamp(saveTime), - }) - self.updateWordCounts() + self.countStatus() + + saveTime = time() editTime = int(self._data.editTime + saveTime - self._projOpened) - # Save Project Meta - xProject = etree.SubElement(nwXML, "project") - self._packProjectValue(xProject, "name", self._data.name) - self._packProjectValue(xProject, "title", self._data.title) - self._packProjectValue(xProject, "author", self._data.authors) - self._packProjectValue(xProject, "saveCount", str(self._data.saveCount)) - self._packProjectValue(xProject, "autoCount", str(self._data.autoCount)) - self._packProjectValue(xProject, "editTime", str(editTime)) - - # Save Project Settings - xSettings = etree.SubElement(nwXML, "settings") - self._packProjectValue(xSettings, "doBackup", self._data.doBackup) - self._packProjectValue(xSettings, "language", self._data.language) - self._packProjectValue(xSettings, "spellCheck", self._data.spellCheck) - self._packProjectValue(xSettings, "spellLang", self._data.spellLang) - self._packProjectValue(xSettings, "lastEdited", self._data.getLastHandle("editor")) - self._packProjectValue(xSettings, "lastViewed", self._data.getLastHandle("viewer")) - self._packProjectValue(xSettings, "lastNovel", self._data.getLastHandle("noveltree")) - self._packProjectValue(xSettings, "lastOutline", self._data.getLastHandle("outline")) - self._packProjectValue(xSettings, "lastWordCount", self._data.getCurrCount("total")) - self._packProjectValue(xSettings, "novelWordCount", self._data.getCurrCount("novel")) - self._packProjectValue(xSettings, "notesWordCount", self._data.getCurrCount("notes")) - self._packProjectKeyValue(xSettings, "autoReplace", self._data.autoReplace) - - xTitleFmt = etree.SubElement(xSettings, "titleFormat") - for aKey, aValue in self._data.titleFormat.items(): - if len(aKey) > 0: - self._packProjectValue(xTitleFmt, aKey, aValue) - - # Save Status/Importance - self.countStatus() - xStatus = etree.SubElement(xSettings, "status") - self._data.itemStatus.packXML(xStatus) - xStatus = etree.SubElement(xSettings, "importance") - self._data.itemImport.packXML(xStatus) - - # Save Tree Content - logger.debug("Writing project content") - self._projTree.packXML(nwXML) - - # Write the xml tree to file - tempFile = os.path.join(self.projPath, nwFiles.PROJ_FILE+"~") - saveFile = os.path.join(self.projPath, nwFiles.PROJ_FILE) - backFile = os.path.join(self.projPath, nwFiles.PROJ_FILE[:-3]+"bak") - try: - with open(tempFile, mode="wb") as outFile: - outFile.write(etree.tostring( - nwXML, - pretty_print=True, - encoding="utf-8", - xml_declaration=True - )) - except Exception as exc: - self.mainGui.makeAlert(self.tr( - "Failed to save project." - ), nwAlert.ERROR, exception=exc) - return False - - # 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: - self.mainGui.makeAlert(self.tr( - "Failed to save project." - ), nwAlert.ERROR, exception=exc) + content = self._projTree.pack() + xmlWriter = ProjectXMLWriter(self.projPath) + if not xmlWriter.write(self._data, content, saveTime, editTime): + self.mainGui.makeAlert(self.tr("Failed to save project."), nwAlert.ERROR) return False # Save project GUI options @@ -1165,28 +1094,6 @@ class NWProject: return False return True - def _packProjectValue(self, xParent, theName, theValue, allowNone=True): - """Pack a list of values into an xml element. - """ - if not isinstance(theValue, list): - theValue = [theValue] - for aValue in theValue: - if (aValue == "" or aValue is None) and not allowNone: - continue - xItem = etree.SubElement(xParent, theName) - xItem.text = str(aValue) - return - - def _packProjectKeyValue(self, xParent, theName, theDict): - """Pack the entries of a dictionary into an xml element. - """ - xAutoRep = etree.SubElement(xParent, theName) - for aKey, aValue in theDict.items(): - if len(aKey) > 0: - xEntry = etree.SubElement(xAutoRep, "entry", attrib={"key": aKey}) - xEntry.text = aValue - return - 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 fe241efa..9b56a5d3 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -26,16 +26,19 @@ along with this program. If not, see . import os import logging +import novelwriter from enum import Enum from lxml import etree from novelwriter.common import ( - checkBool, checkInt, checkStringNone, simplified, checkString + checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString ) +from novelwriter.constants import nwFiles logger = logging.getLogger(__name__) +FILE_VERSION = "1.4" # The current project file format version NUM_VERSION = { "1.0": 0x0100, @@ -62,6 +65,29 @@ class XMLReadState(Enum): class ProjectXMLReader: + """The main project XML file reader class. All data is read into a + NWProjectData instance, which must be provided. + + 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.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. + Introduced in version 2.0. + """ def __init__(self, path): @@ -86,30 +112,44 @@ class ProjectXMLReader: @property def content(self): + """The project content section, a dictionary of project items. + """ return self._content @property def state(self): + """The state of the parsing as an XMLReadState enum value. + """ return self._state @property def xmlRoot(self): + """The root tag name of the XNL file, + """ return self._root @property def xmlVersion(self): + """The project XML version number. + """ return self._version @property def appVersion(self): + """The novelWriter version number who wrote the file. + """ return self._appVersion @property def hexVersion(self): + """The novelWriter version number who wrote the file as hex. + """ return self._hexVersion @property def timeStamp(self): + """The date and time when the file was written. + """ return self._timeStamp ## @@ -149,22 +189,6 @@ class ProjectXMLReader: self._state = XMLReadState.NOT_NWX_FILE return False - # Changes: - # 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. - # 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.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. Introduced in version 1.7. - fileVersion = str(xRoot.attrib.get("fileVersion", "")) if fileVersion in NUM_VERSION: self._version = NUM_VERSION[fileVersion] @@ -425,11 +449,123 @@ class ProjectXMLWriter: return - def write(self): - return + def write(self, projData, projContent, saveTime, editTime): + + nwXML = etree.Element("novelWriterXML", attrib={ + "appVersion": str(novelwriter.__version__), + "hexVersion": str(novelwriter.__hexversion__), + "fileVersion": FILE_VERSION, + "timeStamp": formatTimeStamp(saveTime), + }) + + # Save Project Meta + xProject = etree.SubElement(nwXML, "project") + 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(nwXML, "settings") + self._packSingleValue(xSettings, "doBackup", projData.doBackup) + self._packSingleValue(xSettings, "language", projData.language) + self._packSingleValue(xSettings, "spellCheck", projData.spellCheck) + self._packSingleValue(xSettings, "spellLang", projData.spellLang) + self._packSingleValue(xSettings, "lastEdited", projData.getLastHandle("editor")) + self._packSingleValue(xSettings, "lastViewed", projData.getLastHandle("viewer")) + self._packSingleValue(xSettings, "lastNovel", projData.getLastHandle("noveltree")) + self._packSingleValue(xSettings, "lastOutline", projData.getLastHandle("outline")) + self._packSingleValue(xSettings, "lastWordCount", projData.getCurrCount("total")) + self._packSingleValue(xSettings, "novelWordCount", projData.getCurrCount("novel")) + self._packSingleValue(xSettings, "notesWordCount", projData.getCurrCount("notes")) + self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace) + self._packDictTagValue(xSettings, "titleFormat", projData.titleFormat) + + # Save Status/Importance + xStatus = etree.SubElement(xSettings, "status") + for (label, attr) in projData.itemStatus.pack(): + xEntry = etree.SubElement(xStatus, "entry", attrib=attr) + xEntry.text = label + + xImport = etree.SubElement(xSettings, "importance") + for (label, attr) in projData.itemImport.pack(): + xEntry = etree.SubElement(xImport, "entry", attrib=attr) + xEntry.text = label + + # Save Tree Content + cAttr = {"count": str(len(projContent))} + xContent = etree.SubElement(nwXML, "content", attrib=cAttr) + for item in projContent: + xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {})) + etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {})) + xName = etree.SubElement(xItem, "name", attrib=item.get("nameAttr", {})) + xName.text = item["name"] + + # Write the xml tree to file + tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~") + saveFile = os.path.join(self._path, nwFiles.PROJ_FILE) + backFile = os.path.join(self._path, nwFiles.PROJ_FILE[:-3]+"bak") + try: + with open(tempFile, mode="wb") as outFile: + outFile.write(etree.tostring( + nwXML, + pretty_print=True, + encoding="utf-8", + xml_declaration=True + )) + except Exception: + return False + + # 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: + return False + + return True ## # Internal Functions ## + def _packSingleValue(self, xParent, name, value, allowNone=True): + """Pack a list of values into an xml element. + """ + if (value == "" or value is None) and not allowNone: + return + xItem = etree.SubElement(xParent, name) + xItem.text = str(value) + return + + def _packListValue(self, xParent, name, data, allowNone=True): + """Pack a list of values into an xml element. + """ + for value in data: + self._packSingleValue(xParent, name, value, allowNone=allowNone) + return + + def _packDictKeyValue(self, xParent, name, data): + """Pack the entries of a dictionary into an xml element. + """ + xItem = etree.SubElement(xParent, name) + for key, value in data.items(): + if len(key) > 0: + xEntry = etree.SubElement(xItem, "entry", attrib={"key": key}) + xEntry.text = value + return + + def _packDictTagValue(self, xParent, name, data): + """Pack the entries of a dictionary into an xml element. + """ + xItem = etree.SubElement(xParent, name) + for aKey, value in data.items(): + if len(aKey) > 0: + self._packSingleValue(xItem, aKey, value) + return + # END Class ProjectXMLWriter diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index 63268a4c..b02ed2ea 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -28,8 +28,6 @@ import random import logging import novelwriter -from lxml import etree - from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor from PyQt5.QtCore import QRectF, Qt @@ -204,21 +202,21 @@ class NWStatus: self._store[key]["count"] += 1 return - def packXML(self, xParent): - """Pack the status entries into an XML object for saving to the - main project file. + def pack(self): + """Pack the status entries into a dictionary. """ + result = [] for key, data in self._store.items(): - xSub = etree.SubElement(xParent, "entry", attrib={ - "key": key, - "count": str(data["count"]), - "red": str(data["cols"][0]), - "green": str(data["cols"][1]), - "blue": str(data["cols"][2]), - }) - xSub.text = data["name"] - - return True + result. append(( + data["name"], { + "key": key, + "count": str(data["count"]), + "red": str(data["cols"][0]), + "green": str(data["cols"][1]), + "blue": str(data["cols"][2]), + } + )) + return result def unpack(self, data): """Unpack a data dictionary and set the class values. diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 1db11a27..e6d66f21 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -27,8 +27,6 @@ import os import random import logging -from lxml import etree - from novelwriter.enum import nwItemClass, nwItemLayout from novelwriter.error import logException from novelwriter.common import checkHandle @@ -114,17 +112,16 @@ class NWTree: return True - def packXML(self, xParent): + def pack(self): """Pack the content of the tree into the provided XML object. In the order defined by the _treeOrder list. """ - xContent = etree.SubElement(xParent, "content", attrib={ - "count": str(len(self._treeOrder))} - ) + tree = [] for tHandle in self._treeOrder: tItem = self.__getitem__(tHandle) - tItem.packXML(xContent) - return + if tItem: + tree.append(tItem.pack()) + return tree def unpack(self, data): """Iterate through all items of a list and add them to the diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index dd242336..8a390542 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -59,7 +59,7 @@ World - + Title Page @@ -67,11 +67,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 213106e6..1614eb63 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -59,7 +59,7 @@ World - + Title Page @@ -67,11 +67,11 @@ New Chapter - + New Chapter - + New Scene diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 8acf751c..6075dbe9 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -22,7 +22,6 @@ along with this program. If not, see . import os import pytest -from lxml import etree from shutil import copyfile from zipfile import ZipFile @@ -1033,31 +1032,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): "%s %s 200 100 99\n" ) % (formatTimeStamp(1600002000), formatTimeStamp(1600005600)) - # Pack XML Value - xElem = etree.Element("element") - theProject._packProjectValue(xElem, "A", "B", allowNone=False) - assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == ( - b"B" - ) - - xElem = etree.Element("element") - theProject._packProjectValue(xElem, "A", "", allowNone=False) - assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == ( - b"" - ) - - # Pack XML Key/Value - xElem = etree.Element("element") - theProject._packProjectKeyValue(xElem, "item", {"A": "B", "C": "D"}) - assert etree.tostring(xElem, pretty_print=False, encoding="utf-8") == ( - b"" - b"" - b"B" - b"D" - b"" - b"" - ) - # END Test testCoreProject_Methods From 13b9e5a43487ad7bf2b46600e70404c6898a1a49 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 21:25:40 +0100 Subject: [PATCH 11/18] Move project data class to project file --- novelwriter/core/project.py | 330 ++++++++++++++++++++++++++++++-- novelwriter/core/projectdata.py | 271 -------------------------- novelwriter/core/projectxml.py | 50 +++-- novelwriter/gui/noveltree.py | 10 +- novelwriter/guimain.py | 2 + sample/nwProject.nwx | 16 +- 6 files changed, 353 insertions(+), 326 deletions(-) delete mode 100644 novelwriter/core/projectdata.py diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index c0230758..3dfecf24 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -4,7 +4,8 @@ novelWriter – Project Wrapper Data class for novelWriter projects File History: -Created: 2018-09-29 [0.0.1] +Created: 2018-09-29 [0.0.1] NWProject +Created: 2022-10-30 [2.0rc1] NWProjectData This file is a part of novelWriter Copyright 2018–2022, Veronica Berglyd Olsen @@ -32,38 +33,40 @@ import novelwriter from time import time from functools import partial -from PyQt5.QtCore import QCoreApplication +from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.error import logException -from novelwriter.common import ( - checkStringNone, isHandle, formatTimeStamp, makeFileNameSafe, hexToInt, - minmax, simplified -) 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.document import NWDoc from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState -from novelwriter.core.projectdata import NWProjectData +from novelwriter.common import ( + checkBool, checkInt, checkString, checkStringNone, formatTimeStamp, hexToInt, isHandle, + makeFileNameSafe, minmax, simplified, +) + logger = logging.getLogger(__name__) -class NWProject: +class NWProject(QObject): - FILE_VERSION = "1.4" # The current project file format version + projectStatusChanged = pyqtSignal(bool) def __init__(self, mainGui): + super().__init__(parent=mainGui) # Internal self.mainConf = novelwriter.CONFIG self.mainGui = mainGui # Project Data - self._data = NWProjectData() + self._data = NWProjectData(self) # Core Elements self._optState = OptionState(self) # Project-specific GUI options @@ -240,7 +243,7 @@ class NWProject: # Project Tree self._projTree.clear() - self._data = NWProjectData() + self._data = NWProjectData(self) # Project Settings self.projPath = None @@ -446,7 +449,7 @@ class NWProject: # Open The Project XML File # ========================= - self._data = NWProjectData() + self._data = NWProjectData(self) xmlReader = ProjectXMLReader(fileName) xmlParsed = xmlReader.read(self._data) @@ -857,13 +860,10 @@ class NWProject: information to the GUI statusbar. """ self._projChanged = bValue - self.mainGui.mainStatus.doUpdateProjectStatus(bValue) + self.projectStatusChanged.emit(self._projChanged) if bValue is True: # If we've changed the project at all, this should be True self._projAltered = True - else: - # If we're resetting the status, also reset for data class - self._data.resetProjectChanged() return self._projChanged ## @@ -1310,3 +1310,301 @@ class NWProject: 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 + self._lastHandle = { + "editor": "", + "viewer": "", + "novelTree": "", + "outline": "", + } + self._lastCount = {} + self._currCount = {} + + self._autoReplace = {} + self._titleFormat = { + "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 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 getLastCount(self, type): + """Retrieve the last word count for a given type. + """ + return self._lastCount.get(type, 0) + + def getCurrCount(self, type): + """Retrieve the current word count for a given type. + """ + return self._currCount.get(type, 0) + + 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] = checkString(value, "") + self.theProject.setProjectChanged(True) + elif isinstance(value, dict): + for key, entry in value.items(): + if key in self._lastHandle: + self._lastHandle[key] = checkString(entry, "") + self.theProject.setProjectChanged(True) + return + + def setLastCount(self, value, type): + """Set the word counts from last session. + """ + self._lastCount[type] = checkInt(value, 0) + self.theProject.setProjectChanged(True) + return + + def setCurrCount(self, value, type): + """Set the current word counts. + """ + if value != self._currCount.get(type, 0): + self._currCount[type] = checkInt(value, 0) + self.theProject.setProjectChanged(True) + 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 deleted file mode 100644 index d91c39c7..00000000 --- a/novelwriter/core/projectdata.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -novelWriter – Project Data Class -================================ -Class for holding the project settings - -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 . -""" - -import logging - -from novelwriter.common import ( - checkBool, checkInt, checkStringNone, simplified -) -from novelwriter.core.status import NWStatus - -logger = logging.getLogger(__name__) - - -class NWProjectData: - - def __init__(self): - - # 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 - self._lastHandle = {} - self._lastCount = {} - self._currCount = {} - - self._autoReplace = {} - self._titleFormat = { - "title": "%title%", - "chapter": "%title%", - "unnumbered": "%title%", - "scene": "* * *", - "section": "", - } - - self._status = NWStatus(NWStatus.STATUS) - self._import = NWStatus(NWStatus.IMPORT) - - # Internal - self._changed = False - - 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 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 - - @property - def changed(self): - return self._changed - - ## - # Methods - ## - - def addAuthor(self, value): - self._authors.append(simplified(str(value))) - self._changed = True - return - - def incSaveCount(self): - self._saveCount += 1 - self._changed = True - return - - def incAutoCount(self): - self._autoCount += 1 - self._changed = True - return - - def resetProjectChanged(self): - self._changed = False - - ## - # Getters - ## - - def getLastHandle(self, component): - return self._lastHandle.get(component, None) - - def getLastCount(self, type): - return self._lastCount.get(type, 0) - - def getCurrCount(self, type): - return self._currCount.get(type, 0) - - def getTitleFormat(self, kind): - return self._titleFormat.get(kind, "%title%") - - ## - # Setters - ## - - def setName(self, value): - self._name = simplified(str(value)) - self._changed = True - return - - def setTitle(self, value): - self._title = simplified(str(value)) - self._changed = True - return - - def setAuthors(self, value): - self._authors = [] - self._changed = True - if isinstance(value, str): - for author in value.splitlines(): - author = simplified(author) - if author: - self._authors.append(author) - self._changed = True - elif isinstance(value, list): - self._authors = value - return - - def setSaveCount(self, value): - self._saveCount = checkInt(value, 0) - self._changed = True - return - - def setAutoCount(self, value): - self._autoCount = checkInt(value, 0) - self._changed = True - return - - def setEditTime(self, value): - self._editTime = checkInt(value, 0) - self._changed = True - return - - def setDoBackup(self, value): - self._doBackup = checkBool(value, False) - self._changed = True - return - - def setLanguage(self, value): - self._language = checkStringNone(value, None) - self._changed = True - return - - def setSpellCheck(self, value): - self._spellCheck = checkBool(value, False) - self._changed = True - return - - def setSpellLang(self, value): - self._spellLang = checkStringNone(value, None) - self._changed = True - return - - def setLastHandle(self, value, component): - self._lastHandle[component] = checkStringNone(value, None) - self._changed = True - return - - def setLastCount(self, value, type): - self._lastCount[type] = checkInt(value, 0) - self._changed = True - return - - def setCurrCount(self, value, type): - if value != self._currCount.get(type, 0): - self._currCount[type] = checkInt(value, 0) - self._changed = True - return - - def setAutoReplace(self, value): - if isinstance(value, dict): - self._autoReplace = {} - for key, entry in value.items(): - if isinstance(entry, str): - self._autoReplace[key] = simplified(entry) - self._changed = True - return - - def setTitleFormat(self, value): - 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._changed = True - return - -# END Class NWProjectData diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 9b56a5d3..e16cca0a 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -266,14 +266,10 @@ class ProjectXMLReader: projData.setSpellCheck(xItem.text) elif xItem.tag == "spellLang": projData.setSpellLang(xItem.text) - elif xItem.tag == "lastEdited": + elif xItem.tag == "lastEdited": # Discontinued in 1.4 projData.setLastHandle(xItem.text, "editor") - elif xItem.tag == "lastViewed": + elif xItem.tag == "lastViewed": # Discontinued in 1.4 projData.setLastHandle(xItem.text, "viewer") - elif xItem.tag == "lastNovel": - projData.setLastHandle(xItem.text, "noveltree") - elif xItem.tag == "lastOutline": - projData.setLastHandle(xItem.text, "outline") elif xItem.tag == "lastWordCount": projData.setLastCount(xItem.text, "total") elif xItem.tag == "novelWordCount": @@ -284,9 +280,11 @@ class ProjectXMLReader: self._parseStatusImport(xItem, projData.itemStatus) elif xItem.tag in ("import", "importance"): self._parseStatusImport(xItem, projData.itemImport) + elif xItem.tag == "lastHandle": + projData.setLastHandle(self._parseDictKeyText(xItem, "component")) elif xItem.tag == "autoReplace": if self._version >= 0x0102: - projData.setAutoReplace(self._parseDictKeyText(xItem)) + projData.setAutoReplace(self._parseDictKeyText(xItem, "key")) else: # Pre 1.2 format projData.setAutoReplace(self._parseDictTagText(xItem)) elif xItem.tag == "titleFormat": @@ -304,13 +302,13 @@ class ProjectXMLReader: for xItem in xSection: if xItem.tag == "item": item = {} - item["handle"] = xItem.attrib.get("handle", None) - item["parent"] = xItem.attrib.get("parent", None) - item["root"] = xItem.attrib.get("root", None) + item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None) + item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None) + item["root"] = checkStringNone(xItem.attrib.get("root", None), None) item["order"] = checkInt(xItem.attrib.get("order", 0), 0) - item["type"] = checkString(xItem.attrib.get("type", ""), "") - item["class"] = checkString(xItem.attrib.get("class", ""), "") - item["layout"] = checkString(xItem.attrib.get("layout", ""), "") + item["type"] = checkString(xItem.attrib.get("type", "NO_TYPE"), "NO_TYPE") + item["class"] = checkString(xItem.attrib.get("class", "NO_CLASS"), "NO_CLASS") + item["layout"] = checkString(xItem.attrib.get("layout", "NO_LAYOUT"), "NO_LAYOUT") for xVal in xItem: if xVal.tag == "meta": item["expanded"] = checkBool(xVal.attrib.get("expanded", False), False) @@ -339,7 +337,7 @@ class ProjectXMLReader: return True def _parseProjectContentLegacy(self, xSection): - """Parse the content section of the XML file for older version. + """Parse the content section of the XML file for older versions. """ logger.debug("Parsing xml (legacy format)") depLayout = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") @@ -347,8 +345,8 @@ class ProjectXMLReader: for xItem in xSection: item = {} if xItem.tag == "item": - item["handle"] = xItem.attrib.get("handle", None) - item["parent"] = xItem.attrib.get("parent", None) + 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 @@ -421,14 +419,14 @@ class ProjectXMLReader: self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()} return - def _parseDictKeyText(self, xItem): + def _parseDictKeyText(self, xItem, keyName): """Parse a dictionary stored with key as an attribute and the value as the text porperty. """ result = {} for xEntry in xItem: - if xEntry.tag == "entry" and "key" in xEntry.attrib: - result[xEntry.attrib["key"]] = checkString(xEntry.text, "") + if xEntry.tag == "entry" and keyName in xEntry.attrib: + result[xEntry.attrib[keyName]] = checkString(xEntry.text, "") return result def _parseDictTagText(self, xItem): @@ -473,14 +471,11 @@ class ProjectXMLWriter: self._packSingleValue(xSettings, "language", projData.language) self._packSingleValue(xSettings, "spellCheck", projData.spellCheck) self._packSingleValue(xSettings, "spellLang", projData.spellLang) - self._packSingleValue(xSettings, "lastEdited", projData.getLastHandle("editor")) - self._packSingleValue(xSettings, "lastViewed", projData.getLastHandle("viewer")) - self._packSingleValue(xSettings, "lastNovel", projData.getLastHandle("noveltree")) - self._packSingleValue(xSettings, "lastOutline", projData.getLastHandle("outline")) self._packSingleValue(xSettings, "lastWordCount", projData.getCurrCount("total")) self._packSingleValue(xSettings, "novelWordCount", projData.getCurrCount("novel")) self._packSingleValue(xSettings, "notesWordCount", projData.getCurrCount("notes")) - self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace) + self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle, "component") + self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace, "key") self._packDictTagValue(xSettings, "titleFormat", projData.titleFormat) # Save Status/Importance @@ -549,14 +544,15 @@ class ProjectXMLWriter: self._packSingleValue(xParent, name, value, allowNone=allowNone) return - def _packDictKeyValue(self, xParent, name, data): + def _packDictKeyValue(self, xParent, name, data, keyName): """Pack the entries of a dictionary into an xml element. """ xItem = etree.SubElement(xParent, name) for key, value in data.items(): if len(key) > 0: - xEntry = etree.SubElement(xItem, "entry", attrib={"key": key}) - xEntry.text = value + xEntry = etree.SubElement(xItem, "entry", attrib={keyName: key}) + if value: + xEntry.text = value return def _packDictTagValue(self, xParent, name, data): diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index bfa1da1c..64662e17 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -109,7 +109,7 @@ class GuiNovelView(QWidget): def refreshTree(self): """Refresh the current tree. """ - self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("noveltree")) + self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree")) return def clearProject(self): @@ -122,7 +122,7 @@ class GuiNovelView(QWidget): def openProjectTasks(self): """Run open project tasks. """ - lastNovel = self.theProject.data.getLastHandle("noveltree") + lastNovel = self.theProject.data.getLastHandle("novelTree") if lastNovel not in self.theProject.tree: lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) @@ -319,7 +319,7 @@ class GuiNovelToolBar(QWidget): def _refreshNovelTree(self): """Rebuild the current tree. """ - rootHandle = self.theProject.data.getLastHandle("noveltree") + rootHandle = self.theProject.data.getLastHandle("novelTree") self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return @@ -485,7 +485,7 @@ class GuiNovelTree(QTreeWidget): titleKey = selItem[0].data(self.C_TITLE, self.D_KEY) self._populateTree(rootHandle) - self.theProject.data.setLastHandle(rootHandle, "noveltree") + self.theProject.data.setLastHandle(rootHandle, "novelTree") if titleKey is not None and titleKey in self._treeMap: self._treeMap[titleKey].setSelected(True) @@ -523,7 +523,7 @@ class GuiNovelTree(QTreeWidget): self._lastCol = colType self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN) if doRefresh: - lastNovel = self.theProject.data.getLastHandle("noveltree") + lastNovel = self.theProject.data.getLastHandle("novelTree") self.refreshTree(rootHandle=lastNovel, overRide=True) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 553667f6..9f776210 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -197,6 +197,8 @@ class GuiMain(QMainWindow): # Connect Signals # =============== + self.theProject.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus) + self.viewsBar.viewChangeRequested.connect(self._changeView) self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 6b8d929d..dca0b5a5 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,26 +1,28 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1387 + 1403 236 - 69358 + 69454 False en_GB True None - 636b6aa9b697b - 636b6aa9b697b - 7031beac91f75 - 7031beac91f75 1363 954 409 + + 636b6aa9b697b + 636b6aa9b697b + 7031beac91f75 + 7031beac91f75 + B E From a88a2f5b4078ab3b620683f69551ba195a41f897 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 22:37:05 +0100 Subject: [PATCH 12/18] Change file format to save dictionaries consistently --- novelwriter/core/project.py | 35 +++--- novelwriter/core/projectxml.py | 108 ++++++++++-------- sample/nwProject.nwx | 26 ++--- tests/lipsum/nwProject.nwx | 28 ++--- tests/minimal/nwProject.nwx | 28 ++--- tests/mock.py | 6 +- .../coreProject_NewCustomA_nwProject.nwx | 24 ++-- .../coreProject_NewCustomB_nwProject.nwx | 24 ++-- .../coreProject_NewFileFolder_nwProject.nwx | 24 ++-- .../coreProject_NewMinimal_nwProject.nwx | 24 ++-- .../coreProject_NewRoot_nwProject.nwx | 24 ++-- .../guiEditor_Main_Final_nwProject.nwx | 26 +++-- .../guiEditor_Main_Initial_nwProject.nwx | 24 ++-- 13 files changed, 220 insertions(+), 181 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 3dfecf24..57f9432e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -24,6 +24,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations + import os import json import shutil @@ -46,7 +48,7 @@ from novelwriter.core.options import OptionState from novelwriter.core.document import NWDoc from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.common import ( - checkBool, checkInt, checkString, checkStringNone, formatTimeStamp, hexToInt, isHandle, + checkBool, checkInt, checkStringNone, formatTimeStamp, hexToInt, isHandle, makeFileNameSafe, minmax, simplified, ) @@ -122,7 +124,7 @@ class NWProject(QObject): @property def projChanged(self): - return self._projChanged or self._data.changed + return self._projChanged @property def projAltered(self): @@ -596,7 +598,9 @@ class NWProject(QObject): content = self._projTree.pack() xmlWriter = ProjectXMLWriter(self.projPath) if not xmlWriter.write(self._data, content, saveTime, editTime): - self.mainGui.makeAlert(self.tr("Failed to save project."), nwAlert.ERROR) + self.mainGui.makeAlert(self.tr( + "Failed to save project." + ), nwAlert.ERROR, exception=xmlWriter.error) return False # Save project GUI options @@ -1331,17 +1335,18 @@ class NWProjectData: self._language = None self._spellCheck = False self._spellLang = None - self._lastHandle = { - "editor": "", - "viewer": "", - "novelTree": "", - "outline": "", - } - self._lastCount = {} - self._currCount = {} - self._autoReplace = {} - self._titleFormat = { + # Project Dictionaries + self._lastCount: dict[str, int] = {} + self._currCount: dict[str, int] = {} + 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%", @@ -1562,12 +1567,12 @@ class NWProjectData: values. """ if isinstance(component, str): - self._lastHandle[component] = checkString(value, "") + 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] = checkString(entry, "") + self._lastHandle[key] = str(entry) if isHandle(entry) else None self.theProject.setProjectChanged(True) return diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index e16cca0a..1c3f525d 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -266,11 +266,7 @@ class ProjectXMLReader: projData.setSpellCheck(xItem.text) elif xItem.tag == "spellLang": projData.setSpellLang(xItem.text) - elif xItem.tag == "lastEdited": # Discontinued in 1.4 - projData.setLastHandle(xItem.text, "editor") - elif xItem.tag == "lastViewed": # Discontinued in 1.4 - projData.setLastHandle(xItem.text, "viewer") - elif xItem.tag == "lastWordCount": + elif xItem.tag == "totalWordCount": projData.setLastCount(xItem.text, "total") elif xItem.tag == "novelWordCount": projData.setLastCount(xItem.text, "novel") @@ -281,16 +277,30 @@ class ProjectXMLReader: elif xItem.tag in ("import", "importance"): self._parseStatusImport(xItem, projData.itemImport) elif xItem.tag == "lastHandle": - projData.setLastHandle(self._parseDictKeyText(xItem, "component")) + projData.setLastHandle(self._parseDictKeyText(xItem)) elif xItem.tag == "autoReplace": if self._version >= 0x0102: - projData.setAutoReplace(self._parseDictKeyText(xItem, "key")) + projData.setAutoReplace(self._parseDictKeyText(xItem)) else: # Pre 1.2 format projData.setAutoReplace(self._parseDictTagText(xItem)) elif xItem.tag == "titleFormat": - projData.setTitleFormat(self._parseDictTagText(xItem)) + if self._version >= 0x0104: + projData.setTitleFormat(self._parseDictKeyText(xItem)) + else: # Pre 1.4 format + projData.setTitleFormat(self._parseDictTagText(xItem)) else: - logger.warning("Ignored in xml", xItem.tag) + if self._version < 0x0104: + # Convert some deprecated fields + if xItem.tag == "lastEdited": # Discontinued in 1.4 + projData.setLastHandle(xItem.text, "editor") + elif xItem.tag == "lastViewed": # Discontinued in 1.4 + projData.setLastHandle(xItem.text, "viewer") + elif xItem.tag == "lastWordCount": # Renamed in 1.4 + projData.setLastCount(xItem.text, "total") + else: + logger.warning("Ignored in xml", xItem.tag) + else: + logger.warning("Ignored in xml", xItem.tag) return True @@ -419,14 +429,14 @@ class ProjectXMLReader: self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()} return - def _parseDictKeyText(self, xItem, keyName): + def _parseDictKeyText(self, xItem): """Parse a dictionary stored with key as an attribute and the value as the text porperty. """ result = {} for xEntry in xItem: - if xEntry.tag == "entry" and keyName in xEntry.attrib: - result[xEntry.attrib[keyName]] = checkString(xEntry.text, "") + if xEntry.tag == "entry" and "key" in xEntry.attrib: + result[xEntry.attrib["key"]] = checkString(xEntry.text, "") return result def _parseDictTagText(self, xItem): @@ -447,7 +457,21 @@ class ProjectXMLWriter: return + ## + # Properties + ## + + @property + def error(self): + return self._error + + ## + # Methods + ## + def write(self, projData, projContent, saveTime, editTime): + """Write the project data and content to the XML files. + """ nwXML = etree.Element("novelWriterXML", attrib={ "appVersion": str(novelwriter.__version__), @@ -471,27 +495,24 @@ class ProjectXMLWriter: self._packSingleValue(xSettings, "language", projData.language) self._packSingleValue(xSettings, "spellCheck", projData.spellCheck) self._packSingleValue(xSettings, "spellLang", projData.spellLang) - self._packSingleValue(xSettings, "lastWordCount", projData.getCurrCount("total")) + self._packSingleValue(xSettings, "totalWordCount", projData.getCurrCount("total")) self._packSingleValue(xSettings, "novelWordCount", projData.getCurrCount("novel")) self._packSingleValue(xSettings, "notesWordCount", projData.getCurrCount("notes")) - self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle, "component") - self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace, "key") - self._packDictTagValue(xSettings, "titleFormat", projData.titleFormat) + self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle) + self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace) + self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat) # Save Status/Importance xStatus = etree.SubElement(xSettings, "status") - for (label, attr) in projData.itemStatus.pack(): - xEntry = etree.SubElement(xStatus, "entry", attrib=attr) - xEntry.text = label + for label, attrib in projData.itemStatus.pack(): + self._packSingleValue(xStatus, "entry", label, attrib=attrib) xImport = etree.SubElement(xSettings, "importance") - for (label, attr) in projData.itemImport.pack(): - xEntry = etree.SubElement(xImport, "entry", attrib=attr) - xEntry.text = label + for label, attrib in projData.itemImport.pack(): + self._packSingleValue(xImport, "entry", label, attrib=attrib) # Save Tree Content - cAttr = {"count": str(len(projContent))} - xContent = etree.SubElement(nwXML, "content", attrib=cAttr) + xContent = etree.SubElement(nwXML, "content", attrib={"count": str(len(projContent))}) for item in projContent: xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {})) etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {})) @@ -499,8 +520,8 @@ class ProjectXMLWriter: xName.text = item["name"] # Write the xml tree to file - tempFile = os.path.join(self._path, nwFiles.PROJ_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") try: with open(tempFile, mode="wb") as outFile: @@ -510,7 +531,8 @@ class ProjectXMLWriter: encoding="utf-8", xml_declaration=True )) - except Exception: + except Exception as exc: + self._error = exc return False # If we're here, the file was successfully saved, @@ -519,7 +541,8 @@ class ProjectXMLWriter: if os.path.isfile(saveFile): os.replace(saveFile, backFile) os.replace(tempFile, saveFile) - except OSError: + except OSError as exc: + self._error = exc return False return True @@ -528,40 +551,29 @@ class ProjectXMLWriter: # Internal Functions ## - def _packSingleValue(self, xParent, name, value, allowNone=True): - """Pack a list of values into an xml element. + def _packSingleValue(self, xParent, name, value, attrib=None): + """Pack a single value into an xml element. """ - if (value == "" or value is None) and not allowNone: - return - xItem = etree.SubElement(xParent, name) - xItem.text = str(value) + xItem = etree.SubElement(xParent, name, attrib=attrib) + xItem.text = str(value) or "" return - def _packListValue(self, xParent, name, data, allowNone=True): + def _packListValue(self, xParent, name, data): """Pack a list of values into an xml element. """ for value in data: - self._packSingleValue(xParent, name, value, allowNone=allowNone) + xItem = etree.SubElement(xParent, name) + xItem.text = str(value) or "" return - def _packDictKeyValue(self, xParent, name, data, keyName): + def _packDictKeyValue(self, xParent, name, data): """Pack the entries of a dictionary into an xml element. """ xItem = etree.SubElement(xParent, name) for key, value in data.items(): if len(key) > 0: - xEntry = etree.SubElement(xItem, "entry", attrib={keyName: key}) - if value: - xEntry.text = value - return - - def _packDictTagValue(self, xParent, name, data): - """Pack the entries of a dictionary into an xml element. - """ - xItem = etree.SubElement(xParent, name) - for aKey, value in data.items(): - if len(aKey) > 0: - self._packSingleValue(xItem, aKey, value) + xEntry = etree.SubElement(xItem, "entry", attrib={"key": key}) + xEntry.text = str(value) or "" return # END Class ProjectXMLWriter diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index dca0b5a5..d6eeee62 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,27 +1,27 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1403 + 1407 236 - 69454 + 69424 False en_GB True None - 1363 + 1363 954 409 - 636b6aa9b697b - 636b6aa9b697b - 7031beac91f75 - 7031beac91f75 + 636b6aa9b697b + 636b6aa9b697b + 7031beac91f75 + 7031beac91f75 B @@ -29,11 +29,11 @@ D - %title% - Chapter %chw%: %title% - %title% - Scene %ch%.%sc%: %title% -
+ %title% + Chapter %chw%: %title% + %title% + Scene %ch%.%sc%: %title% +
New diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index bcc0b0ce..5ac1b4de 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,35 +1,37 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 28 + 32 24 - 1874 + 1889 False en_GB False None - 7a992350f3eb6 - None - None - None - 3847 + 3847 3109 738 + + 7a992350f3eb6 + None + b3643d0f92e32 + None + Replace Text 1 Replace Text 2 - %title% - Chapter %ch%: %title% - %title% - * * * -
+ %title% + Chapter %ch%: %title% + %title% + * * * +
New diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index 3b9d81c6..19689fac 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,33 +1,35 @@ - + Test Minimal Minimal Jane Doe John Doh - 21 + 25 2 - 177 + 203 True en_GB False None - None - None - a508bb932959c - None - 10 + 10 10 0 + + None + None + a508bb932959c + None + - %title% - Chapter %ch%: %title% - %title% - * * * -
+ %title% + Chapter %ch%: %title% + %title% + * * * +
New diff --git a/tests/mock.py b/tests/mock.py index ff67f4bf..74623dd1 100644 --- a/tests/mock.py +++ b/tests/mock.py @@ -19,14 +19,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from PyQt5.QtCore import QObject + # =========================================================================== # # Mock GUI # =========================================================================== # -class MockGuiMain: +class MockGuiMain(QObject): def __init__(self): + super().__init__() + self.mainConf = None self.hasProject = True self.theProject = None diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index a42b8290..69987568 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -14,20 +14,22 @@ None False None - None - None - None - None - 0 + 0 0 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 63deaff4..a4a5e280 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -14,20 +14,22 @@ None False None - None - None - None - None - 0 + 0 0 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 8a390542..e3d19827 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,20 +13,22 @@ None False None - None - None - None - None - 13 + 13 10 3 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 2bc604a6..7fcc9b61 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project None @@ -12,20 +12,22 @@ None False None - None - None - None - None - 0 + 0 0 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 1614eb63..d256a9a9 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,20 +13,22 @@ None False None - None - None - None - None - 9 + 9 9 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index ad38f479..69f7208c 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,32 +1,34 @@ - + New Project New Novel Jane Doe 4 2 - 3 + 4 True None True None - 000000000000f - None - 0000000000008 - 0000000000008 - 163 + 163 136 27 + + 000000000000f + None + 0000000000008 + 0000000000008 + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 2e36ef35..9dfb7b59 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,20 +13,22 @@ None False None - None - None - None - None - 9 + 9 9 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New From 6fd7107675db1674d2f54db1fb397e2d08440d64 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:18:16 +0100 Subject: [PATCH 13/18] Remove pre-generatiopn of status and importance items --- novelwriter/core/project.py | 32 +++--- .../coreProject_NewCustomA_nwProject.nwx | 106 ++++++++--------- .../coreProject_NewCustomB_nwProject.nwx | 82 ++++++------- .../coreProject_NewFileFolder_nwProject.nwx | 8 +- .../coreProject_NewMinimal_nwProject.nwx | 50 ++++---- .../coreProject_NewRoot_nwProject.nwx | 18 +-- ...=> guiEditor_Main_Final_0000000000010.nwd} | 2 +- ...=> guiEditor_Main_Final_0000000000011.nwd} | 2 +- ...=> guiEditor_Main_Final_0000000000012.nwd} | 2 +- .../guiEditor_Main_Final_nwProject.nwx | 10 +- tests/test_core/test_core_item.py | 11 +- tests/test_core/test_core_project.py | 108 +++++++++--------- tests/test_core/test_core_tree.py | 63 +--------- tests/test_gui/test_gui_guimain.py | 23 ++-- tests/tools.py | 10 ++ 15 files changed, 242 insertions(+), 285 deletions(-) rename tests/reference/{guiEditor_Main_Final_0000000000020.nwd => guiEditor_Main_Final_0000000000010.nwd} (71%) rename tests/reference/{guiEditor_Main_Final_0000000000021.nwd => guiEditor_Main_Final_0000000000011.nwd} (74%) rename tests/reference/{guiEditor_Main_Final_0000000000022.nwd => guiEditor_Main_Final_0000000000012.nwd} (74%) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 57f9432e..40828056 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -254,14 +254,6 @@ class NWProject(QObject): self.projContent = None self.projDict = None self.projFiles = [] - 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 @@ -294,6 +286,17 @@ class NWProject(QObject): 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 @@ -859,15 +862,16 @@ class NWProject(QObject): """ return self._setStatusImport(newCols, delCols, self._data.itemImport) - def setProjectChanged(self, bValue): + def setProjectChanged(self, value): """Toggle the project changed flag, and propagate the information to the GUI statusbar. """ - self._projChanged = bValue - self.projectStatusChanged.emit(self._projChanged) - if bValue is True: - # If we've changed the project at all, this should be True - self._projAltered = True + if isinstance(value, bool): + self._projChanged = value + self.projectStatusChanged.emit(self._projChanged) + if value: + # If we've changed the project at all, this should be True + self._projAltered = True return self._projChanged ## diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 69987568..ca7b5891 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -32,106 +32,106 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main
- + - Novel + 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 + Plot - + - Main Plot + Main Plot - + - Characters + Characters - + - Protagonist + Protagonist - + - Locations + Locations - + - Main Location + Main Location - + - Archive + Archive - + - Trash + Trash
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index a4a5e280..404b0128 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -32,82 +32,82 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main
- + - Novel + 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 + Plot - + - Main Plot + Main Plot - + - Characters + Characters - + - Protagonist + Protagonist - + - Locations + Locations - + - Main Location + Main Location - + - Archive + Archive - + - Trash + Trash
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index e3d19827..646317f3 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -76,15 +76,15 @@ New Scene
- + Stuff - + Hello - + Jane diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 7fcc9b61..15907d72 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project None @@ -30,50 +30,50 @@ - New - Note - Draft - Finished + New + Note + Draft + Finished - New - Minor - Major - Main + New + Minor + Major + Main
- + - Novel + Novel - + - Title Page + Title Page - + - New Chapter + New Chapter - + - New Scene + New Scene - + - Plot + Plot - + - Characters + Characters - + - Locations + Locations - + - Archive + Archive
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index d256a9a9..6aff4336 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -76,35 +76,35 @@ New Scene - + Novel - + Plot - + Characters - + Locations - + Timeline - + Objects - + Custom - + Custom diff --git a/tests/reference/guiEditor_Main_Final_0000000000020.nwd b/tests/reference/guiEditor_Main_Final_0000000000010.nwd similarity index 71% rename from tests/reference/guiEditor_Main_Final_0000000000020.nwd rename to tests/reference/guiEditor_Main_Final_0000000000010.nwd index c0316819..bc255b88 100644 --- a/tests/reference/guiEditor_Main_Final_0000000000020.nwd +++ b/tests/reference/guiEditor_Main_Final_0000000000010.nwd @@ -1,5 +1,5 @@ %%~name: New Note -%%~path: 000000000000a/0000000000020 +%%~path: 000000000000a/0000000000010 %%~kind: CHARACTER/NOTE # Jane Doe diff --git a/tests/reference/guiEditor_Main_Final_0000000000021.nwd b/tests/reference/guiEditor_Main_Final_0000000000011.nwd similarity index 74% rename from tests/reference/guiEditor_Main_Final_0000000000021.nwd rename to tests/reference/guiEditor_Main_Final_0000000000011.nwd index 5dddd23b..99705061 100644 --- a/tests/reference/guiEditor_Main_Final_0000000000021.nwd +++ b/tests/reference/guiEditor_Main_Final_0000000000011.nwd @@ -1,5 +1,5 @@ %%~name: New Note -%%~path: 0000000000009/0000000000021 +%%~path: 0000000000009/0000000000011 %%~kind: PLOT/NOTE # Main Plot diff --git a/tests/reference/guiEditor_Main_Final_0000000000022.nwd b/tests/reference/guiEditor_Main_Final_0000000000012.nwd similarity index 74% rename from tests/reference/guiEditor_Main_Final_0000000000022.nwd rename to tests/reference/guiEditor_Main_Final_0000000000012.nwd index 092f832a..ea19dae5 100644 --- a/tests/reference/guiEditor_Main_Final_0000000000022.nwd +++ b/tests/reference/guiEditor_Main_Final_0000000000012.nwd @@ -1,5 +1,5 @@ %%~name: New Note -%%~path: 000000000000b/0000000000022 +%%~path: 000000000000b/0000000000012 %%~kind: WORLD/NOTE # Main Location diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 69f7208c..e5c1a1ac 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -68,7 +68,7 @@ Plot - + New Note @@ -76,7 +76,7 @@ Characters - + New Note @@ -84,11 +84,11 @@ World - + New Note - + Trash diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index df841aa2..02fe3f8f 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -25,7 +25,7 @@ from lxml import etree from PyQt5.QtGui import QIcon -from tools import C +from tools import C, buildTestProject from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject @@ -33,10 +33,12 @@ from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout @pytest.mark.core -def testCoreItem_Setters(mockGUI, mockRnd): +def testCoreItem_Setters(mockGUI, mockRnd, fncDir): """Test all the simple setters for the NWItem class. """ theProject = NWProject(mockGUI) + mockRnd.reset() + buildTestProject(theProject, fncDir) theItem = NWItem(theProject) statusKeys = ["s000000", "s000001", "s000002", "s000003"] @@ -192,11 +194,12 @@ def testCoreItem_Setters(mockGUI, mockRnd): @pytest.mark.core -def testCoreItem_Methods(mockGUI, mockRnd): +def testCoreItem_Methods(mockGUI, mockRnd, fncDir): """Test the simple methods of the NWItem class. """ - mockRnd.reset() theProject = NWProject(mockGUI) + mockRnd.reset() + buildTestProject(theProject, fncDir) theItem = NWItem(theProject) # Describe Me diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 6075dbe9..66fa4082 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -263,14 +263,14 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000020" - assert theProject.newRoot(nwItemClass.PLOT) == "0000000000021" - assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000022" - assert theProject.newRoot(nwItemClass.WORLD) == "0000000000023" - assert theProject.newRoot(nwItemClass.TIMELINE) == "0000000000024" - assert theProject.newRoot(nwItemClass.OBJECT) == "0000000000025" - assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000026" - assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000027" + assert theProject.newRoot(nwItemClass.NOVEL) == "0000000000010" + assert theProject.newRoot(nwItemClass.PLOT) == "0000000000011" + assert theProject.newRoot(nwItemClass.CHARACTER) == "0000000000012" + assert theProject.newRoot(nwItemClass.WORLD) == "0000000000013" + assert theProject.newRoot(nwItemClass.TIMELINE) == "0000000000014" + assert theProject.newRoot(nwItemClass.OBJECT) == "0000000000015" + assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000016" + assert theProject.newRoot(nwItemClass.CUSTOM) == "0000000000017" assert theProject.projChanged is True assert theProject.saveProject() is True @@ -281,23 +281,23 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI, mockRnd): assert theProject.projChanged is False # Delete the new items - assert theProject.removeItem("0000000000020") is True - assert theProject.removeItem("0000000000021") is True - assert theProject.removeItem("0000000000022") is True - assert theProject.removeItem("0000000000023") is True - assert theProject.removeItem("0000000000024") is True - assert theProject.removeItem("0000000000025") is True - assert theProject.removeItem("0000000000026") is True - assert theProject.removeItem("0000000000027") is True + assert theProject.removeItem("0000000000010") is True + assert theProject.removeItem("0000000000011") is True + assert theProject.removeItem("0000000000012") is True + assert theProject.removeItem("0000000000013") is True + assert theProject.removeItem("0000000000014") is True + assert theProject.removeItem("0000000000015") is True + assert theProject.removeItem("0000000000016") is True + assert theProject.removeItem("0000000000017") is True - assert "0000000000020" not in theProject.tree - assert "0000000000021" not in theProject.tree - assert "0000000000022" not in theProject.tree - assert "0000000000023" not in theProject.tree - assert "0000000000024" not in theProject.tree - assert "0000000000025" not in theProject.tree - assert "0000000000026" not in theProject.tree - assert "0000000000027" not in theProject.tree + assert "0000000000010" not in theProject.tree + assert "0000000000011" not in theProject.tree + assert "0000000000012" not in theProject.tree + assert "0000000000013" not in theProject.tree + assert "0000000000014" not in theProject.tree + assert "0000000000015" not in theProject.tree + assert "0000000000016" not in theProject.tree + assert "0000000000017" not in theProject.tree # END Test testCoreProject_NewRoot @@ -324,26 +324,26 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI, assert theProject.newFile("New File", "1234567890abc") is None # Add files properly - assert theProject.newFolder("Stuff", C.hNovelRoot) == "0000000000020" - assert theProject.newFile("Hello", "0000000000020") == "0000000000021" - assert theProject.newFile("Jane", C.hCharRoot) == "0000000000022" + assert theProject.newFolder("Stuff", C.hNovelRoot) == "0000000000010" + assert theProject.newFile("Hello", "0000000000010") == "0000000000011" + assert theProject.newFile("Jane", C.hCharRoot) == "0000000000012" - assert "0000000000020" in theProject.tree - assert "0000000000021" in theProject.tree - assert "0000000000022" in theProject.tree + assert "0000000000010" in theProject.tree + assert "0000000000011" in theProject.tree + assert "0000000000012" in theProject.tree # Write to file, failed assert theProject.writeNewFile("blabla", 1, True) is False # Not a handle - assert theProject.writeNewFile("0000000000020", 1, True) is False # Not a file + assert theProject.writeNewFile("0000000000010", 1, True) is False # Not a file assert theProject.writeNewFile(C.hTitlePage, 1, True) is False # Already has content # Write to file, success - assert theProject.writeNewFile("0000000000021", 2, True) is True - assert NWDoc(theProject, "0000000000021").readDocument() == "## Hello\n\n" + assert theProject.writeNewFile("0000000000011", 2, True) is True + assert NWDoc(theProject, "0000000000011").readDocument() == "## Hello\n\n" # Write to file with additional text, success - assert theProject.writeNewFile("0000000000022", 1, False, "Hi Jane\n\n") is True - assert NWDoc(theProject, "0000000000022").readDocument() == "# Jane\n\nHi Jane\n\n" + assert theProject.writeNewFile("0000000000012", 1, False, "Hi Jane\n\n") is True + assert NWDoc(theProject, "0000000000012").readDocument() == "# Jane\n\nHi Jane\n\n" # Save, close and check assert theProject.projChanged is True @@ -356,23 +356,23 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI, # Delete new file, but block access with monkeypatch.context() as mp: mp.setattr("os.unlink", causeOSError) - assert theProject.removeItem("0000000000021") is False - assert "0000000000021" in theProject.tree + assert theProject.removeItem("0000000000011") is False + assert "0000000000011" in theProject.tree # Delete new files and folders - assert os.path.isfile(os.path.join(fncDir, "content", "0000000000022.nwd")) - assert os.path.isfile(os.path.join(fncDir, "content", "0000000000021.nwd")) + assert os.path.isfile(os.path.join(fncDir, "content", "0000000000012.nwd")) + assert os.path.isfile(os.path.join(fncDir, "content", "0000000000011.nwd")) - assert theProject.removeItem("0000000000022") is True - assert theProject.removeItem("0000000000021") is True - assert theProject.removeItem("0000000000020") is True + 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", "0000000000022.nwd")) - assert not os.path.isfile(os.path.join(fncDir, "content", "0000000000021.nwd")) + 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 "0000000000020" not in theProject.tree - assert "0000000000021" not in theProject.tree - assert "0000000000022" not in theProject.tree + assert "0000000000010" not in theProject.tree + assert "0000000000011" not in theProject.tree + assert "0000000000012" not in theProject.tree assert theProject.closeProject() is True @@ -933,7 +933,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): # Trash folder # Should create on first call, and just returned on later calls - hTrash = "0000000000018" + hTrash = "0000000000010" assert theProject.tree[hTrash] is None assert theProject.trashFolder() == hTrash assert theProject.trashFolder() == hTrash @@ -987,14 +987,14 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): # Change project tree order oldOrder = [ - "0000000000010", "0000000000011", "0000000000012", - "0000000000013", "0000000000014", "0000000000015", - "0000000000016", "0000000000017", "0000000000018", + "0000000000008", "0000000000009", "000000000000a", + "000000000000b", "000000000000c", "000000000000d", + "000000000000e", "000000000000f", "0000000000010", ] newOrder = [ - "0000000000013", "0000000000014", "0000000000015", - "0000000000010", "0000000000011", "0000000000012", - "0000000000016", "0000000000017", + "000000000000b", "000000000000c", "000000000000d", + "0000000000008", "0000000000009", "000000000000a", + "000000000000e", "000000000000f", ] assert theProject.tree.handles() == oldOrder assert theProject.setTreeOrder(newOrder) diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index cef9fa58..518122e7 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -23,8 +23,6 @@ import os import pytest import random -from lxml import etree - from tools import readFile from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout @@ -198,7 +196,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert len(theTree) == len(mockItems) + 1 theList = theTree.handles() - nHandle = "0000000000010" + nHandle = "0000000000000" assert theList[-1] == nHandle # Try to add existing handle @@ -395,65 +393,6 @@ def testCoreTree_Reorder(mockGUI, mockItems): # END Test testCoreTree_Reorder -@pytest.mark.core -@pytest.mark.skip -def testCoreTree_XMLPackUnpack(mockGUI, mockItems): - """Test packing and unpacking the tree to and from XML. - """ - theProject = NWProject(mockGUI) - theTree = NWTree(theProject) - - for tHandle, pHandle, nwItem in mockItems: - theTree.append(tHandle, pHandle, nwItem) - theTree.updateItemData(tHandle) - - assert len(theTree) == len(mockItems) - - nwXML = etree.Element("novelWriterXML") - theTree.packXML(nwXML) - assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( - b'' - b'' - b'Novel' - b'Act One' - b'Chapter One' - b'Scene One' - b'Outtakes' - b'Trash' - b'Characters' - b'Jane Doe' - b'' - b'' - ) - - theTree.clear() - assert len(theTree) == 0 - assert not theTree.unpackXML(nwXML) - assert theTree.unpackXML(nwXML[0]) - assert len(theTree) == len(mockItems) - -# END Test testCoreTree_XMLPackUnpack - - @pytest.mark.core def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir): """Test writing the ToC.txt file. diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 28b5b729..28582695 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -489,11 +489,12 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock # Check a Quick Create and Delete assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None) newHandle = nwGUI.projView.getSelectedHandle() - assert nwGUI.theProject.tree["0000000000020"] is not None + assert newHandle == "0000000000013" + assert nwGUI.theProject.tree[newHandle] is not None assert nwGUI.projView.requestDeleteItem() assert nwGUI.projView.setSelectedHandle(newHandle) assert nwGUI.projView.requestDeleteItem() - assert nwGUI.theProject.tree["0000000000024"] is not None # Trash + assert nwGUI.theProject.tree["0000000000014"] is not None # Trash assert nwGUI.saveProject() # Check the files @@ -509,21 +510,21 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) - projFile = os.path.join(fncProj, "content", "0000000000020.nwd") - testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000020.nwd") - compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000020.nwd") + projFile = os.path.join(fncProj, "content", "0000000000010.nwd") + testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000010.nwd") + compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000010.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) - projFile = os.path.join(fncProj, "content", "0000000000021.nwd") - testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000021.nwd") - compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000021.nwd") + projFile = os.path.join(fncProj, "content", "0000000000011.nwd") + testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000011.nwd") + compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000011.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) - projFile = os.path.join(fncProj, "content", "0000000000022.nwd") - testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000022.nwd") - compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000022.nwd") + projFile = os.path.join(fncProj, "content", "0000000000012.nwd") + testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000012.nwd") + compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000012.nwd") copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) diff --git a/tests/tools.py b/tests/tools.py index 1cbc0359..0da846cc 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -167,6 +167,16 @@ def buildTestProject(theObject, projPath): theProject.clearProject() theProject.setProjectPath(projPath, newProject=True) + + 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.data.setName("New Project") theProject.data.setTitle("New Novel") theProject.data.setAuthors("Jane Doe") From 0cfaf4f0219ea14e9869bfeb14d3e9db322be578 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 1 Nov 2022 12:21:16 +0100 Subject: [PATCH 14/18] Drop the total word count value in the XML and rewrite how counts are handled --- novelwriter/core/project.py | 68 +++++++++---------- novelwriter/core/projectxml.py | 24 ++----- novelwriter/guimain.py | 12 ++-- sample/nwProject.nwx | 7 +- .../coreProject_NewCustomA_nwProject.nwx | 3 +- .../coreProject_NewCustomB_nwProject.nwx | 3 +- .../coreProject_NewFileFolder_nwProject.nwx | 3 +- .../coreProject_NewMinimal_nwProject.nwx | 3 +- .../coreProject_NewRoot_nwProject.nwx | 3 +- .../guiEditor_Main_Final_nwProject.nwx | 5 +- .../guiEditor_Main_Initial_nwProject.nwx | 3 +- tests/test_core/test_core_project.py | 7 +- 12 files changed, 59 insertions(+), 82 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 40828056..9b1718c7 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -544,7 +544,7 @@ class NWProject(QObject): # Update recent projects self.mainConf.updateRecentCache( - self.projPath, self._data.name, self._data.getLastCount("total"), time() + self.projPath, self._data.name, sum(self._data.initCounts), time() ) self.mainConf.saveRecentCache() @@ -611,7 +611,7 @@ class NWProject(QObject): # Update recent projects self.mainConf.updateRecentCache( - self.projPath, self._data.name, self._data.getCurrCount("total"), saveTime + self.projPath, self._data.name, sum(self._data.currCounts), saveTime ) self.mainConf.saveRecentCache() @@ -949,10 +949,8 @@ class NWProject(QObject): def updateWordCounts(self): """Update the total word count values. """ - wcNovel, wcNotes = self._projTree.sumWords() - self._data.setCurrCount(wcNovel, "novel") - self._data.setCurrCount(wcNotes, "notes") - self._data.setCurrCount(wcNovel + wcNotes, "total") + novel, notes = self._projTree.sumWords() + self._data.setCurrCounts(novel=novel, notes=notes) return def countStatus(self): @@ -1211,9 +1209,10 @@ class NWProject(QObject): isFile = os.path.isfile(sessionFile) nowTime = time() - lastCount = self._data.getLastCount("total") - currCount = self._data.getCurrCount("total") - sessDiff = currCount - lastCount + iNovel, iNotes = self._data.initCounts + cNovel, cNotes = self._data.currCounts + iTotal = iNovel + iNotes + sessDiff = cNovel + cNotes - iTotal sessTime = nowTime - self._projOpened logger.info("The session lasted %d sec and added %d words", int(sessTime), sessDiff) @@ -1225,8 +1224,8 @@ class NWProject(QObject): with open(sessionFile, mode="a+", encoding="utf-8") as outFile: if not isFile: # It's a new file, so add a header - if lastCount > 0: - outFile.write("# Offset %d\n" % lastCount) + if iTotal > 0: + outFile.write("# Offset %d\n" % iTotal) outFile.write("# %-17s %-19s %8s %8s %8s\n" % ( "Start Time", "End Time", "Novel", "Notes", "Idle" )) @@ -1234,8 +1233,8 @@ class NWProject(QObject): outFile.write("%-19s %-19s %8d %8d %8d\n" % ( formatTimeStamp(self._projOpened), formatTimeStamp(nowTime), - self._data.getCurrCount("novel"), - self._data.getCurrCount("notes"), + cNovel, + cNotes, int(idleTime), )) @@ -1341,8 +1340,8 @@ class NWProjectData: self._spellLang = None # Project Dictionaries - self._lastCount: dict[str, int] = {} - self._currCount: dict[str, int] = {} + self._initCounts = [0, 0] + self._currCounts = [0, 0] self._lastHandle: dict[str, str | None] = { "editor": None, "viewer": None, @@ -1407,6 +1406,14 @@ class NWProjectData: 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 @@ -1461,16 +1468,6 @@ class NWProjectData: """ return self._lastHandle.get(component, None) - def getLastCount(self, type): - """Retrieve the last word count for a given type. - """ - return self._lastCount.get(type, 0) - - def getCurrCount(self, type): - """Retrieve the current word count for a given type. - """ - return self._currCount.get(type, 0) - def getTitleFormat(self, kind): """Retrieve the title format string for a given kind of header. """ @@ -1580,19 +1577,22 @@ class NWProjectData: self.theProject.setProjectChanged(True) return - def setLastCount(self, value, type): - """Set the word counts from last session. + def setInitCounts(self, novel=None, notes=None): + """Set the worc count totals for novel and note files. """ - self._lastCount[type] = checkInt(value, 0) - self.theProject.setProjectChanged(True) + if novel is not None: + self._initCounts[0] = checkInt(novel, 0) + if notes is not None: + self._initCounts[1] = checkInt(notes, 0) return - def setCurrCount(self, value, type): - """Set the current word counts. + def setCurrCounts(self, novel=None, notes=None): + """Set the worc count totals for novel and note files. """ - if value != self._currCount.get(type, 0): - self._currCount[type] = checkInt(value, 0) - self.theProject.setProjectChanged(True) + 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): diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 1c3f525d..66dfb294 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -266,12 +266,10 @@ class ProjectXMLReader: projData.setSpellCheck(xItem.text) elif xItem.tag == "spellLang": projData.setSpellLang(xItem.text) - elif xItem.tag == "totalWordCount": - projData.setLastCount(xItem.text, "total") elif xItem.tag == "novelWordCount": - projData.setLastCount(xItem.text, "novel") + projData.setInitCounts(novel=xItem.text) elif xItem.tag == "notesWordCount": - projData.setLastCount(xItem.text, "notes") + projData.setInitCounts(notes=xItem.text) elif xItem.tag == "status": self._parseStatusImport(xItem, projData.itemStatus) elif xItem.tag in ("import", "importance"): @@ -289,18 +287,7 @@ class ProjectXMLReader: else: # Pre 1.4 format projData.setTitleFormat(self._parseDictTagText(xItem)) else: - if self._version < 0x0104: - # Convert some deprecated fields - if xItem.tag == "lastEdited": # Discontinued in 1.4 - projData.setLastHandle(xItem.text, "editor") - elif xItem.tag == "lastViewed": # Discontinued in 1.4 - projData.setLastHandle(xItem.text, "viewer") - elif xItem.tag == "lastWordCount": # Renamed in 1.4 - projData.setLastCount(xItem.text, "total") - else: - logger.warning("Ignored in xml", xItem.tag) - else: - logger.warning("Ignored in xml", xItem.tag) + logger.warning("Ignored in xml", xItem.tag) return True @@ -495,9 +482,8 @@ class ProjectXMLWriter: self._packSingleValue(xSettings, "language", projData.language) self._packSingleValue(xSettings, "spellCheck", projData.spellCheck) self._packSingleValue(xSettings, "spellLang", projData.spellLang) - self._packSingleValue(xSettings, "totalWordCount", projData.getCurrCount("total")) - self._packSingleValue(xSettings, "novelWordCount", projData.getCurrCount("novel")) - self._packSingleValue(xSettings, "notesWordCount", projData.getCurrCount("notes")) + 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) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 9f776210..f984d5d2 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1559,13 +1559,13 @@ class GuiMain(QMainWindow): self.theProject.updateWordCounts() if self.mainConf.incNotesWCount: - currWords = self.theProject.data.getCurrCount("total") - diffWords = currWords - self.theProject.data.getLastCount("total") + iTotal = sum(self.theProject.data.initCounts) + cTotal = sum(self.theProject.data.currCounts) + self.mainStatus.setProjectStats(cTotal, cTotal - iTotal) else: - currWords = self.theProject.data.getCurrCount("novel") - diffWords = currWords - self.theProject.data.getLastCount("novel") - - self.mainStatus.setProjectStats(currWords, diffWords) + iNovel, _ = self.theProject.data.initCounts + cNovel, _ = self.theProject.data.currCounts + self.mainStatus.setProjectStats(cNovel, cNovel - iNovel) return diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index d6eeee62..1dba26ca 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,20 +1,19 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1407 + 1409 236 - 69424 + 69427 False en_GB True None - 1363 954 409 diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index ca7b5891..76509948 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -14,7 +14,6 @@ None False None - 0 0 0 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 404b0128..afd7627c 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -14,7 +14,6 @@ None False None - 0 0 0 diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 646317f3..b1370820 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,7 +13,6 @@ None False None - 13 10 3 diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 15907d72..d43aee14 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project None @@ -12,7 +12,6 @@ None False None - 0 0 0 diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 6aff4336..ed50e9ca 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,7 +13,6 @@ None False None - 9 9 0 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index e5c1a1ac..853ea1d9 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,19 +1,18 @@ - + New Project New Novel Jane Doe 4 2 - 4 + 3 True None True None - 163 136 27 diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 9dfb7b59..8108fba8 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,7 +13,6 @@ None False None - 9 9 0 diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 66fa4082..710d8160 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -1003,8 +1003,8 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): assert theProject.tree.handles() == oldOrder # Session stats - theProject.data.setCurrCount(200, "total") - theProject.data.setLastCount(100, "total") + theProject._data._initCounts = [50, 50] + theProject._data._currCounts = [100, 100] with monkeypatch.context() as mp: mp.setattr("os.path.isdir", lambda *a, **k: False) assert not theProject._appendSessionStats(idleTime=0) @@ -1019,8 +1019,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd): statsFile = os.path.join(theProject.projMeta, nwFiles.SESS_STATS) theProject._projOpened = 1600002000 - theProject._data.setCurrCount(200, "novel") - theProject._data.setCurrCount(100, "notes") + theProject._data._currCounts = [200, 100] with monkeypatch.context() as mp: mp.setattr("novelwriter.core.project.time", lambda: 1600005600) From f41db978601fa1a0b65b4ca078cff2710bad7b48 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:36:22 +0100 Subject: [PATCH 15/18] Clean up the content loading --- novelwriter/core/item.py | 17 +++-- novelwriter/core/project.py | 16 ++-- novelwriter/core/projectxml.py | 109 ++++++++++++++-------------- novelwriter/core/status.py | 19 ++--- tests/test_base/test_base_common.py | 40 ++++++---- 5 files changed, 101 insertions(+), 100 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index eae4813a..5b7deabe 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -193,21 +193,22 @@ class NWItem: logger.error("XML item entry 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.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.setName(data.get("label", "")) - self.setStatus(data.get("status", None)) - self.setImport(data.get("import", None)) self.setActive(data.get("active", True)) # Make some checks to ensure consistency @@ -216,10 +217,12 @@ class NWItem: self._parent = None # Root items cannot have a parent if self._type != nwItemType.FILE: - self._charCount = 0 # Only set for files - self._wordCount = 0 # Only set for files - self._paraCount = 0 # Only set for files - self._cursorPos = 0 # Only set for files + self._heading = "H0" # Only files have headers + self._active = False # Can only be True for files + self._charCount = 0 # Only set for files + self._wordCount = 0 # Only set for files + self._paraCount = 0 # Only set for files + self._cursorPos = 0 # Only set for files return True diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 9b1718c7..4f192955 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -455,13 +455,13 @@ class NWProject(QObject): # ========================= self._data = NWProjectData(self) - xmlReader = ProjectXMLReader(fileName) - xmlParsed = xmlReader.read(self._data) + projContent = [] + + xmlReader = ProjectXMLReader(fileName) + xmlParsed = xmlReader.read(self._data, projContent) - nwxRoot = xmlReader.xmlRoot appVersion = xmlReader.appVersion or self.tr("Unknown") hexVersion = xmlReader.hexVersion or "0x0" - xmlVersion = xmlReader.xmlVersion or self.tr("Unknown") if not xmlParsed: if xmlReader.state == XMLReadState.NOT_NWX_FILE: @@ -482,9 +482,6 @@ class NWProject(QObject): self.clearProject() return False - logger.debug("XML root is '%s'", nwxRoot) - logger.debug("File version is '%s'", xmlVersion) - # Check Legacy Upgrade # ==================== @@ -522,10 +519,7 @@ class NWProject(QObject): # Extract Data # ============ - logger.info("Project Name: '%s'", self._data.name) - logger.info("Project Title: '%s'", self._data.title) - - self._projTree.unpack(xmlReader.content) + self._projTree.unpack(projContent) self._optState.loadSettings() # Sort out old file locations diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 66dfb294..69ed973a 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -30,6 +30,7 @@ import novelwriter from enum import Enum from lxml import etree +from time import time from novelwriter.common import ( checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString @@ -94,10 +95,6 @@ class ProjectXMLReader: self._path = path self._state = XMLReadState.NO_ACTION - self._content = [] - self._statusData = {} - self._statusMap = {} - self._root = "" self._version = 0x0000 self._appVersion = "" @@ -110,12 +107,6 @@ class ProjectXMLReader: # Properties ## - @property - def content(self): - """The project content section, a dictionary of project items. - """ - return self._content - @property def state(self): """The state of the parsing as an XMLReadState enum value. @@ -156,10 +147,11 @@ class ProjectXMLReader: # Methods ## - def read(self, projData): + def read(self, projData, projContent): """Read and parse the project XML file. """ - self._content = [] + tStart = time() + logger.debug("Reading project XML") try: xml = etree.parse(self._path) @@ -196,6 +188,8 @@ class ProjectXMLReader: self._state = XMLReadState.UNKNOWN_VERSION return False + logger.debug("XML is '%s' version '%s'", self._root, fileVersion) + self._appVersion = str(xRoot.attrib.get("appVersion", "")) self._hexVersion = str(xRoot.attrib.get("appVersion", "")) self._timeStamp = str(xRoot.attrib.get("timeStamp", "")) @@ -208,10 +202,9 @@ class ProjectXMLReader: status &= self._parseProjectSettings(xSection, projData) elif xSection.tag == "content": if self._version >= 0x0104: - status &= self._parseProjectContent(xSection) + status &= self._parseProjectContent(xSection, projContent) else: - self._genLegacyImportStatysMap(projData) - status &= self._parseProjectContentLegacy(xSection) + status &= self._parseProjectContentLegacy(xSection, projContent, projData) else: logger.warning("Ignored in xml", xSection.tag) @@ -224,6 +217,8 @@ class ProjectXMLReader: else: self._state = XMLReadState.WAS_LEGACY + logger.debug("Project XML loaded in %.3f ms", (time() - tStart)*1000) + return True ## @@ -233,7 +228,7 @@ class ProjectXMLReader: def _parseProjectMeta(self, xSection, projData): """Parse the project section of the XML file. """ - logger.debug("Parsing xml ") + logger.debug("Parsing section") for xItem in xSection: if xItem.tag == "name": projData.setName(xItem.text) @@ -255,7 +250,7 @@ class ProjectXMLReader: def _parseProjectSettings(self, xSection, projData): """Parse the settings section of the XML file. """ - logger.debug("Parsing xml ") + logger.debug("Parsing section") for xItem in xSection: if xItem.tag == "doBackup": @@ -291,53 +286,56 @@ class ProjectXMLReader: return True - def _parseProjectContent(self, xSection): + def _parseProjectContent(self, xSection, projContent): """Parse the content section of the XML file. """ - logger.debug("Parsing xml ") + logger.debug("Parsing section") for xItem in xSection: if xItem.tag == "item": item = {} - item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None) - item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None) - item["root"] = checkStringNone(xItem.attrib.get("root", None), None) - item["order"] = checkInt(xItem.attrib.get("order", 0), 0) - item["type"] = checkString(xItem.attrib.get("type", "NO_TYPE"), "NO_TYPE") - item["class"] = checkString(xItem.attrib.get("class", "NO_CLASS"), "NO_CLASS") - item["layout"] = checkString(xItem.attrib.get("layout", "NO_LAYOUT"), "NO_LAYOUT") + 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": - item["expanded"] = checkBool(xVal.attrib.get("expanded", False), False) - item["heading"] = checkString(xVal.attrib.get("heading", "H0"), "H0") - item["charCount"] = checkInt(xVal.attrib.get("charCount", 0), 0) - item["wordCount"] = checkInt(xVal.attrib.get("wordCount", 0), 0) - item["paraCount"] = checkInt(xVal.attrib.get("paraCount", 0), 0) - item["cursorPos"] = checkInt(xVal.attrib.get("cursorPos", 0), 0) + 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) elif xVal.tag == "name": item["label"] = simplified(checkString(xVal.text, "")) - item["status"] = checkStringNone(xVal.attrib.get("status", None), None) - item["import"] = checkStringNone(xVal.attrib.get("import", None), None) - item["active"] = checkBool(xVal.attrib.get("active", False), False) + item["status"] = checkStringNone(xVal.attrib.get("status"), None) + item["import"] = checkStringNone(xVal.attrib.get("import"), None) + item["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), False) + item["active"] = checkBool(xVal.attrib.get("exported"), False) else: logger.warning("Ignored in xml", xVal.tag) - self._content.append(item) + projContent.append(item) else: logger.warning("Ignored item in xml", xItem.tag) return True - def _parseProjectContentLegacy(self, xSection): + def _parseProjectContentLegacy(self, xSection, projContent, projData): """Parse the content section of the XML file for older versions. """ - logger.debug("Parsing xml (legacy format)") - depLayout = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") + 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()} for xItem in xSection: item = {} @@ -377,19 +375,21 @@ class ProjectXMLReader: # Status was split into separate status/import with a key in 1.4 if item.get("class", "") in ("NOVEL", "ARCHIVE"): - item["status"] = self._statusMap.get(tmpStatus, None) + item["status"] = statusMap.get(tmpStatus, None) else: - item["import"] = self._importMap.get(tmpStatus, None) + item["import"] = importMap.get(tmpStatus, None) # A number of layouts were removed in 1.3 - if item.get("layout", "") in depLayout: + 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" - self._content.append(item) + projContent.append(item) else: logger.warning("Ignored in xml", xItem.tag) @@ -409,13 +409,6 @@ class ProjectXMLReader: sObject.write(key, xEntry.text, (red, green, blue), count) return - def _genLegacyImportStatysMap(self, projData): - """Generate a map of legacy import/status values. - """ - self._statusMap = {entry["name"]: key for key, entry in projData.itemStatus.items()} - self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()} - return - def _parseDictKeyText(self, xItem): """Parse a dictionary stored with key as an attribute and the value as the text porperty. @@ -459,8 +452,10 @@ class ProjectXMLWriter: def write(self, projData, projContent, saveTime, editTime): """Write the project data and content to the XML files. """ + tStart = time() + logger.debug("Writing project XML") - nwXML = etree.Element("novelWriterXML", attrib={ + xRoot = etree.Element("novelWriterXML", attrib={ "appVersion": str(novelwriter.__version__), "hexVersion": str(novelwriter.__hexversion__), "fileVersion": FILE_VERSION, @@ -468,7 +463,7 @@ class ProjectXMLWriter: }) # Save Project Meta - xProject = etree.SubElement(nwXML, "project") + xProject = etree.SubElement(xRoot, "project") self._packSingleValue(xProject, "name", projData.name) self._packSingleValue(xProject, "title", projData.title) self._packListValue(xProject, "author", projData.authors) @@ -477,7 +472,7 @@ class ProjectXMLWriter: self._packSingleValue(xProject, "editTime", editTime) # Save Project Settings - xSettings = etree.SubElement(nwXML, "settings") + xSettings = etree.SubElement(xRoot, "settings") self._packSingleValue(xSettings, "doBackup", projData.doBackup) self._packSingleValue(xSettings, "language", projData.language) self._packSingleValue(xSettings, "spellCheck", projData.spellCheck) @@ -498,7 +493,7 @@ class ProjectXMLWriter: self._packSingleValue(xImport, "entry", label, attrib=attrib) # Save Tree Content - xContent = etree.SubElement(nwXML, "content", attrib={"count": str(len(projContent))}) + xContent = etree.SubElement(xRoot, "content", attrib={"count": str(len(projContent))}) for item in projContent: xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {})) etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {})) @@ -512,7 +507,7 @@ class ProjectXMLWriter: try: with open(tempFile, mode="wb") as outFile: outFile.write(etree.tostring( - nwXML, + xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True @@ -531,6 +526,8 @@ class ProjectXMLWriter: self._error = exc return False + logger.debug("Project XML saved in %.3f ms", (time() - tStart)*1000) + return True ## diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index b02ed2ea..fea6397d 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -205,18 +205,15 @@ class NWStatus: def pack(self): """Pack the status entries into a dictionary. """ - result = [] for key, data in self._store.items(): - result. append(( - data["name"], { - "key": key, - "count": str(data["count"]), - "red": str(data["cols"][0]), - "green": str(data["cols"][1]), - "blue": str(data["cols"][2]), - } - )) - return result + yield (data["name"], { + "key": key, + "count": str(data["count"]), + "red": str(data["cols"][0]), + "green": str(data["cols"][1]), + "blue": str(data["cols"][2]), + }) + return def unpack(self, data): """Unpack a data dictionary and set the class values. diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 23b487e7..bb05265f 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -54,53 +54,63 @@ def testBaseCommon_CheckStringNone(): @pytest.mark.base def testBaseCommon_CheckString(): - """Test the checkString function. + """Test the checkString function. Anything that is a string should + be returned, otherwise it returns the default. """ - assert checkString("None", "NotNone") == "None" - assert checkString(None, "NotNone") == "NotNone" - assert checkString(1, "NotNone") == "NotNone" - assert checkString(1.0, "NotNone") == "NotNone" - assert checkString(True, "NotNone") == "NotNone" + assert checkString("None", "default") == "None" + assert checkString("Text", "default") == "Text" + assert checkString(None, "default") == "default" + assert checkString(1, "default") == "default" + assert checkString(1.0, "default") == "default" + assert checkString(True, "default") == "default" # END Test testBaseCommon_CheckString @pytest.mark.base def testBaseCommon_CheckInt(): - """Test the checkInt function. + """Test the checkInt function. Anything that can be converted to an + integer should be returned, otherwise it returns the default. """ - assert checkInt(None, 3) == 3 - assert checkInt("1", 3) == 1 - assert checkInt("1.0", 3) == 3 assert checkInt(1, 3) == 1 assert checkInt(1.0, 3) == 1 assert checkInt(True, 3) == 1 + assert checkInt(False, 3) == 0 + assert checkInt(None, 3) == 3 + assert checkInt("1", 3) == 1 + assert checkInt("1.0", 3) == 3 # END Test testBaseCommon_CheckInt @pytest.mark.base def testBaseCommon_CheckFloat(): - """Test the checkFloat function. + """Test the checkFloat function. Anything that can be converted to an + integer should be returned, otherwise it returns the default. """ - assert checkFloat(None, 3.0) == 3.0 - assert checkFloat("1", 3.0) == 1.0 - assert checkFloat("1.0", 3.0) == 1.0 assert checkFloat(1, 3.0) == 1.0 assert checkFloat(1.0, 3.0) == 1.0 assert checkFloat(True, 3.0) == 1.0 + assert checkFloat(False, 3.0) == 0.0 + assert checkFloat(None, 3.0) == 3.0 + assert checkFloat("1", 3.0) == 1.0 + assert checkFloat("1.0", 3.0) == 1.0 # END Test testBaseCommon_CheckInt @pytest.mark.base def testBaseCommon_CheckBool(): - """Test the checkBool function. + """Test the checkBool function. Any bool, string version of Python + bool, or integer 1 or 0, are returned as bool. Otherwise, the + default is returned. """ 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(0, True) is False assert checkBool(1, False) is True assert checkBool(2, True) is True From 5b7f2b2b555857f1d0dab08bf920f308519c5227 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 1 Nov 2022 17:14:57 +0100 Subject: [PATCH 16/18] Add test coverage of the project xml class --- novelwriter/core/project.py | 2 + novelwriter/core/projectxml.py | 29 +- tests/files/nwProject-1.0.nwx | 2 +- tests/files/nwProject-1.1.nwx | 8 +- tests/files/nwProject-1.2.nwx | 12 +- tests/files/nwProject-1.3.nwx | 10 +- tests/files/nwProject-1.4.nwx | 163 ++++ tests/reference/projectXML_ReadCurrent.json | 515 ++++++++++++ tests/reference/projectXML_ReadLegacy10.json | 362 +++++++++ tests/reference/projectXML_ReadLegacy10.nwx | 143 ++++ tests/reference/projectXML_ReadLegacy11.json | 346 ++++++++ tests/reference/projectXML_ReadLegacy11.nwx | 143 ++++ tests/reference/projectXML_ReadLegacy12.json | 387 +++++++++ tests/reference/projectXML_ReadLegacy12.nwx | 155 ++++ tests/reference/projectXML_ReadLegacy13.json | 387 +++++++++ tests/reference/projectXML_ReadLegacy13.nwx | 155 ++++ tests/test_core/test_core_projectxml.py | 798 +++++++++++++++++++ 17 files changed, 3584 insertions(+), 33 deletions(-) create mode 100644 tests/files/nwProject-1.4.nwx create mode 100644 tests/reference/projectXML_ReadCurrent.json create mode 100644 tests/reference/projectXML_ReadLegacy10.json create mode 100644 tests/reference/projectXML_ReadLegacy10.nwx create mode 100644 tests/reference/projectXML_ReadLegacy11.json create mode 100644 tests/reference/projectXML_ReadLegacy11.nwx create mode 100644 tests/reference/projectXML_ReadLegacy12.json create mode 100644 tests/reference/projectXML_ReadLegacy12.nwx create mode 100644 tests/reference/projectXML_ReadLegacy13.json create mode 100644 tests/reference/projectXML_ReadLegacy13.nwx create mode 100644 tests/test_core/test_core_projectxml.py diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 4f192955..cf48c23d 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -1576,8 +1576,10 @@ class NWProjectData: """ 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): diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 69ed973a..dc08573a 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -58,9 +58,8 @@ class XMLReadState(Enum): CANNOT_PARSE = 3 NOT_NWX_FILE = 4 UNKNOWN_VERSION = 5 - PARSING_ERROR = 6 - PARSED_OK = 7 - WAS_LEGACY = 8 + PARSED_OK = 6 + WAS_LEGACY = 7 # END Class XMLReadState @@ -173,6 +172,7 @@ class ProjectXMLReader: self._state = XMLReadState.CANNOT_PARSE return False else: + self._state = XMLReadState.CANNOT_PARSE return False xRoot = xml.getroot() @@ -191,27 +191,22 @@ 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("appVersion", "")) + self._hexVersion = str(xRoot.attrib.get("hexVersion", "")) self._timeStamp = str(xRoot.attrib.get("timeStamp", "")) - status = True for xSection in xRoot: if xSection.tag == "project": - status &= self._parseProjectMeta(xSection, projData) + self._parseProjectMeta(xSection, projData) elif xSection.tag == "settings": - status &= self._parseProjectSettings(xSection, projData) + self._parseProjectSettings(xSection, projData) elif xSection.tag == "content": if self._version >= 0x0104: - status &= self._parseProjectContent(xSection, projContent) + self._parseProjectContent(xSection, projContent) else: - status &= self._parseProjectContentLegacy(xSection, projContent, projData) + self._parseProjectContentLegacy(xSection, projContent, projData) else: logger.warning("Ignored in xml", xSection.tag) - if not status: - self._state = XMLReadState.PARSING_ERROR - return False - if self._version == 0x0104: self._state = XMLReadState.PARSED_OK else: @@ -245,7 +240,7 @@ class ProjectXMLReader: else: logger.warning("Ignored in xml", xItem.tag) - return True + return def _parseProjectSettings(self, xSection, projData): """Parse the settings section of the XML file. @@ -284,7 +279,7 @@ class ProjectXMLReader: else: logger.warning("Ignored in xml", xItem.tag) - return True + return def _parseProjectContent(self, xSection, projContent): """Parse the content section of the XML file. @@ -326,7 +321,7 @@ class ProjectXMLReader: else: logger.warning("Ignored item in xml", xItem.tag) - return True + return def _parseProjectContentLegacy(self, xSection, projContent, projData): """Parse the content section of the XML file for older versions. @@ -394,7 +389,7 @@ class ProjectXMLReader: else: logger.warning("Ignored in xml", xItem.tag) - return True + return def _parseStatusImport(self, xItem, sObject): """Parse a status or importance entry. diff --git a/tests/files/nwProject-1.0.nwx b/tests/files/nwProject-1.0.nwx index 80465ec6..6d1b1990 100644 --- a/tests/files/nwProject-1.0.nwx +++ b/tests/files/nwProject-1.0.nwx @@ -11,7 +11,7 @@ True True 636b6aa9b697b - ba8a28a246524 + 636b6aa9b697b 914 B diff --git a/tests/files/nwProject-1.1.nwx b/tests/files/nwProject-1.1.nwx index 9e244c8b..d31ab3ff 100644 --- a/tests/files/nwProject-1.1.nwx +++ b/tests/files/nwProject-1.1.nwx @@ -5,12 +5,12 @@ Sample Project Jane Smith Jay Doh - 408 - 71 - 15120 + 5 + 10 + 1000 - False + True True True 636b6aa9b697b diff --git a/tests/files/nwProject-1.2.nwx b/tests/files/nwProject-1.2.nwx index af9892e8..e511afc9 100644 --- a/tests/files/nwProject-1.2.nwx +++ b/tests/files/nwProject-1.2.nwx @@ -5,15 +5,15 @@ Sample Project Jane Smith Jay Doh - 1122 - 191 - 53749 + 5 + 10 + 1000 - False - en + True + en_GB True - None + en_GB True 636b6aa9b697b 636b6aa9b697b diff --git a/tests/files/nwProject-1.3.nwx b/tests/files/nwProject-1.3.nwx index 74b42a6c..6c1d3a2e 100644 --- a/tests/files/nwProject-1.3.nwx +++ b/tests/files/nwProject-1.3.nwx @@ -5,15 +5,15 @@ Sample Project Jane Smith Jay Doh - 1312 - 199 - 65207 + 5 + 10 + 1000 - False + True en_GB True - None + en_GB True 636b6aa9b697b 636b6aa9b697b diff --git a/tests/files/nwProject-1.4.nwx b/tests/files/nwProject-1.4.nwx new file mode 100644 index 00000000..706b2266 --- /dev/null +++ b/tests/files/nwProject-1.4.nwx @@ -0,0 +1,163 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + 5 + 10 + 1000 + + + True + en_GB + True + en_GB + 954 + 409 + + 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/reference/projectXML_ReadCurrent.json b/tests/reference/projectXML_ReadCurrent.json new file mode 100644 index 00000000..141c3c3a --- /dev/null +++ b/tests/reference/projectXML_ReadCurrent.json @@ -0,0 +1,515 @@ +[ + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + }, + { + "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 + } +] \ No newline at end of file diff --git a/tests/reference/projectXML_ReadLegacy10.json b/tests/reference/projectXML_ReadLegacy10.json new file mode 100644 index 00000000..27e9da17 --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy10.json @@ -0,0 +1,362 @@ +[ + { + "handle": "7031beac91f75", + "parent": null, + "root": null, + "order": 0, + "heading": "H0", + "label": "Novel", + "type": "ROOT", + "class": "NOVEL", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "e7ded148d6e4a", + "parent": "7031beac91f75", + "root": null, + "order": 3, + "heading": "H0", + "label": "A Folder", + "type": "FOLDER", + "class": "NOVEL", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "f6622b4617424", + "parent": null, + "root": null, + "order": 1, + "heading": "H0", + "label": "Characters", + "type": "ROOT", + "class": "CHARACTER", + "expanded": true, + "import": null + }, + { + "handle": "f7e2d9f330615", + "parent": "f6622b4617424", + "root": null, + "order": 0, + "heading": "H0", + "label": "Main Characters", + "type": "FOLDER", + "class": "CHARACTER", + "expanded": true, + "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" + }, + { + "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" + }, + { + "handle": "15c4492bd5107", + "parent": null, + "root": null, + "order": 2, + "heading": "H0", + "label": "Locations", + "type": "ROOT", + "class": "WORLD", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "98acd8c76c93a", + "parent": null, + "root": null, + "order": 3, + "heading": "H0", + "label": "Trash", + "type": "ROOT", + "class": "TRASH", + "expanded": true, + "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" + } +] \ No newline at end of file diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx new file mode 100644 index 00000000..8e7f0a99 --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy10.nwx @@ -0,0 +1,143 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + 0 + 0 + 1000 + + + True + None + True + None + 0 + 0 + + None + None + None + None + + + B + E + D + + + %title% + Chapter %ch%: %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 + + + + A Folder + + + + Chapter One + + + + Making a Scene + + + + Another Scene + + + + Interlude + + + + A Note on Structure + + + + Chapter Two + + + + We Found John! + + + + Characters + + + + Main Characters + + + + John Smith + + + + Jane Smith + + + + Locations + + + + Earth + + + + Space + + + + Mars + + + + Trash + + + + Delete Me! + + + diff --git a/tests/reference/projectXML_ReadLegacy11.json b/tests/reference/projectXML_ReadLegacy11.json new file mode 100644 index 00000000..16e84adc --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy11.json @@ -0,0 +1,346 @@ +[ + { + "handle": "7031beac91f75", + "parent": null, + "root": null, + "order": 0, + "heading": "H0", + "label": "Novel", + "type": "ROOT", + "class": "NOVEL", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "e7ded148d6e4a", + "parent": "7031beac91f75", + "root": null, + "order": 3, + "heading": "H0", + "label": "A Folder", + "type": "FOLDER", + "class": "NOVEL", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "f6622b4617424", + "parent": null, + "root": null, + "order": 1, + "heading": "H0", + "label": "Characters", + "type": "ROOT", + "class": "CHARACTER", + "expanded": true, + "import": null + }, + { + "handle": "f7e2d9f330615", + "parent": "f6622b4617424", + "root": null, + "order": 0, + "heading": "H0", + "label": "Main Characters", + "type": "FOLDER", + "class": "CHARACTER", + "expanded": true, + "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" + }, + { + "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" + }, + { + "handle": "15c4492bd5107", + "parent": null, + "root": null, + "order": 2, + "heading": "H0", + "label": "Locations", + "type": "ROOT", + "class": "WORLD", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "98acd8c76c93a", + "parent": null, + "root": null, + "order": 3, + "heading": "H0", + "label": "Trash", + "type": "ROOT", + "class": "TRASH", + "expanded": true, + "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" + } +] \ No newline at end of file diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx new file mode 100644 index 00000000..fce991d3 --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy11.nwx @@ -0,0 +1,143 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + 5 + 10 + 1000 + + + True + None + True + None + 0 + 0 + + None + None + None + None + + + B + E + D + + + %title% + Chapter %ch%: %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 + + + + A Folder + + + + Chapter One + + + + Making a Scene + + + + Another Scene + + + + Interlude + + + + A Note on Structure + + + + Chapter Two + + + + We Found John! + + + + Characters + + + + Main Characters + + + + John Smith + + + + Jane Smith + + + + Locations + + + + Earth + + + + Space + + + + Mars + + + + Trash + + + + Delete Me! + + + diff --git a/tests/reference/projectXML_ReadLegacy12.json b/tests/reference/projectXML_ReadLegacy12.json new file mode 100644 index 00000000..917cb633 --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy12.json @@ -0,0 +1,387 @@ +[ + { + "handle": "7031beac91f75", + "parent": null, + "root": null, + "order": 0, + "heading": "H0", + "label": "Novel", + "type": "ROOT", + "class": "NOVEL", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "e7ded148d6e4a", + "parent": "7031beac91f75", + "root": null, + "order": 3, + "heading": "H0", + "label": "A Folder", + "type": "FOLDER", + "class": "NOVEL", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "f6622b4617424", + "parent": null, + "root": null, + "order": 1, + "heading": "H0", + "label": "Characters", + "type": "ROOT", + "class": "CHARACTER", + "expanded": true, + "import": null + }, + { + "handle": "f7e2d9f330615", + "parent": "f6622b4617424", + "root": null, + "order": 0, + "heading": "H0", + "label": "Main Characters", + "type": "FOLDER", + "class": "CHARACTER", + "expanded": true, + "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" + }, + { + "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" + }, + { + "handle": "15c4492bd5107", + "parent": null, + "root": null, + "order": 2, + "heading": "H0", + "label": "Locations", + "type": "ROOT", + "class": "WORLD", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "6827118336ac1", + "parent": null, + "root": null, + "order": 3, + "heading": "H0", + "label": "Outtakes", + "type": "ROOT", + "class": "ARCHIVE", + "expanded": true, + "status": null + }, + { + "handle": "ae9bf3c3ea159", + "parent": "6827118336ac1", + "root": null, + "order": 0, + "heading": "H0", + "label": "Scenes", + "type": "FOLDER", + "class": "ARCHIVE", + "expanded": true, + "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" + }, + { + "handle": "98acd8c76c93a", + "parent": null, + "root": null, + "order": 4, + "heading": "H0", + "label": "Trash", + "type": "ROOT", + "class": "TRASH", + "expanded": true, + "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" + } +] \ No newline at end of file diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx new file mode 100644 index 00000000..9de6966b --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy12.nwx @@ -0,0 +1,155 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + 5 + 10 + 1000 + + + True + en_GB + True + en_GB + 840 + 376 + + 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 + + + + A Folder + + + + Chapter One + + + + Making a Scene + + + + Another Scene + + + + Interlude + + + + A Note on Structure + + + + Chapter Two + + + + We Found John! + + + + Characters + + + + Main Characters + + + + John Smith + + + + Jane Smith + + + + Locations + + + + Earth + + + + Space + + + + Mars + + + + Outtakes + + + + Scenes + + + + Old File + + + + Trash + + + + Delete Me! + + + diff --git a/tests/reference/projectXML_ReadLegacy13.json b/tests/reference/projectXML_ReadLegacy13.json new file mode 100644 index 00000000..55508758 --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy13.json @@ -0,0 +1,387 @@ +[ + { + "handle": "7031beac91f75", + "parent": null, + "root": null, + "order": 0, + "heading": "H0", + "label": "Novel", + "type": "ROOT", + "class": "NOVEL", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "e7ded148d6e4a", + "parent": "7031beac91f75", + "root": null, + "order": 3, + "heading": "H0", + "label": "A Folder", + "type": "FOLDER", + "class": "NOVEL", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "f6622b4617424", + "parent": null, + "root": null, + "order": 1, + "heading": "H0", + "label": "Characters", + "type": "ROOT", + "class": "CHARACTER", + "expanded": true, + "import": null + }, + { + "handle": "f7e2d9f330615", + "parent": "f6622b4617424", + "root": null, + "order": 0, + "heading": "H0", + "label": "Main Characters", + "type": "FOLDER", + "class": "CHARACTER", + "expanded": true, + "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" + }, + { + "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" + }, + { + "handle": "15c4492bd5107", + "parent": null, + "root": null, + "order": 2, + "heading": "H0", + "label": "Locations", + "type": "ROOT", + "class": "WORLD", + "expanded": true, + "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" + }, + { + "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" + }, + { + "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" + }, + { + "handle": "6827118336ac1", + "parent": null, + "root": null, + "order": 3, + "heading": "H0", + "label": "Archive", + "type": "ROOT", + "class": "ARCHIVE", + "expanded": true, + "status": "s000000" + }, + { + "handle": "ae9bf3c3ea159", + "parent": "6827118336ac1", + "root": null, + "order": 0, + "heading": "H0", + "label": "Scenes", + "type": "FOLDER", + "class": "ARCHIVE", + "expanded": true, + "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" + }, + { + "handle": "98acd8c76c93a", + "parent": null, + "root": null, + "order": 4, + "heading": "H0", + "label": "Trash", + "type": "ROOT", + "class": "TRASH", + "expanded": true, + "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" + } +] \ No newline at end of file diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx new file mode 100644 index 00000000..7fc4cfea --- /dev/null +++ b/tests/reference/projectXML_ReadLegacy13.nwx @@ -0,0 +1,155 @@ + + + + Sample Project + Sample Project + Jane Smith + Jay Doh + 5 + 10 + 1000 + + + True + en_GB + True + en_GB + 830 + 376 + + 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 + + + + A Folder + + + + Chapter One + + + + Making a Scene + + + + Another Scene + + + + Interlude + + + + A Note on Structure + + + + Chapter Two + + + + We Found John! + + + + 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 new file mode 100644 index 00000000..6ad1174c --- /dev/null +++ b/tests/test_core/test_core_projectxml.py @@ -0,0 +1,798 @@ +""" +novelWriter – ProjectXMLReader/Writer 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 . +""" + +import json +import os +import pytest +import shutil + +from datetime import datetime + +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 + + +class MockProject: + def setProjectChanged(self, *a): + pass + + +@pytest.mark.core +def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir): + """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") + + xmlReader = ProjectXMLReader(xmlFile) + assert xmlReader.state == XMLReadState.NO_ACTION + + data = NWProjectData(MockProject()) + content = [] + + # With no valid files, the read should fail + writeFile(xmlFile, "") + assert xmlReader.read(data, content) is False + assert xmlReader.state == XMLReadState.CANNOT_PARSE + + # Also add an invalid backup file + writeFile(bakFile, "") + assert xmlReader.read(data, content) is False + assert xmlReader.state == XMLReadState.CANNOT_PARSE + + # Add a valid backup file, that is not novelWriter + writeFile(bakFile, "") + assert xmlReader.read(data, content) is False + assert xmlReader.state == XMLReadState.NOT_NWX_FILE + + # Add a valid project file, that is not novelWriter + writeFile(xmlFile, "") + assert xmlReader.read(data, content) is False + assert xmlReader.state == XMLReadState.NOT_NWX_FILE + + # Add a valid novelwriter file without a file version + writeFile(xmlFile, "") + assert xmlReader.read(data, content) is False + assert xmlReader.state == XMLReadState.UNKNOWN_VERSION + + # Check parsing of unkown sections + writeFile(xmlFile, ( + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "" + )) + assert xmlReader.read(data, content) is True + assert xmlReader.state == XMLReadState.PARSED_OK + + writeFile(xmlFile, ( + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "" + )) + assert xmlReader.read(data, content) is True + assert xmlReader.state == XMLReadState.WAS_LEGACY + + # Reset data objects + data = NWProjectData(MockProject()) + content = [] + + # Parse a valid, complete file + shutil.copy(refFile, xmlFile) + assert xmlReader.read(data, content) is True + assert xmlReader.state == XMLReadState.PARSED_OK + 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") == "636b6aa9b697b" + assert data.getLastHandle("viewer") == "636b6aa9b697b" + assert data.getLastHandle("novelTree") == "7031beac91f75" + assert data.getLastHandle("outline") == "7031beac91f75" + + 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 = os.path.join(outDir, "projectXML_ReadCurrent.json") + compFile = os.path.join(refDir, "projectXML_ReadCurrent.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) + for entry in content: + item = NWItem(mockProject) + item.unpack(entry) + packedContent.append(item.pack()) + + # Save the project again, which should produce an identical project xml + timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) + xmlWriter = ProjectXMLWriter(fncDir) + + # Fail saving + with monkeypatch.context() as mp: + mp.setattr("builtins.open", 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) + assert xmlWriter.write(data, packedContent, timeStamp, 1000) is False + assert str(xmlWriter.error) == "Mock OSError" + + # 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) + +# END Test testCoreProjectXML_ReadCurrent + + +@pytest.mark.core +def testCoreProjectXML_ReadLegacy10(filesDir, fncDir, outDir, refDir, 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") + shutil.copy(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 == 0x0100 + assert xmlReader.appVersion == "0.6.1" + assert xmlReader.hexVersion == "0x000601f0" + + # Check loaded data + assert data.name == "Sample Project" + assert data.title == "Sample Project" + assert data.authors == ["Jane Smith", "Jay Doh"] + assert data.saveCount == 0 # Doesn't exist in 1.0 + assert data.autoCount == 0 # Doesn't exist in 1.0 + assert data.editTime == 0 # Doesn't exist in 1.0 + + assert data.doBackup is True + assert data.language is None # Doesn't exist in 1.0 + assert data.spellCheck is True + assert data.spellLang is None # Doesn't exist in 1.0 + assert data.initCounts == (0, 0) + assert data.currCounts == (0, 0) + + 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.0 + assert data.getLastHandle("outline") is None # Doesn't exist in 1.0 + + assert data.getTitleFormat("title") == "%title%" + assert data.getTitleFormat("chapter") == "Chapter %ch%: %title%" + assert data.getTitleFormat("unnumbered") == "%title%" + assert data.getTitleFormat("scene") == "Scene %ch%.%sc%: %title%" + assert data.getTitleFormat("section") == "" + + assert data.itemStatus.name("s000000") == "New" + assert data.itemStatus.name("s000001") == "Notes" + assert data.itemStatus.name("s000002") == "Started" + assert data.itemStatus.name("s000003") == "1st Draft" + assert data.itemStatus.name("s000004") == "2nd Draft" + assert data.itemStatus.name("s000005") == "3rd Draft" + assert data.itemStatus.name("s000006") == "Finished" + + assert data.itemImport.name("i000007") == "None" + assert data.itemImport.name("i000008") == "Minor" + assert data.itemImport.name("i000009") == "Major" + assert data.itemImport.name("i00000a") == "Main" + + assert data.itemStatus.cols("s000000") == (100, 100, 100) + assert data.itemStatus.cols("s000001") == (200, 50, 0) + assert data.itemStatus.cols("s000002") == (182, 60, 0) + assert data.itemStatus.cols("s000003") == (193, 129, 0) + assert data.itemStatus.cols("s000004") == (193, 129, 0) + assert data.itemStatus.cols("s000005") == (193, 129, 0) + assert data.itemStatus.cols("s000006") == (58, 180, 58) + + assert data.itemImport.cols("i000007") == (100, 100, 100) + assert data.itemImport.cols("i000008") == (0, 122, 188) + assert data.itemImport.cols("i000009") == (21, 0, 180) + assert data.itemImport.cols("i00000a") == (117, 0, 175) + + assert data.itemStatus.count("s000000") == 0 + assert data.itemStatus.count("s000001") == 0 + assert data.itemStatus.count("s000002") == 0 + assert data.itemStatus.count("s000003") == 0 + assert data.itemStatus.count("s000004") == 0 + assert data.itemStatus.count("s000005") == 0 + assert data.itemStatus.count("s000006") == 0 + + assert data.itemImport.count("i000007") == 0 + assert data.itemImport.count("i000008") == 0 + assert data.itemImport.count("i000009") == 0 + assert data.itemImport.count("i00000a") == 0 + + # Compare content + dumpFile = os.path.join(outDir, "projectXML_ReadLegacy10.json") + compFile = os.path.join(refDir, "projectXML_ReadLegacy10.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": "New", + "e7ded148d6e4a": "1st Draft", + "6a2d6d5f4f401": "Notes", + "636b6aa9b697b": "1st Draft", + "bc0cbd2a407f3": "1st Draft", + "ba8a28a246524": "Finished", + "96b68994dfa3d": "2nd Draft", + "88706ddc78b1b": "1st Draft", + "ae7339df26ded": "1st Draft", + "f6622b4617424": "None", + "f7e2d9f330615": "None", + "14298de4d9524": "Minor", + "bb2c23b3c42cc": "Major", + "15c4492bd5107": "None", + "b3e74dbc1f584": "Main", + "f1471bef9f2ae": "Minor", + "5eaea4e8cdee8": "Major", + "98acd8c76c93a": "None", + "b8136a5a774a0": "New", + } + + # Save the project again, which should produce an identical project xml + timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) + xmlWriter = ProjectXMLWriter(fncDir) + assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True + compFile = os.path.join(refDir, "projectXML_ReadLegacy10.nwx") + assert cmpFiles(outFile, compFile) + +# END Test testCoreProjectXML_ReadLegacy10 + + +@pytest.mark.core +def testCoreProjectXML_ReadLegacy11(filesDir, fncDir, outDir, refDir, 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") + shutil.copy(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 == 0x0101 + assert xmlReader.appVersion == "0.9.2" + assert xmlReader.hexVersion == "0x000902f0" + + # 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 is None # Doesn't exist in 1.1 + assert data.spellCheck is True + assert data.spellLang is None # Doesn't exist in 1.1 + assert data.initCounts == (0, 0) + assert data.currCounts == (0, 0) + + 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.1 + assert data.getLastHandle("outline") is None # Doesn't exist in 1.1 + + assert data.getTitleFormat("title") == "%title%" + assert data.getTitleFormat("chapter") == "Chapter %ch%: %title%" + assert data.getTitleFormat("unnumbered") == "%title%" + assert data.getTitleFormat("scene") == "Scene %ch%.%sc%: %title%" + assert data.getTitleFormat("section") == "" + + assert data.itemStatus.name("s000000") == "New" + assert data.itemStatus.name("s000001") == "Notes" + assert data.itemStatus.name("s000002") == "Started" + assert data.itemStatus.name("s000003") == "1st Draft" + assert data.itemStatus.name("s000004") == "2nd Draft" + assert data.itemStatus.name("s000005") == "3rd Draft" + assert data.itemStatus.name("s000006") == "Finished" + + assert data.itemImport.name("i000007") == "None" + assert data.itemImport.name("i000008") == "Minor" + assert data.itemImport.name("i000009") == "Major" + assert data.itemImport.name("i00000a") == "Main" + + assert data.itemStatus.cols("s000000") == (100, 100, 100) + assert data.itemStatus.cols("s000001") == (200, 50, 0) + assert data.itemStatus.cols("s000002") == (182, 60, 0) + assert data.itemStatus.cols("s000003") == (193, 129, 0) + assert data.itemStatus.cols("s000004") == (193, 129, 0) + assert data.itemStatus.cols("s000005") == (193, 129, 0) + assert data.itemStatus.cols("s000006") == (58, 180, 58) + + assert data.itemImport.cols("i000007") == (100, 100, 100) + assert data.itemImport.cols("i000008") == (0, 122, 188) + assert data.itemImport.cols("i000009") == (21, 0, 180) + assert data.itemImport.cols("i00000a") == (117, 0, 175) + + assert data.itemStatus.count("s000000") == 0 + assert data.itemStatus.count("s000001") == 0 + assert data.itemStatus.count("s000002") == 0 + assert data.itemStatus.count("s000003") == 0 + assert data.itemStatus.count("s000004") == 0 + assert data.itemStatus.count("s000005") == 0 + assert data.itemStatus.count("s000006") == 0 + + assert data.itemImport.count("i000007") == 0 + assert data.itemImport.count("i000008") == 0 + assert data.itemImport.count("i000009") == 0 + assert data.itemImport.count("i00000a") == 0 + + # Compare content + dumpFile = os.path.join(outDir, "projectXML_ReadLegacy11.json") + compFile = os.path.join(refDir, "projectXML_ReadLegacy11.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": "New", + "e7ded148d6e4a": "1st Draft", + "6a2d6d5f4f401": "Notes", + "636b6aa9b697b": "1st Draft", + "bc0cbd2a407f3": "1st Draft", + "ba8a28a246524": "Finished", + "96b68994dfa3d": "2nd Draft", + "88706ddc78b1b": "1st Draft", + "ae7339df26ded": "1st Draft", + "f6622b4617424": "None", + "f7e2d9f330615": "None", + "14298de4d9524": "Minor", + "bb2c23b3c42cc": "Major", + "15c4492bd5107": "None", + "b3e74dbc1f584": "Main", + "f1471bef9f2ae": "Minor", + "5eaea4e8cdee8": "Major", + "98acd8c76c93a": "None", + "b8136a5a774a0": "New", + } + + # Save the project again, which should produce an identical project xml + timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) + xmlWriter = ProjectXMLWriter(fncDir) + assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True + compFile = os.path.join(refDir, "projectXML_ReadLegacy11.nwx") + assert cmpFiles(outFile, compFile) + +# END Test testCoreProjectXML_ReadLegacy11 + + +@pytest.mark.core +def testCoreProjectXML_ReadLegacy12(filesDir, fncDir, outDir, refDir, 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") + shutil.copy(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 == 0x0102 + assert xmlReader.appVersion == "1.4.2" + assert xmlReader.hexVersion == "0x010402f0" + + # 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 == (840, 376) + assert data.currCounts == (840, 376) + + 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.2 + assert data.getLastHandle("outline") is None # Doesn't exist in 1.2 + + 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("s000000") == "New" + assert data.itemStatus.name("s000001") == "Notes" + assert data.itemStatus.name("s000002") == "Started" + assert data.itemStatus.name("s000003") == "1st Draft" + assert data.itemStatus.name("s000004") == "2nd Draft" + assert data.itemStatus.name("s000005") == "3rd Draft" + assert data.itemStatus.name("s000006") == "Finished" + + assert data.itemImport.name("i000007") == "None" + assert data.itemImport.name("i000008") == "Minor" + assert data.itemImport.name("i000009") == "Major" + assert data.itemImport.name("i00000a") == "Main" + + assert data.itemStatus.cols("s000000") == (100, 100, 100) + assert data.itemStatus.cols("s000001") == (200, 50, 0) + assert data.itemStatus.cols("s000002") == (182, 60, 0) + assert data.itemStatus.cols("s000003") == (193, 129, 0) + assert data.itemStatus.cols("s000004") == (193, 129, 0) + assert data.itemStatus.cols("s000005") == (193, 129, 0) + assert data.itemStatus.cols("s000006") == (58, 180, 58) + + assert data.itemImport.cols("i000007") == (100, 100, 100) + assert data.itemImport.cols("i000008") == (0, 122, 188) + assert data.itemImport.cols("i000009") == (21, 0, 180) + assert data.itemImport.cols("i00000a") == (117, 0, 175) + + assert data.itemStatus.count("s000000") == 0 + assert data.itemStatus.count("s000001") == 0 + assert data.itemStatus.count("s000002") == 0 + assert data.itemStatus.count("s000003") == 0 + assert data.itemStatus.count("s000004") == 0 + assert data.itemStatus.count("s000005") == 0 + assert data.itemStatus.count("s000006") == 0 + + assert data.itemImport.count("i000007") == 0 + assert data.itemImport.count("i000008") == 0 + assert data.itemImport.count("i000009") == 0 + assert data.itemImport.count("i00000a") == 0 + + # Compare content + dumpFile = os.path.join(outDir, "projectXML_ReadLegacy12.json") + compFile = os.path.join(refDir, "projectXML_ReadLegacy12.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": "New", + "e7ded148d6e4a": "1st Draft", + "6a2d6d5f4f401": "Notes", + "636b6aa9b697b": "1st Draft", + "bc0cbd2a407f3": "1st Draft", + "ba8a28a246524": "New", + "96b68994dfa3d": "2nd Draft", + "88706ddc78b1b": "1st Draft", + "ae7339df26ded": "1st Draft", + "f6622b4617424": "None", + "f7e2d9f330615": "None", + "14298de4d9524": "Minor", + "bb2c23b3c42cc": "Major", + "15c4492bd5107": "None", + "b3e74dbc1f584": "Main", + "f1471bef9f2ae": "Minor", + "5eaea4e8cdee8": "Major", + "6827118336ac1": "New", # Is now treated as novel-like + "ae9bf3c3ea159": "New", # Is now treated as novel-like + "8a5deb88c0e97": "1st Draft", + "98acd8c76c93a": "None", + "b8136a5a774a0": "New", + } + + # Save the project again, which should produce an identical project xml + timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) + xmlWriter = ProjectXMLWriter(fncDir) + assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True + compFile = os.path.join(refDir, "projectXML_ReadLegacy12.nwx") + assert cmpFiles(outFile, compFile) + +# END Test testCoreProjectXML_ReadLegacy12 + + +@pytest.mark.core +def testCoreProjectXML_ReadLegacy13(filesDir, fncDir, outDir, refDir, 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") + shutil.copy(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 == 0x0103 + assert xmlReader.appVersion == "1.6.6" + assert xmlReader.hexVersion == "0x010606f0" + + # 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 == (830, 376) + assert data.currCounts == (830, 376) + + 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("s000000") == "New" + assert data.itemStatus.name("s000001") == "Notes" + assert data.itemStatus.name("s000002") == "Started" + assert data.itemStatus.name("s000003") == "1st Draft" + assert data.itemStatus.name("s000004") == "2nd Draft" + assert data.itemStatus.name("s000005") == "3rd Draft" + assert data.itemStatus.name("s000006") == "Finished" + + assert data.itemImport.name("i000007") == "None" + assert data.itemImport.name("i000008") == "Minor" + assert data.itemImport.name("i000009") == "Major" + assert data.itemImport.name("i00000a") == "Main" + + assert data.itemStatus.cols("s000000") == (100, 100, 100) + assert data.itemStatus.cols("s000001") == (200, 50, 0) + assert data.itemStatus.cols("s000002") == (182, 60, 0) + assert data.itemStatus.cols("s000003") == (193, 129, 0) + assert data.itemStatus.cols("s000004") == (193, 129, 0) + assert data.itemStatus.cols("s000005") == (193, 129, 0) + assert data.itemStatus.cols("s000006") == (58, 180, 58) + + assert data.itemImport.cols("i000007") == (100, 100, 100) + assert data.itemImport.cols("i000008") == (0, 122, 188) + assert data.itemImport.cols("i000009") == (21, 0, 180) + assert data.itemImport.cols("i00000a") == (117, 0, 175) + + assert data.itemStatus.count("s000000") == 0 + assert data.itemStatus.count("s000001") == 0 + assert data.itemStatus.count("s000002") == 0 + assert data.itemStatus.count("s000003") == 0 + assert data.itemStatus.count("s000004") == 0 + assert data.itemStatus.count("s000005") == 0 + assert data.itemStatus.count("s000006") == 0 + + assert data.itemImport.count("i000007") == 0 + assert data.itemImport.count("i000008") == 0 + assert data.itemImport.count("i000009") == 0 + assert data.itemImport.count("i00000a") == 0 + + # Compare content + dumpFile = os.path.join(outDir, "projectXML_ReadLegacy13.json") + compFile = os.path.join(refDir, "projectXML_ReadLegacy13.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": "New", + "e7ded148d6e4a": "1st Draft", + "6a2d6d5f4f401": "Notes", + "636b6aa9b697b": "1st Draft", + "bc0cbd2a407f3": "1st Draft", + "ba8a28a246524": "New", + "96b68994dfa3d": "2nd Draft", + "88706ddc78b1b": "1st Draft", + "ae7339df26ded": "1st Draft", + "f6622b4617424": "None", + "f7e2d9f330615": "None", + "14298de4d9524": "Minor", + "bb2c23b3c42cc": "Major", + "15c4492bd5107": "None", + "b3e74dbc1f584": "Main", + "f1471bef9f2ae": "Minor", + "5eaea4e8cdee8": "Major", + "6827118336ac1": "New", # Is now treated as novel-like + "ae9bf3c3ea159": "New", # Is now treated as novel-like + "8a5deb88c0e97": "1st Draft", + "98acd8c76c93a": "None", + "b8136a5a774a0": "New", + } + + # Save the project again, which should produce an identical project xml + timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) + xmlWriter = ProjectXMLWriter(fncDir) + assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True + compFile = os.path.join(refDir, "projectXML_ReadLegacy13.nwx") + assert cmpFiles(outFile, compFile) + +# END Test testCoreProjectXML_ReadLegacy13 From 494b94430ca2bdff24fb8ba3520687ec8bd60347 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 1 Nov 2022 18:31:03 +0100 Subject: [PATCH 17/18] Fix tests broken by xml rewrite --- novelwriter/core/item.py | 16 +- tests/test_core/test_core_item.py | 422 ++++++++++++--------------- tests/test_core/test_core_project.py | 312 +++++++------------- tests/test_core/test_core_status.py | 87 +++--- 4 files changed, 355 insertions(+), 482 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 5b7deabe..8a45fa35 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -190,7 +190,7 @@ class NWItem: if "handle" in data: self.setHandle(data["handle"]) else: - logger.error("XML item entry does not have a handle") + logger.error("Item does not have a handle") return False self.setName(data.get("label", "")) @@ -217,12 +217,14 @@ class NWItem: self._parent = None # Root items cannot have a parent if self._type != nwItemType.FILE: - self._heading = "H0" # Only files have headers - self._active = False # Can only be True for files - self._charCount = 0 # Only set for files - self._wordCount = 0 # Only set for files - self._paraCount = 0 # Only set for files - self._cursorPos = 0 # Only set for files + # Reset values that should only be set for files + self._layout = nwItemLayout.NO_LAYOUT + self._heading = "H0" + self._active = False + self._charCount = 0 + self._wordCount = 0 + self._paraCount = 0 + self._cursorPos = 0 return True diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 02fe3f8f..714d6d57 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -21,8 +21,6 @@ along with this program. If not, see . import pytest -from lxml import etree - from PyQt5.QtGui import QIcon from tools import C, buildTestProject @@ -497,257 +495,199 @@ def testCoreItem_ClassDefaults(mockGUI): @pytest.mark.core -@pytest.mark.skip -def testCoreItem_XMLPackUnpack(mockGUI, caplog, mockRnd): - """Test packing and unpacking XML objects for the NWItem class. +def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd): + """Test packing and unpacking entries for the NWItem class. """ theProject = NWProject(mockGUI) - nwXML = etree.Element("novelWriterXML") + theProject.data.itemStatus.write(None, "New", (100, 100, 100)) + theProject.data.itemImport.write(None, "New", (100, 100, 100)) - statusKeys = ["s000000", "s000001", "s000002", "s000003"] - importKeys = ["i000004", "i000005", "i000006", "i000007"] + # Invalid + theItem = NWItem(theProject) + assert theItem.unpack({}) is False # File - # ==== - theItem = NWItem(theProject) - theItem.setHandle("0123456789abc") - theItem.setParent("0123456789abc") - theItem.setRoot("0123456789abc") - theItem.setOrder(1) - theItem.setName("A Name") - theItem.setClass("NOVEL") - theItem.setType("FILE") - theItem.setImport(importKeys[3]) - theItem.setLayout("NOTE") - theItem.setActive(False) - theItem.setParaCount(3) - theItem.setWordCount(5) - theItem.setCharCount(7) - theItem.setCursorPos(11) + 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, + }) is True - # Pack - xContent = etree.SubElement(nwXML, "content") - theItem.packXML(xContent) - assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b'' - b'A Name' - b'' - ) % bytes(importKeys[3], encoding="utf8") - - # Unpack - theItem = NWItem(theProject) - assert theItem.unpackXML(xContent[0]) - assert theItem.itemHandle == "0123456789abc" - assert theItem.itemParent == "0123456789abc" - assert theItem.itemRoot == "0123456789abc" + assert theItem.itemName == "A File" + assert theItem.itemHandle == "0000000000003" + assert theItem.itemParent == "0000000000002" + assert theItem.itemRoot == "0000000000001" assert theItem.itemOrder == 1 + assert theItem.itemType == nwItemType.FILE + assert theItem.itemClass == nwItemClass.NOVEL + assert theItem.itemLayout == nwItemLayout.DOCUMENT + assert theItem.itemStatus == "s000000" + assert theItem.itemImport == "i000001" assert theItem.isActive is False - assert theItem.paraCount == 3 - assert theItem.wordCount == 5 - assert theItem.charCount == 7 - assert theItem.cursorPos == 11 - assert theItem.itemClass == nwItemClass.NOVEL - assert theItem.itemType == nwItemType.FILE - assert theItem.itemLayout == nwItemLayout.NOTE - assert theItem.itemStatus == statusKeys[0] # Was None, should now be default - assert theItem.itemImport == importKeys[3] - - # Folder - # ====== - - theItem = NWItem(theProject) - theItem.setHandle("0123456789abc") - theItem.setParent("0123456789abc") - theItem.setRoot("0123456789abc") - theItem.setOrder(1) - theItem.setName("A Name") - theItem.setClass("NOVEL") - theItem.setType("FOLDER") - theItem.setStatus(statusKeys[1]) - theItem.setLayout("NOTE") - theItem.setExpanded(True) - theItem.setActive(False) - theItem.setParaCount(3) - theItem.setWordCount(5) - theItem.setCharCount(7) - theItem.setCursorPos(11) - - # Pack - xContent = etree.SubElement(nwXML, "content") - theItem.packXML(xContent) - assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b'' - b'A Name' - b'' - ) % bytes(statusKeys[1], encoding="utf8") - - # Unpack - theItem = NWItem(theProject) - assert theItem.unpackXML(xContent[0]) - assert theItem.itemHandle == "0123456789abc" - assert theItem.itemParent == "0123456789abc" - assert theItem.itemRoot == "0123456789abc" - assert theItem.itemOrder == 1 assert theItem.isExpanded is True - assert theItem.isActive is True - assert theItem.paraCount == 0 - assert theItem.wordCount == 0 - assert theItem.charCount == 0 - assert theItem.cursorPos == 0 - assert theItem.itemClass == nwItemClass.NOVEL - assert theItem.itemType == nwItemType.FOLDER - assert theItem.itemLayout == nwItemLayout.NO_LAYOUT - assert theItem.itemStatus == statusKeys[1] - assert theItem.itemImport == importKeys[0] # Was None, should now be default - - # Errors - # ====== - - # Not an Item - mockXml = etree.SubElement(nwXML, "stuff") - assert theItem.unpackXML(mockXml) is False - - # Item without Handle - mockXml = etree.SubElement(nwXML, "item", attrib={"stuff": "nah"}) - assert theItem.unpackXML(mockXml) is False - - # Item with Invalid SubElement is Accepted w/Error - mockXml = etree.SubElement(nwXML, "item", attrib={"handle": "0123456789abc"}) - xParam = etree.SubElement(mockXml, "invalid") - xParam.text = "stuff" - caplog.clear() - assert theItem.unpackXML(mockXml) is True - assert "Unknown tag 'invalid'" in caplog.text - - # Pack Valid Item - mockXml = etree.SubElement(nwXML, "group") - theItem._subPack(mockXml, "subGroup", {"one": "two"}, "value", False) - assert etree.tostring(mockXml, pretty_print=False, encoding="utf-8") == ( - b"value" - ) - - # Pack Not Allowed None - mockXml = etree.SubElement(nwXML, "group") - assert theItem._subPack(mockXml, "subGroup", {}, None, False) is None - assert theItem._subPack(mockXml, "subGroup", {}, "None", False) is None - assert etree.tostring(mockXml, pretty_print=False, encoding="utf-8") == ( - b"" - ) - -# END Test testCoreItem_XMLPackUnpack - - -@pytest.mark.core -@pytest.mark.skip -def testCoreItem_ConvertFromFmt12(mockGUI): - """Test the setter for all the nwItemLayout values for the NWItem - class using the class names that were present in file format 1.2. - """ - theProject = NWProject(mockGUI) - theItem = NWItem(theProject) - - # Deprecated Layouts - theItem.setLayout("TITLE") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("PAGE") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("BOOK") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("PARTITION") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("UNNUMBERED") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("CHAPTER") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("SCENE") - assert theItem.itemLayout == nwItemLayout.DOCUMENT - theItem.setLayout("MUMBOJUMBO") - assert theItem.itemLayout == nwItemLayout.NO_LAYOUT - -# END Test testCoreItem_ConvertFromFmt12 - - -@pytest.mark.core -@pytest.mark.skip -def testCoreItem_ConvertFromFmt13(mockGUI): - """Test packing and unpacking XML objects for the NWItem class from - format version 1.3 - """ - theProject = NWProject(mockGUI) - - # Make Version 1.3 XML - nwXML = etree.Element("novelWriterXML") - xContent = etree.SubElement(nwXML, "content") - - # Folder - xPack = etree.SubElement(xContent, "item", attrib={ - "handle": "a000000000001", - "order": "1", - "parent": "b000000000001", - }) - NWItem._subPack(xPack, "name", text="Folder") - NWItem._subPack(xPack, "type", text="FOLDER") - NWItem._subPack(xPack, "class", text="NOVEL") - NWItem._subPack(xPack, "status", text="New") - NWItem._subPack(xPack, "expanded", text="True") - - # Unpack Folder - theItem = NWItem(theProject) - theItem.unpackXML(xContent[0]) - assert theItem.itemHandle == "a000000000001" - assert theItem.itemParent == "b000000000001" - assert theItem.itemOrder == 1 - assert theItem.isExpanded is True - assert theItem.isActive is True - assert theItem.charCount == 0 - assert theItem.wordCount == 0 - assert theItem.paraCount == 0 - assert theItem.cursorPos == 0 - assert theItem.itemClass == nwItemClass.NOVEL - assert theItem.itemType == nwItemType.FOLDER - assert theItem.itemLayout == nwItemLayout.NO_LAYOUT - - # File - xPack = etree.SubElement(xContent, "item", attrib={ - "handle": "c000000000001", - "order": "2", - "parent": "a000000000001", - }) - NWItem._subPack(xPack, "name", text="Scene") - NWItem._subPack(xPack, "type", text="FILE") - NWItem._subPack(xPack, "class", text="NOVEL") - NWItem._subPack(xPack, "status", text="New") - NWItem._subPack(xPack, "exported", text="True") - NWItem._subPack(xPack, "layout", text="DOCUMENT") - NWItem._subPack(xPack, "charCount", text="600") - NWItem._subPack(xPack, "wordCount", text="100") - NWItem._subPack(xPack, "paraCount", text="6") - NWItem._subPack(xPack, "cursorPos", text="50") - - # Unpack File - theItem = NWItem(theProject) - theItem.unpackXML(xContent[1]) - assert theItem.itemHandle == "c000000000001" - assert theItem.itemParent == "a000000000001" - assert theItem.itemOrder == 2 - assert theItem.isExpanded is False - assert theItem.isActive is True - assert theItem.charCount == 600 - assert theItem.wordCount == 100 - assert theItem.paraCount == 6 + assert theItem.mainHeading == "H1" + assert theItem.charCount == 100 + assert theItem.wordCount == 20 + assert theItem.paraCount == 2 assert theItem.cursorPos == 50 + + assert theItem.pack() == { + "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": "s000000", + "import": "i000001", + "active": "False", + } + } + + # 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, + }) is True + + assert theItem.itemName == "A Folder" + assert theItem.itemHandle == "0000000000003" + assert theItem.itemParent == "0000000000002" + assert theItem.itemRoot == "0000000000001" + assert theItem.itemOrder == 1 + assert theItem.itemType == nwItemType.FOLDER assert theItem.itemClass == nwItemClass.NOVEL - assert theItem.itemType == nwItemType.FILE - assert theItem.itemLayout == nwItemLayout.DOCUMENT + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + assert theItem.itemStatus == "s000000" + assert theItem.itemImport == "i000001" + assert theItem.isActive is False + assert theItem.isExpanded is True + assert theItem.mainHeading == "H0" + assert theItem.charCount == 0 + assert theItem.wordCount == 0 + assert theItem.paraCount == 0 + assert theItem.cursorPos == 0 - # Deprecated Type - theItem.setType("TRASH") + assert theItem.pack() == { + "name": "A Folder", + "itemAttr": { + "handle": "0000000000003", + "parent": "0000000000002", + "root": "0000000000001", + "order": "1", + "type": "FOLDER", + "class": "NOVEL", + }, + "metaAttr": { + "expanded": "True", + }, + "nameAttr": { + "status": "s000000", + "import": "i000001", + } + } + + # 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, + }) is True + + assert theItem.itemName == "A Novel" + assert theItem.itemHandle == "0000000000003" + assert theItem.itemParent is None + assert theItem.itemRoot == "0000000000003" + assert theItem.itemOrder == 1 assert theItem.itemType == nwItemType.ROOT + assert theItem.itemClass == nwItemClass.NOVEL + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + assert theItem.itemStatus == "s000000" + assert theItem.itemImport == "i000001" + assert theItem.isActive is False + assert theItem.isExpanded is True + assert theItem.mainHeading == "H0" + assert theItem.charCount == 0 + assert theItem.wordCount == 0 + assert theItem.paraCount == 0 + assert theItem.cursorPos == 0 -# END Test testCoreItem_ConvertFromFmt13 + assert theItem.pack() == { + "name": "A Novel", + "itemAttr": { + "handle": "0000000000003", + "parent": "None", + "root": "0000000000003", + "order": "1", + "type": "ROOT", + "class": "NOVEL", + }, + "metaAttr": { + "expanded": "True", + }, + "nameAttr": { + "status": "s000000", + "import": "i000001", + } + } + +# END Test testCoreItem_PackUnpack diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 710d8160..02ce6c27 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -20,13 +20,14 @@ along with this program. If not, see . """ import os +import shutil import pytest from shutil import copyfile from zipfile import ZipFile -from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE, C from mock import causeOSError +from tools import cmpFiles, writeFile, readFile, buildTestProject, XML_IGNORE, C from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.common import formatTimeStamp @@ -36,6 +37,7 @@ 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 @pytest.mark.core @@ -380,140 +382,90 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI, @pytest.mark.core -@pytest.mark.skip -def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI): +def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncDir, mockRnd): """Test opening a project. """ theProject = NWProject(mockGUI) + mockRnd.reset() + buildTestProject(theProject, fncDir) # Rename the project file to check handling - rName = os.path.join(nwMinimal, nwFiles.PROJ_FILE) - wName = os.path.join(nwMinimal, nwFiles.PROJ_FILE+"_sdfghj") + rName = os.path.join(fncDir, nwFiles.PROJ_FILE) + wName = os.path.join(fncDir, nwFiles.PROJ_FILE+"_sdfghj") os.rename(rName, wName) - assert theProject.openProject(nwMinimal) is False + assert theProject.openProject(fncDir) is False os.rename(wName, rName) # Fail on folder structure check with monkeypatch.context() as mp: mp.setattr("os.mkdir", causeOSError) - assert theProject.openProject(nwMinimal) is False + shutil.rmtree(os.path.join(fncDir, "meta")) + assert theProject.openProject(fncDir) is False # Fail on lock file - theProject.setProjectPath(nwMinimal) + theProject.setProjectPath(fncDir) assert theProject._writeLockFile() - assert theProject.openProject(nwMinimal) is False + 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) - assert theProject.openProject(nwMinimal) is True + caplog.clear() + assert theProject.openProject(fncDir) is True + assert "Failed to check lock file" in caplog.text assert theProject.closeProject() # Force open with lockfile - theProject.setProjectPath(nwMinimal) + theProject.setProjectPath(fncDir) assert theProject._writeLockFile() - assert theProject.openProject(nwMinimal, overrideLock=True) is True + assert theProject.openProject(fncDir, overrideLock=True) is True assert theProject.closeProject() - # Make a junk XML file - oName = os.path.join(nwMinimal, nwFiles.PROJ_FILE[:-3]+"orig") - bName = os.path.join(nwMinimal, nwFiles.PROJ_FILE[:-3]+"bak") - os.rename(rName, oName) - writeFile(rName, "stuff") - assert theProject.openProject(nwMinimal) 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 "Project file does not appear" in mockGUI.lastAlert - # Also write a jun XML backup file - writeFile(bName, "stuff") - assert theProject.openProject(nwMinimal) is False + # 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 "Unknown or unsupported novelWriter project file" in mockGUI.lastAlert - # Wrong root item - writeFile(rName, "\n") - assert theProject.openProject(nwMinimal) is False + # 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 "Failed to parse project xml" in mockGUI.lastAlert - # Wrong file version - writeFile(rName, ( - "\n" - "\n" - "\n" - )) - mockGUI.askResponse = False - assert theProject.openProject(nwMinimal) is False - mockGUI.undo() + # 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 "The file format of your project is about to be" in mockGUI.lastQuestion[1] + mockGUI.askResponse = True - # Future file version - writeFile(rName, ( - "\n" - "\n" - "\n" - )) - assert theProject.openProject(nwMinimal) is False - - # Update file version - writeFile(rName, ( - "\n" - "\n" - "\n" - )) - mockGUI.askResponse = False - assert theProject.openProject(nwMinimal) is False - assert mockGUI.lastQuestion[0] == "File Version" - mockGUI.undo() - - # Larger hex version - writeFile(rName, ( - "\n" - "\n" - "\n" - ) % theProject.FILE_VERSION) - mockGUI.askResponse = False - assert theProject.openProject(nwMinimal) is False - assert mockGUI.lastQuestion[0] == "Version Conflict" - mockGUI.undo() - - # Test skipping XML entries - writeFile(rName, ( - "\n" - "\n" - "\n" - "\n" - "\n" - )) - assert theProject.openProject(nwMinimal) is True - assert theProject.closeProject() - - # Clean up XML files - os.unlink(rName) - os.unlink(bName) - os.rename(oName, rName) + # Won't convert legacy file + with monkeypatch.context() as mp: + 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] + 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(nwMinimal, "data_0")) - writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.nwd"), "stuff") - writeFile(os.path.join(nwMinimal, "data_0", "123456789abc_main.bak"), "stuff") + 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(nwMinimal) is True + assert theProject.openProject(fncDir) is True assert "There was an error updating the project." in mockGUI.lastAlert assert theProject.closeProject() @@ -522,57 +474,31 @@ def testCoreProject_Open(monkeypatch, nwMinimal, mockGUI): @pytest.mark.core -@pytest.mark.skip -def testCoreProject_Save(monkeypatch, nwMinimal, mockGUI, refDir): +def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir): """Test saving a project. """ theProject = NWProject(mockGUI) - testFile = os.path.join(nwMinimal, "nwProject.nwx") - backFile = os.path.join(nwMinimal, "nwProject.bak") - compFile = os.path.join(refDir, os.path.pardir, "minimal", "nwProject.nwx") # Nothing to save assert theProject.saveProject() is False - # Open test project - assert theProject.openProject(nwMinimal) + mockRnd.reset() + buildTestProject(theProject, fncDir) # Fail on folder structure check with monkeypatch.context() as mp: - mp.setattr("os.path.isdir", lambda *a: False) + mp.setattr("os.mkdir", causeOSError) + shutil.rmtree(os.path.join(fncDir, "meta")) assert theProject.saveProject() is False - # Fail on open file + # Fail writing with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) + mp.setattr(ProjectXMLWriter, "write", lambda *a: False) assert theProject.saveProject() is False - # Fail on creating .bak file - with monkeypatch.context() as mp: - mp.setattr("os.replace", causeOSError) - assert theProject.saveProject() is False - assert os.path.isfile(backFile) is False - - # Successful save - saveCount = theProject.data.saveCount - autoCount = theProject.data.autoCount - assert theProject.saveProject() is True - assert theProject.data.saveCount == saveCount + 1 - assert theProject.data.autoCount == autoCount - assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) - - # Check that a second save creates a .bak file - assert os.path.isfile(backFile) is True - - # Successful autosave - saveCount = theProject.data.saveCount - autoCount = theProject.data.autoCount + # Save with and without autosave + assert theProject.saveProject(autoSave=False) is True assert theProject.saveProject(autoSave=True) is True - assert theProject.data.saveCount == saveCount - assert theProject.data.autoCount == autoCount + 1 - assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) - - # Close test project assert theProject.closeProject() # END Test testCoreProject_Save @@ -683,11 +609,11 @@ def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI): @pytest.mark.core -def testCoreProject_AccessItems(nwMinimal, mockGUI): +def testCoreProject_AccessItems(mockGUI, fncDir, mockRnd): """Test helper functions for the project folder. """ theProject = NWProject(mockGUI) - theProject.openProject(nwMinimal) + buildTestProject(theProject, fncDir) # Storage Objects assert isinstance(theProject.index, NWIndex) @@ -696,34 +622,34 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): # Move Novel ROOT to after its files oldOrder = [ - "a508bb932959c", # ROOT: Novel - "a35baf2e93843", # FILE: Title Page - "a6d311a93600a", # FOLDER: New Chapter - "f5ab3e30151e1", # FILE: New Chapter - "8c659a11cd429", # FILE: New Scene - "7695ce551d265", # ROOT: Plot - "afb3043c7b2b3", # ROOT: Characters - "9d5247ab588e0", # ROOT: World + C.hNovelRoot, + C.hPlotRoot, + C.hCharRoot, + C.hWorldRoot, + C.hTitlePage, + C.hChapterDir, + C.hChapterDoc, + C.hSceneDoc, ] newOrder = [ - "a35baf2e93843", # FILE: Title Page - "f5ab3e30151e1", # FILE: New Chapter - "8c659a11cd429", # FILE: New Scene - "a6d311a93600a", # FOLDER: New Chapter - "a508bb932959c", # ROOT: Novel - "7695ce551d265", # ROOT: Plot - "afb3043c7b2b3", # ROOT: Characters - "9d5247ab588e0", # ROOT: World + C.hTitlePage, + C.hChapterDoc, + C.hSceneDoc, + C.hChapterDir, + C.hNovelRoot, + C.hPlotRoot, + C.hCharRoot, + C.hWorldRoot, ] assert theProject.tree.handles() == oldOrder assert theProject.setTreeOrder(newOrder) assert theProject.tree.handles() == newOrder # Add a non-existing item - theProject.tree._treeOrder.append("01234567789abc") + theProject.tree._treeOrder.append(C.hInvalid) # Add an item with a non-existent parent - nHandle = theProject.newFile("Test File", "a6d311a93600a") + nHandle = theProject.newFile("Test File", C.hChapterDir) theProject.tree[nHandle].setParent("cba9876543210") assert theProject.tree[nHandle].itemParent == "cba9876543210" @@ -732,15 +658,15 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): retOrder.append(tItem.itemHandle) assert retOrder == [ - "a508bb932959c", # ROOT: Novel - "7695ce551d265", # ROOT: Plot - "afb3043c7b2b3", # ROOT: Characters - "9d5247ab588e0", # ROOT: World - nHandle, # FILE: Test File - "a35baf2e93843", # FILE: Title Page - "a6d311a93600a", # FOLDER: New Chapter - "f5ab3e30151e1", # FILE: New Chapter - "8c659a11cd429", # FILE: New Scene + C.hNovelRoot, + C.hPlotRoot, + C.hCharRoot, + C.hWorldRoot, + nHandle, + C.hTitlePage, + C.hChapterDir, + C.hChapterDoc, + C.hSceneDoc, ] assert theProject.tree[nHandle].itemParent is None @@ -748,28 +674,28 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): @pytest.mark.core -@pytest.mark.skip def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): """Test the status and importance flag handling. """ theProject = NWProject(mockGUI) + mockRnd.reset() buildTestProject(theProject, fncDir) - statusKeys = ["s000008", "s000009", "s00000a", "s00000b"] - importKeys = ["i00000c", "i00000d", "i00000e", "i00000f"] + statusKeys = [C.sNew, C.sNote, C.sDraft, C.sFinished] + importKeys = [C.iNew, C.iMinor, C.iMajor, C.iMain] # Change Status # ============= - theProject.tree["0000000000014"].setStatus(statusKeys[3]) - theProject.tree["0000000000015"].setStatus(statusKeys[2]) - theProject.tree["0000000000016"].setStatus(statusKeys[1]) - theProject.tree["0000000000017"].setStatus(statusKeys[3]) + theProject.tree[C.hNovelRoot].setStatus(statusKeys[3]) + theProject.tree[C.hPlotRoot].setStatus(statusKeys[2]) + theProject.tree[C.hCharRoot].setStatus(statusKeys[1]) + theProject.tree[C.hWorldRoot].setStatus(statusKeys[3]) - assert theProject.tree["0000000000014"].itemStatus == statusKeys[3] - assert theProject.tree["0000000000015"].itemStatus == statusKeys[2] - assert theProject.tree["0000000000016"].itemStatus == statusKeys[1] - assert theProject.tree["0000000000017"].itemStatus == statusKeys[3] + assert theProject.tree[C.hNovelRoot].itemStatus == statusKeys[3] + assert theProject.tree[C.hPlotRoot].itemStatus == statusKeys[2] + assert theProject.tree[C.hCharRoot].itemStatus == statusKeys[1] + assert theProject.tree[C.hWorldRoot].itemStatus == statusKeys[3] newList = [ {"key": statusKeys[0], "name": "New", "cols": (1, 1, 1)}, @@ -792,8 +718,8 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): assert theProject.data.itemStatus.cols(statusKeys[3]) == (4, 4, 4) # Check the new entry - lastKey = theProject.data.itemStatus.check("s000018") - assert lastKey == "s000018" + lastKey = theProject.data.itemStatus.check("s000010") + assert lastKey == "s000010" assert theProject.data.itemStatus.name(lastKey) == "Finished" assert theProject.data.itemStatus.cols(lastKey) == (5, 5, 5) @@ -804,7 +730,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): # Change Importance # ================= - fHandle = theProject.newFile("Jane Doe", "0000000000012") + fHandle = theProject.newFile("Jane Doe", C.hCharRoot) theProject.tree[fHandle].setImport(importKeys[3]) assert theProject.tree[fHandle].itemImport == importKeys[3] @@ -829,8 +755,8 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): assert theProject.data.itemImport.cols(importKeys[3]) == (4, 4, 4) # Check the new entry - lastKey = theProject.data.itemImport.check("i00001a") - assert lastKey == "i00001a" + lastKey = theProject.data.itemImport.check("i000012") + assert lastKey == "i000012" assert theProject.data.itemImport.name(lastKey) == "Max" assert theProject.data.itemImport.cols(lastKey) == (5, 5, 5) @@ -854,18 +780,6 @@ def testCoreProject_StatusImport(mockGUI, fncDir, mockRnd): assert theProject.saveProject() is True assert theProject.closeProject() is True - # This should restore the default status/import labels - assert theProject.openProject(fncDir) is True - assert theProject.saveProject() is True - assert theProject.data.itemStatus.name("s000023") == "New" - assert theProject.data.itemStatus.name("s000024") == "Note" - assert theProject.data.itemStatus.name("s000025") == "Draft" - assert theProject.data.itemStatus.name("s000026") == "Finished" - assert theProject.data.itemImport.name("i000027") == "New" - assert theProject.data.itemImport.name("i000028") == "Minor" - assert theProject.data.itemImport.name("i000029") == "Major" - assert theProject.data.itemImport.name("i00002a") == "Main" - # END Test testCoreProject_StatusImport @@ -1268,14 +1182,14 @@ def testCoreProject_LegacyData(monkeypatch, mockGUI, fncDir): @pytest.mark.core -def testCoreProject_Backup(monkeypatch, mockGUI, nwMinimal, tmpDir): +def testCoreProject_Backup(monkeypatch, mockGUI, fncDir, tmpDir): """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) - assert theProject.openProject(nwMinimal) + buildTestProject(theProject, fncDir) # Test faulty settings @@ -1299,11 +1213,11 @@ def testCoreProject_Backup(monkeypatch, mockGUI, nwMinimal, tmpDir): assert theProject.zipIt(doNotify=False) is False # Same folder as project (causes infinite loop in zipping) - theProject.mainConf.backupPath = nwMinimal + theProject.mainConf.backupPath = fncDir assert theProject.zipIt(doNotify=False) is False # Subfolder of project (causes infinite loop in zipping) - theProject.mainConf.backupPath = os.path.join(nwMinimal, "subdir") + theProject.mainConf.backupPath = os.path.join(fncDir, "subdir") assert theProject.zipIt(doNotify=False) is False # Set a valid folder @@ -1335,7 +1249,7 @@ def testCoreProject_Backup(monkeypatch, mockGUI, nwMinimal, tmpDir): # Check that the main project file was restored assert cmpFiles( - os.path.join(nwMinimal, "nwProject.nwx"), + os.path.join(fncDir, "nwProject.nwx"), os.path.join(tmpDir, "extract", "nwProject.nwx") ) diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index 9f01136c..760f31f5 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -20,23 +20,21 @@ along with this program. If not, see . """ import pytest -import random -from lxml import etree +from tools import C from PyQt5.QtGui import QIcon from novelwriter.core.status import NWStatus -statusKeys = ["sa3b179", "s1c8031", "s06671a", "sbdd640"] -importKeys = ["i466852", "i3eb13b", "i392456", "i23b8c1"] +statusKeys = [C.sNew, C.sNote, C.sDraft, C.sFinished] +importKeys = [C.iNew, C.iMinor, C.iMajor, C.iMain] @pytest.mark.core -def testCoreStatus_Internal(): +def testCoreStatus_Internal(mockRnd): """Test all the internal functions of the NWStatus class. """ - random.seed(42) theStatus = NWStatus(NWStatus.STATUS) theImport = NWStatus(NWStatus.IMPORT) @@ -87,10 +85,9 @@ def testCoreStatus_Internal(): @pytest.mark.core -def testCoreStatus_Iterator(): +def testCoreStatus_Iterator(mockRnd): """Test the iterator functions of the NWStatus class. """ - random.seed(42) theStatus = NWStatus(NWStatus.STATUS) theStatus.write(None, "New", (100, 100, 100)) @@ -132,10 +129,9 @@ def testCoreStatus_Iterator(): @pytest.mark.core -def testCoreStatus_Entries(): +def testCoreStatus_Entries(mockRnd): """Test all the simple setters for the NWStatus class. """ - random.seed(42) theStatus = NWStatus(NWStatus.STATUS) # Write @@ -300,11 +296,9 @@ def testCoreStatus_Entries(): @pytest.mark.core -@pytest.mark.skip -def testCoreStatus_XMLPackUnpack(): - """Test all the XML pack/unpack of the NWStatus class. +def testCoreStatus_PackUnpack(mockRnd): + """Test all the pack/unpack of the NWStatus class. """ - random.seed(42) theStatus = NWStatus(NWStatus.STATUS) theStatus.write(None, "New", (100, 100, 100)) theStatus.write(None, "Note", (200, 50, 0)) @@ -316,36 +310,59 @@ def testCoreStatus_XMLPackUnpack(): for _ in range(n): theStatus.increment(statusKeys[i]) - nwXML = etree.Element("novelWriterXML") - # Pack - xStatus = etree.SubElement(nwXML, "status") - theStatus.packXML(xStatus) - assert etree.tostring(xStatus, pretty_print=False, encoding="utf-8") == ( - b'' - b'New' - b'Note' - b'Draft' - b'Finished' - b'' - ) + assert list(theStatus.pack()) == [ + ("New", { + "key": statusKeys[0], + "count": "3", + "red": "100", + "green": "100", + "blue": "100" + }), + ("Note", { + "key": statusKeys[1], + "count": "5", + "red": "200", + "green": "50", + "blue": "0" + }), + ("Draft", { + "key": statusKeys[2], + "count": "7", + "red": "200", + "green": "150", + "blue": "0" + }), + ("Finished", { + "key": statusKeys[3], + "count": "9", + "red": "50", + "green": "200", + "blue": "0" + }), + ] # Unpack theStatus = NWStatus(NWStatus.STATUS) - assert theStatus.unpackXML(xStatus) + assert theStatus.unpack({ + statusKeys[0]: {"label": "New0", "colour": (100, 100, 100), "count": countTo[0]}, + statusKeys[1]: {"label": "New1", "colour": (150, 150, 150), "count": countTo[1]}, + statusKeys[2]: {"label": "New2", "colour": (200, 200, 200), "count": countTo[2]}, + statusKeys[3]: {"label": "New3", "colour": (250, 250, 250), "count": countTo[3]}, + }) assert len(theStatus._store) == 4 assert list(theStatus._store.keys()) == statusKeys - assert theStatus._store[statusKeys[0]]["name"] == "New" - assert theStatus._store[statusKeys[1]]["name"] == "Note" - assert theStatus._store[statusKeys[2]]["name"] == "Draft" - assert theStatus._store[statusKeys[3]]["name"] == "Finished" + assert theStatus._store[statusKeys[0]]["name"] == "New0" + assert theStatus._store[statusKeys[1]]["name"] == "New1" + assert theStatus._store[statusKeys[2]]["name"] == "New2" + assert theStatus._store[statusKeys[3]]["name"] == "New3" assert theStatus._store[statusKeys[0]]["cols"] == (100, 100, 100) - assert theStatus._store[statusKeys[1]]["cols"] == (200, 50, 0) - assert theStatus._store[statusKeys[2]]["cols"] == (200, 150, 0) - assert theStatus._store[statusKeys[3]]["cols"] == (50, 200, 0) + assert theStatus._store[statusKeys[1]]["cols"] == (150, 150, 150) + assert theStatus._store[statusKeys[2]]["cols"] == (200, 200, 200) + assert theStatus._store[statusKeys[3]]["cols"] == (250, 250, 250) assert theStatus._store[statusKeys[0]]["count"] == countTo[0] assert theStatus._store[statusKeys[1]]["count"] == countTo[1] assert theStatus._store[statusKeys[2]]["count"] == countTo[2] assert theStatus._store[statusKeys[3]]["count"] == countTo[3] -# END Test testCoreStatus_XMLPackUnpack +# END Test testCoreStatus_PackUnpack From f1100d44758985ae617d0d89ace5c13f2324300f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 1 Nov 2022 18:32:29 +0100 Subject: [PATCH 18/18] Remove minimal project from test suite --- tests/conftest.py | 21 ---- tests/minimal/content/8c659a11cd429.nwd | 5 - tests/minimal/content/a35baf2e93843.nwd | 6 -- tests/minimal/content/f5ab3e30151e1.nwd | 5 - tests/minimal/nwProject.nwx | 81 --------------- tests/test_core/test_core_document.py | 41 ++++---- tests/test_core/test_core_tokenizer.py | 27 +++-- tests/test_dialogs/test_dlg_projload.py | 10 +- tests/test_dialogs/test_dlg_wordlist.py | 10 +- tests/test_gui/test_gui_doceditor.py | 119 ++++++++++------------ tests/test_tools/test_tools_projwizard.py | 8 +- 11 files changed, 98 insertions(+), 235 deletions(-) delete mode 100644 tests/minimal/content/8c659a11cd429.nwd delete mode 100644 tests/minimal/content/a35baf2e93843.nwd delete mode 100644 tests/minimal/content/f5ab3e30151e1.nwd delete mode 100644 tests/minimal/nwProject.nwx diff --git a/tests/conftest.py b/tests/conftest.py index 12842add..50955a31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -214,27 +214,6 @@ def mockRnd(monkeypatch): # Temp Project Folders ## -@pytest.fixture(scope="function") -def nwMinimal(tmpDir): - """A minimal novelWriter example project. - """ - tstDir = os.path.dirname(__file__) - srcDir = os.path.join(tstDir, "minimal") - dstDir = os.path.join(tmpDir, "minimal") - if os.path.isdir(dstDir): - shutil.rmtree(dstDir) - - shutil.copytree(srcDir, dstDir) - cleanProject(dstDir) - - yield dstDir - - if os.path.isdir(dstDir): - shutil.rmtree(dstDir) - - return - - @pytest.fixture(scope="function") def nwLipsum(tmpDir): """A medium sized novelWriter example project with a lot of Lorem diff --git a/tests/minimal/content/8c659a11cd429.nwd b/tests/minimal/content/8c659a11cd429.nwd deleted file mode 100644 index 2d7072eb..00000000 --- a/tests/minimal/content/8c659a11cd429.nwd +++ /dev/null @@ -1,5 +0,0 @@ -%%~name: New Scene -%%~path: a6d311a93600a/8c659a11cd429 -%%~kind: NOVEL/DOCUMENT -### New Scene - diff --git a/tests/minimal/content/a35baf2e93843.nwd b/tests/minimal/content/a35baf2e93843.nwd deleted file mode 100644 index b328746c..00000000 --- a/tests/minimal/content/a35baf2e93843.nwd +++ /dev/null @@ -1,6 +0,0 @@ -%%~name: Title Page -%%~path: a508bb932959c/a35baf2e93843 -%%~kind: NOVEL/DOCUMENT -#! Minimal - ->> By Jane Doe, John Doh << diff --git a/tests/minimal/content/f5ab3e30151e1.nwd b/tests/minimal/content/f5ab3e30151e1.nwd deleted file mode 100644 index b4bbf9c6..00000000 --- a/tests/minimal/content/f5ab3e30151e1.nwd +++ /dev/null @@ -1,5 +0,0 @@ -%%~name: New Chapter -%%~path: a6d311a93600a/f5ab3e30151e1 -%%~kind: NOVEL/DOCUMENT -## New Chapter - diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx deleted file mode 100644 index 19689fac..00000000 --- a/tests/minimal/nwProject.nwx +++ /dev/null @@ -1,81 +0,0 @@ - - - - Test Minimal - Minimal - Jane Doe - John Doh - 25 - 2 - 203 - - - True - en_GB - False - None - 10 - 10 - 0 - - None - None - a508bb932959c - None - - - - %title% - Chapter %ch%: %title% - %title% - * * * - - - - New - Note - Draft - Finished - - - New - Minor - Major - Main - - - - - - Novel - - - - Title Page - - - - New Chapter - - - - New Chapter - - - - New Scene - - - - Plot - - - - Characters - - - - World - - - diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py index ecc23e9c..761859ef 100644 --- a/tests/test_core/test_core_document.py +++ b/tests/test_core/test_core_document.py @@ -23,7 +23,7 @@ import os import pytest from mock import causeOSError -from tools import readFile, writeFile +from tools import C, buildTestProject, readFile, writeFile from novelwriter.enum import nwItemClass, nwItemLayout from novelwriter.core.project import NWProject @@ -31,14 +31,12 @@ from novelwriter.core.document import NWDoc @pytest.mark.core -def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal): +def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd): """Test loading and saving a document with the NWDoc class. """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwMinimal) is True - assert theProject.projPath == nwMinimal - - sHandle = "8c659a11cd429" + mockRnd.reset() + buildTestProject(theProject, fncDir) # Read Document # ============= @@ -49,25 +47,23 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal): assert theDoc.readDocument() is None # Non-existent handle - theDoc = NWDoc(theProject, "0000000000000") + theDoc = NWDoc(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, sHandle) + theDoc = NWDoc(theProject, C.hSceneDoc) assert theDoc.readDocument() is None assert theDoc.getError() == "OSError: Mock OSError" # Load the text - theDoc = NWDoc(theProject, sHandle) + theDoc = NWDoc(theProject, C.hSceneDoc) assert theDoc.readDocument() == "### New Scene\n\n" # Try to open a new (non-existent) file - nHandle = theProject.tree.findRoot(nwItemClass.NOVEL) - assert nHandle is not None - xHandle = theProject.newFile("New File", nHandle) + xHandle = theProject.newFile("New File", C.hNovelRoot) theDoc = NWDoc(theProject, xHandle) assert bool(theDoc) is True assert repr(theDoc) == f"" @@ -86,10 +82,10 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal): assert theDoc.writeDocument(theText) # Check file content - docPath = os.path.join(nwMinimal, "content", xHandle+".nwd") + docPath = os.path.join(fncDir, "content", xHandle+".nwd") assert readFile(docPath) == ( "%%~name: New File\n" - f"%%~path: a508bb932959c/{xHandle}\n" + f"%%~path: {C.hNovelRoot}/{xHandle}\n" "%%~kind: NOVEL/DOCUMENT\n" "### Test File\n\n" "Text ...\n\n" @@ -153,16 +149,15 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal): @pytest.mark.core -def testCoreDocument_Methods(mockGUI, nwMinimal): +def testCoreDocument_Methods(mockGUI, fncDir, mockRnd): """Test other methods of the NWDoc class. """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwMinimal) - assert theProject.projPath == nwMinimal + mockRnd.reset() + buildTestProject(theProject, fncDir) - sHandle = "8c659a11cd429" - theDoc = NWDoc(theProject, sHandle) - docPath = os.path.join(nwMinimal, "content", sHandle+".nwd") + theDoc = NWDoc(theProject, C.hSceneDoc) + docPath = os.path.join(fncDir, "content", C.hSceneDoc+".nwd") assert theDoc.readDocument() == "### New Scene\n\n" @@ -171,12 +166,12 @@ def testCoreDocument_Methods(mockGUI, nwMinimal): # Check the item assert theDoc.getCurrentItem() is not None - assert theDoc.getCurrentItem().itemHandle == sHandle + assert theDoc.getCurrentItem().itemHandle == C.hSceneDoc # Check the meta theName, theParent, theClass, theLayout = theDoc.getMeta() assert theName == "New Scene" - assert theParent == "a6d311a93600a" + assert theParent == C.hChapterDir assert theClass == nwItemClass.NOVEL assert theLayout == nwItemLayout.DOCUMENT @@ -184,7 +179,7 @@ def testCoreDocument_Methods(mockGUI, nwMinimal): assert theDoc.writeDocument("%%~ stuff\n### Test File\n\nText ...\n\n") assert readFile(docPath) == ( "%%~name: New Scene\n" - f"%%~path: a6d311a93600a/{sHandle}\n" + f"%%~path: {C.hChapterDir}/{C.hSceneDoc}\n" "%%~kind: NOVEL/DOCUMENT\n" "%%~ stuff\n" "### Test File\n\n" diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index fa3035d2..a1cc796f 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -22,7 +22,7 @@ along with this program. If not, see . import os import pytest -from tools import readFile +from tools import C, buildTestProject, readFile from novelwriter.core.project import NWProject from novelwriter.core.document import NWDoc @@ -133,21 +133,20 @@ def testCoreToken_Setters(mockGUI): @pytest.mark.core -def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): +def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncDir): """Test handling files and text in the Tokenizer class. """ theProject = NWProject(mockGUI) + mockRnd.reset() + buildTestProject(theProject, fncDir) + theProject.data.setLanguage("en") theProject._loadProjectLocalisation() theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) - assert theProject.openProject(nwMinimal) - sHandle = "8c659a11cd429" - # Set some content to work with - docText = ( "### Scene Six\n\n" "This is text with _italic text_, some **bold text**, some ~~deleted text~~, " @@ -157,7 +156,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): ) docTextR = docText.replace("", "this").replace("", "that") - nDoc = NWDoc(theProject, sHandle) + nDoc = NWDoc(theProject, C.hSceneDoc) assert nDoc.writeDocument(docText) theProject.data.setAutoReplace({"A": "this", "B": "that"}) @@ -166,17 +165,17 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): # Root Heading assert theToken.addRootHeading("stuff") is False - assert theToken.addRootHeading(sHandle) is False + assert theToken.addRootHeading(C.hSceneDoc) is False # First Page - assert theToken.addRootHeading("7695ce551d265") is True + assert theToken.addRootHeading(C.hPlotRoot) is True assert theToken.theMarkdown[-1] == "# Notes: Plot\n\n" assert theToken._theTokens[-1] == ( Tokenizer.T_TITLE, 0, "Notes: Plot", None, Tokenizer.A_CENTRE ) # Not First Page - assert theToken.addRootHeading("7695ce551d265") is True + assert theToken.addRootHeading(C.hPlotRoot) is True assert theToken.theMarkdown[-1] == "# Notes: Plot\n\n" assert theToken._theTokens[-1] == ( Tokenizer.T_TITLE, 0, "Notes: Plot", None, Tokenizer.A_CENTRE | Tokenizer.A_PBB @@ -184,18 +183,18 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): # Set Text assert theToken.setText("stuff") is False - assert theToken.setText(sHandle) is True + assert theToken.setText(C.hSceneDoc) is True assert theToken._theText == docText with monkeypatch.context() as mp: mp.setattr("novelwriter.constants.nwConst.MAX_DOCSIZE", 100) - assert theToken.setText(sHandle, docText) is True + assert theToken.setText(C.hSceneDoc, docText) is True assert theToken._theText == ( "# ERROR\n\n" "Document 'New Scene' is too big (0.00 MB). Skipping.\n\n" ) - assert theToken.setText(sHandle, docText) is True + assert theToken.setText(C.hSceneDoc, docText) is True assert theToken._theText == docText assert theToken._isNone is False @@ -212,7 +211,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, mockGUI): assert theToken.theResult == "This is text with escapes: ** ~~ __" # Save File - savePath = os.path.join(nwMinimal, "dump.nwd") + savePath = os.path.join(fncDir, "dump.nwd") theToken.saveRawMarkdown(savePath) assert readFile(savePath) == ( "# Notes: Plot\n\n" diff --git a/tests/test_dialogs/test_dlg_projload.py b/tests/test_dialogs/test_dlg_projload.py index 6e81ffe2..7ccf683a 100644 --- a/tests/test_dialogs/test_dlg_projload.py +++ b/tests/test_dialogs/test_dlg_projload.py @@ -22,7 +22,7 @@ along with this program. If not, see . import pytest import os -from tools import getGuiItem +from tools import buildTestProject, getGuiItem from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( @@ -33,10 +33,10 @@ from novelwriter.dialogs.projload import GuiProjectLoad @pytest.mark.gui -def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, nwMinimal): +def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, fncProj): """Test the load project wizard. """ - assert nwGUI.openProject(nwMinimal) + buildTestProject(nwGUI, fncProj) assert nwGUI.closeProject() monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *a: None) @@ -87,10 +87,10 @@ def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, nwMinimal): nwLoad._doDeleteRecent() assert nwLoad.listBox.topLevelItemCount() == recentCount - 1 - getFile = os.path.join(nwMinimal, "nwProject.nwx") + getFile = os.path.join(fncProj, "nwProject.nwx") monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (getFile, None)) qtbot.mouseClick(nwLoad.browseButton, Qt.LeftButton) - assert nwLoad.openPath == nwMinimal + assert nwLoad.openPath == fncProj assert nwLoad.openState == nwLoad.OPEN_STATE nwLoad.close() diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py index 24d61157..3a088934 100644 --- a/tests/test_dialogs/test_dlg_wordlist.py +++ b/tests/test_dialogs/test_dlg_wordlist.py @@ -25,7 +25,7 @@ import pytest from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialog, QAction -from tools import writeFile, readFile, getGuiItem +from tools import buildTestProject, writeFile, readFile, getGuiItem from mock import causeOSError from novelwriter.constants import nwFiles @@ -33,16 +33,18 @@ from novelwriter.dialogs.wordlist import GuiWordList @pytest.mark.gui -def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, nwMinimal): +def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, fncProj): """test the word list editor. """ + buildTestProject(nwGUI, fncProj) + monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None) monkeypatch.setattr(GuiWordList, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiWordList, "accept", lambda *a: None) # Open project - nwGUI.openProject(nwMinimal) - dictFile = os.path.join(nwMinimal, "meta", nwFiles.PROJ_DICT) + nwGUI.openProject(fncProj) + dictFile = os.path.join(fncProj, "meta", nwFiles.PROJ_DICT) # Load the dialog nwGUI.mainMenu.aEditWordList.activate(QAction.Trigger) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 22a960c2..193fda9e 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -22,6 +22,7 @@ along with this program. If not, see . import pytest from mock import causeOSError +from tools import C, buildTestProject from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption @@ -36,12 +37,12 @@ KEY_DELAY = 1 @pytest.mark.gui -def testGuiEditor_Init(qtbot, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_Init(qtbot, nwGUI, fncProj, ipsumText, mockRnd): """Test initialising the editor. """ # Open project - assert nwGUI.openProject(nwMinimal) - assert nwGUI.openDocument("8c659a11cd429") + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) nwGUI.docEditor.setText("### Lorem Ipsum\n\n%s" % ipsumText[0]) assert nwGUI.saveDocument() @@ -79,13 +80,11 @@ def testGuiEditor_Init(qtbot, nwGUI, nwMinimal, ipsumText): @pytest.mark.gui -def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, fncProj, ipsumText, mockRnd): """Test loading text into the editor. """ - # Open project - sHandle = "8c659a11cd429" - assert nwGUI.openProject(nwMinimal) is True - assert nwGUI.openDocument(sHandle) is True + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) is True longText = "### Lorem Ipsum\n\n%s" % "\n\n".join(ipsumText*20) nwGUI.docEditor.replaceText(longText) @@ -101,11 +100,11 @@ def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe # Document too big with monkeypatch.context() as mp: mp.setattr("novelwriter.constants.nwConst.MAX_DOCSIZE", 100) - assert nwGUI.docEditor.loadText(sHandle) is False + assert nwGUI.docEditor.loadText(C.hSceneDoc) is False assert "The document you are trying to open is too big." in caplog.text # Regular open - assert nwGUI.docEditor.loadText(sHandle) is True + assert nwGUI.docEditor.loadText(C.hSceneDoc) is True assert nwGUI.docEditor._bigDoc is False # Reload too big text @@ -116,18 +115,18 @@ def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe # Big doc handling nwGUI.mainConf.bigDocLimit = 50 - assert nwGUI.docEditor.loadText(sHandle) is True + assert nwGUI.docEditor.loadText(C.hSceneDoc) is True assert nwGUI.docEditor._bigDoc is True # Regular open, with line number - assert nwGUI.docEditor.loadText(sHandle, tLine=4) is True + assert nwGUI.docEditor.loadText(C.hSceneDoc, tLine=4) is True cursPos = nwGUI.docEditor.getCursorPosition() assert nwGUI.docEditor.document().findBlock(cursPos).blockNumber() == 4 # Load empty document nwGUI.docEditor.replaceText("") assert nwGUI.saveDocument() is True - assert nwGUI.docEditor.loadText(sHandle) is True + assert nwGUI.docEditor.loadText(C.hSceneDoc) is True assert nwGUI.docEditor.toPlainText() == "" # qtbot.stop() @@ -136,13 +135,11 @@ def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe @pytest.mark.gui -def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, fncProj, ipsumText, mockRnd): """Test saving text from the editor. """ - # Open project - sHandle = "8c659a11cd429" - assert nwGUI.openProject(nwMinimal) is True - assert nwGUI.openDocument(sHandle) is True + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) is True # Save Text # ========= @@ -159,7 +156,7 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe # Unkown handle nwGUI.docEditor._docHandle = "0123456789abcdef" assert nwGUI.docEditor.saveText() is False - nwGUI.docEditor._docHandle = sHandle + nwGUI.docEditor._docHandle = C.hSceneDoc # Cause error when saving with monkeypatch.context() as mp: @@ -168,10 +165,10 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe assert "Could not save document." in caplog.text # Change header level - assert nwGUI.theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT + assert nwGUI.theProject.tree[C.hSceneDoc].itemLayout == nwItemLayout.DOCUMENT nwGUI.docEditor.replaceText(longText[1:]) assert nwGUI.docEditor.saveText() is True - assert nwGUI.theProject.tree[sHandle].itemLayout == nwItemLayout.DOCUMENT + assert nwGUI.theProject.tree[C.hSceneDoc].itemLayout == nwItemLayout.DOCUMENT # Regular save assert nwGUI.docEditor.saveText() is True @@ -182,13 +179,11 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumTe @pytest.mark.gui -def testGuiEditor_MetaData(qtbot, nwGUI, nwMinimal): +def testGuiEditor_MetaData(qtbot, nwGUI, fncProj, mockRnd): """Test extracting various meta data and other values. """ - # Open project - sHandle = "8c659a11cd429" - assert nwGUI.openProject(nwMinimal) is True - assert nwGUI.openDocument(sHandle) is True + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) is True # Get Text # This should replace line and paragraph separators, but preserve @@ -203,7 +198,7 @@ def testGuiEditor_MetaData(qtbot, nwGUI, nwMinimal): # Check Propertoes assert nwGUI.docEditor.docChanged() is True - assert nwGUI.docEditor.docHandle() == sHandle + assert nwGUI.docEditor.docHandle() == C.hSceneDoc assert nwGUI.docEditor.lastActive() > 0.0 assert nwGUI.docEditor.isEmpty() is False @@ -211,9 +206,9 @@ def testGuiEditor_MetaData(qtbot, nwGUI, nwMinimal): assert nwGUI.docEditor.setCursorPosition(None) is False assert nwGUI.docEditor.setCursorPosition(10) is True assert nwGUI.docEditor.getCursorPosition() == 10 - assert nwGUI.theProject.tree[sHandle].cursorPos != 10 + assert nwGUI.theProject.tree[C.hSceneDoc].cursorPos != 10 nwGUI.docEditor.saveCursorPosition() - assert nwGUI.theProject.tree[sHandle].cursorPos == 10 + assert nwGUI.theProject.tree[C.hSceneDoc].cursorPos == 10 assert nwGUI.docEditor.setCursorLine(None) is False assert nwGUI.docEditor.setCursorLine(2) is True @@ -231,16 +226,14 @@ def testGuiEditor_MetaData(qtbot, nwGUI, nwMinimal): @pytest.mark.gui -def testGuiEditor_Actions(qtbot, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_Actions(qtbot, nwGUI, fncProj, ipsumText, mockRnd): """Test the document actions. This is not an extensive test of the action features, just that the actions are actually called. The various action features are tested when their respective functions are tested. """ - # Open project - sHandle = "8c659a11cd429" - assert nwGUI.openProject(nwMinimal) is True - assert nwGUI.openDocument(sHandle) is True + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) is True theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) assert nwGUI.docEditor.replaceText(theText) is True @@ -452,7 +445,7 @@ def testGuiEditor_Actions(qtbot, nwGUI, nwMinimal, ipsumText): # No Document Handle nwGUI.docEditor._docHandle = None assert nwGUI.docEditor.docAction(nwDocAction.BLOCK_TXT) is False - nwGUI.docEditor._docHandle = sHandle + nwGUI.docEditor._docHandle = C.hSceneDoc # Wrong Action Type assert nwGUI.docEditor.docAction(None) is False @@ -466,13 +459,11 @@ def testGuiEditor_Actions(qtbot, nwGUI, nwMinimal, ipsumText): @pytest.mark.gui -def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd): """Test the document insert functions. """ - # Open project - sHandle = "8c659a11cd429" - assert nwGUI.openProject(nwMinimal) is True - assert nwGUI.openDocument(sHandle) is True + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) is True theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) assert nwGUI.docEditor.replaceText(theText) is True @@ -487,7 +478,7 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): nwGUI.docEditor._docHandle = None assert nwGUI.docEditor.setCursorPosition(24) is True assert nwGUI.docEditor.insertText("Stuff") is False - nwGUI.docEditor._docHandle = sHandle + nwGUI.docEditor._docHandle = C.hSceneDoc # Insert String assert nwGUI.docEditor.setCursorPosition(24) is True @@ -551,13 +542,11 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): @pytest.mark.gui -def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd): """Test the text manipulation functions. """ - # Open project - sHandle = "8c659a11cd429" - assert nwGUI.openProject(nwMinimal) is True - assert nwGUI.openDocument(sHandle) is True + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) is True theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) assert nwGUI.docEditor.replaceText(theText) is True @@ -760,13 +749,11 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumTe @pytest.mark.gui -def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd): """Test the block formatting function. """ - # Open project - sHandle = "8c659a11cd429" - assert nwGUI.openProject(nwMinimal) is True - assert nwGUI.openDocument(sHandle) is True + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) is True theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) assert nwGUI.docEditor.replaceText(theText) is True @@ -1075,13 +1062,11 @@ def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumTex @pytest.mark.gui -def testGuiEditor_Tags(qtbot, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_Tags(qtbot, nwGUI, fncProj, ipsumText, mockRnd): """Test the document editor tags functionality. """ - # Open project - sHandle = "8c659a11cd429" - assert nwGUI.openProject(nwMinimal) is True - assert nwGUI.openDocument(sHandle) is True + buildTestProject(nwGUI, fncProj) + assert nwGUI.openDocument(C.hSceneDoc) is True # Create Scene theText = "### A Scene\n\n@char: Jane, John\n\n" + ipsumText[0] + "\n\n" @@ -1089,7 +1074,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, nwMinimal, ipsumText): # Create Character theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n" - cHandle = nwGUI.theProject.newFile("Jane Doe", "afb3043c7b2b3") + cHandle = nwGUI.theProject.newFile("Jane Doe", C.hCharRoot) assert nwGUI.openDocument(cHandle) is True assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.saveDocument() is True @@ -1098,7 +1083,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, nwMinimal, ipsumText): # Follow Tag # ========== - assert nwGUI.openDocument(sHandle) is True + assert nwGUI.openDocument(C.hSceneDoc) is True # Empty Block assert nwGUI.docEditor.setCursorLine(1) is True @@ -1136,7 +1121,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, nwMinimal, ipsumText): @pytest.mark.gui -def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): +def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd): """Test saving text from the editor. """ class MockThreadPool: @@ -1153,7 +1138,8 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): nwGUI.threadPool = MockThreadPool() nwGUI.docEditor.wcTimerDoc.blockSignals(True) nwGUI.docEditor.wcTimerSel.blockSignals(True) - assert nwGUI.openProject(nwMinimal) is True + + buildTestProject(nwGUI, fncProj) # Run on an empty document nwGUI.docEditor._runDocCounter() @@ -1167,10 +1153,9 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" # Open a document and populate it - sHandle = "8c659a11cd429" - nwGUI.theProject.tree[sHandle]._initCount = 0 # Clear item's count - nwGUI.theProject.tree[sHandle]._wordCount = 0 # Clear item's count - assert nwGUI.openDocument(sHandle) is True + nwGUI.theProject.tree[C.hSceneDoc]._initCount = 0 # Clear item's count + nwGUI.theProject.tree[C.hSceneDoc]._wordCount = 0 # Clear item's count + assert nwGUI.openDocument(C.hSceneDoc) is True theText = "\n\n".join(ipsumText) cC, wC, pC = countWords(theText) @@ -1193,9 +1178,9 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): nwGUI.docEditor.wCounterDoc.run() # nwGUI.docEditor._updateDocCounts(cC, wC, pC) - assert nwGUI.theProject.tree[sHandle]._charCount == cC - assert nwGUI.theProject.tree[sHandle]._wordCount == wC - assert nwGUI.theProject.tree[sHandle]._paraCount == pC + assert nwGUI.theProject.tree[C.hSceneDoc]._charCount == cC + assert nwGUI.theProject.tree[C.hSceneDoc]._wordCount == wC + assert nwGUI.theProject.tree[C.hSceneDoc]._paraCount == pC assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})" # Select all text diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py index a2facf6e..5d598e6d 100644 --- a/tests/test_tools/test_tools_projwizard.py +++ b/tests/test_tools/test_tools_projwizard.py @@ -23,7 +23,7 @@ import os import sys import pytest -from tools import getGuiItem +from tools import buildTestProject, getGuiItem from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QFileDialog, QWizard, QDialog @@ -37,7 +37,7 @@ from novelwriter.tools.projwizard import ( @pytest.mark.gui @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") -def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal): +def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, fncProj): """Test the launch of the project wizard. Disabled for macOS because the test segfaults on QWizard.show() """ @@ -45,7 +45,7 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal): # ======================== # New with a project open should cause an error - assert nwGUI.openProject(nwMinimal) + buildTestProject(nwGUI, fncProj) with monkeypatch.context() as mp: mp.setattr(nwGUI, "closeProject", lambda *a: False) assert nwGUI.newProject() is False @@ -61,7 +61,7 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, nwMinimal): assert nwGUI.newProject() is False # Now, with a non-empty folder - mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {"projPath": nwMinimal}) + mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {"projPath": fncProj}) assert nwGUI.newProject() is False # Test the Wizard Launching