Track recent projects via UUID (#2218)

This commit is contained in:
Veronica Berglyd Olsen
2025-01-31 00:03:19 +01:00
committed by GitHub
4 changed files with 65 additions and 27 deletions
+28 -16
View File
@@ -32,6 +32,7 @@ import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from time import time from time import time
from typing import TYPE_CHECKING
from PyQt5.QtCore import ( from PyQt5.QtCore import (
PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo, PYQT_VERSION, PYQT_VERSION_STR, QT_VERSION, QT_VERSION_STR, QLibraryInfo,
@@ -47,6 +48,9 @@ from novelwriter.common import (
from novelwriter.constants import nwFiles, nwUnicode from novelwriter.constants import nwFiles, nwUnicode
from novelwriter.error import formatException, logException from novelwriter.error import formatException, logException
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.projectdata import NWProjectData
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -845,29 +849,30 @@ class RecentProjects:
def __init__(self, config: Config) -> None: def __init__(self, config: Config) -> None:
self._conf = config self._conf = config
self._data = {} self._data: dict[str, dict[str, str | int]] = {}
self._map: dict[str, str] = {}
return return
def loadCache(self) -> bool: def loadCache(self) -> bool:
"""Load the cache file for recent projects.""" """Load the cache file for recent projects."""
self._data = {} self._data = {}
self._map = {}
cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE) cacheFile = self._conf.dataPath(nwFiles.RECENT_FILE)
if cacheFile.is_file(): if cacheFile.is_file():
try: try:
with open(cacheFile, mode="r", encoding="utf-8") as inFile: with open(cacheFile, mode="r", encoding="utf-8") as inFile:
data = json.load(inFile) data = json.load(inFile)
for path, entry in data.items(): for path, entry in data.items():
self._data[path] = { puuid = str(entry.get("uuid", ""))
"title": entry.get("title", ""), title = str(entry.get("title", ""))
"words": entry.get("words", 0), words = checkInt(entry.get("words", 0), 0)
"time": entry.get("time", 0), saved = checkInt(entry.get("time", 0), 0)
} if path and title:
self._setEntry(puuid, path, title, words, saved)
except Exception: except Exception:
logger.error("Could not load recent project cache") logger.error("Could not load recent project cache")
logException() logException()
return False return False
return True return True
def saveCache(self) -> bool: def saveCache(self) -> bool:
@@ -882,7 +887,6 @@ class RecentProjects:
logger.error("Could not save recent project cache") logger.error("Could not save recent project cache")
logException() logException()
return False return False
return True return True
def listEntries(self) -> list[tuple[str, str, int, int]]: def listEntries(self) -> list[tuple[str, str, int, int]]:
@@ -892,14 +896,15 @@ class RecentProjects:
for k, e in self._data.items() for k, e in self._data.items()
] ]
def update(self, path: str | Path, title: str, words: int, saved: float | int) -> None: def update(self, path: str | Path, data: NWProjectData, saved: float | int) -> None:
"""Add or update recent cache information on a given project.""" """Add or update recent cache information on a given project."""
self._data[str(path)] = { try:
"title": title, if (remove := self._map.get(data.uuid)) and (remove != str(path)):
"words": int(words), self.remove(remove)
"time": int(saved), self._setEntry(data.uuid, str(path), data.name, sum(data.currCounts), int(saved))
} self.saveCache()
self.saveCache() except Exception:
pass
return return
def remove(self, path: str | Path) -> None: def remove(self, path: str | Path) -> None:
@@ -909,6 +914,13 @@ class RecentProjects:
self.saveCache() self.saveCache()
return return
def _setEntry(self, puuid: str, path: str, title: str, words: int, saved: int) -> None:
"""Set an entry in the recent projects record."""
self._data[path] = {"uuid": puuid, "title": title, "words": words, "time": saved}
if puuid:
self._map[puuid] = path
return
class RecentPaths: class RecentPaths:
+2 -6
View File
@@ -353,9 +353,7 @@ class NWProject:
# Update recent projects # Update recent projects
if storePath := self._storage.storagePath: if storePath := self._storage.storagePath:
CONFIG.recentProjects.update( CONFIG.recentProjects.update(storePath, self._data, time())
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
@@ -421,9 +419,7 @@ class NWProject:
# Update recent projects # Update recent projects
if storagePath := self._storage.storagePath: if storagePath := self._storage.storagePath:
CONFIG.recentProjects.update( CONFIG.recentProjects.update(storagePath, self._data, saveTime)
storagePath, self._data.name, sum(self._data.currCounts), saveTime
)
SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name)) SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
self.setProjectChanged(False) self.setProjectChanged(False)
+22 -3
View File
@@ -31,6 +31,7 @@ import pytest
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.config import Config, RecentPaths, RecentProjects from novelwriter.config import Config, RecentPaths, RecentProjects
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.project import NWProject
from tests.mocked import MockApp, causeOSError from tests.mocked import MockApp, causeOSError
from tests.tools import cmpFiles, writeFile from tests.tools import cmpFiles, writeFile
@@ -382,7 +383,7 @@ def testBaseConfig_Internal(monkeypatch, fncPath):
@pytest.mark.base @pytest.mark.base
def testBaseConfig_RecentCache(monkeypatch, tstPaths): def testBaseConfig_RecentCache(monkeypatch, tstPaths, nwGUI):
"""Test recent cache file.""" """Test recent cache file."""
cacheFile = tstPaths.cnfDir / nwFiles.RECENT_FILE cacheFile = tstPaths.cnfDir / nwFiles.RECENT_FILE
recent = RecentProjects(CONFIG) recent = RecentProjects(CONFIG)
@@ -396,8 +397,18 @@ def testBaseConfig_RecentCache(monkeypatch, tstPaths):
pathOne = tstPaths.cnfDir / "projPathOne" / nwFiles.PROJ_FILE pathOne = tstPaths.cnfDir / "projPathOne" / nwFiles.PROJ_FILE
pathTwo = tstPaths.cnfDir / "projPathTwo" / nwFiles.PROJ_FILE pathTwo = tstPaths.cnfDir / "projPathTwo" / nwFiles.PROJ_FILE
recent.update(pathOne, "Proj One", 100, 1600002000) prjOne = NWProject()
recent.update(pathTwo, "Proj Two", 200, 1600005600) prjTwo = NWProject()
prjOne.data.setUuid(None)
prjTwo.data.setUuid(None)
prjOne.data.setName("Proj One")
prjTwo.data.setName("Proj Two")
prjOne.data.setCurrCounts(100, 0)
prjTwo.data.setCurrCounts(200, 0)
recent.update(pathOne, prjOne.data, 1600002000)
recent.update(pathTwo, prjTwo.data, 1600005600)
assert recent.listEntries() == [ assert recent.listEntries() == [
(str(pathOne), "Proj One", 100, 1600002000), (str(pathOne), "Proj One", 100, 1600002000),
(str(pathTwo), "Proj Two", 200, 1600005600), (str(pathTwo), "Proj Two", 200, 1600005600),
@@ -429,6 +440,14 @@ def testBaseConfig_RecentCache(monkeypatch, tstPaths):
(str(pathTwo), "Proj Two", 200, 1600005600), (str(pathTwo), "Proj Two", 200, 1600005600),
] ]
# Pass Invalid
recent.update(None, None, 0) # type: ignore
recent.update(None, None, 0) # type: ignore
assert recent.listEntries() == [
(str(pathOne), "Proj One", 100, 1600002000),
(str(pathTwo), "Proj Two", 200, 1600005600),
]
# Remove Non-Existent Entry # Remove Non-Existent Entry
recent.remove("stuff") recent.remove("stuff")
assert recent.listEntries() == [ assert recent.listEntries() == [
+13 -2
View File
@@ -31,6 +31,7 @@ from pytestqt.qtbot import QtBot
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.projectdata import NWProjectData
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.tools.welcome import GuiWelcome from novelwriter.tools.welcome import GuiWelcome
from novelwriter.types import QtMouseLeft from novelwriter.types import QtMouseLeft
@@ -72,8 +73,18 @@ def testToolWelcome_Open(qtbot: QtBot, monkeypatch, nwGUI, fncPath):
monkeypatch.setattr(QMenu, "exec", lambda *a: None) monkeypatch.setattr(QMenu, "exec", lambda *a: None)
monkeypatch.setattr(QMenu, "setParent", lambda *a: None) monkeypatch.setattr(QMenu, "setParent", lambda *a: None)
CONFIG.recentProjects.update("/stuff/project_one", "Project One", 12345, 1690000000) data1 = NWProjectData(SHARED.project)
CONFIG.recentProjects.update("/stuff/project_two", "Project Two", 54321, 1700000000) data2 = NWProjectData(SHARED.project)
data1.setUuid(None)
data2.setUuid(None)
data1.setName("Project One")
data2.setName("Project Two")
data1.setCurrCounts(12345, 0)
data2.setCurrCounts(54321, 0)
CONFIG.recentProjects.update("/stuff/project_one", data1, 1690000000)
CONFIG.recentProjects.update("/stuff/project_two", data2, 1700000000)
dateOne = CONFIG.localDate(datetime.fromtimestamp(1700000000)) dateOne = CONFIG.localDate(datetime.fromtimestamp(1700000000))
dateTwo = CONFIG.localDate(datetime.fromtimestamp(1690000000)) dateTwo = CONFIG.localDate(datetime.fromtimestamp(1690000000))