From 1b85f782a61fe4db79f067eedffc2f461bcfaae0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 13 Jun 2023 17:29:15 +0200 Subject: [PATCH] Update writing stats dialog to work with new log file --- novelwriter/common.py | 3 +- novelwriter/core/sessions.py | 16 +++++- novelwriter/tools/writingstats.py | 94 +++++++++++++------------------ 3 files changed, 54 insertions(+), 59 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 739c9e53..46a18c85 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -104,8 +104,7 @@ def checkBool(value: Any, default: bool) -> bool: def checkHandle(value, default, allowNone=False): - """Check if a value is a handle. - """ + """Check if a value is a handle.""" if allowNone and (value is None or value == "None"): return None if isHandle(value): diff --git a/novelwriter/core/sessions.py b/novelwriter/core/sessions.py index a071e0fb..0f3dfcf0 100644 --- a/novelwriter/core/sessions.py +++ b/novelwriter/core/sessions.py @@ -27,7 +27,7 @@ import json import logging from time import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Iterator from pathlib import Path from novelwriter.error import logException @@ -109,6 +109,20 @@ class NWSessionLog: return True + def iterRecords(self) -> Iterator[dict]: + """Iterate through all records in the log.""" + sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE) + if not isinstance(sessFile, Path): + return + try: + with open(sessFile, mode="r", encoding="utf-8") as fObj: + for line in fObj: + yield json.loads(line) + except Exception: + logger.error("Failed to process session stats file") + logException() + return + def createInitial(self, total: int) -> str: """Low level function to create the initial log file record.""" data = json.dumps({"type": "initial", "offset": total}) diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index ce7fc184..d4223e60 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -1,7 +1,6 @@ """ novelWriter – GUI Writing Statistics ==================================== -GUI class for the session statistics dialog File History: Created: 2019-10-20 [0.3] @@ -22,12 +21,13 @@ 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 . """ +from __future__ import annotations import json import logging -from pathlib import Path from datetime import datetime +from typing import TYPE_CHECKING from PyQt5.QtGui import QPixmap, QCursor from PyQt5.QtCore import Qt @@ -40,13 +40,20 @@ from novelwriter import CONFIG from novelwriter.enum import nwAlert from novelwriter.error import formatException from novelwriter.common import formatTime, checkInt, checkIntTuple, minmax -from novelwriter.constants import nwConst, nwFiles +from novelwriter.constants import nwConst from novelwriter.extensions.switch import NSwitch +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) class GuiWritingStats(QDialog): + """GUI Tools: Writing Statistics + + Displays data from the NWSessionLog object. + """ C_TIME = 0 C_LENGTH = 1 @@ -57,7 +64,7 @@ class GuiWritingStats(QDialog): FMT_JSON = 0 FMT_CSV = 1 - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain): super().__init__(parent=mainGui) logger.debug("Create: GuiWritingStats") @@ -295,8 +302,7 @@ class GuiWritingStats(QDialog): return def populateGUI(self): - """Populate list box with data from the log file. - """ + """Populate list box with data from the log file.""" qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) self._loadLogFile() self._updateListBox() @@ -308,8 +314,7 @@ class GuiWritingStats(QDialog): ## def _doClose(self): - """Save the state of the window, clear cache, end close. - """ + """Save the state of the window, clear cache, end close.""" self.logData = [] winWidth = CONFIG.rpxInt(self.width()) @@ -350,8 +355,7 @@ class GuiWritingStats(QDialog): return def _saveData(self, dataFmt): - """Save the content of the list box to a file. - """ + """Save the content of the list box to a file.""" fileExt = "" textFmt = "" @@ -424,8 +428,7 @@ class GuiWritingStats(QDialog): ## def _loadLogFile(self): - """Load the content of the log file into a buffer. - """ + """Load the content of the log file into a buffer.""" logger.debug("Loading session log file") self.logData = [] @@ -436,50 +439,30 @@ class GuiWritingStats(QDialog): ttTime = 0 ttIdle = 0 - logFile = self.theProject.storage.getMetaFile(nwFiles.SESS_STATS) - if not isinstance(logFile, Path) or not logFile.exists(): - logger.info("This project has no writing stats logfile") - return False + for record in self.theProject.session.iterRecords(): + rType = record.get("type") + if rType == "initial": + self.wordOffset = checkInt(record.get("offset"), 0) + logger.debug("Initial word count when log was started is %d" % self.wordOffset) + elif rType == "record": + try: + dStart = datetime.fromisoformat(str(record.get("start"))) + dEnd = datetime.fromisoformat(str(record.get("end"))) + except Exception: + logger.error("Invalid session log record") + continue + wcNovel = checkInt(record.get("novel"), 0) + wcNotes = checkInt(record.get("notes"), 0) + sIdle = checkInt(record.get("idle"), 0) - try: - with open(logFile, mode="r", encoding="utf-8") as inFile: - for inLine in inFile: - if inLine.startswith("#"): - if inLine.startswith("# Offset"): - self.wordOffset = checkInt(inLine[9:].strip(), 0) - logger.debug( - "Initial word count when log was started is %d" % self.wordOffset - ) - continue + tDiff = dEnd - dStart + sDiff = tDiff.total_seconds() + ttTime += sDiff + ttIdle += sIdle + ttNovel = wcNovel + ttNotes = wcNotes - inData = inLine.split() - if len(inData) < 6: - continue - - dStart = datetime.fromisoformat(" ".join(inData[0:2])) - dEnd = datetime.fromisoformat(" ".join(inData[2:4])) - - sIdle = 0 - if len(inData) > 6: - sIdle = checkInt(inData[6], 0) - - tDiff = dEnd - dStart - sDiff = tDiff.total_seconds() - ttTime += sDiff - ttIdle += sIdle - - wcNovel = int(inData[4]) - wcNotes = int(inData[5]) - ttNovel = wcNovel - ttNotes = wcNotes - - self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle)) - - except Exception as exc: - self.mainGui.makeAlert(self.tr( - "Failed to read session log file." - ), nwAlert.ERROR, exception=exc) - return False + self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle)) ttWords = ttNovel + ttNotes self.labelTotal.setText(formatTime(round(ttTime))) @@ -495,8 +478,7 @@ class GuiWritingStats(QDialog): ## def _updateListBox(self): - """Load/reload the content of the list box. - """ + """Load/reload the content of the list box.""" self.listBox.clear() self.timeFilter = 0.0