diff --git a/novelwriter/common.py b/novelwriter/common.py index 60d1f7fa..5a84302d 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -25,6 +25,7 @@ along with this program. If not, see . import os import json +import uuid import hashlib import logging @@ -113,6 +114,15 @@ def checkHandle(value, default, allowNone=False): return default +def checkUuid(value, default): + """Try to process a value as an uuid, or return a default. + """ + try: + return str(uuid.UUID(value)) + except Exception: + return default + + # =============================================================================================== # # Validator Functions # =============================================================================================== # diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 0c24fc29..1ff73d2e 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -322,6 +322,7 @@ class ProjectBuilder: projTitle = data.get("projTitle", lblNewProject) projAuthors = data.get("projAuthors", "") + project.data.setUuid(None) project.data.setName(projName) project.data.setTitle(projTitle) project.data.setAuthors(projAuthors) diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index 913a8aa8..40bce1a7 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -25,9 +25,12 @@ along with this program. If not, see . from __future__ import annotations +import uuid import logging -from novelwriter.common import checkBool, checkInt, checkStringNone, isHandle, simplified +from novelwriter.common import ( + checkBool, checkInt, checkStringNone, checkUuid, isHandle, simplified +) from novelwriter.core.status import NWStatus logger = logging.getLogger(__name__) @@ -196,6 +199,17 @@ class NWProjectData: # Setters ## + def setUuid(self, value): + """Set the project id. + """ + value = checkUuid(value, "") + if not value: + self._uuid = str(uuid.uuid4()) + elif value != self._uuid: + self._uuid = value + self.theProject.setProjectChanged(True) + return + def setName(self, value): """Set a new project name. """ diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 8f15b0f5..ed0d2e9a 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -224,6 +224,7 @@ class ProjectXMLReader: """Parse the project section of the XML file. """ logger.debug("Parsing section") + projData.setUuid(xSection.attrib.get("id", None)) for xItem in xSection: if xItem.tag == "name": projData.setName(xItem.text) @@ -476,7 +477,7 @@ class ProjectXMLWriter: }) # Save Project Meta - xProject = etree.SubElement(xRoot, "project") + xProject = etree.SubElement(xRoot, "project", attrib={"id": projData.uuid}) self._packSingleValue(xProject, "name", projData.name) self._packSingleValue(xProject, "title", projData.title) self._packListValue(xProject, "author", projData.authors) diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 1dba26ca..d7d7bde1 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - - + + Sample Project Sample Project Jane Smith Jay Doh - 1409 + 1421 236 - 69427 + 69454 False diff --git a/tests/files/nwProject-1.4.nwx b/tests/files/nwProject-1.4.nwx index 706b2266..71baddbc 100644 --- a/tests/files/nwProject-1.4.nwx +++ b/tests/files/nwProject-1.4.nwx @@ -1,6 +1,6 @@ - + Sample Project Sample Project Jane Smith diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index 5ac1b4de..d78a4aa0 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,19 +1,18 @@ - - + + Lorem Ipsum Lorem Ipsum lipsum.com - 32 + 34 24 - 1889 + 1893 False en_GB False None - 3847 3109 738 diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index b1370820..973306cf 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project New Novel Jane Doe diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index ed50e9ca..6aea2f22 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project New Novel Jane Doe diff --git a/tests/reference/coreTools_NewCustomA_nwProject.nwx b/tests/reference/coreTools_NewCustomA_nwProject.nwx index f6314c1a..1b083ddc 100644 --- a/tests/reference/coreTools_NewCustomA_nwProject.nwx +++ b/tests/reference/coreTools_NewCustomA_nwProject.nwx @@ -1,6 +1,6 @@ - - + + Test Custom Test Novel Jane Doe diff --git a/tests/reference/coreTools_NewCustomB_nwProject.nwx b/tests/reference/coreTools_NewCustomB_nwProject.nwx index 23e0c509..637afa4f 100644 --- a/tests/reference/coreTools_NewCustomB_nwProject.nwx +++ b/tests/reference/coreTools_NewCustomB_nwProject.nwx @@ -1,6 +1,6 @@ - - + + Test Custom Test Novel Jane Doe diff --git a/tests/reference/coreTools_NewMinimal_nwProject.nwx b/tests/reference/coreTools_NewMinimal_nwProject.nwx index 0f2388f5..bed1b6bf 100644 --- a/tests/reference/coreTools_NewMinimal_nwProject.nwx +++ b/tests/reference/coreTools_NewMinimal_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project New Project 1 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 853ea1d9..2c849f63 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,12 +1,12 @@ - - + + New Project New Novel Jane Doe 4 2 - 3 + 4 True diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 8108fba8..84c353e8 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project New Novel Jane Doe diff --git a/tests/reference/projectXML_ReadLegacy10.nwx b/tests/reference/projectXML_ReadLegacy10.nwx index 8e7f0a99..b557dc19 100644 --- a/tests/reference/projectXML_ReadLegacy10.nwx +++ b/tests/reference/projectXML_ReadLegacy10.nwx @@ -1,6 +1,6 @@ - + Sample Project Sample Project Jane Smith diff --git a/tests/reference/projectXML_ReadLegacy11.nwx b/tests/reference/projectXML_ReadLegacy11.nwx index fce991d3..84df9eef 100644 --- a/tests/reference/projectXML_ReadLegacy11.nwx +++ b/tests/reference/projectXML_ReadLegacy11.nwx @@ -1,6 +1,6 @@ - + Sample Project Sample Project Jane Smith diff --git a/tests/reference/projectXML_ReadLegacy12.nwx b/tests/reference/projectXML_ReadLegacy12.nwx index 9de6966b..e87015fe 100644 --- a/tests/reference/projectXML_ReadLegacy12.nwx +++ b/tests/reference/projectXML_ReadLegacy12.nwx @@ -1,6 +1,6 @@ - + Sample Project Sample Project Jane Smith diff --git a/tests/reference/projectXML_ReadLegacy13.nwx b/tests/reference/projectXML_ReadLegacy13.nwx index 7fc4cfea..a8be3ecd 100644 --- a/tests/reference/projectXML_ReadLegacy13.nwx +++ b/tests/reference/projectXML_ReadLegacy13.nwx @@ -1,6 +1,6 @@ - + Sample Project Sample Project Jane Smith diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index bb05265f..ca487aa2 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -30,11 +30,11 @@ from tools import writeFile from novelwriter.guimain import GuiMain from novelwriter.common import ( checkStringNone, checkString, checkInt, checkFloat, checkBool, checkHandle, - isHandle, isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, - minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified, - splitVersionNumber, transferCase, fuzzyTime, numberToRoman, jsonEncode, - readTextFile, makeFileNameSafe, ensureFolder, sha256sum, getGuiItem, - NWConfigParser + checkUuid, isHandle, isTitleTag, isItemClass, isItemType, isItemLayout, + hexToInt, minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, + simplified, splitVersionNumber, transferCase, fuzzyTime, numberToRoman, + jsonEncode, readTextFile, makeFileNameSafe, ensureFolder, sha256sum, + getGuiItem, NWConfigParser ) @@ -137,6 +137,20 @@ def testBaseCommon_CheckHandle(): # END Test testBaseCommon_CheckHandle +@pytest.mark.base +def testBaseCommon_CheckUuid(): + """Test the checkUuid function. + """ + testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea" + assert checkUuid("", None) is None + assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None + assert checkUuid("e2be99af-f9bf-qq03-857a-c3d1ac25abea", None) is None + assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abeaa", None) is None + assert checkUuid(testUuid, None) == testUuid + +# END Test testBaseCommon_CheckUuid + + @pytest.mark.base def testBaseCommon_IsHandle(): """Test the isHandle function. diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index f98e32f1..afb4d07b 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -20,6 +20,7 @@ along with this program. If not, see . """ import os +import uuid import pytest from shutil import copyfile @@ -264,10 +265,12 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mock @pytest.mark.core -def testCoreTools_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreTools_NewMinimal(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. With default setting, creating a Minimal project. """ + monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) + projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(outDir, "coreTools_NewMinimal_nwProject.nwx") compFile = os.path.join(refDir, "coreTools_NewMinimal_nwProject.nwx") @@ -294,10 +297,12 @@ def testCoreTools_NewMinimal(fncDir, outDir, refDir, mockGUI, mockRnd): @pytest.mark.core -def testCoreTools_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreTools_NewCustomA(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. Custom type with chapters and scenes. """ + monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) + projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(outDir, "coreTools_NewCustomA_nwProject.nwx") compFile = os.path.join(refDir, "coreTools_NewCustomA_nwProject.nwx") @@ -330,10 +335,12 @@ def testCoreTools_NewCustomA(fncDir, outDir, refDir, mockGUI, mockRnd): @pytest.mark.core -def testCoreTools_NewCustomB(fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreTools_NewCustomB(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. Custom type without chapters, but with scenes. """ + monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) + projFile = os.path.join(fncDir, "nwProject.nwx") testFile = os.path.join(outDir, "coreTools_NewCustomB_nwProject.nwx") compFile = os.path.join(refDir, "coreTools_NewCustomB_nwProject.nwx") diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index ac2b4a3c..b3af7289 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -232,7 +232,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath): assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True copyfile(outFile, tstFile) - assert cmpFiles(tstFile, xmlFile) + assert cmpFiles(tstFile, refFile) # END Test testCoreProjectXML_ReadCurrent @@ -369,6 +369,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd): # Save the project again, which should produce an identical project xml timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) xmlWriter = ProjectXMLWriter(fncPath) + data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True testFile = tstPaths.outDir / "projectXML_ReadLegacy10.nwx" compFile = tstPaths.refDir / "projectXML_ReadLegacy10.nwx" @@ -510,6 +511,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd): # Save the project again, which should produce an identical project xml timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) xmlWriter = ProjectXMLWriter(fncPath) + data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True testFile = tstPaths.outDir / "projectXML_ReadLegacy11.nwx" compFile = tstPaths.refDir / "projectXML_ReadLegacy11.nwx" @@ -654,6 +656,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd): # Save the project again, which should produce an identical project xml timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) xmlWriter = ProjectXMLWriter(fncPath) + data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True testFile = tstPaths.outDir / "projectXML_ReadLegacy12.nwx" compFile = tstPaths.refDir / "projectXML_ReadLegacy12.nwx" @@ -798,6 +801,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd): # Save the project again, which should produce an identical project xml timeStamp = int(datetime.fromisoformat(xmlReader.timeStamp).timestamp()) xmlWriter = ProjectXMLWriter(fncPath) + data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") assert xmlWriter.write(data, packedContent, timeStamp, 1000) is True testFile = tstPaths.outDir / "projectXML_ReadLegacy13.nwx" compFile = tstPaths.refDir / "projectXML_ReadLegacy13.nwx" diff --git a/tests/tools.py b/tests/tools.py index 5094fa62..7824268b 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -170,6 +170,7 @@ def buildTestProject(theObject, projPath): theProject.storage.openProjectInPlace(theProject.projPath) theProject.setDefaultStatusImport() + theProject.data.setUuid("d0f3fe10-c6e6-4310-8bfd-181eb4224eed") theProject.data.setName("New Project") theProject.data.setTitle("New Novel") theProject.data.setAuthors("Jane Doe")