Allow creating project from other projects (#1680)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user