Let the shared class handle project open/save/close and recreating the project instance

This commit is contained in:
Veronica Berglyd Olsen
2023-08-13 20:16:37 +02:00
parent 8154293d92
commit 150f6603cd
10 changed files with 79 additions and 86 deletions
+21 -55
View File
@@ -129,8 +129,21 @@ class NWProject(QObject):
@property
def isValid(self) -> bool:
"""Return True if a project is loaded."""
return self._valid
@property
def lockStatus(self) -> list | None:
"""Return the project lock information."""
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
return self._lockedBy
return None
@property
def currentEditTime(self) -> int:
"""Return total edit time, including the current session."""
return self._data.editTime + round(time() - self._session.start)
##
# Item Methods
##
@@ -206,35 +219,12 @@ class NWProject(QObject):
# Project Methods
##
def clearProject(self) -> None:
"""Clear the data for the current project, and set them to
default values.
Note: Don't clear the lockedBy data here as it is needed after
this function is called.
"""
# Core Elements
self._options = OptionState(self)
self._storage.clear()
self._data = NWProjectData(self)
self._tree.clear()
self._index.clearIndex()
self._session = NWSessionLog(self)
# Project Status
self._langData = {}
self._changed = False
self._valid = False
return
def openProject(self, projPath: str | Path, overrideLock: bool = False) -> bool:
def openProject(self, projPath: str | Path) -> bool:
"""Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful,
parse the XML of the file and populate the project variables and
build the tree of project items.
"""
self.clearProject()
logger.info("Opening project: %s", projPath)
if not self._storage.openProjectInPlace(projPath):
self.mainGui.makeAlert(self.tr(
@@ -245,9 +235,6 @@ class NWProject(QObject):
# Project Lock
# ============
if overrideLock:
self._storage.clearLockFile()
lockStatus = self._storage.readLockFile()
if len(lockStatus) > 0:
if lockStatus[0] == "ERROR":
@@ -255,7 +242,6 @@ class NWProject(QObject):
else:
logger.error("Project is locked, so not opening")
self._lockedBy = lockStatus
self.clearProject()
return False
else:
logger.debug("Project is not locked")
@@ -265,7 +251,6 @@ class NWProject(QObject):
xmlReader = self._storage.getXmlReader()
if not isinstance(xmlReader, ProjectXMLReader):
self.clearProject()
return False
self._data = NWProjectData(self)
@@ -289,8 +274,6 @@ class NWProject(QObject):
self.mainGui.makeAlert(self.tr(
"Failed to parse project xml."
), level=nwAlert.ERROR)
self.clearProject()
return False
# Check Legacy Upgrade
@@ -303,7 +286,6 @@ class NWProject(QObject):
"longer be able to open this project. Continue?"
))
if not msgYes:
self.clearProject()
return False
# Check novelWriter Version
@@ -318,7 +300,6 @@ class NWProject(QObject):
"should be fine. Continue opening the project?"
).format(appVersion, __version__))
if not msgYes:
self.clearProject()
return False
# Extract Data
@@ -329,9 +310,11 @@ class NWProject(QObject):
self._loadProjectLocalisation()
# Update recent projects
CONFIG.recentProjects.update(
self._storage.storagePath, self._data.name, sum(self._data.initCounts), time()
)
storePath = self._storage.storagePath
if storePath:
CONFIG.recentProjects.update(
storePath, self._data.name, sum(self._data.initCounts), time()
)
# Check the project tree consistency
# This also handles any orphaned files found
@@ -416,7 +399,6 @@ class NWProject(QObject):
self._tree.writeToCFile()
self._session.appendSession(idleTime)
self._storage.closeSession()
self.clearProject()
self._lockedBy = None
return
@@ -522,22 +504,10 @@ class NWProject(QObject):
return self._changed
##
# Getters
# Class Methods
##
def getLockStatus(self) -> list | None:
"""Return the project lock information for the project."""
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
return self._lockedBy
return None
def getCurrentEditTime(self) -> int:
"""Get the total project edit time, including the time spent in
the current session.
"""
return self._data.editTime + round(time() - self._session.start)
def getProjectItems(self) -> Iterator[NWItem]:
def iterProjectItems(self) -> Iterator[NWItem]:
"""This function ensures that the item tree loaded is sent to
the GUI tree view in such a way that the tree can be built. That
is, the parent item must be sent before its child. In principle,
@@ -580,10 +550,6 @@ class NWProject(QObject):
yield tItem
return
##
# Class Methods
##
def updateWordCounts(self) -> None:
"""Update the total word count values."""
novel, notes = self._tree.sumWords()
+3 -4
View File
@@ -237,14 +237,13 @@ class GuiProjectDetailsMain(QWidget):
return
def updateValues(self):
"""Set all the values.
"""
def updateValues(self) -> None:
"""Set all the values."""
project = SHARED.project
pIndex = project.index
hCounts = pIndex.getNovelTitleCounts()
nwCount = pIndex.getNovelWordCount()
edTime = project.getCurrentEditTime()
edTime = project.currentEditTime
self.bookTitle.setText(project.data.title or project.data.name)
self.projName.setText(self.tr("Project: {0}").format(project.data.name))
+1 -1
View File
@@ -1064,7 +1064,7 @@ class GuiProjectTree(QTreeWidget):
logger.debug("Building the project tree ...")
self.clearTree()
count = 0
for nwItem in SHARED.project.getProjectItems():
for nwItem in SHARED.project.iterProjectItems():
count += 1
self._addTreeItem(nwItem)
if count > 0:
+6 -8
View File
@@ -454,9 +454,7 @@ class GuiMain(QMainWindow):
if SHARED.project.data.doBackup and CONFIG.backupOnClose:
doBackup = True
if CONFIG.askBeforeBackup:
msgYes = self.askQuestion(self.tr("Backup the current project?"))
if not msgYes:
doBackup = False
doBackup = self.askQuestion(self.tr("Backup the current project?"))
if doBackup:
SHARED.project.backupProject(False)
@@ -490,9 +488,9 @@ class GuiMain(QMainWindow):
self._changeView(nwView.PROJECT)
# Try to open the project
if not SHARED.project.openProject(projFile):
if not SHARED.openProject(projFile):
# The project open failed.
lockStatus = SHARED.project.getLockStatus()
lockStatus = SHARED.projectLock
if lockStatus is None:
# The project is not locked, so failed for some other
# reason handled by the project class.
@@ -522,7 +520,8 @@ class GuiMain(QMainWindow):
lockDetails = ""
if self.askQuestion(lockText, info=lockInfo, details=lockDetails, level=nwAlert.WARN):
if not SHARED.project.openProject(projFile, overrideLock=True):
SHARED.unlockProject()
if not SHARED.openProject(projFile):
return False
else:
return False
@@ -578,8 +577,7 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
self.projView.saveProjectTasks()
SHARED.project.saveProject(autoSave=autoSave)
return True
return SHARED.saveProject(autoSave=autoSave)
##
# Document Actions
+33 -4
View File
@@ -42,6 +42,7 @@ class SharedData:
self._gui = None
self._theme = None
self._project = None
self._lockedBy = None
return
@property
@@ -69,6 +70,10 @@ class SharedData:
def hasProject(self) -> bool:
return self.project.isValid
@property
def projectLock(self) -> list | None:
return self._lockedBy
##
# Methods
##
@@ -84,16 +89,38 @@ class SharedData:
logger.debug("SharedData instance initialised")
return
def openProject(self, path: str | Path) -> None:
return
def openProject(self, path: str | Path) -> bool:
"""Open a project."""
if self.project.isValid:
logger.error("A project is already open")
return False
def saveProject(self):
return
self._lockedBy = None
status = self.project.openProject(path)
if status is False:
# We must cache the lock status before resetting the project
self._lockedBy = self.project.lockStatus
self._resetProject()
return status
def saveProject(self, autoSave: bool = False) -> bool:
"""Save the current project."""
if not self.project.isValid:
logger.error("There is no project open")
return False
return self.project.saveProject(autoSave=autoSave)
def closeProject(self, idleTime: float) -> None:
"""Close the current project."""
self.project.closeProject(idleTime)
self._resetProject()
return
def unlockProject(self) -> bool:
"""Remove the project lock."""
return self.project.storage.clearLockFile()
##
# Internal Functions
##
@@ -101,6 +128,8 @@ class SharedData:
def _resetProject(self) -> None:
"""Create a new project instance."""
from novelwriter.core.project import NWProject
if isinstance(self._project, NWProject):
self._project.deleteLater()
self._project = NWProject(self.mainGui)
return
+1 -1
View File
@@ -443,7 +443,7 @@ class _FilterTab(QWidget):
logger.debug("Building project tree")
self._treeMap = {}
self.optTree.clear()
for nwItem in SHARED.project.getProjectItems():
for nwItem in SHARED.project.iterProjectItems():
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
+6 -5
View File
@@ -176,7 +176,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK
assert theProject.storage.writeLockFile() is True
assert theProject.openProject(fncPath) is False
assert isinstance(theProject.getLockStatus(), list)
assert isinstance(theProject.lockStatus, list)
# Fail to read lockfile (which still opens the project)
with monkeypatch.context() as mp:
@@ -189,9 +189,10 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
# Force open with lockfile
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK
assert theProject.storage.writeLockFile() is True
assert theProject.openProject(fncPath, overrideLock=True) is True
theProject.storage.clearLockFile()
assert theProject.openProject(fncPath) is True
theProject.closeProject()
assert theProject.getLockStatus() is None
assert theProject.lockStatus is None
# Fail getting xml reader
with monkeypatch.context() as mp:
@@ -331,7 +332,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
assert theProject.tree[nHandle].itemParent == "cba9876543210"
retOrder = []
for tItem in theProject.getProjectItems():
for tItem in theProject.iterProjectItems():
retOrder.append(tItem.itemHandle)
assert retOrder == [
@@ -482,7 +483,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
theProject._session._start = 1600000000
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.project.time", lambda: 1600005600)
assert theProject.getCurrentEditTime() == 6834
assert theProject.currentEditTime == 6834
# Trash folder
# Should create on first call, and just returned on later calls
+3 -3
View File
@@ -104,9 +104,8 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, prjLipsum):
@pytest.mark.gui
def testGuiMain_NewProject(monkeypatch, nwGUI, projPath):
"""Test creating a new project.
"""
# No data
"""Test creating a new project."""
# Open wizard, but return no data
with monkeypatch.context() as mp:
mp.setattr(GuiProjectWizard, "exec_", lambda *a: None)
assert nwGUI.newProject(projData=None) is False
@@ -116,6 +115,7 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, projPath):
SHARED.project._valid = True
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert nwGUI.newProject(projData={"projPath": projPath}) is False
SHARED.project._valid = False
# No project path
assert nwGUI.newProject(projData={}) is False
+5 -4
View File
@@ -377,13 +377,14 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
# IOError
# =======
monkeypatch.setattr("builtins.open", causeOSError)
assert not sessLog._loadLogFile()
assert not sessLog._saveData(sessLog.FMT_CSV)
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert not sessLog._loadLogFile()
assert not sessLog._saveData(sessLog.FMT_CSV)
# qtbot.stop()
sessLog._doClose()
assert nwGUI.closeProject()
assert nwGUI.closeProject() is True
# END Test testToolWritingStats_Main
-1
View File
@@ -168,7 +168,6 @@ def buildTestProject(obj, projPath):
nwGUI = obj
project = obj.project
project.clearProject()
project.storage.openProjectInPlace(projPath)
project.setDefaultStatusImport()