From b0f1db0aeaa0d8d40dca29cc14e0246fd52e33e6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 31 Jan 2024 23:56:09 +0100 Subject: [PATCH 1/2] Add copy project feature to project builder class --- novelwriter/core/coretools.py | 58 +++++++++++++++++++++++--- tests/test_core/test_core_coretools.py | 2 +- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index c220d14e..2ac855eb 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -36,8 +36,8 @@ from functools import partial from PyQt5.QtCore import QCoreApplication from novelwriter import CONFIG, SHARED -from novelwriter.common import minmax, simplified -from novelwriter.constants import nwItemClass +from novelwriter.common import isHandle, minmax, simplified +from novelwriter.constants import nwFiles, nwItemClass from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject from novelwriter.core.storage import NWStorageCreate @@ -330,9 +330,8 @@ class ProjectBuilder: self._path = Path(path).resolve() if data.get("sample", False): return self._extractSampleProject(self._path) - elif data.get("template"): # pragma: no cover - # Not implemented yet - return True + elif data.get("template"): + return self._copyProject(self._path, data) else: return self._buildAndPopulate(self._path, data) SHARED.error("A project path is required.") @@ -348,7 +347,7 @@ class ProjectBuilder: status = project.storage.createNewProject(path) if status == NWStorageCreate.NOT_EMPTY: SHARED.error(self.tr( - "A project already exists in that location. " + "The target folder is not empty. " "Please choose another folder." )) return False @@ -454,6 +453,53 @@ class ProjectBuilder: return True + def _copyProject(self, path: Path, data: dict) -> bool: + """Copy an existing project content, but not the meta data, and + update new settings. + """ + source = data.get("template") + if not (isinstance(source, Path) and source.is_file() + and source.name == nwFiles.PROJ_FILE): + return False + + logger.info("Copying project: %s", source) + if path.exists(): + SHARED.error(self.tr( + "The target folder already exists. " + "Please choose another folder." + )) + return False + + # Begin copying + srcPath = source.parent + dstPath = path.resolve() + srcCont = srcPath / "content" + dstCont = dstPath / "content" + dstPath.mkdir(exist_ok=True) + dstCont.mkdir(exist_ok=True) + shutil.copy2(srcPath / nwFiles.PROJ_FILE, dstPath) + for contFile in srcCont.iterdir(): + if contFile.is_file() and contFile.suffix == ".nwd" and isHandle(contFile.stem): + shutil.copy2(contFile, dstCont) + + # Open the copied project and update settings + project = NWProject() + project.openProject(dstPath) + project.data.setUuid("") # Creates a fresh uuid + project.data.setName(data.get("name", "None")) + project.data.setAuthor(data.get("author", "")) + project.data.setLanguage(data.get("language", "en_GB")) + project.data.setSpellCheck(True) + project.data.setSpellLang(None) + project.data.setDoBackup(True) + project.data.setSaveCount(0) + project.data.setAutoCount(0) + project.data.setEditTime(0) + project.saveProject() + project.closeProject() + + return True + def _extractSampleProject(self, path: Path) -> bool: """Make a copy of the sample project by extracting the sample.zip file to the new path. diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 9625a17a..bcf545d8 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -425,7 +425,7 @@ def testCoreTools_ProjectBuilderWrapper(monkeypatch, caplog, fncPath, mockGUI): # Creating the project once more should fail caplog.clear() assert builder.buildProject({"path": fncPath}) is False - assert "A project already exists" in caplog.text + assert "The target folder is not empty." in caplog.text # END Test testCoreTools_ProjectBuilderWrapper From 58fa3e64ec0248ae93f39a71acfb91b86537152d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 1 Feb 2024 00:15:39 +0100 Subject: [PATCH 2/2] Add test coverage --- tests/test_core/test_core_coretools.py | 69 +++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index bcf545d8..d8dec316 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -31,7 +31,7 @@ from tools import C, NWD_IGNORE, buildTestProject, cmpFiles, XML_IGNORE from mocked import causeOSError from novelwriter import CONFIG -from novelwriter.constants import nwItemClass +from novelwriter.constants import nwFiles, nwItemClass from novelwriter.core.project import NWProject from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, ProjectBuilder @@ -466,7 +466,7 @@ def testCoreTools_ProjectBuilderA(monkeypatch, fncPath, tstPaths, mockRnd): @pytest.mark.core -def testCoreTools_ProjectBuilderB(monkeypatch, fncPath, tstPaths, mockRnd): +def testCoreTools_ProjectBuilderB(monkeypatch, fncPath, tstPaths, mockRnd): """Create a new project from a project dictionary, without chapters.""" monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) @@ -500,11 +500,68 @@ def testCoreTools_ProjectBuilderB(monkeypatch, fncPath, tstPaths, mockRnd): # END Test testCoreTools_ProjectBuilderB +@pytest.mark.core +def testCoreTools_ProjectBuilderTemplate(monkeypatch, mockGUI, prjLipsum, fncPath): + """Create a new project copied from existing project.""" + srcPath = prjLipsum / nwFiles.PROJ_FILE + dstPath = fncPath / "lipsum" + data = { + "name": "Test Project", + "author": "Jane Doe", + "language": "en_US", + "path": dstPath, + "template": srcPath, + } + + builder = ProjectBuilder() + + # No path set + assert builder.buildProject({"template": srcPath}) is False + + # No project at path + assert builder.buildProject({"path": fncPath, "template": fncPath}) is False + + # Cannot copy to existing folder + assert builder.buildProject({"path": fncPath, "template": srcPath}) is False + + # Copy project properly + assert builder.buildProject(data) is True + + # Check Copy + # ========== + + srcProject = NWProject() + srcProject.openProject(srcPath) + + dstProject = NWProject() + dstProject.openProject(dstPath) + + # UUID should be different + assert srcProject.data.uuid != dstProject.data.uuid + + # Name should be different + assert srcProject.data.name == "Lorem Ipsum" + assert dstProject.data.name == "Test Project" + + # Author should be different + assert srcProject.data.author == "lipsum.com" + assert dstProject.data.author == "Jane Doe" + + # Language should be different + assert srcProject.data.language == "en_GB" + assert dstProject.data.language == "en_US" + + # Counts should be more or less zeroed + assert dstProject.data.saveCount < 5 + assert dstProject.data.autoCount < 5 + assert dstProject.data.editTime < 10 + +# END Test testCoreTools_ProjectBuilderTemplate + + @pytest.mark.core def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths): - """Check that we can create a new project can be created from the - provided sample project via a zip file. - """ + """Create a new sample project.""" data = { "name": "Test Sample", "author": "Jane Doe", @@ -515,7 +572,7 @@ def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths): builder = ProjectBuilder() # No path set - assert builder.buildProject({"popSample": True}) is False + assert builder.buildProject({"sample": True}) is False # Force the lookup path for assets to our temp folder srcSample = CONFIG._appRoot / "sample"