Move session log handling to a separate class, and use jsonl format
This commit is contained in:
@@ -91,6 +91,7 @@ class nwFiles:
|
|||||||
OPTS_FILE = "options.json"
|
OPTS_FILE = "options.json"
|
||||||
PROJ_DICT = "wordlist.txt"
|
PROJ_DICT = "wordlist.txt"
|
||||||
SESS_STATS = "sessionStats.log"
|
SESS_STATS = "sessionStats.log"
|
||||||
|
SESS_FILE = "sessions.jsonl"
|
||||||
|
|
||||||
# END Class nwFiles
|
# END Class nwFiles
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
novelWriter – Project Document Tools
|
novelWriter – Project Document Tools
|
||||||
====================================
|
====================================
|
||||||
A collection of tools to create and manipulate documents
|
|
||||||
|
|
||||||
File History:
|
File History:
|
||||||
Created: 2022-10-02 [2.0rc1] DocMerger
|
Created: 2022-10-02 [2.0rc1] DocMerger
|
||||||
@@ -28,7 +27,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from time import time
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication
|
from PyQt5.QtCore import QCoreApplication
|
||||||
@@ -319,7 +317,7 @@ class ProjectBuilder:
|
|||||||
project.data.setTitle(projTitle)
|
project.data.setTitle(projTitle)
|
||||||
project.data.setAuthor(projAuthor)
|
project.data.setAuthor(projAuthor)
|
||||||
project.setDefaultStatusImport()
|
project.setDefaultStatusImport()
|
||||||
project._projOpened = int(time())
|
project.session.startSession()
|
||||||
|
|
||||||
# Add Root Folders
|
# Add Root Folders
|
||||||
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
|
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
|
||||||
|
|||||||
+17
-58
@@ -22,6 +22,7 @@ General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -33,9 +34,10 @@ from functools import partial
|
|||||||
from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal
|
from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal
|
||||||
|
|
||||||
from novelwriter import CONFIG, __version__, __hexversion__
|
from novelwriter import CONFIG, __version__, __hexversion__
|
||||||
|
from novelwriter.core.sessions import NWSessionLog
|
||||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
|
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
from novelwriter.constants import trConst, nwFiles, nwLabels
|
from novelwriter.constants import trConst, nwLabels
|
||||||
from novelwriter.core.tree import NWTree
|
from novelwriter.core.tree import NWTree
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.index import NWIndex
|
from novelwriter.core.index import NWIndex
|
||||||
@@ -66,12 +68,12 @@ class NWProject(QObject):
|
|||||||
self._data = NWProjectData(self) # The project settings
|
self._data = NWProjectData(self) # The project settings
|
||||||
self._tree = NWTree(self) # The project tree
|
self._tree = NWTree(self) # The project tree
|
||||||
self._index = NWIndex(self) # The projecty index
|
self._index = NWIndex(self) # The projecty index
|
||||||
|
self._session = NWSessionLog(self) # The session record
|
||||||
|
|
||||||
# Data Cache
|
# Data Cache
|
||||||
self._langData = {} # Localisation data
|
self._langData = {} # Localisation data
|
||||||
|
|
||||||
# Project Status
|
# Project Status
|
||||||
self._projOpened = 0 # The time stamp of when the project file was opened
|
|
||||||
self._projChanged = False # The project has unsaved changes
|
self._projChanged = False # The project has unsaved changes
|
||||||
self._lockedBy = None # Data on which computer has the project open
|
self._lockedBy = None # Data on which computer has the project open
|
||||||
self._projFiles = [] # A list of all files in the content folder on load
|
self._projFiles = [] # A list of all files in the content folder on load
|
||||||
@@ -108,9 +110,13 @@ class NWProject(QObject):
|
|||||||
def index(self):
|
def index(self):
|
||||||
return self._index
|
return self._index
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self) -> NWSessionLog:
|
||||||
|
return self._session
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def projOpened(self):
|
def projOpened(self):
|
||||||
return self._projOpened
|
return self._session.start
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def projChanged(self):
|
def projChanged(self):
|
||||||
@@ -228,7 +234,6 @@ class NWProject(QObject):
|
|||||||
default values.
|
default values.
|
||||||
"""
|
"""
|
||||||
# Project Status
|
# Project Status
|
||||||
self._projOpened = 0
|
|
||||||
self._projChanged = False
|
self._projChanged = False
|
||||||
|
|
||||||
# Project Tree
|
# Project Tree
|
||||||
@@ -236,6 +241,7 @@ class NWProject(QObject):
|
|||||||
self._tree.clear()
|
self._tree.clear()
|
||||||
self._index.clearIndex()
|
self._index.clearIndex()
|
||||||
self._data = NWProjectData(self)
|
self._data = NWProjectData(self)
|
||||||
|
self._session = NWSessionLog(self)
|
||||||
|
|
||||||
# Project Settings
|
# Project Settings
|
||||||
self._projFiles = []
|
self._projFiles = []
|
||||||
@@ -370,8 +376,7 @@ class NWProject(QObject):
|
|||||||
self._index.rebuildIndex()
|
self._index.rebuildIndex()
|
||||||
|
|
||||||
self.updateWordCounts()
|
self.updateWordCounts()
|
||||||
self._projOpened = time()
|
self._session.startSession()
|
||||||
|
|
||||||
self._storage.writeLockFile()
|
self._storage.writeLockFile()
|
||||||
self.setProjectChanged(False)
|
self.setProjectChanged(False)
|
||||||
self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self._data.name))
|
self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self._data.name))
|
||||||
@@ -407,7 +412,7 @@ class NWProject(QObject):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
saveTime = time()
|
saveTime = time()
|
||||||
editTime = int(self._data.editTime + saveTime - self._projOpened)
|
editTime = self._data.editTime + max(round(saveTime - self._session.start), 0)
|
||||||
content = self._tree.pack()
|
content = self._tree.pack()
|
||||||
if not xmlWriter.write(self._data, content, saveTime, editTime):
|
if not xmlWriter.write(self._data, content, saveTime, editTime):
|
||||||
self.mainGui.makeAlert(self.tr(
|
self.mainGui.makeAlert(self.tr(
|
||||||
@@ -437,7 +442,7 @@ class NWProject(QObject):
|
|||||||
logger.info("Closing project")
|
logger.info("Closing project")
|
||||||
self._options.saveSettings()
|
self._options.saveSettings()
|
||||||
self._tree.writeToCFile()
|
self._tree.writeToCFile()
|
||||||
self._appendSessionStats(idleTime)
|
self._session.appendSession(idleTime)
|
||||||
self._storage.clearLockFile()
|
self._storage.clearLockFile()
|
||||||
self._storage.closeSession()
|
self._storage.closeSession()
|
||||||
self.clearProject()
|
self.clearProject()
|
||||||
@@ -560,18 +565,17 @@ class NWProject(QObject):
|
|||||||
# Getters
|
# Getters
|
||||||
##
|
##
|
||||||
|
|
||||||
def getLockStatus(self):
|
def getLockStatus(self) -> list | None:
|
||||||
"""Return the project lock information for the project.
|
"""Return the project lock information for the project."""
|
||||||
"""
|
|
||||||
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
|
if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4:
|
||||||
return self._lockedBy
|
return self._lockedBy
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def getCurrentEditTime(self):
|
def getCurrentEditTime(self) -> int:
|
||||||
"""Get the total project edit time, including the time spent in
|
"""Get the total project edit time, including the time spent in
|
||||||
the current session.
|
the current session.
|
||||||
"""
|
"""
|
||||||
return round(self._data.editTime + time() - self._projOpened)
|
return self._data.editTime + round(time() - self._session.start)
|
||||||
|
|
||||||
def getProjectItems(self):
|
def getProjectItems(self):
|
||||||
"""This function ensures that the item tree loaded is sent to
|
"""This function ensures that the item tree loaded is sent to
|
||||||
@@ -798,49 +802,4 @@ class NWProject(QObject):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _appendSessionStats(self, idleTime):
|
|
||||||
"""Append session statistics to the sessions log file.
|
|
||||||
"""
|
|
||||||
sessionFile = self._storage.getMetaFile(nwFiles.SESS_STATS)
|
|
||||||
if not isinstance(sessionFile, Path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
nowTime = time()
|
|
||||||
iNovel, iNotes = self._data.initCounts
|
|
||||||
cNovel, cNotes = self._data.currCounts
|
|
||||||
iTotal = iNovel + iNotes
|
|
||||||
sessDiff = cNovel + cNotes - iTotal
|
|
||||||
sessTime = nowTime - self._projOpened
|
|
||||||
|
|
||||||
logger.info("The session lasted %d sec and added %d words", int(sessTime), sessDiff)
|
|
||||||
if sessTime < 300 and sessDiff == 0:
|
|
||||||
logger.info("Session too short, skipping log entry")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
isFile = sessionFile.exists() # We must save the state before we open
|
|
||||||
with open(sessionFile, mode="a+", encoding="utf-8") as outFile:
|
|
||||||
if not isFile:
|
|
||||||
# It's a new file, so add a header
|
|
||||||
if iTotal > 0:
|
|
||||||
outFile.write("# Offset %d\n" % iTotal)
|
|
||||||
outFile.write("# %-17s %-19s %8s %8s %8s\n" % (
|
|
||||||
"Start Time", "End Time", "Novel", "Notes", "Idle"
|
|
||||||
))
|
|
||||||
|
|
||||||
outFile.write("%-19s %-19s %8d %8d %8d\n" % (
|
|
||||||
formatTimeStamp(self._projOpened),
|
|
||||||
formatTimeStamp(nowTime),
|
|
||||||
cNovel,
|
|
||||||
cNotes,
|
|
||||||
int(idleTime),
|
|
||||||
))
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.error("Failed to write session stats file")
|
|
||||||
logException()
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
# END Class NWProject
|
# END Class NWProject
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – Project Session Log Class
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
File History:
|
||||||
|
Created: 2023-06-11 [2.1b1]
|
||||||
|
|
||||||
|
This file is a part of novelWriter
|
||||||
|
Copyright 2018–2023, Veronica Berglyd Olsen
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from novelwriter.error import logException
|
||||||
|
from novelwriter.common import formatTimeStamp
|
||||||
|
from novelwriter.constants import nwFiles
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from novelwriter.core.project import NWProject
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NWSessionLog:
|
||||||
|
"""Core: Session JSON Lines Log File
|
||||||
|
|
||||||
|
The class that wraps the session log file, which is in JSON Lines
|
||||||
|
format. That is, one JSON object per line.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, project: NWProject):
|
||||||
|
self._project = project
|
||||||
|
self._start = 0.0
|
||||||
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Properties
|
||||||
|
##
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start(self) -> float:
|
||||||
|
"""The session start time."""
|
||||||
|
return self._start
|
||||||
|
|
||||||
|
##
|
||||||
|
# Methods
|
||||||
|
##
|
||||||
|
|
||||||
|
def startSession(self):
|
||||||
|
"""Start the writng session."""
|
||||||
|
self._start = time()
|
||||||
|
return
|
||||||
|
|
||||||
|
def appendSession(self, idleTime: float) -> bool:
|
||||||
|
"""Append session statistics to the sessions log file."""
|
||||||
|
sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE)
|
||||||
|
if not isinstance(sessFile, Path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = time()
|
||||||
|
iNovel, iNotes = self._project.data.initCounts
|
||||||
|
cNovel, cNotes = self._project.data.currCounts
|
||||||
|
iTotal = iNovel + iNotes
|
||||||
|
wDiff = cNovel + cNotes - iTotal
|
||||||
|
sTime = now - self._start
|
||||||
|
|
||||||
|
logger.info("The session lasted %d sec and added %d words", int(sTime), wDiff)
|
||||||
|
if sTime < 300 and wDiff == 0:
|
||||||
|
logger.info("Session too short, skipping log entry")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not sessFile.exists():
|
||||||
|
with open(sessFile, mode="w", encoding="utf-8") as fObj:
|
||||||
|
fObj.write(self.createInitial(iTotal))
|
||||||
|
|
||||||
|
with open(sessFile, mode="a+", encoding="utf-8") as fObj:
|
||||||
|
fObj.write(self.createRecord(
|
||||||
|
start=formatTimeStamp(self._start),
|
||||||
|
end=formatTimeStamp(now),
|
||||||
|
novel=cNovel,
|
||||||
|
notes=cNotes,
|
||||||
|
idle=round(idleTime)
|
||||||
|
))
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.error("Failed to write to session stats file")
|
||||||
|
logException()
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def createInitial(self, total: int) -> str:
|
||||||
|
"""Low level function to create the initial log file record."""
|
||||||
|
data = json.dumps({"type": "initial", "offset": total})
|
||||||
|
return f"{data}\n"
|
||||||
|
|
||||||
|
def createRecord(self, start: str, end: str, novel: int, notes: int, idle: int) -> str:
|
||||||
|
"""Low level function to create a log record."""
|
||||||
|
data = json.dumps({
|
||||||
|
"type": "record", "start": start, "end": end,
|
||||||
|
"novel": novel, "notes": notes, "idle": idle,
|
||||||
|
})
|
||||||
|
return f"{data}\n"
|
||||||
|
|
||||||
|
# END Class NWSessionLog
|
||||||
@@ -254,7 +254,7 @@ class NWStorage:
|
|||||||
(baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
|
(baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
|
||||||
(baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
|
(baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
|
||||||
(baseMeta / nwFiles.PROJ_DICT, f"meta/{nwFiles.PROJ_DICT}"),
|
(baseMeta / nwFiles.PROJ_DICT, f"meta/{nwFiles.PROJ_DICT}"),
|
||||||
(baseMeta / nwFiles.SESS_STATS, f"meta/{nwFiles.SESS_STATS}"),
|
(baseMeta / nwFiles.SESS_FILE, f"meta/{nwFiles.SESS_FILE}"),
|
||||||
]
|
]
|
||||||
for contItem in baseCont.iterdir():
|
for contItem in baseCont.iterdir():
|
||||||
name = contItem.name
|
name = contItem.name
|
||||||
@@ -374,6 +374,10 @@ class NWStorage:
|
|||||||
|
|
||||||
def _deprecatedFiles(self, path: Path):
|
def _deprecatedFiles(self, path: Path):
|
||||||
"""Handle files that are no longer used by novelWriter."""
|
"""Handle files that are no longer used by novelWriter."""
|
||||||
|
sessLog = path / "meta" / "sessionStats.log"
|
||||||
|
if sessLog.is_file():
|
||||||
|
self._convertOldLogFile(sessLog, path / "meta" / nwFiles.SESS_FILE)
|
||||||
|
|
||||||
remove = [
|
remove = [
|
||||||
path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1
|
path / "meta" / "tagsIndex.json", # Renamed in 2.1 Beta 1
|
||||||
path / "meta" / "mainOptions.json", # Replaced in 0.5
|
path / "meta" / "mainOptions.json", # Replaced in 0.5
|
||||||
@@ -409,4 +413,45 @@ class NWStorage:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _convertOldLogFile(self, sessLog: Path, sessJson: Path) -> bool:
|
||||||
|
"""Convert the old text log file format to the new JSON Lines
|
||||||
|
format.
|
||||||
|
"""
|
||||||
|
if sessJson.exists() or not sessLog.exists():
|
||||||
|
# If the new file already exists, we won't overwrite it
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = []
|
||||||
|
offset = 0
|
||||||
|
session = self._project.session
|
||||||
|
with open(sessLog, mode="r", encoding="utf-8") as fObj:
|
||||||
|
for record in fObj:
|
||||||
|
bits = record.split()
|
||||||
|
nBits = len(bits)
|
||||||
|
if record.startswith("# Offset") and nBits == 3:
|
||||||
|
offset = int(bits[2])
|
||||||
|
elif not record.startswith("#") and nBits > 5:
|
||||||
|
data.append(session.createRecord(
|
||||||
|
start=f"{bits[0]} {bits[1]}",
|
||||||
|
end=f"{bits[2]} {bits[3]}",
|
||||||
|
novel=int(bits[4]),
|
||||||
|
notes=int(bits[5]),
|
||||||
|
idle=int(bits[6]) if nBits > 6 else -1,
|
||||||
|
))
|
||||||
|
|
||||||
|
with open(sessJson, mode="a+", encoding="utf-8") as fObj:
|
||||||
|
fObj.write(session.createInitial(offset))
|
||||||
|
fObj.write("".join(data))
|
||||||
|
|
||||||
|
# If we're here, we remove the old file
|
||||||
|
sessLog.unlink()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.error("Failed to convert old stats file")
|
||||||
|
logException()
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
# END Class NWStorage
|
# END Class NWStorage
|
||||||
|
|||||||
@@ -487,7 +487,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
|
|
||||||
# Edit Time
|
# Edit Time
|
||||||
theProject.data.setEditTime(1234)
|
theProject.data.setEditTime(1234)
|
||||||
theProject._projOpened = 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.getCurrentEditTime() == 6834
|
||||||
@@ -585,32 +585,32 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
|||||||
# No path for writing
|
# No path for writing
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
|
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
|
||||||
assert theProject._appendSessionStats(idleTime=0) is False
|
assert theProject.session.appendSession(idleTime=0) is False
|
||||||
|
|
||||||
# Block open
|
# Block open
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("builtins.open", causeOSError)
|
mp.setattr("builtins.open", causeOSError)
|
||||||
assert theProject._appendSessionStats(idleTime=0) is False
|
assert theProject.session.appendSession(idleTime=0) is False
|
||||||
|
|
||||||
# Session too short
|
# Session too short
|
||||||
theProject._projOpened = time()
|
theProject._session._start = time()
|
||||||
theProject.data.setInitCounts(50, 50)
|
theProject.data.setInitCounts(50, 50)
|
||||||
theProject.data.setCurrCounts(50, 50)
|
theProject.data.setCurrCounts(50, 50)
|
||||||
assert theProject._appendSessionStats(idleTime=0) is False
|
assert theProject.session.appendSession(idleTime=0) is False
|
||||||
|
|
||||||
# Write entry
|
# Write entry
|
||||||
statsFile = theProject.storage.getMetaFile(nwFiles.SESS_STATS)
|
statsFile = theProject.storage.getMetaFile(nwFiles.SESS_FILE)
|
||||||
assert isinstance(statsFile, Path)
|
assert isinstance(statsFile, Path)
|
||||||
if statsFile.exists():
|
if statsFile.exists():
|
||||||
statsFile.unlink()
|
statsFile.unlink()
|
||||||
|
|
||||||
theProject._projOpened = 1600002000
|
theProject._session._start = 1600002000
|
||||||
theProject.data._initCounts = [50, 50]
|
theProject.data._initCounts = [50, 50]
|
||||||
theProject.data._currCounts = [200, 100]
|
theProject.data._currCounts = [200, 100]
|
||||||
|
|
||||||
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._appendSessionStats(idleTime=99)
|
assert theProject.session.appendSession(idleTime=99)
|
||||||
|
|
||||||
assert statsFile.read_text(encoding="utf-8") == (
|
assert statsFile.read_text(encoding="utf-8") == (
|
||||||
"# Offset 100\n"
|
"# Offset 100\n"
|
||||||
|
|||||||
+1
-2
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -201,7 +200,7 @@ def buildTestProject(theObject, projPath):
|
|||||||
aDoc.writeDocument("### %s\n\n" % theProject.tr("New Scene"))
|
aDoc.writeDocument("### %s\n\n" % theProject.tr("New Scene"))
|
||||||
theProject.index.reIndexHandle(xHandle[8])
|
theProject.index.reIndexHandle(xHandle[8])
|
||||||
|
|
||||||
theProject._projOpened = time.time()
|
theProject.session.startSession()
|
||||||
theProject.setProjectChanged(True)
|
theProject.setProjectChanged(True)
|
||||||
theProject.saveProject(autoSave=True)
|
theProject.saveProject(autoSave=True)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user