Allow creating project from other projects (#1680)

This commit is contained in:
Veronica Berglyd Olsen
2024-02-01 00:24:16 +01:00
committed by GitHub
2 changed files with 116 additions and 13 deletions
+52 -6
View File
@@ -36,8 +36,8 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import QCoreApplication
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import minmax, simplified from novelwriter.common import isHandle, minmax, simplified
from novelwriter.constants import nwItemClass from novelwriter.constants import nwFiles, nwItemClass
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.storage import NWStorageCreate from novelwriter.core.storage import NWStorageCreate
@@ -330,9 +330,8 @@ class ProjectBuilder:
self._path = Path(path).resolve() self._path = Path(path).resolve()
if data.get("sample", False): if data.get("sample", False):
return self._extractSampleProject(self._path) return self._extractSampleProject(self._path)
elif data.get("template"): # pragma: no cover elif data.get("template"):
# Not implemented yet return self._copyProject(self._path, data)
return True
else: else:
return self._buildAndPopulate(self._path, data) return self._buildAndPopulate(self._path, data)
SHARED.error("A project path is required.") SHARED.error("A project path is required.")
@@ -348,7 +347,7 @@ class ProjectBuilder:
status = project.storage.createNewProject(path) status = project.storage.createNewProject(path)
if status == NWStorageCreate.NOT_EMPTY: if status == NWStorageCreate.NOT_EMPTY:
SHARED.error(self.tr( SHARED.error(self.tr(
"A project already exists in that location. " "The target folder is not empty. "
"Please choose another folder." "Please choose another folder."
)) ))
return False return False
@@ -454,6 +453,53 @@ class ProjectBuilder:
return True 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: def _extractSampleProject(self, path: Path) -> bool:
"""Make a copy of the sample project by extracting the """Make a copy of the sample project by extracting the
sample.zip file to the new path. sample.zip file to the new path.
+64 -7
View File
@@ -31,7 +31,7 @@ from tools import C, NWD_IGNORE, buildTestProject, cmpFiles, XML_IGNORE
from mocked import causeOSError from mocked import causeOSError
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.constants import nwItemClass from novelwriter.constants import nwFiles, nwItemClass
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, ProjectBuilder from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, ProjectBuilder
@@ -425,7 +425,7 @@ def testCoreTools_ProjectBuilderWrapper(monkeypatch, caplog, fncPath, mockGUI):
# Creating the project once more should fail # Creating the project once more should fail
caplog.clear() caplog.clear()
assert builder.buildProject({"path": fncPath}) is False 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 # END Test testCoreTools_ProjectBuilderWrapper
@@ -466,7 +466,7 @@ def testCoreTools_ProjectBuilderA(monkeypatch, fncPath, tstPaths, mockRnd):
@pytest.mark.core @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.""" """Create a new project from a project dictionary, without chapters."""
monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) 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 # 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 @pytest.mark.core
def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths): def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths):
"""Check that we can create a new project can be created from the """Create a new sample project."""
provided sample project via a zip file.
"""
data = { data = {
"name": "Test Sample", "name": "Test Sample",
"author": "Jane Doe", "author": "Jane Doe",
@@ -515,7 +572,7 @@ def testCoreTools_ProjectBuilderSample(monkeypatch, mockGUI, fncPath, tstPaths):
builder = ProjectBuilder() builder = ProjectBuilder()
# No path set # 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 # Force the lookup path for assets to our temp folder
srcSample = CONFIG._appRoot / "sample" srcSample = CONFIG._appRoot / "sample"