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 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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user