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