Merge pull request #453 from vkbo/test_coverage

Even More Test Coverage
This commit is contained in:
Veronica K. Berglyd Olsen
2020-10-01 23:21:50 +02:00
committed by GitHub
82 changed files with 4447 additions and 1267 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
- name: Python Setup
uses: actions/setup-python@v1
with:
python-version: 3.7
python-version: 3
architecture: x64
- name: Checkout Source
uses: actions/checkout@v2
@@ -1,4 +1,4 @@
name: python 3.6
name: Linux (3.6)
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [ main, dev ]
jobs:
pyTest36:
testLinux36:
runs-on: ubuntu-latest
steps:
- name: Python Setup
@@ -18,16 +18,16 @@ jobs:
- name: Install Packages
run: |
sudo apt update
sudo apt install xvfb libenchant-dev qt5-default
sudo apt install libenchant-dev qt5-default
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install PyVirtualDisplay
pip install pytest-timeout
pip install pytest-xvfb
pip install pytest-qt
- name: Run Tests
run: xvfb-run pytest -v --timeout=60
run: |
export QT_QPA_PLATFORM=offscreen
pytest -v --timeout=60
@@ -1,4 +1,4 @@
name: python 3.7
name: Linux (3.7)
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [ main, dev ]
jobs:
pyTest37:
testLinux37:
runs-on: ubuntu-latest
steps:
- name: Python Setup
@@ -18,16 +18,16 @@ jobs:
- name: Install Packages
run: |
sudo apt update
sudo apt install xvfb libenchant-dev qt5-default
sudo apt install libenchant-dev qt5-default
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install PyVirtualDisplay
pip install pytest-timeout
pip install pytest-xvfb
pip install pytest-qt
- name: Run Tests
run: xvfb-run pytest -v --timeout=60
run: |
export QT_QPA_PLATFORM=offscreen
pytest -v --timeout=60
@@ -1,4 +1,4 @@
name: python 3.8
name: Linux (3.8)
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [ main, dev ]
jobs:
pyTest38Cov:
testLinux38:
runs-on: ubuntu-latest
steps:
- name: Python Setup
@@ -18,20 +18,20 @@ jobs:
- name: Install Packages
run: |
sudo apt update
sudo apt install xvfb libenchant-dev qt5-default
sudo apt install libenchant-dev qt5-default
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install PyVirtualDisplay
pip install pytest-timeout
pip install pytest-cov
pip install pytest-xvfb
pip install pytest-qt
pip install codecov
- name: Run Tests
run: xvfb-run pytest -v --cov=nw --timeout=60
run: |
export QT_QPA_PLATFORM=offscreen
pytest -v --cov=nw --timeout=60
- name: Upload to Codecov
uses: codecov/codecov-action@v1
+36
View File
@@ -0,0 +1,36 @@
name: macOS (3.8)
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
jobs:
testMac38:
runs-on: macos-latest
steps:
- name: Python Setup
uses: actions/setup-python@v1
with:
python-version: 3.8
architecture: x64
- name: Install Packages
run: |
brew install enchant
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-timeout
pip install pytest-cov
pip install pytest-qt
pip install codecov
- name: Run Tests
run: |
export QT_QPA_PLATFORM=offscreen
pytest -v --cov=nw --timeout=60
- name: Upload to Codecov
uses: codecov/codecov-action@v1
+32
View File
@@ -0,0 +1,32 @@
name: Windows (3.8)
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
jobs:
testWin38:
runs-on: windows-latest
steps:
- name: Python Setup
uses: actions/setup-python@v1
with:
python-version: 3.8
architecture: x64
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest-timeout
pip install pytest-cov
pip install pytest-qt
pip install codecov
- name: Run Tests
run: |
pytest -v --cov=nw --timeout=60
- name: Upload to Codecov
uses: codecov/codecov-action@v1
+6 -2
View File
@@ -8,11 +8,12 @@
# Documentation
/docs/build/
novelWriter.qch
novelWriter.qhc
*.qch
*.qhc
# Python Temp
__pycache__
*.pyc
# Sample Project
/nw/assets/sample.zip
@@ -27,6 +28,9 @@ __pycache__
/tests/temp
/tests/lipsum/cache
/tests/lipsum/meta
/tests/minimal/cache
/tests/minimal/meta
/tests/oldproj/cache
/.pytest_cache
/pytestdebug.log
+1 -1
View File
@@ -4,7 +4,7 @@ codecov:
coverage:
precision: 2
round: nearest
range: "70...90"
range: "70...95"
status:
project:
+22 -48
View File
@@ -9,7 +9,7 @@
Created: 2018-09-22 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -29,8 +29,6 @@ import sys
import getopt
import logging
from os import path, remove, rename
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QErrorMessage
@@ -97,15 +95,13 @@ def main(sysArgs=None):
sysArgs = sys.argv[1:]
# Valid Input Options
shortOpt = "hvq"
shortOpt = "hv"
longOpt = [
"help",
"version",
"info",
"debug",
"verbose",
"quiet",
"logfile=",
"style=",
"config=",
"data=",
@@ -127,8 +123,6 @@ def main(sysArgs=None):
" --info Print additional runtime information.\n"
" --debug Print debug output. Includes --info.\n"
" --verbose Increase verbosity of debug output. Includes --debug.\n"
" -q, --quiet Disable output to command line. Does not affect log file.\n"
" --logfile= Specify log file.\n"
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n"
" --data= Alternative user data path.\n"
@@ -143,9 +137,6 @@ def main(sysArgs=None):
# Defaults
debugLevel = logging.WARN
logFormat = "{levelname:8} {message:}"
logFile = ""
toFile = False
toStd = True
confPath = None
dataPath = None
testMode = False
@@ -166,22 +157,17 @@ def main(sysArgs=None):
for inOpt, inArg in inOpts:
if inOpt in ("-h", "--help"):
print(helpMsg)
sys.exit()
sys.exit(0)
elif inOpt in ("-v", "--version"):
print("novelWriter %s Version %s [%s]" % (
__status__, __version__, __date__)
)
sys.exit()
sys.exit(0)
elif inOpt == "--info":
debugLevel = logging.INFO
elif inOpt == "--debug":
debugLevel = logging.DEBUG
logFormat = "[{asctime:}] {name:>22}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt == "--logfile":
logFile = inArg
toFile = True
elif inOpt in ("-q", "--quiet"):
toStd = False
elif inOpt == "--verbose":
debugLevel = VERBOSE
logFormat = "[{asctime:}] {name:>22}:{lineno:<4d} {levelname:8} {message:}"
@@ -201,23 +187,10 @@ def main(sysArgs=None):
# Set Logging
logFmt = logging.Formatter(fmt=logFormat, style="{")
if not logFile == "" and toFile:
if path.isfile(logFile+".bak"):
remove(logFile+".bak")
if path.isfile(logFile):
rename(logFile, logFile+".bak")
fHandle = logging.FileHandler(logFile)
fHandle.setLevel(debugLevel)
fHandle.setFormatter(logFmt)
logger.addHandler(fHandle)
if toStd:
cHandle = logging.StreamHandler()
cHandle.setLevel(debugLevel)
cHandle.setFormatter(logFmt)
logger.addHandler(cHandle)
cHandle = logging.StreamHandler()
cHandle.setLevel(debugLevel)
cHandle.setFormatter(logFmt)
logger.addHandler(cHandle)
logger.setLevel(debugLevel)
logger.info("Starting novelWriter %s (%s) %s" % (
@@ -250,19 +223,20 @@ def main(sysArgs=None):
errorData.append("Python module 'lxml' is missing.")
if errorData:
errApp = QApplication([])
errMsg = QErrorMessage()
errMsg.resize(500, 300)
errMsg.showMessage((
"<h3>A critical error has been encountered</h3>"
"<p>novelWriter cannot start due to the following issues:<p>"
"<p>&nbsp;-&nbsp;%s</p>"
"<p>Shutting down ...</p>"
) % (
"<br>&nbsp;-&nbsp;".join(errorData)
))
errApp.exec_()
sys.exit(1)
if not testMode:
errApp = QApplication([])
errMsg = QErrorMessage()
errMsg.resize(500, 300)
errMsg.showMessage((
"<h3>A critical error has been encountered</h3>"
"<p>novelWriter cannot start due to the following issues:<p>"
"<p>&nbsp;-&nbsp;%s</p>"
"<p>Shutting down ...</p>"
) % (
"<br>&nbsp;-&nbsp;".join(errorData)
))
errApp.exec_()
sys.exit(10 + len(errorData))
# Finish initialising config
CONFIG.initConfig(confPath, dataPath)
+1 -1
View File
@@ -9,7 +9,7 @@
Created: 2019-05-12 [0.1.0]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
+60 -62
View File
@@ -9,7 +9,7 @@
Created: 2018-09-22 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -29,8 +29,8 @@ import logging
import configparser
import json
import sys
import os
from os import path, mkdir, unlink, rename
from time import time
from shutil import which
@@ -54,45 +54,44 @@ class Config:
# Set Application Variables
self.appName = "novelWriter"
self.appHandle = self.appName.lower()
self.cmdOpen = None
# Debug Settings
self.showGUI = True
self.debugInfo = False
self.showGUI = True # Allow blocking the GUI (disabled for testing)
self.debugInfo = False # True if log level is DEBUG or VERBOSE
# Config Error Handling
self.hasError = False
self.errData = []
self.hasError = False # True if the config class encountered an error
self.errData = [] # List of error messages
# Set Paths
self.confPath = None
self.confFile = None
self.dataPath = None
self.homePath = None
self.lastPath = None
self.appPath = None
self.appRoot = None
self.appIcon = None
self.assetPath = None
self.themeRoot = None
self.graphPath = None
self.dictPath = None
self.iconPath = None
self.helpPath = None
self.cmdOpen = None # Path from command line for project to be opened on launch
self.confPath = None # Folder where the config is saved
self.confFile = None # The config file name
self.dataPath = None # Folder where app data is stored
self.homePath = None # The user's home folder
self.lastPath = None # The last user-selected folder (browse dialogs)
self.appPath = None # The full path to the novelwriter package folder
self.appRoot = None # The full path to the novelwriter root folder
self.appIcon = None # The full path to the novelwriter icon file
self.assetPath = None # The full path to the nw/assets folder
self.themeRoot = None # The full path to the nw/assets/themes folder
self.dictPath = None # The full path to the nw/assets/dict folder
self.iconPath = None # The full path to the nw/assets/icons folder
self.helpPath = None # The full path to the novelwriter .qhc help file
# Runtime Settings and Variables
self.confChanged = False
self.hasHelp = False
self.confChanged = False # True whenever the config has chenged, false after save
self.hasHelp = False # True if the Qt help files are present in the assets folder
## General
self.guiTheme = "default"
self.guiSyntax = "default_light"
self.guiIcons = "typicons_colour_light"
self.guiDark = False
self.guiLang = "en" # Hardcoded for now since the GUI is only in English
self.guiFont = ""
self.guiDark = False # Load icons for dark backgrounds, if available
self.guiLang = "en" # Hardcoded for now since the GUI is only in English
self.guiFont = "" # Defaults to system defualt font
self.guiFontSize = 11
self.guiScale = 1.0 # Set automatically by Theme class
self.guiScale = 1.0 # Set automatically by Theme class
## Sizes
self.winGeometry = [1100, 650]
@@ -239,7 +238,7 @@ class Config:
logger.debug("Initialising Config ...")
if confPath is None:
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
self.confPath = path.join(path.abspath(confRoot), self.appHandle)
self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle)
else:
logger.info("Setting config from alternative path: %s" % confPath)
self.confPath = confPath
@@ -249,7 +248,7 @@ class Config:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
else:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
self.dataPath = path.join(path.abspath(dataRoot), self.appHandle)
self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle)
else:
logger.info("Setting data path from alternative path: %s" % dataPath)
self.dataPath = dataPath
@@ -258,25 +257,24 @@ class Config:
logger.verbose("Data path: %s" % self.dataPath)
self.confFile = self.appHandle+".conf"
self.homePath = path.expanduser("~")
self.homePath = os.path.expanduser("~")
self.lastPath = self.homePath
self.appPath = getattr(sys, "_MEIPASS", path.abspath(path.dirname(__file__)))
self.appRoot = path.join(self.appPath, path.pardir)
self.assetPath = path.join(self.appPath, "assets")
self.themeRoot = path.join(self.assetPath, "themes")
self.graphPath = path.join(self.assetPath, "graphics")
self.dictPath = path.join(self.assetPath, "dict")
self.iconPath = path.join(self.assetPath, "icons")
self.appIcon = path.join(self.iconPath, "novelwriter.svg")
self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__)))
self.appRoot = os.path.join(self.appPath, os.path.pardir)
self.assetPath = os.path.join(self.appPath, "assets")
self.themeRoot = os.path.join(self.assetPath, "themes")
self.dictPath = os.path.join(self.assetPath, "dict")
self.iconPath = os.path.join(self.assetPath, "icons")
self.appIcon = os.path.join(self.iconPath, "novelwriter.svg")
logger.verbose("App path: %s" % self.appPath)
logger.verbose("Home path: %s" % self.homePath)
# If config folder does not exist, make it.
# This assumes that the os config folder itself exists.
if not path.isdir(self.confPath):
if not os.path.isdir(self.confPath):
try:
mkdir(self.confPath)
os.mkdir(self.confPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.confPath)
logger.error(str(e))
@@ -287,7 +285,7 @@ class Config:
# Check if config file exists
if self.confPath is not None:
if path.isfile(path.join(self.confPath, self.confFile)):
if os.path.isfile(os.path.join(self.confPath, self.confFile)):
# If it exists, load it
self.loadConfig()
else:
@@ -297,9 +295,9 @@ class Config:
# If data folder does not exist, make it.
# This assumes that the os data folder itself exists.
if self.dataPath is not None:
if not path.isdir(self.dataPath):
if not os.path.isdir(self.dataPath):
try:
mkdir(self.dataPath)
os.mkdir(self.dataPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.dataPath)
logger.error(str(e))
@@ -320,9 +318,9 @@ class Config:
self.spellLanguage = "en"
# Check if local help files exist
self.helpPath = path.join(self.assetPath, "help", "novelWriter.qhc")
self.hasHelp = path.isfile(self.helpPath)
self.hasHelp &= path.isfile(path.join(self.assetPath, "help", "novelWriter.qch"))
self.helpPath = os.path.join(self.assetPath, "help", "novelWriter.qhc")
self.hasHelp = os.path.isfile(self.helpPath)
self.hasHelp &= os.path.isfile(os.path.join(self.assetPath, "help", "novelWriter.qch"))
logger.debug("Config initialisation complete")
@@ -336,7 +334,7 @@ class Config:
return False
cnfParse = configparser.ConfigParser()
cnfPath = path.join(self.confPath, self.confFile)
cnfPath = os.path.join(self.confPath, self.confFile)
try:
with open(cnfPath, mode="r", encoding="utf8") as inFile:
cnfParse.read_file(inFile)
@@ -631,7 +629,7 @@ class Config:
cnfParse.set(cnfSec, "lastpath", str(self.lastPath))
# Write config file
cnfPath = path.join(self.confPath, self.confFile)
cnfPath = os.path.join(self.confPath, self.confFile)
try:
with open(cnfPath, mode="w", encoding="utf8") as outFile:
cnfParse.write(outFile)
@@ -652,10 +650,10 @@ class Config:
if self.dataPath is None:
return False
cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE)
self.recentProj = {}
if path.isfile(cacheFile):
if os.path.isfile(cacheFile):
try:
with open(cacheFile, mode="r", encoding="utf8") as inFile:
theJson = inFile.read()
@@ -692,8 +690,8 @@ class Config:
if self.dataPath is None:
return False
cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
cacheTemp = path.join(self.dataPath, nwFiles.RECENT_FILE+"~")
cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE)
cacheTemp = os.path.join(self.dataPath, nwFiles.RECENT_FILE+"~")
try:
with open(cacheTemp, mode="w+", encoding="utf8") as outFile:
@@ -704,16 +702,16 @@ class Config:
self.errData.append(str(e))
return False
if path.isfile(cacheFile):
unlink(cacheFile)
rename(cacheTemp, cacheFile)
if os.path.isfile(cacheFile):
os.unlink(cacheFile)
os.rename(cacheTemp, cacheFile)
return True
def updateRecentCache(self, projPath, projTitle, wordCount, saveTime):
"""Add or update recent cache information o9n a given project.
"""
self.recentProj[path.abspath(projPath)] = {
self.recentProj[os.path.abspath(projPath)] = {
"title" : projTitle,
"time" : int(saveTime),
"words" : int(wordCount),
@@ -739,27 +737,27 @@ class Config:
def setConfPath(self, newPath):
if newPath is None:
return True
if not path.isfile(newPath):
if not os.path.isfile(newPath):
logger.error("File not found, using default config path instead")
return False
self.confPath = path.dirname(newPath)
self.confFile = path.basename(newPath)
self.confPath = os.path.dirname(newPath)
self.confFile = os.path.basename(newPath)
return True
def setDataPath(self, newPath):
if newPath is None:
return True
if not path.isdir(newPath):
if not os.path.isdir(newPath):
logger.error("Path not found, using default data path instead")
return False
self.dataPath = path.abspath(newPath)
self.dataPath = os.path.abspath(newPath)
return True
def setLastPath(self, lastPath):
if lastPath is None or lastPath == "":
self.lastPath = ""
else:
self.lastPath = path.dirname(lastPath)
self.lastPath = os.path.dirname(lastPath)
return True
def setWinSize(self, newWidth, newHeight):
+9 -9
View File
@@ -9,20 +9,20 @@ from nw.constants.enum import (
)
__all__ = [
"isoLanguage",
"isoCountry",
"nwConst",
"nwRegEx",
"nwFiles",
"nwKeyWords",
"nwLabels",
"nwQuotes",
"nwUnicode",
"isoLanguage",
"nwAlert",
"nwConst",
"nwDocAction",
"nwDocInsert",
"nwFiles",
"nwItemClass",
"nwItemLayout",
"nwItemType",
"nwKeyWords",
"nwLabels",
"nwOutline",
"nwDocInsert",
"nwQuotes",
"nwRegEx",
"nwUnicode",
]
+1 -1
View File
@@ -9,7 +9,7 @@
Created: 2019-04-28 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
+1 -1
View File
@@ -9,7 +9,7 @@
Created: 2018-11-02 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
+1 -5
View File
@@ -9,7 +9,7 @@
Created: 2019-11-05 [0.4.0]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,10 +25,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
logger = logging.getLogger(__name__)
class isoLanguage():
ISO_639_1 = {
+3 -3
View File
@@ -8,6 +8,9 @@ from nw.core.tohtml import ToHtml
from nw.core.tools import countWords, numberToRoman, numberToWord
__all__ = [
"countWords",
"numberToRoman",
"numberToWord",
"NWDoc",
"NWIndex",
"NWProject",
@@ -15,7 +18,4 @@ __all__ = [
"NWSpellEnchant",
"NWSpellSimple",
"ToHtml",
"countWords",
"numberToRoman",
"numberToWord",
]
+13 -14
View File
@@ -9,7 +9,7 @@
Created: 2018-09-29 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -26,8 +26,7 @@
"""
import logging
from os import path, rename, unlink
import os
from nw.constants import nwAlert
from nw.common import isHandle
@@ -89,12 +88,12 @@ class NWDoc():
docFile = self._docHandle+".nwd"
logger.debug("Opening document %s" % docFile)
docPath = path.join(self.theProject.projContent, docFile)
docPath = os.path.join(self.theProject.projContent, docFile)
self._fileLoc = docPath
theText = ""
self._docMeta = ""
if path.isfile(docPath):
if os.path.isfile(docPath):
try:
with open(docPath, mode="r", encoding="utf8") as inFile:
fstLine = inFile.readline()
@@ -137,8 +136,8 @@ class NWDoc():
docFile = self._docHandle+".nwd"
logger.debug("Saving document %s" % docFile)
docPath = path.join(self.theProject.projContent, docFile)
docTemp = path.join(self.theProject.projContent, docFile+"~")
docPath = os.path.join(self.theProject.projContent, docFile)
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
if self._theItem is None:
docMeta = ""
@@ -163,9 +162,9 @@ class NWDoc():
# If we're here, the file was successfully saved, so we can
# replace the temp file with the actual file
if path.isfile(docPath):
unlink(docPath)
rename(docTemp, docPath)
if os.path.isfile(docPath):
os.unlink(docPath)
os.rename(docTemp, docPath)
self.theParent.statusBar.setStatus("Saved Document: %s" % self._theItem.itemName)
@@ -181,13 +180,13 @@ class NWDoc():
docFile = tHandle+".nwd"
chkList = []
chkList.append(path.join(self.theProject.projContent, docFile))
chkList.append(path.join(self.theProject.projContent, docFile+"~"))
chkList.append(os.path.join(self.theProject.projContent, docFile))
chkList.append(os.path.join(self.theProject.projContent, docFile+"~"))
for chkFile in chkList:
if path.isfile(chkFile):
if os.path.isfile(chkFile):
try:
unlink(chkFile)
os.unlink(chkFile)
logger.debug("Deleted: %s" % chkFile)
except Exception as e:
self.makeAlert(["Could not delete document file.", str(e)], nwAlert.ERROR)
+25 -33
View File
@@ -9,7 +9,7 @@
Created: 2019-05-27 [0.1.4]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,11 +25,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import json
import nw
import os
from os import path
from time import time
from nw.constants import (
@@ -153,9 +153,9 @@ class NWIndex():
"""Load index from last session from the project meta folder.
"""
theData = {}
indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
if path.isfile(indexFile):
if os.path.isfile(indexFile):
logger.debug("Loading index file")
try:
with open(indexFile, mode="r", encoding="utf8") as inFile:
@@ -190,7 +190,7 @@ class NWIndex():
"""Save the current index as a json file in the project meta
data folder.
"""
indexFile = path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
logger.debug("Saving index file")
if self.mainConf.debugInfo:
@@ -279,8 +279,8 @@ class NWIndex():
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
logger.error("Not indexing no-layout item %s" % tHandle)
return False
if theRoot is None:
logger.error("Not indexing homeless item %s" % tHandle)
if theItem.parHandle is None:
logger.error("Not indexing orphaned item %s" % tHandle)
return False
# Run word counter for the whole text
@@ -288,7 +288,7 @@ class NWIndex():
self.textCounts[tHandle] = [cC, wC, pC]
# If the file is archived or trashed, we don't index the file itself
if theItem.parHandle == self.theProject.projTree.trashRoot():
if self.theProject.projTree.isTrashRoot(theItem.parHandle):
logger.error("Not indexing trash item %s" % tHandle)
return False
if theRoot.itemClass == nwItemClass.ARCHIVE:
@@ -356,7 +356,7 @@ class NWIndex():
# Count words for remaining text after last heading
if nTitle > 0:
lastText = "\n".join(theLines[nTitle-1:nLine-1])
lastText = "\n".join(theLines[nTitle-1:])
self._indexWordCounts(tHandle, isNovel, lastText, nTitle)
# Update timestamps for index changes
@@ -459,16 +459,12 @@ class NWIndex():
"""Validate and save the information about a reference to a tag
in another file.
"""
isValid, theBits, thePos = self.scanThis(aLine)
isValid, theBits, _ = self.scanThis(aLine)
if not isValid or len(theBits) == 0:
return False
sTitle = "T%06d" % nTitle
if sTitle not in self.refIndex[tHandle]:
logger.error("Cannot save tags to file %s, no title %s" % (tHandle, sTitle))
return False
if theBits[0] != nwKeyWords.TAG_KEY:
if sTitle in self.refIndex[tHandle] and theBits[0] != nwKeyWords.TAG_KEY:
for aVal in theBits[1:]:
self.refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
@@ -574,15 +570,14 @@ class NWIndex():
"""
theStructure = []
for tItem in self.theProject.projTree:
if tItem is None:
continue
if not tItem.isExported and skipExcluded:
continue
tHandle = tItem.itemHandle
if tHandle not in self.novelIndex:
continue
for sTitle in sorted(self.novelIndex[tHandle].keys()):
theStructure.append("%s:%s" % (tHandle, sTitle))
if tItem is not None:
if not tItem.isExported and skipExcluded:
continue
tHandle = tItem.itemHandle
if tHandle not in self.novelIndex:
continue
for sTitle in sorted(self.novelIndex[tHandle].keys()):
theStructure.append("%s:%s" % (tHandle, sTitle))
return theStructure
@@ -624,14 +619,11 @@ class NWIndex():
if tHandle not in self.refIndex:
return theRefs
try:
for refTitle in self.refIndex[tHandle]:
for nLine, tKey, tTag in self.refIndex[tHandle][refTitle]["tags"]:
if sTitle is None or sTitle == refTitle:
theRefs[tKey].append(tTag)
except Exception as e:
logger.error("Failed to generate reference list")
logger.error(str(e))
for refTitle in self.refIndex[tHandle]:
theTags = self.refIndex[tHandle][refTitle].get("tags", None)
for aTag in theTags:
if len(aTag) == 3 and (sTitle is None or sTitle == refTitle):
theRefs[aTag[1]].append(aTag[2])
return theRefs
+1 -1
View File
@@ -9,7 +9,7 @@
Created: 2018-10-27 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
+5 -6
View File
@@ -10,7 +10,7 @@
Rewritten: 2020-02-19 [0.4.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -28,8 +28,7 @@
import logging
import json
from os import path
import os
from nw.constants import nwFiles
@@ -100,10 +99,10 @@ class OptionState():
if self.theProject.projMeta is None:
return False
stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
theState = {}
if path.isfile(stateFile):
if os.path.isfile(stateFile):
logger.debug("Loading GUI options file")
try:
with open(stateFile, mode="r", encoding="utf8") as inFile:
@@ -130,7 +129,7 @@ class OptionState():
if self.theProject.projMeta is None:
return False
stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
logger.debug("Saving GUI options file")
try:
+115 -106
View File
@@ -9,7 +9,7 @@
Created: 2018-09-29 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,10 +25,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
import os
from os import path, mkdir, listdir, unlink, rename, rmdir
from lxml import etree
from time import time
from shutil import make_archive, unpack_archive, copyfile
@@ -41,7 +41,8 @@ from nw.core.document import NWDoc
from nw.core.status import NWStatus
from nw.core.options import OptionState
from nw.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp, makeFileNameSafe
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
makeFileNameSafe
)
from nw.constants import (
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwLabels, nwAlert
@@ -350,14 +351,11 @@ class NWProject():
aDoc.saveDocument("### %s\n\n" % scTitle)
aDoc.clearDocument()
else:
# Fallback just in case. We shouldn't reach here.
self.newRoot("Novel", nwItemClass.NOVEL)
# Finalise
self.projOpened = time()
self.setProjectChanged(True)
self.saveProject(autoSave=True)
if popCustom or popMinimal:
self.projOpened = time()
self.setProjectChanged(True)
self.saveProject(autoSave=True)
return True
@@ -367,37 +365,33 @@ class NWProject():
parse the XML of the file and populate the project variables and
build the tree of project items.
"""
if not path.isfile(fileName):
fileName = path.join(fileName, nwFiles.PROJ_FILE)
if not path.isfile(fileName):
if not os.path.isfile(fileName):
fileName = os.path.join(fileName, nwFiles.PROJ_FILE)
if not os.path.isfile(fileName):
self.makeAlert("File not found: %s" % fileName, nwAlert.ERROR)
return False
self.clearProject()
self.projPath = path.abspath(path.dirname(fileName))
self.projPath = os.path.abspath(os.path.dirname(fileName))
logger.debug("Opening project: %s" % self.projPath)
# Standard Folders and Files
# ==========================
if not self.ensureFolderStructure():
self.clearProject()
return False
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
self.projDict = os.path.join(self.projMeta, nwFiles.PROJ_DICT)
# Check for Old Legacy Data
# =========================
errList = []
for projItem in listdir(self.projPath):
legacyList = [] # Cleanup is done later
for projItem in os.listdir(self.projPath):
logger.verbose("Project contains: %s" % projItem)
if projItem.startswith("data_"):
errList = self._legacyDataFolder(projItem, errList)
if errList:
self.makeAlert(errList, nwAlert.ERROR)
self._deprecatedFiles()
legacyList.append(projItem)
# Project Lock
# ============
@@ -427,7 +421,7 @@ class NWProject():
# Trying to open backup file instead
backFile = fileName[:-3]+"bak"
if path.isfile(backFile):
if os.path.isfile(backFile):
self.makeAlert("Attempting to open backup project file instead.", nwAlert.INFO)
try:
nwXML = etree.parse(backFile)
@@ -474,6 +468,7 @@ class NWProject():
"Project file does not appear to be a novelWriterXML file.",
nwAlert.ERROR
)
self.clearProject()
return False
# Check Project Storage Version
@@ -488,7 +483,7 @@ class NWProject():
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
if fileVersion == "1.0":
if fileVersion == "1.0" and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Old Project Version", (
"The project file and data is created by a novelWriter version "
@@ -498,9 +493,10 @@ class NWProject():
"any more, so make sure you have a recent backup."
))
if msgRes != QMessageBox.Yes:
self.clearProject()
return False
elif fileVersion != "1.1" and fileVersion != "1.2":
elif fileVersion != "1.1" and fileVersion != "1.2" and self.mainConf.showGUI:
self.makeAlert((
"Unknown or unsupported novelWriter project file format. "
"The project cannot be opened by this version of novelWriter. "
@@ -508,6 +504,7 @@ class NWProject():
).format(
vers = appVersion,
), nwAlert.ERROR)
self.clearProject()
return False
# Check novelWriter Version
@@ -524,6 +521,7 @@ class NWProject():
appVersion, nw.__version__
))
if msgRes != QMessageBox.Yes:
self.clearProject()
return False
# Start Parsing the XML
@@ -605,6 +603,17 @@ class NWProject():
self.optState.loadSettings()
# Sort out old file locations
if legacyList:
errList = []
for projItem in legacyList:
errList = self._legacyDataFolder(projItem, errList)
if errList:
self.makeAlert(errList, nwAlert.ERROR)
# Clean up old files
self._deprecatedFiles()
# Update recent projects
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache()
@@ -694,9 +703,9 @@ class NWProject():
self.projTree.packXML(nwXML)
# Write the xml tree to file
tempFile = path.join(self.projPath, self.projFile+"~")
saveFile = path.join(self.projPath, self.projFile)
backFile = path.join(self.projPath, self.projFile[:-3]+"bak")
tempFile = os.path.join(self.projPath, self.projFile+"~")
saveFile = os.path.join(self.projPath, self.projFile)
backFile = os.path.join(self.projPath, self.projFile[:-3]+"bak")
try:
with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring(
@@ -711,11 +720,11 @@ class NWProject():
# If we're here, the file was successfully saved,
# so let's sort out the temps and backups
if path.isfile(backFile):
unlink(backFile)
if path.isfile(saveFile):
rename(saveFile, backFile)
rename(tempFile, saveFile)
if os.path.isfile(backFile):
os.unlink(backFile)
if os.path.isfile(saveFile):
os.rename(saveFile, backFile)
os.rename(tempFile, saveFile)
# Save project GUI options
self.optState.saveSettings()
@@ -748,9 +757,9 @@ class NWProject():
if self.projPath is None or self.projPath == "":
return False
self.projMeta = path.join(self.projPath, "meta")
self.projCache = path.join(self.projPath, "cache")
self.projContent = path.join(self.projPath, "content")
self.projMeta = os.path.join(self.projPath, "meta")
self.projCache = os.path.join(self.projPath, "cache")
self.projContent = os.path.join(self.projPath, "content")
if not self._checkFolder(self.projMeta):
return False
@@ -785,7 +794,7 @@ class NWProject():
), nwAlert.ERROR)
return False
if not path.isdir(self.mainConf.backupPath):
if not os.path.isdir(self.mainConf.backupPath):
self.theParent.makeAlert((
"Cannot backup project because the backup path does not exist. "
"Please set a valid backup location in Tools > Preferences."
@@ -793,10 +802,10 @@ class NWProject():
return False
cleanName = makeFileNameSafe(self.projName)
baseDir = path.abspath(path.join(self.mainConf.backupPath, cleanName))
if not path.isdir(baseDir):
baseDir = os.path.abspath(os.path.join(self.mainConf.backupPath, cleanName))
if not os.path.isdir(baseDir):
try:
mkdir(baseDir)
os.mkdir(baseDir)
logger.debug("Created folder %s" % baseDir)
except Exception as e:
self.theParent.makeAlert(
@@ -805,7 +814,7 @@ class NWProject():
)
return False
if path.commonpath([self.projPath, baseDir]) == self.projPath:
if os.path.commonpath([self.projPath, baseDir]) == self.projPath:
self.theParent.makeAlert((
"Cannot backup project because the backup path is within the "
"project folder to be backed up. Please choose a different "
@@ -814,7 +823,7 @@ class NWProject():
return False
archName = "Backup from %s" % formatTimeStamp(time(), fileSafe=True)
baseName = path.join(baseDir, archName)
baseName = os.path.join(baseDir, archName)
try:
self._clearLockFile()
@@ -822,7 +831,7 @@ class NWProject():
self._writeLockFile()
if doNotify:
self.theParent.makeAlert(
"Backup archive file written to: %s.zip" % path.join(cleanName, archName),
"Backup archive file written to: %s.zip" % os.path.join(cleanName, archName),
nwAlert.INFO
)
else:
@@ -849,11 +858,11 @@ class NWProject():
logger.error("No project path set for the example project")
return False
srcSample = path.abspath(path.join(self.mainConf.appRoot, "sample"))
pkgSample = path.join(self.mainConf.assetPath, "sample.zip")
srcSample = os.path.abspath(os.path.join(self.mainConf.appRoot, "sample"))
pkgSample = os.path.join(self.mainConf.assetPath, "sample.zip")
isSuccess = False
if path.isfile(pkgSample):
if os.path.isfile(pkgSample):
self.setProjectPath(projPath, newProject=True)
try:
@@ -864,19 +873,19 @@ class NWProject():
["Failed to create a new example project.", str(e)], nwAlert.ERROR
)
elif path.isdir(srcSample):
elif os.path.isdir(srcSample):
self.setProjectPath(projPath, newProject=True)
try:
srcProj = path.join(srcSample, nwFiles.PROJ_FILE)
dstProj = path.join(projPath, nwFiles.PROJ_FILE)
srcProj = os.path.join(srcSample, nwFiles.PROJ_FILE)
dstProj = os.path.join(projPath, nwFiles.PROJ_FILE)
copyfile(srcProj, dstProj)
srcContent = path.join(srcSample, "content")
dstContent = path.join(projPath, "content")
for srcFile in listdir(srcContent):
srcDoc = path.join(srcContent, srcFile)
dstDoc = path.join(dstContent, srcFile)
srcContent = os.path.join(srcSample, "content")
dstContent = os.path.join(projPath, "content")
for srcFile in os.listdir(srcContent):
srcDoc = os.path.join(srcContent, srcFile)
dstDoc = os.path.join(dstContent, srcFile)
copyfile(srcDoc, dstDoc)
isSuccess = True
@@ -911,13 +920,13 @@ class NWProject():
self.projPath = None
else:
if projPath.startswith("~"):
projPath = path.expanduser(projPath)
self.projPath = path.abspath(projPath)
projPath = os.path.expanduser(projPath)
self.projPath = os.path.abspath(projPath)
if newProject:
if not path.isdir(projPath):
if not os.path.isdir(projPath):
try:
mkdir(projPath)
os.mkdir(projPath)
logger.debug("Created folder %s" % projPath)
except Exception as e:
self.theParent.makeAlert((
@@ -925,8 +934,8 @@ class NWProject():
), nwAlert.ERROR)
return False
if path.isdir(projPath):
if self.mainConf.showGUI and listdir(self.projPath):
if os.path.isdir(projPath):
if os.listdir(self.projPath):
self.theParent.makeAlert((
"New project folder is not empty. "
"Each project requires a dedicated project folder."
@@ -972,11 +981,11 @@ class NWProject():
def setProjBackup(self, doBackup):
"""Set whether projects should be backed up or not. The user
will notified in case dependant settings are missing.
will be notified in case required settings are missing.
"""
self.doBackup = doBackup
if doBackup:
if not path.isdir(self.mainConf.backupPath):
if not os.path.isdir(self.mainConf.backupPath):
self.theParent.makeAlert((
"You must set a valid backup path in preferences to use "
"the automatic project backup feature."
@@ -1175,8 +1184,8 @@ class NWProject():
if self.projPath is None:
return ["ERROR"]
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
if not path.isfile(lockFile):
lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK)
if not os.path.isfile(lockFile):
return []
try:
@@ -1201,7 +1210,7 @@ class NWProject():
if self.projPath is None:
return False
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK)
try:
with open(lockFile, mode="w+", encoding="utf8") as outFile:
outFile.write("%s\n" % self.mainConf.hostName)
@@ -1222,10 +1231,10 @@ class NWProject():
if self.projPath is None:
return False
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
if path.isfile(lockFile):
lockFile = os.path.join(self.projPath, nwFiles.PROJ_LOCK)
if os.path.isfile(lockFile):
try:
unlink(lockFile)
os.unlink(lockFile)
return True
except Exception as e:
logger.error("Failed to remove project lockfile")
@@ -1237,9 +1246,9 @@ class NWProject():
def _checkFolder(self, thePath):
"""Check if a folder exists, and if it doesn't, create it.
"""
if not path.isdir(thePath):
if not os.path.isdir(thePath):
try:
mkdir(thePath)
os.mkdir(thePath)
logger.debug("Created folder %s" % thePath)
except Exception as e:
self.makeAlert(["Could not create folder.", str(e)], nwAlert.ERROR)
@@ -1280,7 +1289,7 @@ class NWProject():
# Then check the files in the data folder
logger.debug("Checking files in project content folder")
orphanFiles = []
for fileItem in listdir(self.projContent):
for fileItem in os.listdir(self.projContent):
if not fileItem.endswith(".nwd"):
logger.warning("Skipping file %s" % fileItem)
continue
@@ -1343,8 +1352,8 @@ class NWProject():
if not self.ensureFolderStructure():
return False
sessionFile = path.join(self.projMeta, nwFiles.SESS_STATS)
isFile = path.isfile(sessionFile)
sessionFile = os.path.join(self.projMeta, nwFiles.SESS_STATS)
isFile = os.path.isfile(sessionFile)
with open(sessionFile, mode="a+", encoding="utf8") as outFile:
if not isFile:
@@ -1371,8 +1380,8 @@ class NWProject():
def _legacyDataFolder(self, theFolder, errList):
"""Clean up legacy data folders.
"""
theData = path.join(self.projPath, theFolder)
if not path.isdir(theData):
theData = os.path.join(self.projPath, theFolder)
if not os.path.isdir(theData):
errList.append("Not a folder: %s" % theData)
return errList
@@ -1380,9 +1389,9 @@ class NWProject():
# Move Documents to Content
# =========================
for dataItem in listdir(theData):
theFile = path.join(theData, dataItem)
if not path.isfile(theFile):
for dataItem in os.listdir(theData):
theFile = os.path.join(theData, dataItem)
if not os.path.isfile(theFile):
theErr = self._moveUnknownItem(theData, dataItem)
if theErr:
errList.append(theErr)
@@ -1390,9 +1399,9 @@ class NWProject():
if len(dataItem) == 21 and dataItem.endswith("_main.nwd"):
tHandle = theFolder[-1]+dataItem[:12]
newPath = path.join(self.projContent, tHandle+".nwd")
newPath = os.path.join(self.projContent, tHandle+".nwd")
try:
rename(theFile, newPath)
os.rename(theFile, newPath)
logger.info("Moved file: %s" % theFile)
logger.info("New location: %s" % newPath)
except Exception as e:
@@ -1401,7 +1410,7 @@ class NWProject():
elif len(dataItem) == 21 and dataItem.endswith("_main.bak"):
try:
unlink(theFile)
os.unlink(theFile)
logger.info("Deleted file: %s" % theFile)
except Exception as e:
logger.error(str(e))
@@ -1415,7 +1424,7 @@ class NWProject():
# Remove Data Folder
# ==================
try:
rmdir(theData)
os.rmdir(theData)
logger.info("Removed folder: %s" % theFolder)
except Exception as e:
logger.error(str(e))
@@ -1427,15 +1436,15 @@ class NWProject():
"""Move an item that doesn't belong in the project folder to
a junk folder.
"""
theJunk = path.join(self.projPath, "junk")
theJunk = os.path.join(self.projPath, "junk")
if not self._checkFolder(theJunk):
return "Could not make folder: %s" % theJunk
theSrc = path.join(theDir, theItem)
theDst = path.join(theJunk, theItem)
theSrc = os.path.join(theDir, theItem)
theDst = os.path.join(theJunk, theItem)
try:
rename(theSrc, theDst)
os.rename(theSrc, theDst)
logger.info("Moved to junk: %s" % theSrc)
except Exception as e:
logger.error(str(e))
@@ -1447,29 +1456,29 @@ class NWProject():
"""Delete files that are no longer used by novelWriter.
"""
rmList = [
path.join(self.projCache, "nwProject.nwx.0"),
path.join(self.projCache, "nwProject.nwx.1"),
path.join(self.projCache, "nwProject.nwx.2"),
path.join(self.projCache, "nwProject.nwx.3"),
path.join(self.projCache, "nwProject.nwx.4"),
path.join(self.projCache, "nwProject.nwx.5"),
path.join(self.projCache, "nwProject.nwx.6"),
path.join(self.projCache, "nwProject.nwx.7"),
path.join(self.projCache, "nwProject.nwx.8"),
path.join(self.projCache, "nwProject.nwx.9"),
path.join(self.projMeta, "mainOptions.json"),
path.join(self.projMeta, "exportOptions.json"),
path.join(self.projMeta, "outlineOptions.json"),
path.join(self.projMeta, "timelineOptions.json"),
path.join(self.projMeta, "docMergeOptions.json"),
path.join(self.projMeta, "sessionLogOptions.json"),
os.path.join(self.projCache, "nwProject.nwx.0"),
os.path.join(self.projCache, "nwProject.nwx.1"),
os.path.join(self.projCache, "nwProject.nwx.2"),
os.path.join(self.projCache, "nwProject.nwx.3"),
os.path.join(self.projCache, "nwProject.nwx.4"),
os.path.join(self.projCache, "nwProject.nwx.5"),
os.path.join(self.projCache, "nwProject.nwx.6"),
os.path.join(self.projCache, "nwProject.nwx.7"),
os.path.join(self.projCache, "nwProject.nwx.8"),
os.path.join(self.projCache, "nwProject.nwx.9"),
os.path.join(self.projMeta, "mainOptions.json"),
os.path.join(self.projMeta, "exportOptions.json"),
os.path.join(self.projMeta, "outlineOptions.json"),
os.path.join(self.projMeta, "timelineOptions.json"),
os.path.join(self.projMeta, "docMergeOptions.json"),
os.path.join(self.projMeta, "sessionLogOptions.json"),
]
for rmFile in rmList:
if path.isfile(rmFile):
if os.path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
os.unlink(rmFile)
except Exception as e:
logger.error(str(e))
+7 -7
View File
@@ -9,7 +9,7 @@
Created: 2019-06-11 [0.1.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,10 +25,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
import os
from os import path, listdir
from difflib import get_close_matches
from nw.constants import isoLanguage
@@ -108,7 +108,7 @@ class NWSpellCheck():
self.PROJW = []
if projectDict is not None:
self.projectDict = projectDict
if not path.isfile(projectDict):
if not os.path.isfile(projectDict):
return
try:
logger.debug("Loading project word list")
@@ -228,7 +228,7 @@ class NWSpellSimple(NWSpellCheck):
"""Load a dictionary as a list from the app assets folder.
"""
self.WORDS = []
dictFile = path.join(self.mainConf.dictPath, theLang+".dict")
dictFile = os.path.join(self.mainConf.dictPath, theLang+".dict")
try:
with open(dictFile, mode="r", encoding="utf-8") as wordsFile:
for theLine in wordsFile:
@@ -297,9 +297,9 @@ class NWSpellSimple(NWSpellCheck):
"""Lists the dictionary files in the app assets folder.
"""
retList = []
for dictFile in listdir(self.mainConf.dictPath):
for dictFile in os.listdir(self.mainConf.dictPath):
theBits = path.splitext(dictFile)
theBits = os.path.splitext(dictFile)
if len(theBits) != 2:
continue
if theBits[1] != ".dict":
+1 -1
View File
@@ -9,7 +9,7 @@
Created: 2019-05-19 [0.1.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
+1 -1
View File
@@ -9,7 +9,7 @@
Created: 2019-05-07 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
+1 -27
View File
@@ -9,7 +9,7 @@
Created: 2019-05-05 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -122,32 +122,6 @@ class Tokenizer():
return
def clearData(self):
"""Clear the data arrays and variables, but not settings, so the class
can be reused for multiple documents.
"""
self.theText = None
self.theHandle = None
self.theItem = None
self.theTokens = None
self.theResult = None
self.theMarkdown = None
self.numChapter = 0
self.firstScene = False
self.isNone = False
self.isTitle = False
self.isBook = False
self.isPage = False
self.isPart = False
self.isUnNum = False
self.isChap = False
self.isScene = False
self.isNote = False
self.isNovel = False
return
##
# Setters
##
+1 -2
View File
@@ -11,7 +11,7 @@
Merged: 2020-05-08 [0.4.5] All of the above into this file
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -121,7 +121,6 @@ def numberToWord(numVal, theLanguage):
numWord = _numberToWordEN(numVal)
else:
numWord = _numberToWordEN(numVal)
# print("%4d : %s" % (numVal, numWord))
return numWord
def _numberToWordEN(numVal):
+14 -7
View File
@@ -9,7 +9,7 @@
Created: 2020-05-07 [0.4.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -27,8 +27,8 @@
import logging
import json
import os
from os import path
from lxml import etree
from hashlib import sha256
from time import time
@@ -144,8 +144,8 @@ class NWTree():
the project directory. These files are there to assist the user
if they wish to browse the stored files.
"""
tocText = path.join(self.theProject.projPath, nwFiles.TOC_TXT)
tocJson = path.join(self.theProject.projPath, nwFiles.TOC_JSON)
tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT)
tocJson = os.path.join(self.theProject.projPath, nwFiles.TOC_JSON)
jsonData = []
try:
@@ -162,14 +162,14 @@ class NWTree():
if tItem is None:
continue
tFile = tHandle+".nwd"
if path.isfile(path.join(self.theProject.projContent, tFile)):
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
outFile.write(" %-25s %-9s %s\n" % (
path.join("content", tFile),
os.path.join("content", tFile),
tItem.itemClass.name,
tItem.itemName,
))
jsonData.append([
path.join("content", tFile),
os.path.join("content", tFile),
tItem.itemClass.name,
tItem.itemName,
])
@@ -213,6 +213,13 @@ class NWTree():
return self._trashRoot
return None
def isTrashRoot(self, tHandle):
"""Check if a handle is the trash folder.
"""
if self._trashRoot is None:
return False
return tHandle == self._trashRoot
def archiveRoot(self):
"""Returns the handle of the archive folder, or None if there
isn't one.
+1 -1
View File
@@ -9,7 +9,7 @@
Created: 2020-08-02 [0.10.2]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
+5 -6
View File
@@ -16,9 +16,9 @@ from nw.gui.projload import GuiProjectLoad
from nw.gui.projsettings import GuiProjectSettings
from nw.gui.projtree import GuiProjectTree
from nw.gui.projwizard import GuiProjectWizard
from nw.gui.writingstats import GuiWritingStats
from nw.gui.statusbar import GuiMainStatus
from nw.gui.theme import GuiIcons, GuiTheme
from nw.gui.theme import GuiTheme
from nw.gui.writingstats import GuiWritingStats
__all__ = [
"GuiAbout",
@@ -26,11 +26,12 @@ __all__ = [
"GuiDocEditor",
"GuiDocMerge",
"GuiDocSplit",
"GuiDocViewer",
"GuiDocViewDetails",
"GuiDocViewer",
"GuiItemDetails",
"GuiItemEditor",
"GuiMainMenu",
"GuiMainStatus",
"GuiOutline",
"GuiOutlineDetails",
"GuiPreferences",
@@ -38,8 +39,6 @@ __all__ = [
"GuiProjectSettings",
"GuiProjectTree",
"GuiProjectWizard",
"GuiWritingStats",
"GuiMainStatus",
"GuiIcons",
"GuiTheme",
"GuiWritingStats",
]
+6 -5
View File
@@ -9,7 +9,7 @@
Created: 2020-05-21 [0.5.2]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,10 +25,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
import os
from os import path
from datetime import datetime
from PyQt5.QtCore import Qt
@@ -45,6 +45,7 @@ class GuiAbout(QDialog):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiAbout ...")
self.setObjectName("GuiAbout")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -196,8 +197,8 @@ class GuiAbout(QDialog):
"""Load the content for the License page.
"""
docName = "gplv3_%s.htm" % self.mainConf.guiLang
docPath = path.join(self.mainConf.assetPath, "text", docName)
if path.isfile(docPath):
docPath = os.path.join(self.mainConf.assetPath, "text", docName)
if os.path.isfile(docPath):
with open(docPath, mode="r", encoding="utf8") as inFile:
helpText = inFile.read()
self.pageLicense.setHtml(helpText)
+11 -10
View File
@@ -9,7 +9,7 @@
Created: 2020-05-09 [0.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,11 +25,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import json
import nw
import os
from os import path
from time import time
from datetime import datetime
@@ -68,6 +68,7 @@ class GuiBuildNovel(QDialog):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiBuildNovel ...")
self.setObjectName("GuiBuildNovel")
self.mainConf = nw.CONFIG
self.theProject = theProject
@@ -467,7 +468,7 @@ class GuiBuildNovel(QDialog):
self.buildProgress.setMaximum(len(self.theProject.projTree))
self.buildProgress.setValue(0)
tStart = time()
tStart = int(time())
self.htmlText = []
self.htmlStyle = []
@@ -510,7 +511,7 @@ class GuiBuildNovel(QDialog):
# Update progress bar, also for skipped items
self.buildProgress.setValue(nItt+1)
tEnd = time()
tEnd = int(time())
logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
self.htmlStyle = makeHtml.getStyleSheet()
self.buildTime = tEnd
@@ -629,8 +630,8 @@ class GuiBuildNovel(QDialog):
cleanName = makeFileNameSafe(self.theProject.projName)
fileName = "%s.%s" % (cleanName, fileExt)
saveDir = self.mainConf.lastPath
savePath = path.join(saveDir, fileName)
if not path.isdir(saveDir):
savePath = os.path.join(saveDir, fileName)
if not os.path.isdir(saveDir):
saveDir = self.mainConf.homePath
if self.mainConf.showGUI:
@@ -791,9 +792,9 @@ class GuiBuildNovel(QDialog):
def _loadCache(self):
"""Save the current data to cache.
"""
buildCache = path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
dataCount = 0
if path.isfile(buildCache):
if os.path.isfile(buildCache):
logger.debug("Loading build cache")
try:
@@ -822,7 +823,7 @@ class GuiBuildNovel(QDialog):
def _saveCache(self):
"""Save the current data to cache.
"""
buildCache = path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
buildCache = os.path.join(self.theProject.projCache, nwFiles.BUILD_CACHE)
if self.mainConf.debugInfo:
nIndent = 2
+4 -2
View File
@@ -11,7 +11,7 @@
Created: 2020-05-17 [0.5.1] PagedDialog
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -27,8 +27,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtGui import QColor, QPalette, QPainter, QFontMetrics
from PyQt5.QtCore import (
@@ -443,6 +443,8 @@ class VerticalTabBar(QTabBar):
class QuotesDialog(QDialog):
selectedQuote = ""
def __init__(self, theParent=None, currentQuote="\""):
QDialog.__init__(self, parent=theParent)
+12 -14
View File
@@ -14,7 +14,7 @@
Created: 2020-06-27 [0.10.0] GuiDocEditFooter
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -30,8 +30,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from time import time
@@ -423,10 +423,10 @@ class GuiDocEditor(QTextEdit):
return True
def getCursorPosition(self):
"""Find the cursor position in the document.
"""Find the cursor position in the document. If the editor has a
selection, return the position of the end of the selection.
"""
theCursor = self.textCursor()
return theCursor.position()
return self.textCursor().selectionEnd()
def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document.
@@ -608,10 +608,8 @@ class GuiDocEditor(QTextEdit):
"""
if isinstance(theInsert, str):
theText = theInsert
elif theInsert in nwDocInsert:
if theInsert == nwDocInsert.NO_INSERT:
theText = ""
elif theInsert == nwDocInsert.HARD_BREAK:
elif isinstance(theInsert, nwDocInsert):
if theInsert == nwDocInsert.HARD_BREAK:
theText = " \n"
elif theInsert == nwDocInsert.NB_SPACE:
theText = nwUnicode.U_NBSP
@@ -1177,17 +1175,17 @@ class GuiDocEditor(QTextEdit):
theBlock = theCursor.block()
if not theBlock.isValid():
logger.debug("Invalid block selected for action %s" % str(docAction))
return
return False
theText = theBlock.text()
if len(theText.strip()) == 0:
logger.debug("Empty block selected for action %s" % str(docAction))
return
return False
# Remove existing format first, if any
if theText.startswith("@"):
logger.error("Cannot apply block format to keyword/value line")
return
return False
elif theText.startswith("% "):
newText = theText[2:]
cOffset = 2
@@ -1233,7 +1231,7 @@ class GuiDocEditor(QTextEdit):
logger.error(
"Unknown or unsupported block format requested: %s" % str(docAction)
)
return
return False
# Replace the block text
theCursor.beginEditBlock()
@@ -1250,7 +1248,7 @@ class GuiDocEditor(QTextEdit):
theCursor.endEditBlock()
self.setTextCursor(theCursor)
return
return True
def _makeSelection(self, selMode):
"""Wrapper function to select text based on a selection mode.
+2 -2
View File
@@ -9,7 +9,7 @@
Created: 2019-04-06 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import Qt, QRegularExpression
from PyQt5.QtGui import (
+5 -5
View File
@@ -9,7 +9,7 @@
Created: 2020-01-23 [0.4.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
@@ -46,6 +46,7 @@ class GuiDocMerge(QDialog):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiDocMerge ...")
self.setObjectName("GuiDocMerge")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -132,17 +133,16 @@ class GuiDocMerge(QDialog):
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
self.theParent.treeView.revealTreeItem(nHandle)
self.theParent.treeView.revealNewTreeItem(nHandle)
self.theParent.openDocument(nHandle, doScroll=True)
self.close()
self._doClose()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiDocMerge close button clicked")
self.close()
return
+20 -19
View File
@@ -9,7 +9,7 @@
Created: 2020-02-01 [0.4.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
@@ -34,9 +34,11 @@ from PyQt5.QtWidgets import (
QListWidgetItem, QDialogButtonBox, QLabel, QMessageBox
)
from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout, nwConst
from nw.gui.custom import QHelpLabel
from nw.core import NWDoc
from nw.gui.custom import QHelpLabel
from nw.constants import (
nwAlert, nwItemType, nwItemClass, nwItemLayout, nwConst
)
logger = logging.getLogger(__name__)
@@ -46,6 +48,7 @@ class GuiDocSplit(QDialog):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiDocSplit ...")
self.setObjectName("GuiDocSplit")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -160,23 +163,22 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR)
return
if self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Split Document", (
"The document will be split into %d file(s) in a new folder. "
"The original document will remain intact.<br><br>"
"Continue with the splitting process?"
) % nFiles
)
if msgRes != QMessageBox.Yes:
return
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Split Document", (
"The document will be split into %d file(s) in a new folder. "
"The original document will remain intact.<br><br>"
"Continue with the splitting process?"
) % nFiles
)
if msgRes != QMessageBox.Yes:
return
# Create the folder
fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.parHandle
)
self.theParent.treeView.revealTreeItem(fHandle)
self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle)
# Loop through, and create the files
@@ -211,16 +213,15 @@ class GuiDocSplit(QDialog):
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
theDoc.clearDocument()
self.theParent.treeView.revealTreeItem(nHandle)
self.theParent.treeView.revealNewTreeItem(nHandle)
self.close()
self._doClose()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiDocSplit close button clicked")
self.optState.saveSettings()
self.close()
return
+2 -2
View File
@@ -13,7 +13,7 @@
Created: 2020-09-08 [1.0b1] GuiDocViewHistory
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -29,8 +29,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot
from PyQt5.QtGui import (
+2 -2
View File
@@ -9,7 +9,7 @@
Created: 2019-04-24 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QPixmap
+3 -2
View File
@@ -9,7 +9,7 @@
Created: 2019-04-27 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QGridLayout, QLineEdit, QComboBox, QLabel,
@@ -44,6 +44,7 @@ class GuiItemEditor(QDialog):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiItemEditor ...")
self.setObjectName("GuiItemEditor")
self.mainConf = nw.CONFIG
self.theProject = theProject
+6 -6
View File
@@ -9,7 +9,7 @@
Created: 2019-04-27 [0.0.1] (Split from winmain)
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import QUrl, QProcess
from PyQt5.QtGui import QDesktopServices
@@ -192,7 +192,7 @@ class GuiMainMenu(QMenuBar):
self.aOpenProject = QAction("Open Project", self)
self.aOpenProject.setStatusTip("Open project")
self.aOpenProject.setShortcut("Ctrl+Shift+O")
self.aOpenProject.triggered.connect(lambda: self.theParent.manageProjects())
self.aOpenProject.triggered.connect(lambda: self.theParent.showProjectLoadDialog())
self.projMenu.addAction(self.aOpenProject)
# Project > Save Project
@@ -213,7 +213,7 @@ class GuiMainMenu(QMenuBar):
self.aProjectSettings = QAction("Project Settings", self)
self.aProjectSettings.setStatusTip("Project settings")
self.aProjectSettings.setShortcut("Ctrl+Shift+,")
self.aProjectSettings.triggered.connect(lambda: self.theParent.editProjectDialog())
self.aProjectSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog())
self.projMenu.addAction(self.aProjectSettings)
# Project > Separator
@@ -829,7 +829,7 @@ class GuiMainMenu(QMenuBar):
self.aBuildProject = QAction("Build Novel Project", self)
self.aBuildProject.setStatusTip("Launch the Build novel project tool")
self.aBuildProject.setShortcut("F5")
self.aBuildProject.triggered.connect(lambda: self.theParent.buildProjectDialog())
self.aBuildProject.triggered.connect(lambda: self.theParent.showBuildProjectDialog())
self.toolsMenu.addAction(self.aBuildProject)
# Tools > Writing Stats
@@ -843,7 +843,7 @@ class GuiMainMenu(QMenuBar):
self.aPreferences = QAction("Preferences", self)
self.aPreferences.setStatusTip("Preferences")
self.aPreferences.setShortcut("Ctrl+,")
self.aPreferences.triggered.connect(lambda: self.theParent.editConfigDialog())
self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog())
self.toolsMenu.addAction(self.aPreferences)
return
+2 -2
View File
@@ -9,7 +9,7 @@
Created: 2019-11-16 [0.4.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from time import time
+2 -2
View File
@@ -9,7 +9,7 @@
Created: 2020-06-02 [0.7.0]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
+7 -7
View File
@@ -9,7 +9,7 @@
Created: 2019-06-10 [0.1.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,10 +25,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
import logging
import os
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
@@ -48,6 +47,7 @@ class GuiPreferences(PagedDialog):
PagedDialog.__init__(self, theParent)
logger.debug("Initialising GuiPreferences ...")
self.setObjectName("GuiPreferences")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -103,7 +103,7 @@ class GuiPreferences(PagedDialog):
validEntries &= retA
needsRestart |= retB
if needsRestart and self.mainConf.showGUI:
if needsRestart:
msgBox = QMessageBox()
msgBox.information(
self, "Preferences",
@@ -119,7 +119,7 @@ class GuiPreferences(PagedDialog):
"""Close the preferences without saving the changes.
"""
logger.verbose("ConfigEditor close button clicked")
self.close()
self.reject()
return
# END Class GuiPreferences
@@ -334,7 +334,7 @@ class GuiConfigEditGeneralTab(QWidget):
"""Open a dialog to select the backup folder.
"""
currDir = self.backupPath
if not path.isdir(currDir):
if not os.path.isdir(currDir):
currDir = ""
dlgOpt = QFileDialog.Options()
+23 -29
View File
@@ -9,7 +9,7 @@
Created: 2020-02-26 [0.4.5]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,10 +25,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
import os
from os import path
from datetime import datetime
from PyQt5.QtCore import Qt, QSize
@@ -58,6 +58,7 @@ class GuiProjectLoad(QDialog):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiProjectLoad ...")
self.setObjectName("GuiProjectLoad")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -177,20 +178,19 @@ class GuiProjectLoad(QDialog):
"""Browse for a folder path.
"""
logger.verbose("GuiProjectLoad browse button clicked")
if self.mainConf.showGUI:
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
projFile, _ = QFileDialog.getOpenFileName(
self, "Open novelWriter Project", "",
"novelWriter Project File (%s);;All Files (*)" % nwFiles.PROJ_FILE,
options=dlgOpt
)
if projFile:
thePath = path.abspath(path.dirname(projFile))
self.selPath.setText(thePath)
self.openPath = thePath
self.openState = self.OPEN_STATE
self.accept()
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
projFile, _ = QFileDialog.getOpenFileName(
self, "Open novelWriter Project", "",
"novelWriter Project File (%s);;All Files (*)" % nwFiles.PROJ_FILE,
options=dlgOpt
)
if projFile:
thePath = os.path.abspath(os.path.dirname(projFile))
self.selPath.setText(thePath)
self.openPath = thePath
self.openState = self.OPEN_STATE
self.accept()
return
@@ -217,18 +217,12 @@ class GuiProjectLoad(QDialog):
"""
selList = self.listBox.selectedItems()
if selList:
doRemove = False
if self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Remove Entry",
"Remove the selected entry from the recent projects list?"
)
doRemove = (msgRes == QMessageBox.Yes)
else:
doRemove = True
if doRemove:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Remove Entry",
"Remove the selected entry from the recent projects list?"
)
if msgRes == QMessageBox.Yes:
self.mainConf.removeFromRecentCache(
selList[0].data(self.C_NAME, Qt.UserRole)
)
+16 -5
View File
@@ -9,7 +9,7 @@
Created: 2018-09-29 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
@@ -47,6 +47,7 @@ class GuiProjectSettings(PagedDialog):
PagedDialog.__init__(self, theParent)
logger.debug("Initialising GuiProjectSettings ...")
self.setObjectName("GuiProjectSettings")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -123,13 +124,25 @@ class GuiProjectSettings(PagedDialog):
newList = self.tabReplace.getNewList()
self.theProject.setAutoReplace(newList)
self._doClose()
self._saveGuiSettings()
self.accept()
return
def _doClose(self):
"""Save settings and close the dialog.
"""
self._saveGuiSettings()
self.reject()
return
##
# Internal Functions
##
def _saveGuiSettings(self):
"""Save GUI settings.
"""
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0))
@@ -138,8 +151,6 @@ class GuiProjectSettings(PagedDialog):
self.optState.setValue("GuiProjectSettings", "winHeight", winHeight)
self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW)
self.close()
return
# END Class GuiProjectSettings
+18 -12
View File
@@ -10,7 +10,7 @@
Created: 2020-06-04 [0.7.0] GuiProjectTreeMenu
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -26,8 +26,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon
@@ -178,6 +178,9 @@ class GuiProjectTree(QTreeWidget):
if itemType == nwItemType.ROOT:
tHandle = self.theProject.newRoot(nwLabels.CLASS_NAME[itemClass], itemClass)
if tHandle is None:
logger.error("No root item added")
return False
else:
# If no parent has been selected, make the new file under
@@ -206,7 +209,7 @@ class GuiProjectTree(QTreeWidget):
)
return False
if pHandle == self.theProject.projTree.trashRoot():
if self.theProject.projTree.isTrashRoot(pHandle):
self.makeAlert(
"Cannot add new files or folders to the %s folder." % (
nwLabels.CLASS_NAME[nwItemClass.TRASH]
@@ -236,12 +239,13 @@ class GuiProjectTree(QTreeWidget):
return False
# Add the new item to the tree
self.revealTreeItem(tHandle, nHandle)
self.theParent.editItem(tHandle)
if tHandle is not None:
self.revealNewTreeItem(tHandle, nHandle)
self.theParent.editItem(tHandle)
return True
def revealTreeItem(self, tHandle, nHandle=None):
def revealNewTreeItem(self, tHandle, nHandle=None):
"""Reveal a newly added project item in the project tree.
"""
nwItem = self.theProject.projTree[tHandle]
@@ -257,7 +261,8 @@ class GuiProjectTree(QTreeWidget):
"""Move an item up or down in the tree, but only if the treeView
has focus. This also applies when the menu is used.
"""
if qApp.focusWidget() == self and self.theParent.hasProject:
hasFocus = qApp.focusWidget() == self or not self.mainConf.showGUI
if hasFocus and self.theParent.hasProject:
tHandle = self.getSelectedHandle()
tItem = self._getTreeItem(tHandle)
@@ -406,11 +411,11 @@ class GuiProjectTree(QTreeWidget):
return False
pHandle = nwItemS.parHandle
if pHandle is not None and pHandle == self.theProject.projTree.trashRoot():
if self.theProject.projTree.isTrashRoot(pHandle):
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
doPermanent = False
if self.mainConf.showGUI and not alreadyAsked:
if not alreadyAsked:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
@@ -439,7 +444,7 @@ class GuiProjectTree(QTreeWidget):
# The file is not already in the trash folder, so we
# move it there.
doTrash = False
if self.mainConf.showGUI and askForTrash:
if askForTrash:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
@@ -754,6 +759,7 @@ class GuiProjectTree(QTreeWidget):
self.theIndex.reIndexHandle(sHandle)
else:
theEvent.ignore()
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
@@ -920,14 +926,14 @@ class GuiProjectTree(QTreeWidget):
nwItemS.setClass(nwItemD.itemClass)
if trItemP is None:
logger.error("Failed to find new parent item of %s" % tHandle)
return
return False
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle)
self.setTreeItemValues(tHandle)
self._setTreeChanged(True)
return
return True
def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project.
+6 -6
View File
@@ -9,7 +9,7 @@
Created: 2020-07-11 [0.10.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,10 +25,9 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from os import path
import logging
import os
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
@@ -55,6 +54,7 @@ class GuiProjectWizard(QWizard):
QWizard.__init__(self, theParent)
logger.debug("Initialising GuiProjectWizard ...")
self.setObjectName("GuiProjectWizard")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -207,7 +207,7 @@ class ProjWizardFolderPage(QWizardPage):
"""Select a project folder.
"""
lastPath = self.mainConf.lastPath
if not path.isdir(lastPath):
if not os.path.isdir(lastPath):
lastPath = ""
dlgOpt = QFileDialog.Options()
@@ -219,7 +219,7 @@ class ProjWizardFolderPage(QWizardPage):
if projDir:
projName = self.field("projName")
if projName is not None:
fullDir = path.join(path.abspath(projDir), makeFileNameSafe(projName))
fullDir = os.path.join(os.path.abspath(projDir), makeFileNameSafe(projName))
self.projPath.setText(fullDir)
else:
self.projPath.setText("")
+2 -2
View File
@@ -9,7 +9,7 @@
Created: 2019-04-20 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,8 +25,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
from time import time
+43 -42
View File
@@ -10,7 +10,7 @@
Created: 2019-11-08 [0.4.0] GuiIcons
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -26,15 +26,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import configparser
import nw
import os
from os import path, listdir
from math import ceil
from PyQt5.QtCore import Qt
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import QStyle, qApp
from PyQt5.QtGui import (
QPalette, QColor, QIcon, QFont, QFontMetrics, QFontDatabase, QPixmap
@@ -183,21 +182,21 @@ class GuiTheme:
"""Add the fonts in the assets fonts folder to the app.
"""
ttfList = []
fontAssets = path.join(self.mainConf.assetPath, self.fontPath)
for fontFam in listdir(fontAssets):
fontDir = path.join(fontAssets, fontFam)
if path.isdir(fontDir):
fontAssets = os.path.join(self.mainConf.assetPath, self.fontPath)
for fontFam in os.listdir(fontAssets):
fontDir = os.path.join(fontAssets, fontFam)
if os.path.isdir(fontDir):
if fontFam not in self.guiFontDB.families():
for fontFile in listdir(fontDir):
ttfFile = path.join(fontDir, fontFile)
if path.isfile(ttfFile) and fontFile.endswith(".ttf"):
for fontFile in os.listdir(fontDir):
ttfFile = os.path.join(fontDir, fontFile)
if os.path.isfile(ttfFile) and fontFile.endswith(".ttf"):
ttfList.append(ttfFile)
for ttfFile in ttfList:
logger.verbose("Font asset: %s" % path.relpath(ttfFile))
logger.verbose("Font asset: %s" % os.path.relpath(ttfFile))
fontID = self.guiFontDB.addApplicationFont(ttfFile)
if fontID < 0:
logger.error("Failed to add font: %s" % path.relpath(ttfFile))
logger.error("Failed to add font: %s" % os.path.relpath(ttfFile))
return
@@ -228,10 +227,10 @@ class GuiTheme:
self.guiTheme = self.mainConf.guiTheme
self.guiSyntax = self.mainConf.guiSyntax
self.themeRoot = self.mainConf.themeRoot
self.themePath = path.join(self.mainConf.themeRoot, self.guiPath, self.guiTheme)
self.syntaxFile = path.join(self.themeRoot, self.syntaxPath, self.guiSyntax+".conf")
self.confFile = path.join(self.themePath, self.confName)
self.cssFile = path.join(self.themePath, self.cssName)
self.themePath = os.path.join(self.mainConf.themeRoot, self.guiPath, self.guiTheme)
self.syntaxFile = os.path.join(self.themeRoot, self.syntaxPath, self.guiSyntax+".conf")
self.confFile = os.path.join(self.themePath, self.confName)
self.cssFile = os.path.join(self.themePath, self.cssName)
self.loadTheme()
self.loadSyntax()
@@ -260,7 +259,7 @@ class GuiTheme:
# CSS File
cssData = ""
try:
if path.isfile(self.cssFile):
if os.path.isfile(self.cssFile):
with open(self.cssFile, mode="r", encoding="utf8") as inFile:
cssData = inFile.read()
except Exception as e:
@@ -376,8 +375,10 @@ class GuiTheme:
return self.themeList
confParser = configparser.ConfigParser()
for themeDir in listdir(path.join(self.mainConf.themeRoot, self.guiPath)):
themeConf = path.join(self.mainConf.themeRoot, self.guiPath, themeDir, self.confName)
for themeDir in os.listdir(os.path.join(self.mainConf.themeRoot, self.guiPath)):
themeConf = os.path.join(
self.mainConf.themeRoot, self.guiPath, themeDir, self.confName
)
logger.verbose("Checking theme config for '%s'" % themeDir)
try:
with open(themeConf, mode="r", encoding="utf8") as inFile:
@@ -406,10 +407,10 @@ class GuiTheme:
return self.syntaxList
confParser = configparser.ConfigParser()
syntaxDir = path.join(self.mainConf.themeRoot, self.syntaxPath)
for syntaxFile in listdir(syntaxDir):
syntaxPath = path.join(syntaxDir, syntaxFile)
if not path.isfile(syntaxPath):
syntaxDir = os.path.join(self.mainConf.themeRoot, self.syntaxPath)
for syntaxFile in os.listdir(syntaxDir):
syntaxPath = os.path.join(syntaxDir, syntaxFile)
if not os.path.isfile(syntaxPath):
continue
logger.verbose("Checking theme syntax for '%s'" % syntaxFile)
try:
@@ -613,11 +614,11 @@ class GuiIcons:
logger.debug("Loading icon theme files")
self.themeMap = {}
checkPath = path.join(self.mainConf.iconPath, self.mainConf.guiIcons)
if path.isdir(checkPath):
checkPath = os.path.join(self.mainConf.iconPath, self.mainConf.guiIcons)
if os.path.isdir(checkPath):
logger.debug("Loading icon theme '%s'" % self.mainConf.guiIcons)
self.iconPath = checkPath
self.confFile = path.join(checkPath, self.confName)
self.confFile = os.path.join(checkPath, self.confName)
else:
return False
@@ -649,8 +650,8 @@ class GuiIcons:
if iconName not in self.ICON_MAP:
logger.error("Unknown icon name '%s' in config file" % iconName)
else:
iconPath = path.join(self.iconPath, iconFile)
if path.isfile(iconPath):
iconPath = os.path.join(self.iconPath, iconFile)
if os.path.isfile(iconPath):
self.themeMap[iconName] = iconPath
logger.verbose("Icon slot '%s' using file '%s'" % (iconName, iconFile))
else:
@@ -670,12 +671,12 @@ class GuiIcons:
"""
if decoKey not in self.DECO_MAP:
logger.error("Decoration with name '%s' does not exist" % decoKey)
return QSvgWidget()
return QPixmap()
imgPath = path.join(
imgPath = os.path.join(
self.mainConf.assetPath, "images", self.DECO_MAP[decoKey]
)
if not path.isfile(imgPath):
if not os.path.isfile(imgPath):
logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
return QPixmap()
@@ -715,11 +716,11 @@ class GuiIcons:
return self.themeList
confParser = configparser.ConfigParser()
for themeDir in listdir(self.mainConf.iconPath):
themePath = path.join(self.mainConf.iconPath, themeDir)
if not path.isdir(themePath) or themeDir == self.fbackName:
for themeDir in os.listdir(self.mainConf.iconPath):
themePath = os.path.join(self.mainConf.iconPath, themeDir)
if not os.path.isdir(themePath) or themeDir == self.fbackName:
continue
themeConf = path.join(themePath, self.confName)
themeConf = os.path.join(themePath, self.confName)
logger.verbose("Checking icon theme config for '%s'" % themeDir)
try:
with open(themeConf, mode="r", encoding="utf8") as inFile:
@@ -757,12 +758,12 @@ class GuiIcons:
# If we just want the app icon, return it right away
if iconKey == "novelwriter":
return QIcon(path.join(self.mainConf.iconPath, "novelwriter.svg"))
return QIcon(os.path.join(self.mainConf.iconPath, "novelwriter.svg"))
# Otherwise, we start looking for it
# First in the theme folder
if iconKey in self.themeMap:
logger.verbose("Loading: %s" % path.relpath(self.themeMap[iconKey]))
logger.verbose("Loading: %s" % os.path.relpath(self.themeMap[iconKey]))
return QIcon(self.themeMap[iconKey])
# Next, we try to load the Qt style icons
@@ -778,14 +779,14 @@ class GuiIcons:
# Finally. we check if we have a fallback icon
if self.mainConf.guiDark:
fbackIcon = path.join(
fbackIcon = os.path.join(
self.mainConf.iconPath, self.fbackName, "%s-dark.svg" % iconKey
)
if path.isfile(fbackIcon):
if os.path.isfile(fbackIcon):
logger.verbose("Loading icon '%s' from fallback theme (dark mode)" % iconKey)
return QIcon(fbackIcon)
fbackIcon = path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey)
if path.isfile(fbackIcon):
fbackIcon = os.path.join(self.mainConf.iconPath, self.fbackName, "%s.svg" % iconKey)
if os.path.isfile(fbackIcon):
logger.verbose("Loading icon '%s' from fallback theme (light mode)" % iconKey)
return QIcon(fbackIcon)
+28 -29
View File
@@ -9,7 +9,7 @@
Created: 2019-10-20 [0.3]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,11 +25,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import json
import nw
import os
from os import path
from datetime import datetime
from PyQt5.QtCore import Qt
@@ -58,6 +58,7 @@ class GuiWritingStats(QDialog):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiWritingStats ...")
self.setObjectName("GuiWritingStats")
self.mainConf = nw.CONFIG
self.theParent = theParent
@@ -321,20 +322,19 @@ class GuiWritingStats(QDialog):
if fileExt:
fileName = "sessionStats.%s" % fileExt
saveDir = self.mainConf.lastPath
savePath = path.join(saveDir, fileName)
if not path.isdir(saveDir):
savePath = os.path.join(saveDir, fileName)
if not os.path.isdir(saveDir):
saveDir = self.mainConf.homePath
if self.mainConf.showGUI:
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if saveTo[0]:
savePath = saveTo[0]
else:
return False
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if saveTo:
savePath = saveTo[0]
else:
return False
self.mainConf.setLastPath(savePath)
@@ -379,19 +379,18 @@ class GuiWritingStats(QDialog):
errMsg = str(e)
# Report to user
if self.mainConf.showGUI:
if wSuccess:
self.theParent.makeAlert(
"%s file successfully written to:<br> %s" % (
textFmt, savePath
), nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write %s file. %s" % (
textFmt, errMsg
), nwAlert.ERROR
)
if wSuccess:
self.theParent.makeAlert(
"%s file successfully written to:<br> %s" % (
textFmt, savePath
), nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write %s file. %s" % (
textFmt, errMsg
), nwAlert.ERROR
)
return True
@@ -411,7 +410,7 @@ class GuiWritingStats(QDialog):
ttTime = 0
try:
logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
with open(logFile, mode="r", encoding="utf8") as inFile:
for inLine in inFile:
if inLine.startswith("#"):
+134 -130
View File
@@ -9,7 +9,7 @@
Created: 2018-09-22 [0.0.1]
This file is a part of novelWriter
Copyright 2020, Veronica Berglyd Olsen
Copyright 20182020, 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
@@ -25,10 +25,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import nw
import logging
import os
from os import path
from datetime import datetime
from time import time
@@ -40,11 +40,11 @@ from PyQt5.QtWidgets import (
)
from nw.gui import (
GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails,
GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus,
GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme,
GuiProjectSettings, GuiProjectTree, GuiWritingStats, GuiProjectWizard,
GuiAbout
GuiAbout, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit,
GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor,
GuiMainMenu, GuiMainStatus, GuiOutline, GuiOutlineDetails, GuiPreferences,
GuiProjectLoad, GuiProjectSettings, GuiProjectTree, GuiProjectWizard,
GuiTheme, GuiWritingStats
)
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwItemType, nwItemClass, nwAlert
@@ -195,8 +195,8 @@ class GuiMain(QMainWindow):
self.setStatus = self.statusBar.setStatus
self.setProjectStatus = self.statusBar.setProjectStatus
if self.mainConf.showGUI:
self.show()
# Force a show of the GUI
self.show()
# Check that config loaded fine
self.reportConfErr()
@@ -218,7 +218,8 @@ class GuiMain(QMainWindow):
logger.debug("Opening project from additional command line option")
self.openProject(self.mainConf.cmdOpen)
else:
self.manageProjects()
if self.mainConf.showGUI:
self.showProjectLoadDialog()
logger.debug("novelWriter is ready ...")
self.statusBar.setStatus("novelWriter is ready ...")
@@ -245,38 +246,19 @@ class GuiMain(QMainWindow):
# Project Actions
##
def manageProjects(self):
"""Opens the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached.
"""
if not self.mainConf.showGUI:
return False
dlgProj = GuiProjectLoad(self)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
if dlgProj.openState == GuiProjectLoad.OPEN_STATE:
self.openProject(dlgProj.openPath)
elif dlgProj.openState == GuiProjectLoad.NEW_STATE:
self.newProject()
return True
def newProject(self, projData=None, forceNew=False):
def newProject(self, projData=None):
"""Create new project with a few default files and folders.
The variable forceNew is used for testing.
"""
if self.hasProject:
msgBox = QMessageBox()
msgBox.warning(
self, "New Project",
"Please close the current project before making a new one."
self.makeAlert(
"Please close the current project before making a new one.",
nwAlert.ERROR
)
return False
if projData is None and self.mainConf.showGUI:
projData = self.newProjectDialog()
if projData is None:
projData = self.showNewProjectDialog()
if projData is None:
return False
@@ -286,11 +268,10 @@ class GuiMain(QMainWindow):
logger.error("No projData or projPath set")
return False
if path.isfile(path.join(projPath, self.theProject.projFile)) and not forceNew:
msgBox = QMessageBox()
msgBox.critical(
self, "New Project",
"A project already exists in that location. Please choose another folder."
if os.path.isfile(os.path.join(projPath, self.theProject.projFile)):
self.makeAlert(
"A project already exists in that location. Please choose another folder.",
nwAlert.ERROR
)
return False
@@ -316,7 +297,7 @@ class GuiMain(QMainWindow):
# There is no project loaded, everything OK
return True
if self.mainConf.showGUI and not isYes:
if not isYes:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Close Project", "Save changes and close current project?"
@@ -332,7 +313,7 @@ class GuiMain(QMainWindow):
doBackup = False
if self.theProject.doBackup and self.mainConf.backupOnClose:
doBackup = True
if self.mainConf.showGUI and self.mainConf.askBeforeBackup:
if self.mainConf.askBeforeBackup:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Backup Project", "Backup current project?"
@@ -379,39 +360,38 @@ class GuiMain(QMainWindow):
# reason handled by the project class.
return False
if self.mainConf.showGUI:
try:
lockDetails = (
"<br><br>The project was locked by the computer "
"'%s' (%s %s), last active on %s"
) % (
self.theProject.lockedBy[0],
self.theProject.lockedBy[1],
self.theProject.lockedBy[2],
datetime.fromtimestamp(
int(self.theProject.lockedBy[3])
).strftime("%x %X")
)
except Exception:
lockDetails = ""
msgBox = QMessageBox()
msgRes = msgBox.warning(
self, "Project Locked", (
"The project is already open by another instance of novelWriter, and "
"is therefore locked. Override lock and continue anyway?<br><br>"
"Note: If the program or the computer previously crashed, the lock "
"can safely be overridden. If, however, another instance of "
"novelWriter has the project open, overriding the lock may corrupt "
"the project, and is not recommended.%s"
) % lockDetails,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
try:
lockDetails = (
"<br><br>The project was locked by the computer "
"'%s' (%s %s), last active on %s"
) % (
self.theProject.lockedBy[0],
self.theProject.lockedBy[1],
self.theProject.lockedBy[2],
datetime.fromtimestamp(
int(self.theProject.lockedBy[3])
).strftime("%x %X")
)
if msgRes == QMessageBox.Yes:
if not self.theProject.openProject(projFile, overrideLock=True):
return False
else:
except Exception:
lockDetails = ""
msgBox = QMessageBox()
msgRes = msgBox.warning(
self, "Project Locked", (
"The project is already open by another instance of novelWriter, and "
"is therefore locked. Override lock and continue anyway?<br><br>"
"Note: If the program or the computer previously crashed, the lock "
"can safely be overridden. If, however, another instance of "
"novelWriter has the project open, overriding the lock may corrupt "
"the project, and is not recommended.%s"
) % lockDetails,
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if msgRes == QMessageBox.Yes:
if not self.theProject.openProject(projFile, overrideLock=True):
return False
else:
return False
# Project is loaded
self.hasProject = True
@@ -449,7 +429,7 @@ class GuiMain(QMainWindow):
# If the project is new, it may not have a path, so we need one
if self.theProject.projPath is None:
projPath = self.saveProjectDialog()
projPath = self.selectProjectPath()
self.theProject.setProjectPath(projPath)
if self.theProject.projPath is None:
return False
@@ -611,15 +591,12 @@ class GuiMain(QMainWindow):
return False
if not self.docEditor.isEmpty():
if self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(self, "Import Document", (
"Importing the file will overwrite the current content of the document. "
"Do you want to proceed?"
))
if msgRes != QMessageBox.Yes:
return False
else:
msgBox = QMessageBox()
msgRes = msgBox.question(self, "Import Document", (
"Importing the file will overwrite the current content of the document. "
"Do you want to proceed?"
))
if msgRes != QMessageBox.Yes:
return False
self.docEditor.replaceText(theText)
@@ -629,18 +606,16 @@ class GuiMain(QMainWindow):
def mergeDocuments(self):
"""Merge multiple documents to one single new document.
"""
if self.mainConf.showGUI:
dlgMerge = GuiDocMerge(self, self.theProject)
dlgMerge.exec_()
return True
dlgMerge = GuiDocMerge(self, self.theProject)
dlgMerge.exec_()
return
def splitDocument(self):
"""Split a single document into multiple documents.
"""
if self.mainConf.showGUI:
dlgSplit = GuiDocSplit(self, self.theProject)
dlgSplit.exec_()
return True
dlgSplit = GuiDocSplit(self, self.theProject)
dlgSplit.exec_()
return
def passDocumentAction(self, theAction):
"""Pass on document action theAction to the document viewer if
@@ -684,13 +659,13 @@ class GuiMain(QMainWindow):
return
logger.verbose("Requesting change to item %s" % tHandle)
if self.mainConf.showGUI:
dlgProj = GuiItemEditor(self, self.theProject, tHandle)
if dlgProj.exec_():
self.treeView.setTreeItemValues(tHandle)
self.treeMeta.updateViewBox(tHandle)
self.docEditor.updateDocInfo(tHandle)
self.docViewer.updateDocInfo(tHandle)
dlgProj = GuiItemEditor(self, self.theProject, tHandle)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
self.treeView.setTreeItemValues(tHandle)
self.treeMeta.updateViewBox(tHandle)
self.docEditor.updateDocInfo(tHandle)
self.docViewer.updateDocInfo(tHandle)
return
@@ -745,7 +720,7 @@ class GuiMain(QMainWindow):
qApp.restoreOverrideCursor()
if self.mainConf.showGUI and not beQuiet:
if not beQuiet:
self.makeAlert("The project index has been successfully rebuilt.", nwAlert.INFO)
return True
@@ -762,7 +737,7 @@ class GuiMain(QMainWindow):
# Main Dialogs
##
def saveProjectDialog(self):
def selectProjectPath(self):
"""Select where to save project.
"""
dlgOpt = QFileDialog.Options()
@@ -775,7 +750,22 @@ class GuiMain(QMainWindow):
return projPath
return None
def newProjectDialog(self):
def showProjectLoadDialog(self):
"""Opens the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached.
"""
dlgProj = GuiProjectLoad(self)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
if dlgProj.openState == GuiProjectLoad.OPEN_STATE:
self.openProject(dlgProj.openPath)
elif dlgProj.openState == GuiProjectLoad.NEW_STATE:
self.newProject()
return True
def showNewProjectDialog(self):
"""Open the wizard and assemble the project options dict.
"""
newProj = GuiProjectWizard(self)
@@ -786,62 +776,76 @@ class GuiMain(QMainWindow):
return None
def editConfigDialog(self):
def showPreferencesDialog(self):
"""Open the preferences dialog.
"""
dlgConf = GuiPreferences(self, self.theProject)
if dlgConf.exec_() == QDialog.Accepted:
dlgConf.exec_()
if dlgConf.result() == QDialog.Accepted:
logger.debug("Applying new preferences")
self.initMain()
self.theTheme.updateTheme()
self.saveDocument()
self.docEditor.initEditor()
self.docViewer.initViewer()
return True
def editProjectDialog(self):
return
def showProjectSettingsDialog(self):
"""Open the project settings dialog.
"""
if self.hasProject:
dlgProj = GuiProjectSettings(self, self.theProject)
dlgProj.exec_()
if not self.hasProject:
logger.error("No project open")
return
dlgProj = GuiProjectSettings(self, self.theProject)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
logger.debug("Applying new project settings")
self.docEditor.setDictionaries()
self._setWindowTitle(self.theProject.projName)
return True
def buildProjectDialog(self):
return
def showBuildProjectDialog(self):
"""Open the build project dialog.
"""
if self.hasProject:
dlgBuild = GuiBuildNovel(self, self.theProject)
dlgBuild.setModal(False)
dlgBuild.show()
return True
if not self.hasProject:
logger.error("No project open")
return
dlgBuild = GuiBuildNovel(self, self.theProject)
dlgBuild.setModal(False)
dlgBuild.show()
return
def showWritingStatsDialog(self):
"""Open the session log dialog.
"""
if self.hasProject:
dlgStats = GuiWritingStats(self, self.theProject)
dlgStats.setModal(False)
dlgStats.show()
return True
if not self.hasProject:
logger.error("No project open")
return
dlgStats = GuiWritingStats(self, self.theProject)
dlgStats.setModal(False)
dlgStats.show()
return
def showAboutNWDialog(self):
"""Show the about dialog for novelWriter.
"""
if self.mainConf.showGUI:
dlgAbout = GuiAbout(self)
dlgAbout.exec_()
return True
dlgAbout = GuiAbout(self)
dlgAbout.exec_()
return
def showAboutQtDialog(self):
"""Show the about dialog for Qt.
"""
if self.mainConf.showGUI:
msgBox = QMessageBox()
msgBox.aboutQt(self, "About Qt")
return True
msgBox = QMessageBox()
msgBox.aboutQt(self, "About Qt")
return
def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
"""Alert both the user and the logger at the same time. Message
@@ -901,7 +905,7 @@ class GuiMain(QMainWindow):
def closeMain(self):
"""Save everything, and close novelWriter.
"""
if self.mainConf.showGUI and self.hasProject:
if self.hasProject:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Exit", "Do you want to save changes and exit?"
+2 -3
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env python3
import os
import sys
import shutil
import subprocess
import setuptools
@@ -42,7 +41,7 @@ if buildDocs:
buildFail = False
try:
subprocess.call(["make","-C", "docs", "qthelp"])
subprocess.call(["make", "-C", "docs", "qthelp"])
except Exception as e:
print("Failed with error:")
print(str(e))
@@ -98,7 +97,7 @@ if buildSample:
from zipfile import ZipFile
with ZipFile(dstSample, "w") as zipObj:
zipObj.write(os.path.join("sample", "nwProject.nwx"), "nwProject.nwx")
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
for docFile in os.listdir(os.path.join(srcSample, "content")):
srcDoc = os.path.join(srcSample, "content", docFile)
zipObj.write(srcDoc, "content/"+docFile)
+128 -46
View File
@@ -5,116 +5,198 @@
import sys
import pytest
import shutil
import os
from os import path, mkdir
from nwdummy import DummyMain
sys.path.insert(1, path.abspath(path.join(path.dirname(__file__), path.pardir)))
from PyQt5.QtWidgets import QMessageBox
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)))
from nw.config import Config # noqa: E402
##
# Core Test Folders
##
@pytest.fixture(scope="session")
def nwTemp():
testDir = path.dirname(__file__)
tempDir = path.join(testDir, "temp")
if path.isdir(tempDir):
"""A temporary folder for the test session. This folder is
presistent after the test so that the status of generated files can
be checked. The folder is instead cleared before a new test session.
"""
testDir = os.path.dirname(__file__)
tempDir = os.path.join(testDir, "temp")
if os.path.isdir(tempDir):
shutil.rmtree(tempDir)
if not path.isdir(tempDir):
mkdir(tempDir)
if not os.path.isdir(tempDir):
os.mkdir(tempDir)
return tempDir
@pytest.fixture(scope="session")
def nwRef():
testDir = path.dirname(__file__)
refDir = path.join(testDir, "reference")
"""The folder where all the reference files are stored for verifying
the results of tests.
"""
testDir = os.path.dirname(__file__)
refDir = os.path.join(testDir, "reference")
return refDir
@pytest.fixture(scope="session")
def nwConf(nwRef, nwTemp):
theConf = Config()
theConf.initConfig(nwRef, nwTemp)
return theConf
##
# novelWriter Objects
##
@pytest.fixture(scope="session")
def tmpConf(nwTemp):
"""Create a temporary novelWriter configuration object.
"""
theConf = Config()
theConf.initConfig(nwTemp, nwTemp)
theConf.setLastPath("")
return theConf
@pytest.fixture(scope="session")
def nwConf(nwRef, nwTemp):
"""Temporary novelWriter configuration used for the dummy instance
of novelWriter's main GUI.
"""
theConf = Config()
theConf.initConfig(nwRef, nwTemp)
return theConf
@pytest.fixture(scope="session")
def nwDummy(nwRef, nwTemp, nwConf):
"""Create a dummy instance of novelWriter's main GUI class.
"""
theDummy = DummyMain()
theDummy.mainConf = nwConf
return theDummy
##
# Temporary Test Folders
##
@pytest.fixture(scope="session")
def nwTempProj(nwTemp):
projDir = path.join(nwTemp, "proj")
if not path.isdir(projDir):
mkdir(projDir)
"""A temporary folder for project tests.
"""
projDir = os.path.join(nwTemp, "proj")
if not os.path.isdir(projDir):
os.mkdir(projDir)
return projDir
@pytest.fixture(scope="session")
def nwTempGUI(nwTemp):
guiDir = path.join(nwTemp, "gui")
if not path.isdir(guiDir):
mkdir(guiDir)
"""A temporary folder for GUI tests.
"""
guiDir = os.path.join(nwTemp, "gui")
if not os.path.isdir(guiDir):
os.mkdir(guiDir)
return guiDir
@pytest.fixture(scope="session")
def nwTempBuild(nwTemp):
buildDir = path.join(nwTemp, "build")
if not path.isdir(buildDir):
mkdir(buildDir)
"""A temporary folder for build tests.
"""
buildDir = os.path.join(nwTemp, "build")
if not os.path.isdir(buildDir):
os.mkdir(buildDir)
return buildDir
@pytest.fixture(scope="function")
def nwFuncTemp(nwTemp):
funcDir = path.join(nwTemp, "ftemp")
if path.isdir(funcDir):
"""A temporary folder for a single test function.
"""
funcDir = os.path.join(nwTemp, "ftemp")
if os.path.isdir(funcDir):
shutil.rmtree(funcDir)
if not path.isdir(funcDir):
mkdir(funcDir)
if not os.path.isdir(funcDir):
os.mkdir(funcDir)
yield funcDir
if path.isdir(funcDir):
if os.path.isdir(funcDir):
shutil.rmtree(funcDir)
return
##
# Temp Folders for Projects
##
@pytest.fixture(scope="function")
def nwMinimal(nwTemp):
testDir = path.dirname(__file__)
minimalStore = path.join(testDir, "minimal")
minimalDir = path.join(nwTemp, "minimal")
if path.isdir(minimalDir):
"""A minimal novelWriter example project.
"""
testDir = os.path.dirname(__file__)
minimalStore = os.path.join(testDir, "minimal")
minimalDir = os.path.join(nwTemp, "minimal")
if os.path.isdir(minimalDir):
shutil.rmtree(minimalDir)
shutil.copytree(minimalStore, minimalDir)
cacheDir = path.join(minimalDir, "cache")
if path.isdir(cacheDir):
cacheDir = os.path.join(minimalDir, "cache")
if os.path.isdir(cacheDir):
shutil.rmtree(cacheDir)
metaDir = path.join(minimalDir, "meta")
if path.isdir(metaDir):
metaDir = os.path.join(minimalDir, "meta")
if os.path.isdir(metaDir):
shutil.rmtree(metaDir)
yield minimalDir
if path.isdir(minimalDir):
if os.path.isdir(minimalDir):
shutil.rmtree(minimalDir)
return
@pytest.fixture(scope="function")
def nwLipsum(nwTemp):
testDir = path.dirname(__file__)
lipsumStore = path.join(testDir, "lipsum")
lipsumDir = path.join(nwTemp, "lipsum")
if path.isdir(lipsumDir):
"""A medium sized novelWriter example project with a lot of Lorem
Ipsum dummy text.
"""
testDir = os.path.dirname(__file__)
lipsumStore = os.path.join(testDir, "lipsum")
lipsumDir = os.path.join(nwTemp, "lipsum")
if os.path.isdir(lipsumDir):
shutil.rmtree(lipsumDir)
shutil.copytree(lipsumStore, lipsumDir)
cacheDir = path.join(lipsumDir, "cache")
if path.isdir(cacheDir):
cacheDir = os.path.join(lipsumDir, "cache")
if os.path.isdir(cacheDir):
shutil.rmtree(cacheDir)
metaDir = path.join(lipsumDir, "meta")
if path.isdir(metaDir):
metaDir = os.path.join(lipsumDir, "meta")
if os.path.isdir(metaDir):
shutil.rmtree(metaDir)
yield lipsumDir
if path.isdir(lipsumDir):
if os.path.isdir(lipsumDir):
shutil.rmtree(lipsumDir)
return
@pytest.fixture(scope="function")
def nwOldProj(nwTemp):
"""A minimal movelWriter project using the old folder structure.
"""
testDir = os.path.dirname(__file__)
oldProjStore = os.path.join(testDir, "oldproj")
oldProjDir = os.path.join(nwTemp, "oldproj")
if os.path.isdir(oldProjDir):
shutil.rmtree(oldProjDir)
shutil.copytree(oldProjStore, oldProjDir)
yield oldProjDir
if os.path.isdir(oldProjDir):
shutil.rmtree(oldProjDir)
return
##
# Monkey Patch Dialogs
##
@pytest.fixture(scope="function")
def yesToAll(monkeypatch):
"""Make the message boxes/questions always say yes.
"""
monkeypatch.setattr(
QMessageBox, "question", lambda *args, **kwargs: QMessageBox.Yes
)
monkeypatch.setattr(
QMessageBox, "information", lambda *args, **kwargs: QMessageBox.Yes
)
monkeypatch.setattr(
QMessageBox, "warning", lambda *args, **kwargs: QMessageBox.Yes
)
monkeypatch.setattr(
QMessageBox, "critical", lambda *args, **kwargs: QMessageBox.Yes
)
return
-1
View File
@@ -1 +0,0 @@
{}
-2
View File
@@ -1,2 +0,0 @@
# Start Time End Time Novel Notes
2020-08-28 11:36:36 2020-08-28 11:36:48 10 0
-91
View File
@@ -1,91 +0,0 @@
{
"tagIndex": {},
"refIndex": {
"a35baf2e93843": {
"T000000": {
"tags": [],
"updated": 1598607396
},
"T000001": {
"tags": [],
"updated": 1598607396
}
},
"f5ab3e30151e1": {
"T000000": {
"tags": [],
"updated": 1598607396
},
"T000001": {
"tags": [],
"updated": 1598607396
}
},
"8c659a11cd429": {
"T000000": {
"tags": [],
"updated": 1598607396
},
"T000001": {
"tags": [],
"updated": 1598607396
}
}
},
"novelIndex": {
"a35baf2e93843": {
"T000001": {
"level": "H1",
"title": "Minimal",
"layout": "TITLE",
"synopsis": "",
"cCount": 7,
"wCount": 1,
"pCount": 0,
"updated": 1598607396
}
},
"f5ab3e30151e1": {
"T000001": {
"level": "H2",
"title": "New Chapter",
"layout": "CHAPTER",
"synopsis": "",
"cCount": 11,
"wCount": 2,
"pCount": 0,
"updated": 1598607396
}
},
"8c659a11cd429": {
"T000001": {
"level": "H3",
"title": "New Scene",
"layout": "SCENE",
"synopsis": "",
"cCount": 9,
"wCount": 2,
"pCount": 0,
"updated": 1598607396
}
}
},
"noteIndex": {},
"textCounts": {
"a35baf2e93843": [
28,
6,
1
],
"f5ab3e30151e1": [
11,
2,
0
],
"8c659a11cd429": [
9,
2,
0
]
}
}
+9 -7
View File
@@ -2,17 +2,15 @@
"""novelWriter Test Tools
"""
from os import path, mkdir
from itertools import chain
from PyQt5.QtWidgets import qApp
def ensureDir(theDir):
if not path.isdir(theDir):
mkdir(theDir)
return
def cmpFiles(fileOne, fileTwo, ignoreLines=[]):
def cmpFiles(fileOne, fileTwo, ignoreLines=None):
"""Compare two files, but optionally ignore lines given by a list.
"""
if ignoreLines is None:
ignoreLines = []
try:
foOne = open(fileOne, mode="r", encoding="utf8")
@@ -54,6 +52,8 @@ def cmpFiles(fileOne, fileTwo, ignoreLines=[]):
return not diffFound
def cmpList(listOne, listTwo):
"""Compare two iterable objects.
"""
flatOne = list(chain.from_iterable([listOne]))
flatTwo = list(chain.from_iterable([listTwo]))
if len(flatOne) != len(flatTwo):
@@ -64,6 +64,8 @@ def cmpList(listOne, listTwo):
return True
def getGuiItem(theName):
"""Returns a QtWidget based on its objectName.
"""
for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == theName:
return qWidget
@@ -0,0 +1,4 @@
### Scene Four
Scene Four
@@ -0,0 +1,4 @@
# Antagonist
Antagonist
@@ -0,0 +1,4 @@
### Scene Two
Scene Two
@@ -0,0 +1,4 @@
# Protagonist
Protagonist
@@ -0,0 +1,4 @@
### Scene Three
Scene Three
@@ -0,0 +1,4 @@
### Scene Five
Scene Five
@@ -0,0 +1,4 @@
### Scene One
Scene One
+2
View File
@@ -0,0 +1,2 @@
Start: 2020-09-26 16:13:00 End: 2020-09-26 16:15:54 Words: 24
Start: 2020-09-26 16:16:28 End: 2020-09-26 16:16:40 Words: -1
+72
View File
@@ -0,0 +1,72 @@
{
"tagIndex": {},
"refIndex": {
"f528d831f5b24": [],
"88124a4292d8b": [],
"91239bf2f8b69": [],
"19752e7f9d8af": [],
"a764d5acf5a21": [],
"9058ae29f0dfd": [],
"7ff63b8afc4cd": []
},
"novelIndex": {
"f528d831f5b24": [
[
1,
3,
"Scene One",
"SCENE"
]
],
"88124a4292d8b": [
[
1,
3,
"Scene Two",
"SCENE"
]
],
"91239bf2f8b69": [
[
1,
3,
"Scene Three",
"SCENE"
]
],
"19752e7f9d8af": [
[
1,
3,
"Scene Four",
"SCENE"
]
],
"a764d5acf5a21": [
[
1,
3,
"Scene Five",
"SCENE"
]
]
},
"noteIndex": {
"9058ae29f0dfd": [
[
1,
1,
"Protagonist",
"NOTE"
]
],
"7ff63b8afc4cd": [
[
1,
1,
"Antagonist",
"NOTE"
]
]
}
}
+148
View File
@@ -0,0 +1,148 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.4.5" fileVersion="1.0" timeStamp="2020-09-26 16:16:39">
<project>
<name></name>
<title></title>
<backup>True</backup>
</project>
<settings>
<spellCheck>False</spellCheck>
<lastEdited>a764d5acf5a21</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>23</lastWordCount>
<autoReplace/>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
</importance>
</settings>
<content count="12">
<item handle="095c337ad50c8" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
</item>
<item handle="04d47c4b31af7" order="0" parent="095c337ad50c8">
<name>Chapter One</name>
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>True</expanded>
</item>
<item handle="f528d831f5b24" order="0" parent="04d47c4b31af7">
<name>Scene One</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>18</charCount>
<wordCount>4</wordCount>
<paraCount>1</paraCount>
<cursorPos>3</cursorPos>
</item>
<item handle="88124a4292d8b" order="1" parent="04d47c4b31af7">
<name>Scene Two</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>18</charCount>
<wordCount>4</wordCount>
<paraCount>1</paraCount>
<cursorPos>2</cursorPos>
</item>
<item handle="91239bf2f8b69" order="2" parent="04d47c4b31af7">
<name>Scene Three</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>22</charCount>
<wordCount>4</wordCount>
<paraCount>1</paraCount>
<cursorPos>2</cursorPos>
</item>
<item handle="19752e7f9d8af" order="3" parent="04d47c4b31af7">
<name>Scene Four</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>20</charCount>
<wordCount>4</wordCount>
<paraCount>1</paraCount>
<cursorPos>2</cursorPos>
</item>
<item handle="a764d5acf5a21" order="4" parent="04d47c4b31af7">
<name>Scene Five</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>20</charCount>
<wordCount>4</wordCount>
<paraCount>1</paraCount>
<cursorPos>2</cursorPos>
</item>
<item handle="a11282c943444" order="1" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>True</expanded>
</item>
<item handle="9058ae29f0dfd" order="0" parent="a11282c943444">
<name>Protagonist</name>
<type>FILE</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<layout>NOTE</layout>
<charCount>11</charCount>
<wordCount>1</wordCount>
<paraCount>0</paraCount>
<cursorPos>28</cursorPos>
</item>
<item handle="7ff63b8afc4cd" order="1" parent="a11282c943444">
<name>Antagonist</name>
<type>FILE</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
<layout>NOTE</layout>
<charCount>13</charCount>
<wordCount>2</wordCount>
<paraCount>1</paraCount>
<cursorPos>26</cursorPos>
</item>
<item handle="dabb9d158b2b2" order="2" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="4a817a3a84b42" order="3" parent="None">
<name>World</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
</content>
</novelWriterXML>
+3 -4
View File
@@ -2,15 +2,14 @@
# -*- coding: utf-8 -*-
import pstats
import os
from os import path
profDir = path.abspath(path.join(path.dirname(__file__), path.pardir, "prof"))
profDir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir, "prof"))
print("")
print("Profiles directory: %s" % profDir)
print("")
profMainWindows = pstats.Stats(path.join(profDir, "testMainWindows.prof"))
profMainWindows = pstats.Stats(os.path.join(profDir, "testMainWindows.prof"))
profMainWindows.sort_stats("cumtime")
profMainWindows.print_stats("nw/")
+71
View File
@@ -0,0 +1,71 @@
{
"meta": {
"workingTitle": "Lorem Ipsum",
"novelTitle": "Lorem Ipsum",
"authors": [
"lipsum.com"
],
"buildTime": 1601120680
},
"text": {
"css": [
"p {text-align: justify;}",
"h1, h2 {color: rgb(66, 113, 174);}",
"h3, h4 {color: rgb(50, 50, 50);}",
"h1, h2, h3, h4 {page-break-after: avoid;}",
"a {color: rgb(66, 113, 174);}",
".title {font-size: 2.5em;}",
".tags {color: rgb(245, 135, 31); font-weight: bold;}",
".break {text-align: left;}",
".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}",
".skip {margin-top: 1em; margin-bottom: 1em;}",
".synopsis {font-style: italic;}",
".comment {font-style: italic; color: rgb(100, 100, 100);}"
],
"html": [
[
"<h1 class='title' style='text-align: center; page-break-before: never;'>Lorem Ipsum</h1>"
],
[
""
],
[
"<h1 style='page-break-before: always;'>Prologue</h1>",
"<p class='synopsis'><strong>Synopsis: </strong>Explanation from the lipsum.com website.</p>"
],
[
"<h1 style='text-align: center; page-break-before: always;'>Act One</h1>"
],
[
"<h1 style='page-break-before: always;'>Chapter One: Chapter One</h1>",
"<div><span class='tags'>Point of View:</span>&nbsp;<a href='#tag_Bod'>Bod</a></div><div><span class='tags'>Plot:</span>&nbsp;<a href='#tag_Main'>Main</a></div><div><span class='tags'>Locations:</span>&nbsp;<a href='#tag_Europe'>Europe</a></div><p class='synopsis'><strong>Synopsis: </strong>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.</p>"
],
[
"<h2>Scene 1: Scene One</h2>",
"<div><span class='tags'>Point of View:</span>&nbsp;<a href='#tag_Bod'>Bod</a></div><div><span class='tags'>Plot:</span>&nbsp;<a href='#tag_Main'>Main</a></div><div><span class='tags'>Locations:</span>&nbsp;<a href='#tag_Europe'>Europe</a></div><p class='synopsis'><strong>Synopsis: </strong>Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.</p>",
"<h3>Section: Scene One, Section Two</h3>"
],
[
"<h2>Scene 2: Scene Two</h2>",
"<div><span class='tags'>Point of View:</span>&nbsp;<a href='#tag_Bod'>Bod</a></div><div><span class='tags'>Plot:</span>&nbsp;<a href='#tag_Main'>Main</a></div><div><span class='tags'>Locations:</span>&nbsp;<a href='#tag_Europe'>Europe</a></div><p class='synopsis'><strong>Synopsis: </strong>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.</p>",
"<h3>Section: Scene Two, Section Two</h3>"
],
[
"<h1 style='page-break-before: always;'>Chapter Two: Chapter Two</h1>",
"<div><span class='tags'>Point of View:</span>&nbsp;<a href='#tag_Bod'>Bod</a></div><div><span class='tags'>Plot:</span>&nbsp;<a href='#tag_Main'>Main</a></div><div><span class='tags'>Locations:</span>&nbsp;<a href='#tag_Europe'>Europe</a></div><p class='synopsis'><strong>Synopsis: </strong>Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.</p>"
],
[
"<h2>Scene 3: Scene Three</h2>",
"<div><span class='tags'>Point of View:</span>&nbsp;<a href='#tag_Bod'>Bod</a></div><div><span class='tags'>Plot:</span>&nbsp;<a href='#tag_Main'>Main</a></div><div><span class='tags'>Locations:</span>&nbsp;<a href='#tag_Europe'>Europe</a></div><p class='synopsis'><strong>Synopsis: </strong>Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.</p>"
],
[
"<h2>Scene 4: Scene Four</h2>",
"<div><span class='tags'>Point of View:</span>&nbsp;<a href='#tag_Bod'>Bod</a></div><div><span class='tags'>Plot:</span>&nbsp;<a href='#tag_Main'>Main</a></div><div><span class='tags'>Locations:</span>&nbsp;<a href='#tag_Europe'>Europe</a></div><p class='synopsis'><strong>Synopsis: </strong>Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.</p>"
],
[
"<h2>Scene 5: Scene Five</h2>",
"<div><span class='tags'>Point of View:</span>&nbsp;<a href='#tag_Bod'>Bod</a></div><div><span class='tags'>Plot:</span>&nbsp;<a href='#tag_Main'>Main</a></div><div><span class='tags'>Locations:</span>&nbsp;<a href='#tag_Europe'>Europe</a></div><p class='synopsis'><strong>Synopsis: </strong>Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.</p>"
]
]
}
}
+150
View File
@@ -0,0 +1,150 @@
{
"meta": {
"workingTitle": "Lorem Ipsum",
"novelTitle": "Lorem Ipsum",
"authors": [
"lipsum.com"
],
"buildTime": 1601120788
},
"text": {
"nwd": [
[
"# Lorem Ipsum",
"",
"",
"",
"",
""
],
[
"",
"",
"",
""
],
[
"## Prologue",
"",
"% Synopsis:Explanation from the lipsum.com website.",
"",
"",
""
],
[
"# Act One",
"",
"",
""
],
[
"## Chapter One",
"",
"@pov: Bod",
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.",
"",
"",
""
],
[
"### Scene One",
"",
"@pov: Bod",
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.",
"",
"",
"",
"#### Scene One, Section Two",
"",
"",
"",
""
],
[
"### Scene Two",
"",
"@pov: Bod",
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. ",
"",
"",
"",
"",
"#### Scene Two, Section Two",
"",
"",
"",
"",
""
],
[
"## Chapter Two",
"",
"@pov: Bod",
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.",
"",
"",
""
],
[
"### Scene Three",
"",
"@pov: Bod",
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.",
"",
"",
"",
"",
"",
""
],
[
"### Scene Four",
"",
"@pov: Bod",
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.",
"",
"",
"",
"",
"",
"",
"",
""
],
[
"### Scene Five",
"",
"@pov: Bod",
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.",
"",
"",
"",
"",
"",
"",
""
]
]
}
}
+5 -1
View File
@@ -19,5 +19,9 @@
This is a paragraph of dummy text.
This is another paragraph of much longer dummy text. It is in fact very very dumb dummy text! We can also try replacing “quotes”, even singles quotes are replaced. We can hyphen-ate, make dashes and even longer dashes — if we want. Ellipsis? Not a problem either …
This is another paragraph of much longer dummy text. It is in fact very very dumb dummy text! We can also try replacing “quotes”, even single quotes are replaced. Isnt that nice? We can hyphen-ate, make dashes and even longer dashes — if we want. Ellipsis? Not a problem either … How about three hyphens — for long dash? It works too.
“Full line double quoted text.”
Full line single quoted text.
+8 -8
View File
@@ -1,11 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.0b1" hexVersion="0x010000b1" fileVersion="1.2" timeStamp="2020-09-05 17:34:07">
<novelWriterXML appVersion="1.0b3" hexVersion="0x010000b3" fileVersion="1.2" timeStamp="2020-09-21 19:58:53">
<project>
<name>New Project</name>
<title></title>
<saveCount>4</saveCount>
<autoCount>1</autoCount>
<editTime>8</editTime>
<editTime>11</editTime>
</project>
<settings>
<doBackup>True</doBackup>
@@ -14,8 +14,8 @@
<autoOutline>True</autoOutline>
<lastEdited>0e17daca5f3e1</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>90</lastWordCount>
<novelWordCount>63</novelWordCount>
<lastWordCount>113</lastWordCount>
<novelWordCount>86</novelWordCount>
<notesWordCount>27</notesWordCount>
<autoReplace/>
<titleFormat>
@@ -84,10 +84,10 @@
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>331</charCount>
<wordCount>59</wordCount>
<paraCount>2</paraCount>
<cursorPos>465</cursorPos>
<charCount>464</charCount>
<wordCount>82</wordCount>
<paraCount>4</paraCount>
<cursorPos>602</cursorPos>
</item>
<item handle="44cb730c42048" order="1" parent="None">
<name>Plot</name>
+2 -2
View File
@@ -44,7 +44,7 @@
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<expanded>True</expanded>
</item>
<item handle="25fc0e7096fc6" order="0" parent="73475cb40a568">
<name>Title Page</name>
@@ -63,7 +63,7 @@
<type>FOLDER</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<expanded>True</expanded>
</item>
<item handle="98010bd9270f9" order="0" parent="31489056e0916">
<name>New Chapter</name>
+1 -1
View File
@@ -49,7 +49,7 @@ highlightquotes = False
highlightemph = False
[Backup]
backuppath =
backuppath = some/dir
backuponclose = True
askbeforebackup = True
+560
View File
@@ -0,0 +1,560 @@
{
"tagIndex": {
"Bod": [
3,
"4c4f28287af27",
"CHARACTER",
"T000001"
],
"Main": [
3,
"2426c6f0ca922",
"PLOT",
"T000001"
],
"Europe": [
3,
"04468803b92e1",
"WORLD",
"T000001"
]
},
"refIndex": {
"7a992350f3eb6": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [],
"updated": 123
}
},
"8c58a65414c23": {
"T000000": {
"tags": [],
"updated": 123
}
},
"88d59a277361b": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [],
"updated": 123
}
},
"db7e733775d4d": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [],
"updated": 123
}
},
"fb609cd8319dc": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [
[
3,
"@pov",
"Bod"
],
[
4,
"@plot",
"Main"
],
[
5,
"@location",
"Europe"
]
],
"updated": 123
}
},
"88243afbe5ed8": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [
[
3,
"@pov",
"Bod"
],
[
4,
"@plot",
"Main"
],
[
5,
"@location",
"Europe"
]
],
"updated": 123
},
"T000013": {
"tags": [],
"updated": 123
}
},
"f96ec11c6a3da": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [
[
3,
"@pov",
"Bod"
],
[
4,
"@plot",
"Main"
],
[
5,
"@location",
"Europe"
]
],
"updated": 123
},
"T000015": {
"tags": [],
"updated": 123
}
},
"846352075de7d": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [],
"updated": 123
}
},
"441420a886d82": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [
[
3,
"@pov",
"Bod"
],
[
4,
"@plot",
"Main"
],
[
5,
"@location",
"Europe"
]
],
"updated": 123
}
},
"eb103bc70c90c": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [
[
3,
"@pov",
"Bod"
],
[
4,
"@plot",
"Main"
],
[
5,
"@location",
"Europe"
]
],
"updated": 123
}
},
"f8c0562e50f1b": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [
[
3,
"@pov",
"Bod"
],
[
4,
"@plot",
"Main"
],
[
5,
"@location",
"Europe"
]
],
"updated": 123
}
},
"47666c91c7ccf": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [
[
3,
"@pov",
"Bod"
],
[
4,
"@plot",
"Main"
],
[
5,
"@location",
"Europe"
]
],
"updated": 123
}
},
"4c4f28287af27": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [],
"updated": 123
}
},
"2426c6f0ca922": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [],
"updated": 123
}
},
"04468803b92e1": {
"T000000": {
"tags": [],
"updated": 123
},
"T000001": {
"tags": [],
"updated": 123
}
}
},
"novelIndex": {
"7a992350f3eb6": {
"T000001": {
"level": "H1",
"title": "Lorem Ipsum",
"layout": "TITLE",
"synopsis": "",
"cCount": 230,
"wCount": 40,
"pCount": 3,
"updated": 123
}
},
"8c58a65414c23": {},
"88d59a277361b": {
"T000001": {
"level": "H2",
"title": "Prologue",
"layout": "UNNUMBERED",
"synopsis": "Explanation from the lipsum.com website.",
"cCount": 584,
"wCount": 92,
"pCount": 1,
"updated": 123
}
},
"db7e733775d4d": {
"T000001": {
"level": "H1",
"title": "Act One",
"layout": "PARTITION",
"synopsis": "",
"cCount": 35,
"wCount": 6,
"pCount": 1,
"updated": 123
}
},
"fb609cd8319dc": {
"T000001": {
"level": "H2",
"title": "Chapter One",
"layout": "CHAPTER",
"synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.",
"cCount": 419,
"wCount": 67,
"pCount": 1,
"updated": 123
}
},
"88243afbe5ed8": {
"T000001": {
"level": "H3",
"title": "Scene One",
"layout": "SCENE",
"synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.",
"cCount": 1197,
"wCount": 174,
"pCount": 2,
"updated": 123
},
"T000013": {
"level": "H4",
"title": "Scene One, Section Two",
"layout": "SCENE",
"synopsis": "",
"cCount": 1561,
"wCount": 230,
"pCount": 2,
"updated": 123
}
},
"f96ec11c6a3da": {
"T000001": {
"level": "H3",
"title": "Scene Two",
"layout": "SCENE",
"synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.",
"cCount": 2034,
"wCount": 299,
"pCount": 3,
"updated": 123
},
"T000015": {
"level": "H4",
"title": "Scene Two, Section Two",
"layout": "SCENE",
"synopsis": "",
"cCount": 2009,
"wCount": 301,
"pCount": 3,
"updated": 123
}
},
"846352075de7d": {
"T000001": {
"level": "H2",
"title": "Why do we use it?",
"layout": "BOOK",
"synopsis": "",
"cCount": 630,
"wCount": 109,
"pCount": 1,
"updated": 123
}
},
"441420a886d82": {
"T000001": {
"level": "H2",
"title": "Chapter Two",
"layout": "CHAPTER",
"synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.",
"cCount": 477,
"wCount": 70,
"pCount": 1,
"updated": 123
}
},
"eb103bc70c90c": {
"T000001": {
"level": "H3",
"title": "Scene Three",
"layout": "SCENE",
"synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.",
"cCount": 3006,
"wCount": 439,
"pCount": 4,
"updated": 123
}
},
"f8c0562e50f1b": {
"T000001": {
"level": "H3",
"title": "Scene Four",
"layout": "SCENE",
"synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.",
"cCount": 3839,
"wCount": 563,
"pCount": 6,
"updated": 123
}
},
"47666c91c7ccf": {
"T000001": {
"level": "H3",
"title": "Scene Five",
"layout": "SCENE",
"synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.",
"cCount": 3644,
"wCount": 543,
"pCount": 5,
"updated": 123
}
}
},
"noteIndex": {
"4c4f28287af27": {
"T000001": {
"level": "H1",
"title": "Nobody Owens",
"layout": "NOTE",
"synopsis": "",
"cCount": 1864,
"wCount": 284,
"pCount": 3,
"updated": 123
}
},
"2426c6f0ca922": {
"T000001": {
"level": "H1",
"title": "Main Plot",
"layout": "NOTE",
"synopsis": "",
"cCount": 1369,
"wCount": 195,
"pCount": 2,
"updated": 123
}
},
"04468803b92e1": {
"T000001": {
"level": "H1",
"title": "Ancient Europe",
"layout": "NOTE",
"synopsis": "",
"cCount": 1770,
"wCount": 259,
"pCount": 3,
"updated": 123
}
}
},
"textCounts": {
"7a992350f3eb6": [
230,
40,
3
],
"8c58a65414c23": [
1058,
176,
2
],
"88d59a277361b": [
584,
92,
1
],
"db7e733775d4d": [
35,
6,
1
],
"fb609cd8319dc": [
419,
67,
1
],
"88243afbe5ed8": [
2758,
404,
4
],
"f96ec11c6a3da": [
4043,
600,
6
],
"846352075de7d": [
630,
109,
1
],
"441420a886d82": [
477,
70,
1
],
"eb103bc70c90c": [
3006,
439,
4
],
"f8c0562e50f1b": [
3839,
563,
6
],
"47666c91c7ccf": [
3644,
543,
5
],
"4c4f28287af27": [
1864,
284,
3
],
"2426c6f0ca922": [
1369,
195,
2
],
"04468803b92e1": [
1770,
259,
3
]
}
}
+178
View File
@@ -0,0 +1,178 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.0b3" hexVersion="0x010000b3" fileVersion="1.2" timeStamp="2020-09-29 21:12:51">
<project>
<name>Test Custom</name>
<title>Test Novel</title>
<author>Jane Doe</author>
<author>John Doh</author>
<saveCount>1</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
<settings>
<doBackup>True</doBackup>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
<chapter>Chapter %ch%: %title%</chapter>
<unnumbered>%title%</unnumbered>
<scene>* * *</scene>
<section></section>
</titleFormat>
<status>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Note</entry>
<entry blue="0" green="150" red="200">Draft</entry>
<entry blue="0" green="200" red="50">Finished</entry>
</status>
<importance>
<entry blue="100" green="100" red="100">New</entry>
<entry blue="0" green="50" red="200">Minor</entry>
<entry blue="0" green="150" red="200">Major</entry>
<entry blue="0" green="200" red="50">Main</entry>
</importance>
</settings>
<content count="14">
<item handle="73475cb40a568" order="None" parent="None">
<name>Novel</name>
<type>ROOT</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="44cb730c42048" order="None" parent="None">
<name>Plot</name>
<type>ROOT</type>
<class>PLOT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="71ee45a3c0db9" order="None" parent="None">
<name>Characters</name>
<type>ROOT</type>
<class>CHARACTER</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="811786ad1ae74" order="None" parent="None">
<name>Locations</name>
<type>ROOT</type>
<class>WORLD</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="25fc0e7096fc6" order="None" parent="None">
<name>Timeline</name>
<type>ROOT</type>
<class>TIMELINE</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="31489056e0916" order="None" parent="None">
<name>Objects</name>
<type>ROOT</type>
<class>OBJECT</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="98010bd9270f9" order="None" parent="None">
<name>Entity</name>
<type>ROOT</type>
<class>ENTITY</class>
<status>New</status>
<expanded>False</expanded>
</item>
<item handle="0e17daca5f3e1" order="None" parent="73475cb40a568">
<name>Title Page</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>TITLE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="1a6562590ef19" order="None" parent="73475cb40a568">
<name>Scene 1</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="031b4af5197ec" order="None" parent="73475cb40a568">
<name>Scene 2</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="41cfc0d1f2d12" order="None" parent="73475cb40a568">
<name>Scene 3</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="2858dcd1057d3" order="None" parent="73475cb40a568">
<name>Scene 4</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="2fca346db6561" order="None" parent="73475cb40a568">
<name>Scene 5</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="02d20bbd7e394" order="None" parent="73475cb40a568">
<name>Scene 6</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<cursorPos>0</cursorPos>
</item>
</content>
</novelWriterXML>
+16 -15
View File
@@ -3,13 +3,14 @@
"""
import pytest
import os
from nwtools import cmpFiles
from os import path
@pytest.mark.core
def testConfigCore(tmpConf, nwTemp, nwRef):
refConf = path.join(nwRef, "novelwriter.conf")
testConf = path.join(tmpConf.confPath, "novelwriter.conf")
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
assert tmpConf.confPath == nwTemp
assert tmpConf.saveConfig()
@@ -22,8 +23,8 @@ def testConfigCore(tmpConf, nwTemp, nwRef):
@pytest.mark.core
def testConfigSetConfPath(tmpConf, nwTemp):
assert tmpConf.setConfPath(None)
assert not tmpConf.setConfPath(path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setConfPath(path.join(nwTemp, "novelwriter.conf"))
assert not tmpConf.setConfPath(os.path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setConfPath(os.path.join(nwTemp, "novelwriter.conf"))
assert tmpConf.confPath == nwTemp
assert tmpConf.confFile == "novelwriter.conf"
assert not tmpConf.confChanged
@@ -31,15 +32,15 @@ def testConfigSetConfPath(tmpConf, nwTemp):
@pytest.mark.core
def testConfigSetDataPath(tmpConf, nwTemp):
assert tmpConf.setDataPath(None)
assert not tmpConf.setDataPath(path.join("somewhere", "over", "the", "rainbow"))
assert not tmpConf.setDataPath(os.path.join("somewhere", "over", "the", "rainbow"))
assert tmpConf.setDataPath(nwTemp)
assert tmpConf.dataPath == nwTemp
assert not tmpConf.confChanged
@pytest.mark.core
def testConfigSetWinSize(tmpConf, nwTemp, nwRef):
refConf = path.join(nwRef, "novelwriter.conf")
testConf = path.join(tmpConf.confPath, "novelwriter.conf")
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
tmpConf.guiScale = 1.0
assert tmpConf.confPath == nwTemp
@@ -55,8 +56,8 @@ def testConfigSetWinSize(tmpConf, nwTemp, nwRef):
@pytest.mark.core
def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef):
refConf = path.join(nwRef, "novelwriter.conf")
testConf = path.join(tmpConf.confPath, "novelwriter.conf")
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
assert tmpConf.confPath == nwTemp
tmpConf.guiScale = 1.0
@@ -77,8 +78,8 @@ def testConfigSetTreeColWidths(tmpConf, nwTemp, nwRef):
@pytest.mark.core
def testConfigSetPanePos(tmpConf, nwTemp, nwRef):
refConf = path.join(nwRef, "novelwriter.conf")
testConf = path.join(tmpConf.confPath, "novelwriter.conf")
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
assert tmpConf.confPath == nwTemp
@@ -113,8 +114,8 @@ def testConfigSetPanePos(tmpConf, nwTemp, nwRef):
@pytest.mark.core
def testConfigFlags(tmpConf, nwTemp, nwRef):
refConf = path.join(nwRef, "novelwriter.conf")
testConf = path.join(tmpConf.confPath, "novelwriter.conf")
refConf = os.path.join(nwRef, "novelwriter.conf")
testConf = os.path.join(tmpConf.confPath, "novelwriter.conf")
assert tmpConf.confPath == nwTemp
@@ -150,7 +151,7 @@ def testTextSizes(tmpConf, nwTemp, nwRef):
@pytest.mark.core
def testConfigErrors(tmpConf):
nonPath = path.join("somewhere", "over", "the", "rainbow")
nonPath = os.path.join("somewhere", "over", "the", "rainbow")
assert tmpConf.initConfig(nonPath, nonPath)
assert tmpConf.hasError
assert not tmpConf.loadConfig()
+418 -139
View File
@@ -5,51 +5,70 @@
import nw
import pytest
import json
from shutil import copyfile
from nwtools import cmpFiles
import os
import sys
from os import path
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialogButtonBox, QTreeWidgetItem
from shutil import copyfile
from nwtools import cmpFiles, getGuiItem
from PyQt5.QtCore import Qt, QItemSelectionModel
from PyQt5.QtWidgets import (
QDialogButtonBox, QTreeWidgetItem, QListWidgetItem, QDialog, QAction,
QMessageBox, QFileDialog, QFontDialog
)
from nw.gui import (
GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel,
GuiDocMerge, GuiDocSplit, GuiWritingStats, GuiProjectWizard,
GuiProjectLoad, GuiPreferences
)
from nw.gui.custom import QuotesDialog
from nw.constants import nwItemType, nwItemLayout, nwItemClass
keyDelay = 2
typeDelay = 1
stepDelay = 20
@pytest.mark.gui
def testProjectEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
def testProjectSettings(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
# Create new, save, open project
# Check that we cannot open when there is no project
nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger)
assert getGuiItem("GuiProjectSettings") is None
# Create new project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": nwFuncTemp}, True)
assert nwGUI.newProject({"projPath": nwFuncTemp})
nwGUI.mainConf.backupPath = nwFuncTemp
projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject)
# Get the dialog object
monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *args: None)
monkeypatch.setattr(GuiProjectSettings, "result", lambda *args: QDialog.Accepted)
nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiProjectSettings") is not None, timeout=1000)
projEdit = getGuiItem("GuiProjectSettings")
assert isinstance(projEdit, GuiProjectSettings)
projEdit.show()
qtbot.addWidget(projEdit)
# Main settings
qtbot.wait(stepDelay)
projEdit.tabMain.editName.setText("")
for c in "Project Name":
qtbot.keyClick(projEdit.tabMain.editName, c, delay=keyDelay)
qtbot.keyClick(projEdit.tabMain.editName, c, delay=typeDelay)
for c in "Project Title":
qtbot.keyClick(projEdit.tabMain.editTitle, c, delay=keyDelay)
qtbot.keyClick(projEdit.tabMain.editTitle, c, delay=typeDelay)
for c in "Jane Doe":
qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=keyDelay)
qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=typeDelay)
qtbot.keyClick(projEdit.tabMain.editAuthors, Qt.Key_Return, delay=keyDelay)
for c in "John Doh":
qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=keyDelay)
qtbot.keyClick(projEdit.tabMain.editAuthors, c, delay=typeDelay)
# Test Status Tab
qtbot.wait(stepDelay)
@@ -59,9 +78,9 @@ def testProjectEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
qtbot.mouseClick(projEdit.tabStatus.newButton, Qt.LeftButton)
projEdit.tabStatus.listBox.item(3).setSelected(True)
for n in range(8):
qtbot.keyClick(projEdit.tabStatus.editName, Qt.Key_Backspace, delay=keyDelay)
qtbot.keyClick(projEdit.tabStatus.editName, Qt.Key_Backspace, delay=typeDelay)
for c in "Final":
qtbot.keyClick(projEdit.tabStatus.editName, c, delay=keyDelay)
qtbot.keyClick(projEdit.tabStatus.editName, c, delay=typeDelay)
qtbot.mouseClick(projEdit.tabStatus.saveButton, Qt.LeftButton)
# Auto-Replace Tab
@@ -71,9 +90,9 @@ def testProjectEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
qtbot.mouseClick(projEdit.tabReplace.addButton, Qt.LeftButton)
projEdit.tabReplace.listBox.topLevelItem(0).setSelected(True)
for c in "Th is ":
qtbot.keyClick(projEdit.tabReplace.editKey, c, delay=keyDelay)
qtbot.keyClick(projEdit.tabReplace.editKey, c, delay=typeDelay)
for c in "With This Stuff ":
qtbot.keyClick(projEdit.tabReplace.editValue, c, delay=keyDelay)
qtbot.keyClick(projEdit.tabReplace.editValue, c, delay=typeDelay)
qtbot.mouseClick(projEdit.tabReplace.saveButton, Qt.LeftButton)
qtbot.wait(stepDelay)
@@ -111,9 +130,9 @@ def testProjectEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
qtbot.wait(stepDelay)
# Check the files
projFile = path.join(nwFuncTemp, "nwProject.nwx")
testFile = path.join(nwTempGUI, "2_nwProject.nwx")
refFile = path.join(nwRef, "gui", "2_nwProject.nwx")
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempGUI, "2_nwProject.nwx")
refFile = os.path.join(nwRef, "gui", "2_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 8, 9, 10])
@@ -121,8 +140,8 @@ def testProjectEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI.closeMain()
@pytest.mark.gui
def testItemEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
def testItemEditor(qtbot, yesToAll, monkeypatch, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -130,10 +149,18 @@ def testItemEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
# Create new, save, open project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": nwFuncTemp}, True)
assert nwGUI.newProject({"projPath": nwFuncTemp})
assert nwGUI.openDocument("0e17daca5f3e1")
assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True)
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *args: None)
nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
itemEdit = getGuiItem("GuiItemEditor")
assert isinstance(itemEdit, GuiItemEditor)
itemEdit.show()
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "0e17daca5f3e1")
qtbot.addWidget(itemEdit)
assert itemEdit.editName.text() == "New Scene"
@@ -141,7 +168,7 @@ def testItemEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
assert itemEdit.editLayout.currentData() == nwItemLayout.SCENE
for c in "Just a Page":
qtbot.keyClick(itemEdit.editName, c, delay=keyDelay)
qtbot.keyClick(itemEdit.editName, c, delay=typeDelay)
itemEdit.editStatus.setCurrentIndex(1)
layoutIdx = itemEdit.editLayout.findData(nwItemLayout.PAGE)
itemEdit.editLayout.setCurrentIndex(layoutIdx)
@@ -150,7 +177,13 @@ def testItemEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
assert not itemEdit.editExport.isChecked()
itemEdit._doSave()
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "0e17daca5f3e1")
nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
itemEdit = getGuiItem("GuiItemEditor")
assert isinstance(itemEdit, GuiItemEditor)
itemEdit.show()
qtbot.addWidget(itemEdit)
assert itemEdit.editName.text() == "Just a Page"
assert itemEdit.editStatus.currentData() == "Note"
@@ -170,18 +203,18 @@ def testItemEditor(qtbot, nwFuncTemp, nwTempGUI, nwRef, nwTemp):
qtbot.wait(stepDelay)
# Check the files
projFile = path.join(nwFuncTemp, "nwProject.nwx")
testFile = path.join(nwTempGUI, "3_nwProject.nwx")
refFile = path.join(nwRef, "gui", "3_nwProject.nwx")
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempGUI, "3_nwProject.nwx")
refFile = os.path.join(nwRef, "gui", "3_nwProject.nwx")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
nwGUI.closeMain()
# qtbot.stopForInteraction()
nwGUI.closeMain()
@pytest.mark.gui
def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
def testWritingStatsExport(qtbot, monkeypatch, yesToAll, nwFuncTemp, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
@@ -189,11 +222,15 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
# Create new, save, close project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": nwFuncTemp}, True)
assert nwGUI.newProject({"projPath": nwFuncTemp})
assert nwGUI.saveProject()
assert nwGUI.closeProject()
qtbot.wait(stepDelay)
# Check that we cannot open when there is no project
nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger)
assert getGuiItem("GuiWritingStats") is None
assert nwGUI.openProject(nwFuncTemp)
qtbot.wait(stepDelay)
@@ -234,16 +271,23 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
qtbot.wait(stepDelay)
nwGUI.mainConf.lastPath = nwFuncTemp
sessLog = GuiWritingStats(nwGUI, nwGUI.theProject)
sessLog.show()
nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000)
sessLog = getGuiItem("GuiWritingStats")
assert isinstance(sessLog, GuiWritingStats)
qtbot.wait(stepDelay)
monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda *args, **kwargs: [])
assert not sessLog._saveData(sessLog.FMT_CSV)
monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: [pp])
assert sessLog._saveData(sessLog.FMT_CSV)
qtbot.wait(stepDelay)
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
@@ -259,7 +303,7 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
@@ -276,7 +320,7 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
@@ -293,7 +337,7 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
@@ -306,7 +350,7 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
@@ -318,7 +362,7 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwFuncTemp, "sessionStats.json")
jsonStats = os.path.join(nwFuncTemp, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
@@ -332,23 +376,39 @@ def testWritingStatsExport(qtbot, nwFuncTemp, nwTemp):
nwGUI.closeMain()
@pytest.mark.gui
def testAboutBox(qtbot, nwFuncTemp, nwTemp):
def testAboutBox(qtbot, monkeypatch, nwFuncTemp, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwFuncTemp, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
msgAbout = GuiAbout(nwGUI)
# NW About
monkeypatch.setattr(GuiAbout, "exec_", lambda *args: None)
nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
msgAbout = getGuiItem("GuiAbout")
assert isinstance(msgAbout, GuiAbout)
msgAbout.show()
assert msgAbout.pageAbout.document().characterCount() > 100
assert msgAbout.pageLicense.document().characterCount() > 100
msgAbout.mainConf.guiLang = "whatever"
msgAbout._fillLicensePage()
assert msgAbout.pageLicense.toPlainText() == "Error loading license text ..."
# Qt About
monkeypatch.setattr(QMessageBox, "aboutQt", lambda *args, **kwargs: None)
nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
# qtbot.stopForInteraction()
msgAbout._doClose()
nwGUI.closeMain()
@pytest.mark.gui
def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp):
def testBuildTool(qtbot, yesToAll, nwTempBuild, nwLipsum, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
@@ -356,11 +416,20 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp):
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
assert nwGUI.openProject(nwLipsum)
# Check that we cannot open when there is no project
nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger)
assert getGuiItem("GuiBuildNovel") is None
# Open a project
assert nwGUI.openProject(nwLipsum)
nwGUI.mainConf.lastPath = nwLipsum
nwBuild = GuiBuildNovel(nwGUI, nwGUI.theProject)
# Open the tool
nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiBuildNovel") is not None, timeout=1000)
nwBuild = getGuiItem("GuiBuildNovel")
assert isinstance(nwBuild, GuiBuildNovel)
# Default Settings
qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton)
@@ -368,15 +437,15 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp):
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = path.join(nwTempBuild, "1_LoremIpsum.nwd")
refFile = path.join(nwRef, "build", "1_LoremIpsum.nwd")
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = os.path.join(nwTempBuild, "1_LoremIpsum.nwd")
refFile = os.path.join(nwRef, "build", "1_LoremIpsum.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = path.join(nwTempBuild, "1_LoremIpsum.htm")
refFile = path.join(nwRef, "build", "1_LoremIpsum.htm")
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = os.path.join(nwTempBuild, "1_LoremIpsum.htm")
refFile = os.path.join(nwRef, "build", "1_LoremIpsum.htm")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -407,15 +476,15 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp):
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = path.join(nwTempBuild, "2_LoremIpsum.nwd")
refFile = path.join(nwRef, "build", "2_LoremIpsum.nwd")
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = os.path.join(nwTempBuild, "2_LoremIpsum.nwd")
refFile = os.path.join(nwRef, "build", "2_LoremIpsum.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = path.join(nwTempBuild, "2_LoremIpsum.htm")
refFile = path.join(nwRef, "build", "2_LoremIpsum.htm")
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = os.path.join(nwTempBuild, "2_LoremIpsum.htm")
refFile = os.path.join(nwRef, "build", "2_LoremIpsum.htm")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -438,27 +507,74 @@ def testBuildTool(qtbot, nwTempBuild, nwLipsum, nwRef, nwTemp):
qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton)
# Save files that can be compared
assert nwBuild._saveDocument(nwBuild.FMT_NWD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = os.path.join(nwTempBuild, "3_LoremIpsum.nwd")
refFile = os.path.join(nwRef, "build", "3_LoremIpsum.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = path.join(nwLipsum, "Lorem Ipsum.nwd")
testFile = path.join(nwTempBuild, "3_LoremIpsum.nwd")
refFile = path.join(nwRef, "build", "3_LoremIpsum.nwd")
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = os.path.join(nwTempBuild, "3_LoremIpsum.htm")
refFile = os.path.join(nwRef, "build", "3_LoremIpsum.htm")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = path.join(nwLipsum, "Lorem Ipsum.htm")
testFile = path.join(nwTempBuild, "3_LoremIpsum.htm")
refFile = path.join(nwRef, "build", "3_LoremIpsum.htm")
# Check the JSON files too at this stage
assert nwBuild._saveDocument(nwBuild.FMT_JSON_H)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
testFile = os.path.join(nwTempBuild, "3H_LoremIpsum.json")
refFile = os.path.join(nwRef, "build", "3H_LoremIpsum.json")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
assert cmpFiles(testFile, refFile, [8])
assert nwBuild._saveDocument(nwBuild.FMT_JSON_M)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json")
testFile = os.path.join(nwTempBuild, "3M_LoremIpsum.json")
refFile = os.path.join(nwRef, "build", "3M_LoremIpsum.json")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [8])
# Save other file types handled by Qt
# We assume the export itself by the Qt library works, so we just
# check that novelWriter successfully writes the files.
assert nwBuild._saveDocument(nwBuild.FMT_ODT)
assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.odt"))
if not nwGUI.mainConf.osDarwin:
assert nwBuild._saveDocument(nwBuild.FMT_PDF)
assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.pdf"))
assert nwBuild._saveDocument(nwBuild.FMT_MD)
assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.md"))
assert nwBuild._saveDocument(nwBuild.FMT_TXT)
assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.txt"))
# Close the build tool
htmlText = nwBuild.htmlText
htmlStyle = nwBuild.htmlStyle
nwdText = nwBuild.nwdText
buildTime = nwBuild.buildTime
nwBuild._doClose()
# Re-open build dialog from cahce
nwBuild = GuiBuildNovel(nwGUI, nwGUI.theProject)
assert nwBuild.htmlText == htmlText
assert nwBuild.htmlStyle == htmlStyle
assert nwBuild.nwdText == nwdText
assert nwBuild.buildTime == buildTime
nwBuild._doClose()
# qtbot.stopForInteraction()
nwBuild._doClose()
nwGUI.closeMain()
@pytest.mark.gui
def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
def testMergeSplitTools(qtbot, monkeypatch, yesToAll, nwTempGUI, nwLipsum, nwRef, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
@@ -473,7 +589,13 @@ def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
assert nwGUI.treeView.setSelectedHandle("45e6b01ca35c1")
qtbot.wait(stepDelay)
nwMerge = GuiDocMerge(nwGUI, nwGUI.theProject)
monkeypatch.setattr(GuiDocMerge, "exec_", lambda *args: None)
nwGUI.mainMenu.aMergeDocs.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiDocMerge") is not None, timeout=1000)
nwMerge = getGuiItem("GuiDocMerge")
assert isinstance(nwMerge, GuiDocMerge)
nwMerge.show()
qtbot.wait(stepDelay)
nwMerge._doMerge()
@@ -481,17 +603,25 @@ def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
assert nwGUI.theProject.projTree["73475cb40a568"] is not None
projFile = path.join(nwLipsum, "content", "73475cb40a568.nwd")
testFile = path.join(nwTempGUI, "4_73475cb40a568.nwd")
refFile = path.join(nwRef, "gui", "4_73475cb40a568.nwd")
projFile = os.path.join(nwLipsum, "content", "73475cb40a568.nwd")
testFile = os.path.join(nwTempGUI, "4_73475cb40a568.nwd")
refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
# Split By Chapter
assert nwGUI.treeView.setSelectedHandle("73475cb40a568")
qtbot.wait(stepDelay)
nwSplit = GuiDocSplit(nwGUI, nwGUI.theProject)
monkeypatch.setattr(GuiDocSplit, "exec_", lambda *args: None)
nwGUI.mainMenu.aSplitDoc.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiDocSplit") is not None, timeout=1000)
nwSplit = getGuiItem("GuiDocSplit")
assert isinstance(nwSplit, GuiDocSplit)
nwSplit.show()
qtbot.wait(stepDelay)
nwSplit.splitLevel.setCurrentIndex(1)
qtbot.wait(stepDelay)
@@ -499,9 +629,9 @@ def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
assert nwGUI.theProject.projTree["71ee45a3c0db9"] is not None
# This should give us back the file as it was before
projFile = path.join(nwLipsum, "content", "71ee45a3c0db9.nwd")
testFile = path.join(nwTempGUI, "4_71ee45a3c0db9.nwd")
refFile = path.join(nwRef, "gui", "4_73475cb40a568.nwd")
projFile = os.path.join(nwLipsum, "content", "71ee45a3c0db9.nwd")
testFile = os.path.join(nwTempGUI, "4_71ee45a3c0db9.nwd")
refFile = os.path.join(nwRef, "gui", "4_73475cb40a568.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [1])
@@ -519,21 +649,21 @@ def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
assert nwGUI.theProject.projTree["31489056e0916"] is not None
assert nwGUI.theProject.projTree["98010bd9270f9"] is not None
projFile = path.join(nwLipsum, "content", "25fc0e7096fc6.nwd")
testFile = path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
refFile = path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd")
projFile = os.path.join(nwLipsum, "content", "25fc0e7096fc6.nwd")
testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = path.join(nwLipsum, "content", "31489056e0916.nwd")
testFile = path.join(nwTempGUI, "5_31489056e0916.nwd")
refFile = path.join(nwRef, "gui", "5_31489056e0916.nwd")
projFile = os.path.join(nwLipsum, "content", "31489056e0916.nwd")
testFile = os.path.join(nwTempGUI, "5_31489056e0916.nwd")
refFile = os.path.join(nwRef, "gui", "5_31489056e0916.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = path.join(nwLipsum, "content", "98010bd9270f9.nwd")
testFile = path.join(nwTempGUI, "5_98010bd9270f9.nwd")
refFile = path.join(nwRef, "gui", "5_98010bd9270f9.nwd")
projFile = os.path.join(nwLipsum, "content", "98010bd9270f9.nwd")
testFile = os.path.join(nwTempGUI, "5_98010bd9270f9.nwd")
refFile = os.path.join(nwRef, "gui", "5_98010bd9270f9.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -553,33 +683,33 @@ def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
assert nwGUI.theProject.projTree["2858dcd1057d3"] is not None
assert nwGUI.theProject.projTree["2fca346db6561"] is not None
projFile = path.join(nwLipsum, "content", "1a6562590ef19.nwd")
testFile = path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
refFile = path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd")
projFile = os.path.join(nwLipsum, "content", "1a6562590ef19.nwd")
testFile = os.path.join(nwTempGUI, "5_25fc0e7096fc6.nwd")
refFile = os.path.join(nwRef, "gui", "5_25fc0e7096fc6.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [1])
projFile = path.join(nwLipsum, "content", "031b4af5197ec.nwd")
testFile = path.join(nwTempGUI, "5_031b4af5197ec.nwd")
refFile = path.join(nwRef, "gui", "5_031b4af5197ec.nwd")
projFile = os.path.join(nwLipsum, "content", "031b4af5197ec.nwd")
testFile = os.path.join(nwTempGUI, "5_031b4af5197ec.nwd")
refFile = os.path.join(nwRef, "gui", "5_031b4af5197ec.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd")
testFile = path.join(nwTempGUI, "5_41cfc0d1f2d12.nwd")
refFile = path.join(nwRef, "gui", "5_41cfc0d1f2d12.nwd")
projFile = os.path.join(nwLipsum, "content", "41cfc0d1f2d12.nwd")
testFile = os.path.join(nwTempGUI, "5_41cfc0d1f2d12.nwd")
refFile = os.path.join(nwRef, "gui", "5_41cfc0d1f2d12.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = path.join(nwLipsum, "content", "2858dcd1057d3.nwd")
testFile = path.join(nwTempGUI, "5_2858dcd1057d3.nwd")
refFile = path.join(nwRef, "gui", "5_2858dcd1057d3.nwd")
projFile = os.path.join(nwLipsum, "content", "2858dcd1057d3.nwd")
testFile = os.path.join(nwTempGUI, "5_2858dcd1057d3.nwd")
refFile = os.path.join(nwRef, "gui", "5_2858dcd1057d3.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
projFile = path.join(nwLipsum, "content", "2fca346db6561.nwd")
testFile = path.join(nwTempGUI, "5_2fca346db6561.nwd")
refFile = path.join(nwRef, "gui", "5_2fca346db6561.nwd")
projFile = os.path.join(nwLipsum, "content", "2fca346db6561.nwd")
testFile = os.path.join(nwTempGUI, "5_2fca346db6561.nwd")
refFile = os.path.join(nwRef, "gui", "5_2fca346db6561.nwd")
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@@ -587,7 +717,11 @@ def testMergeSplitTools(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
nwGUI.closeMain()
@pytest.mark.gui
def testNewProjectWizard(qtbot, nwLipsum, nwTemp):
def testNewProjectWizard(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
if sys.platform.startswith("darwin"):
# Disable for macOS because the test segfaults on QWizard.show()
return
from PyQt5.QtWidgets import QWizard
from nw.gui.projwizard import (
@@ -595,18 +729,54 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp):
ProjWizardCustomPage, ProjWizardFinalPage
)
nwGUI = nw.main(["--testmode", "--config=%s" % nwLipsum, "--data=%s" % nwTemp])
nwGUI = nw.main(["--testmode", "--config=%s" % nwTemp, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
for wStep in range(3):
##
# Test New Project Function
##
# The Wizard
nwWiz = GuiProjectWizard(nwGUI)
nwWiz.show()
qtbot.waitForWindowShown(nwWiz)
# New with a project open should cause an error
assert nwGUI.openProject(nwMinimal)
assert not nwGUI.newProject()
# Close project, but call with invalid path
assert nwGUI.closeProject()
monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: None)
assert not nwGUI.newProject()
# Now, with an empty dictionary
monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: {})
assert not nwGUI.newProject()
# Now, with a non-empty folder
monkeypatch.setattr(nwGUI, "showNewProjectDialog", lambda *args: {"projPath": nwMinimal})
assert not nwGUI.newProject()
monkeypatch.undo()
##
# Test the Wizard
##
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *args: None)
nwGUI.mainConf.lastPath = " "
nwGUI.closeProject()
nwGUI.showNewProjectDialog()
qtbot.waitUntil(lambda: getGuiItem("GuiProjectWizard") is not None, timeout=1000)
nwWiz = getGuiItem("GuiProjectWizard")
assert isinstance(nwWiz, GuiProjectWizard)
nwWiz.show()
qtbot.wait(stepDelay)
for wStep in range(4):
# This does not actually create the project, it just generates the
# dictionary that defines it.
# Intro Page
introPage = nwWiz.currentPage()
@@ -614,16 +784,16 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp):
assert not nwWiz.button(QWizard.NextButton).isEnabled()
qtbot.wait(stepDelay)
for c in "Test Minimal":
qtbot.keyClick(introPage.projName, c, delay=keyDelay)
for c in ("Test Minimal %d" % wStep):
qtbot.keyClick(introPage.projName, c, delay=typeDelay)
qtbot.wait(stepDelay)
for c in "Minimal Novel":
qtbot.keyClick(introPage.projTitle, c, delay=keyDelay)
qtbot.keyClick(introPage.projTitle, c, delay=typeDelay)
qtbot.wait(stepDelay)
for c in "Jane Doe":
qtbot.keyClick(introPage.projAuthors, c, delay=keyDelay)
qtbot.keyClick(introPage.projAuthors, c, delay=typeDelay)
# Setting projName should activate the button
assert nwWiz.button(QWizard.NextButton).isEnabled()
@@ -636,10 +806,20 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp):
assert isinstance(storagePage, ProjWizardFolderPage)
assert not nwWiz.button(QWizard.NextButton).isEnabled()
if wStep == 0:
# Check invalid path first, the first time we reach here
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: "")
qtbot.wait(stepDelay)
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
assert storagePage.projPath.text() == ""
# Then, we always return nwMinimal as path
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *a, **kw: nwMinimal)
qtbot.wait(stepDelay)
projPath = path.join(nwTemp, "dummy")
for c in projPath:
qtbot.keyClick(storagePage.projPath, c, delay=keyDelay)
qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
projPath = os.path.join(nwMinimal, "Test Minimal %d" % wStep)
assert storagePage.projPath.text() == projPath
# Setting projPath should activate the button
assert nwWiz.button(QWizard.NextButton).isEnabled()
@@ -658,13 +838,15 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp):
elif wStep == 1:
popPage.popCustom.setChecked(True)
elif wStep == 2:
popPage.popCustom.setChecked(True)
elif wStep == 3:
popPage.popSample.setChecked(True)
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Custom Page
if wStep == 1:
if wStep == 1 or wStep == 2:
customPage = nwWiz.currentPage()
assert isinstance(customPage, ProjWizardCustomPage)
assert nwWiz.button(QWizard.NextButton).isEnabled()
@@ -676,25 +858,29 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp):
customPage.addObject.setChecked(True)
customPage.addEntity.setChecked(True)
if wStep == 2:
customPage.numChapters.setValue(0)
customPage.numScenes.setValue(10)
customPage.chFolders.setChecked(False)
qtbot.wait(stepDelay)
qtbot.mouseClick(nwWiz.button(QWizard.NextButton), Qt.LeftButton)
# Final Page
finalPage = nwWiz.currentPage()
assert isinstance(finalPage, ProjWizardFinalPage)
assert nwWiz.button(QWizard.FinishButton).isEnabled()
qtbot.mouseClick(nwWiz.button(QWizard.FinishButton), Qt.LeftButton)
assert nwWiz.button(QWizard.FinishButton).isEnabled() # But we don't click it
# Check Data
projData = nwGUI._assembleProjectWizardData(nwWiz)
assert projData["projName"] == "Test Minimal"
assert projData["projName"] == "Test Minimal %d" % wStep
assert projData["projTitle"] == "Minimal Novel"
assert projData["projAuthors"] == "Jane Doe"
assert projData["projPath"] == projPath
assert projData["popMinimal"] == (wStep == 0)
assert projData["popCustom"] == (wStep == 1)
assert projData["popSample"] == (wStep == 2)
if wStep == 1:
assert projData["popCustom"] == (wStep == 1 or wStep == 2)
assert projData["popSample"] == (wStep == 3)
if wStep == 1 or wStep == 2:
assert projData["addRoots"] == [
nwItemClass.PLOT,
nwItemClass.CHARACTER,
@@ -703,20 +889,32 @@ def testNewProjectWizard(qtbot, nwLipsum, nwTemp):
nwItemClass.OBJECT,
nwItemClass.ENTITY,
]
assert projData["numChapters"] == 5
assert projData["numScenes"] == 5
assert projData["chFolders"]
if wStep == 1:
assert projData["numChapters"] == 5
assert projData["numScenes"] == 5
assert projData["chFolders"]
else:
assert projData["numChapters"] == 0
assert projData["numScenes"] == 10
assert not projData["chFolders"]
else:
assert projData["addRoots"] == []
assert projData["numChapters"] == 0
assert projData["numScenes"] == 0
assert not projData["chFolders"]
# Restart the wizard for next iteration
nwWiz.restart()
nwWiz.reject()
nwWiz.close()
# qtbot.stopForInteraction()
nwGUI.closeMain()
nwGUI.close()
@pytest.mark.gui
def testLoadProject(qtbot, nwMinimal, nwTemp):
def testLoadProject(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
@@ -726,7 +924,13 @@ def testLoadProject(qtbot, nwMinimal, nwTemp):
assert nwGUI.openProject(nwMinimal)
assert nwGUI.closeProject()
nwLoad = GuiProjectLoad(nwGUI)
monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *args: None)
monkeypatch.setattr(GuiProjectLoad, "result", lambda *args: QDialog.Accepted)
nwGUI.mainMenu.aOpenProject.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiProjectLoad") is not None, timeout=1000)
nwLoad = getGuiItem("GuiProjectLoad")
assert isinstance(nwLoad, GuiProjectLoad)
nwLoad.show()
recentCount = nwLoad.listBox.topLevelItemCount()
@@ -745,6 +949,7 @@ def testLoadProject(qtbot, nwMinimal, nwTemp):
assert nwLoad.openPath == selPath
assert nwLoad.openState == nwLoad.OPEN_STATE
# Just create a new project load from scratch for the rest of the test
del nwLoad
nwLoad = GuiProjectLoad(nwGUI)
nwLoad.show()
@@ -762,12 +967,19 @@ def testLoadProject(qtbot, nwMinimal, nwTemp):
nwLoad._keyPressDelete()
assert nwLoad.listBox.topLevelItemCount() == recentCount - 1
nwLoad.close()
getFile = os.path.join(nwMinimal, "nwProject.nwx")
monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *args, **kwargs: (getFile, None))
qtbot.mouseClick(nwLoad.browseButton, Qt.LeftButton)
assert nwLoad.openPath == nwMinimal
assert nwLoad.openState == nwLoad.OPEN_STATE
# qtbot.stopForInteraction()
nwLoad.close()
nwGUI.closeMain()
nwGUI.close()
@pytest.mark.gui
def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf):
def testPreferences(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp, nwRef, tmpConf):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
@@ -775,12 +987,19 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf):
qtbot.wait(stepDelay)
assert nwGUI.openProject(nwMinimal)
nwPrefs = GuiPreferences(nwGUI, nwGUI.theProject)
monkeypatch.setattr(GuiPreferences, "exec_", lambda *args: None)
monkeypatch.setattr(GuiPreferences, "result", lambda *args: QDialog.Accepted)
nwGUI.mainMenu.aPreferences.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiPreferences") is not None, timeout=1000)
nwPrefs = getGuiItem("GuiPreferences")
assert isinstance(nwPrefs, GuiPreferences)
nwPrefs.show()
# Override Config
tmpConf.showGUI = False
tmpConf.confPath = nwMinimal
tmpConf.showGUI = False
nwGUI.mainConf = tmpConf
nwPrefs.mainConf = tmpConf
nwPrefs.tabGeneral.mainConf = tmpConf
@@ -792,7 +1011,7 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf):
qtbot.wait(keyDelay)
tabGeneral = nwPrefs.tabGeneral
nwPrefs._tabBox.setCurrentWidget(tabGeneral)
tabGeneral.backupPath = nwTemp
tabGeneral.backupPath = "no/where"
qtbot.wait(keyDelay)
assert not tabGeneral.preferDarkIcons.isChecked()
@@ -804,6 +1023,16 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf):
qtbot.mouseClick(tabGeneral.showFullPath, Qt.LeftButton)
assert not tabGeneral.showFullPath.isChecked()
# Check Browse button
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "")
assert not tabGeneral._backupFolder()
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: "some/dir")
qtbot.mouseClick(tabGeneral.backupGetPath, Qt.LeftButton)
# Check font button
monkeypatch.setattr(QFontDialog, "getFont", lambda font, obj: (font, True))
qtbot.mouseClick(tabGeneral.fontButton, Qt.LeftButton)
qtbot.wait(keyDelay)
assert not tabGeneral.backupOnClose.isChecked()
qtbot.mouseClick(tabGeneral.backupOnClose, Qt.LeftButton)
@@ -819,6 +1048,9 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf):
tabLayout = nwPrefs.tabLayout
nwPrefs._tabBox.setCurrentWidget(tabLayout)
qtbot.wait(keyDelay)
qtbot.mouseClick(tabLayout.fontButton, Qt.LeftButton)
qtbot.wait(keyDelay)
tabLayout.textStyleSize.setValue(13)
tabLayout.textFlowMax.setValue(700)
@@ -890,26 +1122,73 @@ def testPreferences(qtbot, nwMinimal, nwTemp, nwRef, tmpConf):
assert not tabAutoRep.autoReplaceDash.isEnabled()
assert not tabAutoRep.autoReplaceDots.isEnabled()
monkeypatch.setattr(QuotesDialog, "selectedQuote", "'")
monkeypatch.setattr(QuotesDialog, "exec_", lambda *args: QDialog.Accepted)
qtbot.mouseClick(tabAutoRep.btnDoubleStyleC, Qt.LeftButton)
# Save and Check Config
# qtbot.stopForInteraction()
qtbot.mouseClick(nwPrefs.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton)
assert tmpConf.confChanged
assert tmpConf.backupPath == nwTemp
tmpConf.backupPath = ""
tmpConf.lastPath = ""
assert nwGUI.mainConf.saveConfig()
# qtbot.stopForInteraction()
nwGUI.closeMain()
refConf = path.join(nwRef, "novelwriter_prefs.conf")
projConf = path.join(nwGUI.mainConf.confPath, "novelwriter.conf")
testConf = path.join(nwTemp, "novelwriter_prefs.conf")
refConf = os.path.join(nwRef, "novelwriter_prefs.conf")
projConf = os.path.join(nwGUI.mainConf.confPath, "novelwriter.conf")
testConf = os.path.join(nwTemp, "novelwriter_prefs.conf")
copyfile(projConf, testConf)
ignoreLines = [
2, # Timestamp
11, 12, 13, 14, 15, 16, 17, # Window sizes
7, 25, # Fonts (depends in system default)
7, 25, # Fonts (depends on system default)
]
assert cmpFiles(testConf, refConf, ignoreLines)
@pytest.mark.gui
def testQuotesDialog(qtbot, yesToAll, nwMinimal, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
nwQuot = QuotesDialog(nwGUI)
nwQuot.show()
lastItem = ""
for i in range(nwQuot.listBox.count()):
anItem = nwQuot.listBox.item(i)
assert isinstance(anItem, QListWidgetItem)
nwQuot.listBox.clearSelection()
nwQuot.listBox.setCurrentItem(anItem, QItemSelectionModel.Select)
lastItem = anItem.text()[2]
assert nwQuot.previewLabel.text() == lastItem
nwQuot._doAccept()
assert nwQuot.result() == QDialog.Accepted
assert nwQuot.selectedQuote == lastItem
# qtbot.stopForInteraction()
nwQuot._doReject()
nwQuot.close()
nwGUI.closeMain()
nwGUI.close()
@pytest.mark.gui
def testDialogsOpenClose(qtbot, monkeypatch, yesToAll, nwMinimal, nwTemp):
nwGUI = nw.main(["--testmode", "--config=%s" % nwMinimal, "--data=%s" % nwTemp, nwMinimal])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: nwTemp)
assert nwGUI.selectProjectPath() == nwTemp
# qtbot.stopForInteraction()
nwGUI.closeMain()
nwGUI.close()
+1025 -90
View File
File diff suppressed because it is too large Load Diff
+538
View File
@@ -0,0 +1,538 @@
# -*- coding: utf-8 -*-
"""novelWriter Project Class Tester
"""
import pytest
import os
import json
from shutil import copyfile
from nwtools import cmpFiles
from nw.core.project import NWProject
from nw.core.index import NWIndex
from nw.constants import nwItemClass, nwItemLayout
@pytest.mark.project
def testIndexBuildCheck(monkeypatch, nwLipsum, nwDummy, nwTempProj, nwRef):
"""Test core functionality of scaning, saving, loading and checking
the index cache file.
"""
projFile = os.path.join(nwLipsum, "meta", "tagsIndex.json")
testFile = os.path.join(nwTempProj, "1_tagsIndex.json")
refFile = os.path.join(nwRef, "proj", "1_tagsIndex.json")
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwLipsum)
theProject.mainConf.debugInfo = True
monkeypatch.setattr("nw.core.index.time", lambda: 123.4)
theIndex = NWIndex(theProject, nwDummy)
notIndexable = {
"b3643d0f92e32": False, # Novel ROOT
"45e6b01ca35c1": False, # Chapter One FOLDER
"6bd935d2490cd": False, # Chapter Two FOLDER
"67a8707f2f249": False, # Character ROOT
"6c6afb1247750": False, # Plot ROOT
"60bdf227455cc": False, # World ROOT
}
for tItem in theProject.projTree:
assert theIndex.reIndexHandle(tItem.itemHandle) is notIndexable.get(tItem.itemHandle, True)
assert not theIndex.reIndexHandle(None)
# Dummy exception function
def doPanic(*arg, **kwargs):
raise Exception
# Make the save fail
monkeypatch.setattr(json, "dumps", doPanic)
assert not theIndex.saveIndex()
# Make the save pass
monkeypatch.undo()
assert theIndex.saveIndex()
# Take a copy of the index
tagIndex = str(theIndex.tagIndex)
refIndex = str(theIndex.refIndex)
novelIndex = str(theIndex.novelIndex)
noteIndex = str(theIndex.noteIndex)
textCounts = str(theIndex.textCounts)
# Delete a handle
assert theIndex.tagIndex.get("Bod", None) is not None
assert theIndex.refIndex.get("4c4f28287af27", None) is not None
assert theIndex.noteIndex.get("4c4f28287af27", None) is not None
assert theIndex.textCounts.get("4c4f28287af27", None) is not None
theIndex.deleteHandle("4c4f28287af27")
assert theIndex.tagIndex.get("Bod", None) is None
assert theIndex.refIndex.get("4c4f28287af27", None) is None
assert theIndex.noteIndex.get("4c4f28287af27", None) is None
assert theIndex.textCounts.get("4c4f28287af27", None) is None
# Clear the index
theIndex.clearIndex()
assert not theIndex.tagIndex
assert not theIndex.refIndex
assert not theIndex.novelIndex
assert not theIndex.noteIndex
assert not theIndex.textCounts
# Make the load fail
monkeypatch.setattr(json, "loads", doPanic)
assert not theIndex.loadIndex()
# Make the load pass
monkeypatch.undo()
assert theIndex.loadIndex()
assert str(theIndex.tagIndex) == tagIndex
assert str(theIndex.refIndex) == refIndex
assert str(theIndex.novelIndex) == novelIndex
assert str(theIndex.noteIndex) == noteIndex
assert str(theIndex.textCounts) == textCounts
# Break the index and check that we notice
assert not theIndex.indexBroken
theIndex.tagIndex["Bod"].append("Stuff") # No longer len() == 4
theIndex.checkIndex()
assert theIndex.indexBroken
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex.refIndex["fb609cd8319dc"]["T000001"]["tags"].append("Stuff") # No longer len() == 3
theIndex.checkIndex()
assert theIndex.indexBroken
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex.novelIndex["7a992350f3eb6"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8
theIndex.checkIndex()
assert theIndex.indexBroken
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex.noteIndex["4c4f28287af27"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8
theIndex.checkIndex()
assert theIndex.indexBroken
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex.textCounts["7a992350f3eb6"].append("Stuff") # No longer len() == 3
theIndex.checkIndex()
assert theIndex.indexBroken
# Make the try/except trigger as well
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex.refIndex["fb609cd8319dc"]["T000001"] = {"tagssss": []} # Wrong key name
theIndex.checkIndex()
assert theIndex.indexBroken
# Finalise
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile)
@pytest.mark.project
def testIndexScanThis(nwMinimal, nwDummy):
"""Test the tag scanner function scanThis.
"""
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
assert not isValid
isValid, theBits, thePos = theIndex.scanThis("@")
assert not isValid
isValid, theBits, thePos = theIndex.scanThis("@:")
assert not isValid
isValid, theBits, thePos = theIndex.scanThis(" @a: b")
assert not isValid
isValid, theBits, thePos = theIndex.scanThis("@a:")
assert isValid
assert str(theBits) == "['@a']"
assert str(thePos) == "[0]"
isValid, theBits, thePos = theIndex.scanThis("@a:b")
assert isValid
assert str(theBits) == "['@a', 'b']"
assert str(thePos) == "[0, 3]"
isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d")
assert isValid
assert str(theBits) == "['@a', 'b', 'c', 'd']"
assert str(thePos) == "[0, 3, 5, 7]"
isValid, theBits, thePos = theIndex.scanThis("@a : b , c , d")
assert isValid
assert str(theBits) == "['@a', 'b', 'c', 'd']"
assert str(thePos) == "[0, 5, 9, 13]"
isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this")
assert isValid
assert str(theBits) == "['@tag', 'this', 'and this']"
assert str(thePos) == "[0, 6, 12]"
assert theProject.closeProject()
@pytest.mark.project
def testIndexCheckThese(nwMinimal, nwDummy):
"""Test the tag checker function checkThese.
"""
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
nItem = theProject.projTree[nHandle]
cItem = theProject.projTree[cHandle]
assert theIndex.scanText(cHandle, (
"# Jane Smith\n"
"@tag: Jane"
))
assert theIndex.scanText(nHandle, (
"# Hello World!\n"
"@pov: Jane"
))
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]"
assert str(theIndex.checkThese(["@tag", "John"], cItem)) == "[True, True]"
assert str(theIndex.checkThese(["@tag", "Jane"], nItem)) == "[True, False]"
assert str(theIndex.checkThese(["@tag", "John"], nItem)) == "[True, True]"
assert str(theIndex.checkThese(["@pov", "John"], nItem)) == "[True, False]"
assert str(theIndex.checkThese(["@pov", "Jane"], nItem)) == "[True, True]"
assert str(theIndex.checkThese(["@ pov", "Jane"], nItem)) == "[False, False]"
assert str(theIndex.checkThese(["@what", "Jane"], nItem)) == "[False, False]"
assert theProject.closeProject()
@pytest.mark.project
def testIndexScanText(nwMinimal, nwDummy):
"""Check the index data extraction functions.
"""
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
# Some items for fail to scan tests
dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c")
xHandle = theProject.newFile("No Layout", nwItemClass.NOVEL, "a508bb932959c")
xItem = theProject.projTree[xHandle]
xItem.setLayout(nwItemLayout.NO_LAYOUT)
# Check invalid data
assert not theIndex.scanText(None, "Hello World!")
assert not theIndex.scanText(dHandle, "Hello World!")
assert not theIndex.scanText(xHandle, "Hello World!")
xItem.setLayout(nwItemLayout.SCENE)
xItem.setParent(None)
assert not theIndex.scanText(xHandle, "Hello World!")
# Create the trash folder
tHandle = theProject.trashFolder()
assert theProject.projTree[tHandle] is not None
xItem.setParent(tHandle)
assert not theIndex.scanText(xHandle, "Hello World!")
# Create the archive root
aHandle = theProject.newRoot("Outtakes", nwItemClass.ARCHIVE)
assert theProject.projTree[aHandle] is not None
xItem.setParent(aHandle)
assert not theIndex.scanText(xHandle, "Hello World!")
# Make some usable items
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
sHandle = theProject.newFile("Scene", nwItemClass.NOVEL, "a508bb932959c")
# Index correct text
assert theIndex.scanText(cHandle, (
"# Jane Smith\n"
"@tag: Jane\n"
))
assert theIndex.scanText(nHandle, (
"# Hello World!\n"
"@pov: Jane\n"
"@char: Jane\n\n"
"% this is a comment\n\n"
"This is a story about Jane Smith.\n\n"
"Well, not really.\n"
))
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
# Check that title sections are indexed properly
assert theIndex.scanText(nHandle, (
"# Title One\n\n"
"% synopsis: Synopsis One.\n\n"
"Paragraph One.\n\n"
"## Title Two\n\n"
"% synopsis: Synopsis Two.\n\n"
"Paragraph Two.\n\n"
"### Title Three\n\n"
"% synopsis: Synopsis Three.\n\n"
"Paragraph Three.\n\n"
"#### Title Four\n\n"
"% synopsis: Synopsis Four.\n\n"
"Paragraph Four.\n\n"
"##### Title Five\n\n" # Not interpreted as a title, the hashes is counted as a word
"Paragraph Five.\n\n"
))
assert theIndex.refIndex[nHandle].get("T000000", None) is not None # Always there
assert theIndex.refIndex[nHandle].get("T000001", None) is not None # Heading 1
assert theIndex.refIndex[nHandle].get("T000002", None) is None
assert theIndex.refIndex[nHandle].get("T000003", None) is None
assert theIndex.refIndex[nHandle].get("T000004", None) is None
assert theIndex.refIndex[nHandle].get("T000005", None) is None
assert theIndex.refIndex[nHandle].get("T000006", None) is None
assert theIndex.refIndex[nHandle].get("T000007", None) is not None # Heading 2
assert theIndex.refIndex[nHandle].get("T000008", None) is None
assert theIndex.refIndex[nHandle].get("T000009", None) is None
assert theIndex.refIndex[nHandle].get("T000010", None) is None
assert theIndex.refIndex[nHandle].get("T000011", None) is None
assert theIndex.refIndex[nHandle].get("T000012", None) is None
assert theIndex.refIndex[nHandle].get("T000013", None) is not None # Heading 3
assert theIndex.refIndex[nHandle].get("T000014", None) is None
assert theIndex.refIndex[nHandle].get("T000015", None) is None
assert theIndex.refIndex[nHandle].get("T000016", None) is None
assert theIndex.refIndex[nHandle].get("T000017", None) is None
assert theIndex.refIndex[nHandle].get("T000018", None) is None
assert theIndex.refIndex[nHandle].get("T000019", None) is not None # Heading 4
assert theIndex.refIndex[nHandle].get("T000020", None) is None
assert theIndex.refIndex[nHandle].get("T000021", None) is None
assert theIndex.refIndex[nHandle].get("T000022", None) is None
assert theIndex.refIndex[nHandle].get("T000023", None) is None
assert theIndex.refIndex[nHandle].get("T000024", None) is None
assert theIndex.refIndex[nHandle].get("T000025", None) is None
assert theIndex.refIndex[nHandle].get("T000026", None) is None
assert theIndex.novelIndex[nHandle]["T000001"]["level"] == "H1"
assert theIndex.novelIndex[nHandle]["T000007"]["level"] == "H2"
assert theIndex.novelIndex[nHandle]["T000013"]["level"] == "H3"
assert theIndex.novelIndex[nHandle]["T000019"]["level"] == "H4"
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Title One"
assert theIndex.novelIndex[nHandle]["T000007"]["title"] == "Title Two"
assert theIndex.novelIndex[nHandle]["T000013"]["title"] == "Title Three"
assert theIndex.novelIndex[nHandle]["T000019"]["title"] == "Title Four"
assert theIndex.novelIndex[nHandle]["T000001"]["layout"] == "SCENE"
assert theIndex.novelIndex[nHandle]["T000007"]["layout"] == "SCENE"
assert theIndex.novelIndex[nHandle]["T000013"]["layout"] == "SCENE"
assert theIndex.novelIndex[nHandle]["T000019"]["layout"] == "SCENE"
assert theIndex.novelIndex[nHandle]["T000001"]["synopsis"] == "Synopsis One."
assert theIndex.novelIndex[nHandle]["T000007"]["synopsis"] == "Synopsis Two."
assert theIndex.novelIndex[nHandle]["T000013"]["synopsis"] == "Synopsis Three."
assert theIndex.novelIndex[nHandle]["T000019"]["synopsis"] == "Synopsis Four."
assert theIndex.novelIndex[nHandle]["T000001"]["cCount"] == 23
assert theIndex.novelIndex[nHandle]["T000007"]["cCount"] == 23
assert theIndex.novelIndex[nHandle]["T000013"]["cCount"] == 27
assert theIndex.novelIndex[nHandle]["T000019"]["cCount"] == 56
assert theIndex.novelIndex[nHandle]["T000001"]["wCount"] == 4
assert theIndex.novelIndex[nHandle]["T000007"]["wCount"] == 4
assert theIndex.novelIndex[nHandle]["T000013"]["wCount"] == 4
assert theIndex.novelIndex[nHandle]["T000019"]["wCount"] == 9
assert theIndex.novelIndex[nHandle]["T000001"]["pCount"] == 1
assert theIndex.novelIndex[nHandle]["T000007"]["pCount"] == 1
assert theIndex.novelIndex[nHandle]["T000013"]["pCount"] == 1
assert theIndex.novelIndex[nHandle]["T000019"]["pCount"] == 3
assert theIndex.scanText(cHandle, (
"# Title One\n\n"
"@tag: One\n\n"
"% synopsis: Synopsis One.\n\n"
"Paragraph One.\n\n"
))
assert theIndex.refIndex[cHandle].get("T000000", None) is not None
assert theIndex.refIndex[cHandle].get("T000001", None) is not None
assert theIndex.refIndex[cHandle].get("T000002", None) is None
assert theIndex.refIndex[cHandle].get("T000003", None) is None
assert theIndex.refIndex[cHandle].get("T000004", None) is None
assert theIndex.refIndex[cHandle].get("T000005", None) is None
assert theIndex.refIndex[cHandle].get("T000006", None) is None
assert theIndex.refIndex[cHandle].get("T000007", None) is None
assert theIndex.noteIndex[cHandle]["T000001"]["level"] == "H1"
assert theIndex.noteIndex[cHandle]["T000001"]["title"] == "Title One"
assert theIndex.noteIndex[cHandle]["T000001"]["layout"] == "NOTE"
assert theIndex.noteIndex[cHandle]["T000001"]["synopsis"] == "Synopsis One."
assert theIndex.noteIndex[cHandle]["T000001"]["cCount"] == 23
assert theIndex.noteIndex[cHandle]["T000001"]["wCount"] == 4
assert theIndex.noteIndex[cHandle]["T000001"]["pCount"] == 1
assert theIndex.scanText(sHandle, (
"# Title One\n\n"
"@pov: One\n\n" # Valid
"@char: Two\n\n" # Invalid tag
"@:\n\n" # Invalid line
"% synopsis: Synopsis One.\n\n"
"Paragraph One.\n\n"
))
assert str(theIndex.refIndex[sHandle]["T000001"]["tags"]) == (
"[[3, '@pov', 'One'], [5, '@char', 'Two']]"
)
assert theProject.closeProject()
@pytest.mark.project
def testIndexExtractData(nwMinimal, nwDummy):
"""Check the index data extraction functions.
"""
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
assert theIndex.scanText(cHandle, (
"# Jane Smith\n"
"@tag: Jane\n"
))
assert theIndex.scanText(nHandle, (
"# Hello World!\n"
"@pov: Jane\n"
"@char: Jane\n\n"
"% this is a comment\n\n"
"This is a story about Jane Smith.\n\n"
"Well, not really.\n"
))
# The novel structure should contain the pointer to the novel file header
assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle
# The novel file should have the correct counts
cC, wC, pC = theIndex.getCounts(nHandle)
assert cC == 62 # Characters in text and title only
assert wC == 12 # Words in text and title only
assert pC == 2 # Paragraphs in text only
##
# getReferences
##
# Look up an ivalid handle
theRefs = theIndex.getReferences("Not a handle")
assert theRefs["@pov"] == []
assert theRefs["@char"] == []
# The novel file should now refer to Jane as @pov and @char
theRefs = theIndex.getReferences(nHandle)
assert str(theRefs["@pov"]) == "['Jane']"
assert str(theRefs["@char"]) == "['Jane']"
##
# getBackReferenceList
##
# None handle should return an empty dict
assert theIndex.getBackReferenceList(None) == {}
# The character file should have a record of the reference from the novel file
theRefs = theIndex.getBackReferenceList(cHandle)
assert str(theRefs) == "{'%s': 'T000001'}" % nHandle
##
# getTagSource
##
assert theIndex.getTagSource("Jane") == (cHandle, 2, "T000001")
assert theIndex.getTagSource("John") == (None, 0, "T000000")
##
# getCounts for whole text and sections
##
# Get section counts for a novel file
assert theIndex.scanText(nHandle, (
"# Hello World!\n"
"@pov: Jane\n"
"@char: Jane\n\n"
"% this is a comment\n\n"
"This is a story about Jane Smith.\n\n"
"Well, not really.\n\n"
"# Hello World!\n"
"@pov: Jane\n"
"@char: Jane\n\n"
"% this is a comment\n\n"
"This is a story about Jane Smith.\n\n"
"Well, not really.\n"
))
# Whole document
cC, wC, pC = theIndex.getCounts(nHandle)
assert cC == 124
assert wC == 24
assert pC == 4
# First part
cC, wC, pC = theIndex.getCounts(nHandle, "T000001")
assert cC == 62
assert wC == 12
assert pC == 2
# First part
cC, wC, pC = theIndex.getCounts(nHandle, "T000011")
assert cC == 62
assert wC == 12
assert pC == 2
# Get section counts for a note file
assert theIndex.scanText(cHandle, (
"# Hello World!\n"
"@pov: Jane\n"
"@char: Jane\n\n"
"% this is a comment\n\n"
"This is a story about Jane Smith.\n\n"
"Well, not really.\n\n"
"# Hello World!\n"
"@pov: Jane\n"
"@char: Jane\n\n"
"% this is a comment\n\n"
"This is a story about Jane Smith.\n\n"
"Well, not really.\n"
))
# Whole document
cC, wC, pC = theIndex.getCounts(cHandle)
assert cC == 124
assert wC == 24
assert pC == 4
# First part
cC, wC, pC = theIndex.getCounts(cHandle, "T000001")
assert cC == 62
assert wC == 12
assert pC == 2
# First part
cC, wC, pC = theIndex.getCounts(cHandle, "T000011")
assert cC == 62
assert wC == 12
assert pC == 2
assert theProject.closeProject()
+336 -158
View File
@@ -3,31 +3,41 @@
"""
import pytest
from os import path
import os
from shutil import copyfile
from zipfile import ZipFile
from nwtools import cmpFiles
from nw.core.project import NWProject
from nw.core.document import NWDoc
from nw.core.index import NWIndex
from nw.core.spellcheck import NWSpellEnchant, NWSpellSimple
from nw.constants import nwItemClass, nwItemType, nwItemLayout, nwFiles
@pytest.mark.project
def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy):
projFile = path.join(nwFuncTemp, "nwProject.nwx")
testFile = path.join(nwTempProj, "1_nwProject.nwx")
refFile = path.join(nwRef, "proj", "1_nwProject.nwx")
"""Test that a basic project can be created, and opened and saved.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "1_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "1_nwProject.nwx")
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
# Setting no data should fail
assert not theProject.newProject({})
# Try again with a proper path
assert theProject.newProject({"projPath": nwFuncTemp})
assert theProject.setProjectPath(nwFuncTemp)
assert theProject.saveProject()
assert theProject.closeProject()
# Creating the project once more should fail
assert not theProject.newProject({"projPath": nwFuncTemp})
# Check the new project
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
@@ -53,9 +63,11 @@ def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy):
@pytest.mark.project
def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy):
projFile = path.join(nwFuncTemp, "nwProject.nwx")
testFile = path.join(nwTempProj, "2_nwProject.nwx")
refFile = path.join(nwRef, "proj", "2_nwProject.nwx")
"""Check that new root folders can be added to the project.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "2_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "2_nwProject.nwx")
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
@@ -85,9 +97,11 @@ def testProjectNewRoot(nwFuncTemp, nwTempProj, nwRef, nwDummy):
@pytest.mark.project
def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy):
projFile = path.join(nwFuncTemp, "nwProject.nwx")
testFile = path.join(nwTempProj, "3_nwProject.nwx")
refFile = path.join(nwRef, "proj", "3_nwProject.nwx")
"""Check that new files can be added to the project.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "3_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "3_nwProject.nwx")
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
@@ -109,143 +123,13 @@ def testProjectNewFile(nwFuncTemp, nwTempProj, nwRef, nwDummy):
assert not theProject.projChanged
@pytest.mark.project
def testIndexScanThis(nwMinimal, nwDummy):
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
assert not isValid
isValid, theBits, thePos = theIndex.scanThis("@")
assert not isValid
isValid, theBits, thePos = theIndex.scanThis("@:")
assert not isValid
isValid, theBits, thePos = theIndex.scanThis(" @a: b")
assert not isValid
isValid, theBits, thePos = theIndex.scanThis("@a:")
assert isValid
assert str(theBits) == "['@a']"
assert str(thePos) == "[0]"
isValid, theBits, thePos = theIndex.scanThis("@a:b")
assert isValid
assert str(theBits) == "['@a', 'b']"
assert str(thePos) == "[0, 3]"
isValid, theBits, thePos = theIndex.scanThis("@a:b,c,d")
assert isValid
assert str(theBits) == "['@a', 'b', 'c', 'd']"
assert str(thePos) == "[0, 3, 5, 7]"
isValid, theBits, thePos = theIndex.scanThis("@a : b , c , d")
assert isValid
assert str(theBits) == "['@a', 'b', 'c', 'd']"
assert str(thePos) == "[0, 5, 9, 13]"
isValid, theBits, thePos = theIndex.scanThis("@tag: this, and this")
assert isValid
assert str(theBits) == "['@tag', 'this', 'and this']"
assert str(thePos) == "[0, 6, 12]"
assert theProject.closeProject()
@pytest.mark.project
def testIndexCheckThese(nwMinimal, nwDummy):
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
nItem = theProject.projTree[nHandle]
cItem = theProject.projTree[cHandle]
assert theIndex.scanText(cHandle, (
"# Jane Smith\n"
"@tag: Jane"
))
assert theIndex.scanText(nHandle, (
"# Hello World!\n"
"@pov: Jane"
))
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
assert str(theIndex.checkThese(["@tag", "Jane"], cItem)) == "[True, True]"
assert str(theIndex.checkThese(["@tag", "John"], cItem)) == "[True, True]"
assert str(theIndex.checkThese(["@tag", "Jane"], nItem)) == "[True, False]"
assert str(theIndex.checkThese(["@tag", "John"], nItem)) == "[True, True]"
assert str(theIndex.checkThese(["@pov", "John"], nItem)) == "[True, False]"
assert str(theIndex.checkThese(["@pov", "Jane"], nItem)) == "[True, True]"
assert str(theIndex.checkThese(["@ pov", "Jane"], nItem)) == "[False, False]"
assert str(theIndex.checkThese(["@what", "Jane"], nItem)) == "[False, False]"
assert theProject.closeProject()
@pytest.mark.project
def testIndexMeta(nwMinimal, nwDummy):
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
theIndex = NWIndex(theProject, nwDummy)
nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
assert theIndex.scanText(cHandle, (
"# Jane Smith\n"
"@tag: Jane\n"
))
assert theIndex.scanText(nHandle, (
"# Hello World!\n"
"@pov: Jane\n"
"@char: Jane\n"
"\n"
"% this is a comment\n"
"\n"
"This is a story about Jane Smith.\n"
"\n"
"Well, not really.\n"
))
assert str(theIndex.tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
assert theIndex.novelIndex[nHandle]["T000001"]["title"] == "Hello World!"
# The novel structure should contain the pointer to the novel file header
assert str(theIndex.getNovelStructure()) == "['%s:T000001']" % nHandle
# The novel file should have the correct counts
cC, wC, pC = theIndex.getCounts(nHandle)
assert cC == 62 # Characters in text and title only
assert wC == 12 # Words in text and title only
assert pC == 2 # Paragraphs in text only
# The novel file should now refer to Jane as @pov and @char
theRefs = theIndex.getReferences(nHandle)
assert str(theRefs["@pov"]) == "['Jane']"
assert str(theRefs["@char"]) == "['Jane']"
# The character file should have a record of the reference from the novel file
theRefs = theIndex.getBackReferenceList(cHandle)
assert str(theRefs) == "{'%s': 'T000001'}" % nHandle
assert theProject.closeProject()
@pytest.mark.project
def testProjectNewCustom(nwFuncTemp, nwTempProj, nwRef, nwDummy):
projFile = path.join(nwFuncTemp, "nwProject.nwx")
testFile = path.join(nwTempProj, "4_nwProject.nwx")
refFile = path.join(nwRef, "proj", "4_nwProject.nwx")
def testProjectNewCustomA(nwFuncTemp, nwTempProj, nwRef, nwDummy):
"""Create a new project from a project wizard dictionary.
Custom type with chapters and scenes.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "4_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "4_nwProject.nwx")
projData = {
"projName": "Test Custom",
@@ -278,7 +162,49 @@ def testProjectNewCustom(nwFuncTemp, nwTempProj, nwRef, nwDummy):
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
@pytest.mark.project
def testProjectNewSample(nwFuncTemp, nwRef, nwConf, nwDummy):
def testProjectNewCustomB(nwFuncTemp, nwTempProj, nwRef, nwDummy):
"""Create a new project from a project wizard dictionary.
Custom type without chapters, but with scenes.
"""
projFile = os.path.join(nwFuncTemp, "nwProject.nwx")
testFile = os.path.join(nwTempProj, "5_nwProject.nwx")
refFile = os.path.join(nwRef, "proj", "5_nwProject.nwx")
projData = {
"projName": "Test Custom",
"projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": nwFuncTemp,
"popSample": False,
"popMinimal": False,
"popCustom": True,
"addRoots": [
nwItemClass.PLOT,
nwItemClass.CHARACTER,
nwItemClass.WORLD,
nwItemClass.TIMELINE,
nwItemClass.OBJECT,
nwItemClass.ENTITY,
],
"numChapters": 0,
"numScenes": 6,
"chFolders": True,
}
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.newProject(projData)
assert theProject.saveProject()
assert theProject.closeProject()
copyfile(projFile, testFile)
assert cmpFiles(testFile, refFile, [2, 6, 7, 8])
@pytest.mark.project
def testProjectNewSampleA(nwFuncTemp, nwConf, nwDummy, nwTemp):
"""Check that we can create a new project can be created from the
provided sample project via a zip file.
"""
projData = {
"projName": "Test Sample",
"projTitle": "Test Novel",
@@ -292,14 +218,117 @@ def testProjectNewSample(nwFuncTemp, nwRef, nwConf, nwDummy):
theProject.projTree.setSeed(42)
theProject.mainConf = nwConf
# Sample set, but no path
assert not theProject.newProject({"popSample": True})
# Force the lookup path for assets to our temp folder
srcSample = os.path.abspath(os.path.join(nwConf.appRoot, "sample"))
dstSample = os.path.join(nwTemp, "sample.zip")
nwConf.assetPath = nwTemp
# Create and open a defective zip file
with open(dstSample, mode="w+") as outFile:
outFile.write("foo")
assert not theProject.newProject(projData)
os.unlink(dstSample)
# Create a real zip file, and unpack it
with ZipFile(dstSample, "w") as zipObj:
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
for docFile in os.listdir(os.path.join(srcSample, "content")):
srcDoc = os.path.join(srcSample, "content", docFile)
zipObj.write(srcDoc, "content/"+docFile)
assert theProject.newProject(projData)
assert theProject.openProject(nwFuncTemp)
assert theProject.projName == "Sample Project"
assert theProject.saveProject()
assert theProject.closeProject()
os.unlink(dstSample)
@pytest.mark.project
def testProjectNewSampleB(monkeypatch, nwFuncTemp, nwConf, nwDummy, nwTemp):
"""Check that we can create a new project can be created from the
provided sample project folder.
"""
projData = {
"projName": "Test Sample",
"projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": nwFuncTemp,
"popSample": True,
"popMinimal": False,
"popCustom": False,
}
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
theProject.mainConf = nwConf
# Make sure we do not pick up the nw/assets/sample.zip file
nwConf.assetPath = nwTemp
# Set a fake project file name
monkeypatch.setattr(nwFiles, "PROJ_FILE", "nothing.nwx")
assert not theProject.newProject(projData)
monkeypatch.setattr(nwFiles, "PROJ_FILE", "nwProject.nwx")
assert theProject.newProject(projData)
assert theProject.openProject(nwFuncTemp)
assert theProject.projName == "Sample Project"
assert theProject.saveProject()
assert theProject.closeProject()
# Misdirect the appRoot path so neither is possible
nwConf.appRoot = nwTemp
assert not theProject.newProject(projData)
@pytest.mark.project
def testProjectMethods(monkeypatch, nwMinimal, nwDummy):
"""Test other project class methods and functions.
"""
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwMinimal)
assert theProject.projPath == nwMinimal
# Setting project path
assert theProject.setProjectPath(None)
assert theProject.projPath is None
assert theProject.setProjectPath("")
assert theProject.projPath is None
assert theProject.setProjectPath("~")
assert theProject.projPath == os.path.expanduser("~")
# Create a new folder and populate it
projPath = os.path.join(nwMinimal, "dummy1")
assert theProject.setProjectPath(projPath, newProject=True)
# Make os.mkdir fail
def altMkdir(*args):
raise Exception("Oops!")
monkeypatch.setattr("os.mkdir", altMkdir)
projPath = os.path.join(nwMinimal, "dummy2")
assert not theProject.setProjectPath(projPath, newProject=True)
# Project Name
assert theProject.setProjectName(" A Name ")
assert theProject.projName == "A Name"
# Project Title
assert theProject.setBookTitle(" A Title ")
assert theProject.bookTitle == "A Title"
# Project Authors
assert not theProject.setBookAuthors([])
assert theProject.setBookAuthors(" Jane Doe \n John Doh \n ")
assert theProject.bookAuthors == ["Jane Doe", "John Doh"]
@pytest.mark.project
def testDocMeta(nwDummy, nwLipsum):
"""Check that the document meta data string is parsed correctly.
"""
theProject = NWProject(nwDummy)
theProject.projTree.setSeed(42)
assert theProject.openProject(nwLipsum)
@@ -325,7 +354,7 @@ def testDocMeta(nwDummy, nwLipsum):
@pytest.mark.project
def testSpellEnchant(nwTemp, nwConf):
wList = path.join(nwTemp, "wordlist.txt")
wList = os.path.join(nwTemp, "wordlist.txt")
with open(wList, mode="w") as wFile:
wFile.write("a_word\nb_word\nc_word\n")
@@ -350,7 +379,7 @@ def testSpellEnchant(nwTemp, nwConf):
@pytest.mark.project
def testSpellSimple(nwTemp, nwConf):
wList = path.join(nwTemp, "wordlist.txt")
wList = os.path.join(nwTemp, "wordlist.txt")
with open(wList, mode="w") as wFile:
wFile.write("a_word\nb_word\nc_word\n")
@@ -375,6 +404,10 @@ def testSpellSimple(nwTemp, nwConf):
@pytest.mark.project
def testProjectOptions(nwDummy, nwLipsum):
"""Test the class that holds all the GUI state user options that are
tied to the current open project. Non-project related GUI options
are handled by the Config class.
"""
theProject = NWProject(nwDummy)
assert theProject.projMeta is None
@@ -389,7 +422,7 @@ def testProjectOptions(nwDummy, nwLipsum):
assert str(theOpts.theState) == r"{}"
# Read Invalid Settings and Filter
stateFile = path.join(theProject.projMeta, nwFiles.OPTS_FILE)
stateFile = os.path.join(theProject.projMeta, nwFiles.OPTS_FILE)
with open(stateFile, mode="w", encoding="utf8") as outFile:
outFile.write(
r'{"GuiProjectSettings": {"winWidth": 100, "winHeight": 50}, "NoGroup": {"NoName": 0}}'
@@ -433,35 +466,40 @@ def testProjectOptions(nwDummy, nwLipsum):
assert theOpts.getFloat("GuiWritingStats", "winWidth", False) is False
@pytest.mark.project
def testOrphanedFiles(nwDummy, nwLipsum):
def testProjectOrphanedFiles(nwDummy, nwLipsum):
"""Check that files in the content folder that are not tracked in
the project XML file are handled correctly by the orphaned files
function. It should also restore as much meta data as possible from
the meta line at the top of the document file.
"""
theProject = NWProject(nwDummy)
assert theProject.openProject(nwLipsum)
assert theProject.projTree["636b6aa9b697b"] is None
assert theProject.closeProject()
# First Item with Meta Data
orphPath = path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
with open(orphPath, mode="w", encoding="utf8") as outFile:
outFile.write(r"%%~ 5eaea4e8cdee8:15c4492bd5107:WORLD:NOTE:Mars")
outFile.write("\n")
# Second Item without Meta Data
orphPath = path.join(nwLipsum, "content", "736b6aa9b697b.nwd")
orphPath = os.path.join(nwLipsum, "content", "736b6aa9b697b.nwd")
with open(orphPath, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
# Invalid File Name
dummyPath = path.join(nwLipsum, "content", "636b6aa9b697b.txt")
dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.txt")
with open(dummyPath, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
# Invalid File Name
dummyPath = path.join(nwLipsum, "content", "636b6aa9b697bb.nwd")
dummyPath = os.path.join(nwLipsum, "content", "636b6aa9b697bb.nwd")
with open(dummyPath, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
# Invalid File Name
dummyPath = path.join(nwLipsum, "content", "abcdefghijklm.nwd")
dummyPath = os.path.join(nwLipsum, "content", "abcdefghijklm.nwd")
with open(dummyPath, mode="w", encoding="utf8") as outFile:
outFile.write("\n")
@@ -492,3 +530,143 @@ def testOrphanedFiles(nwDummy, nwLipsum):
assert theProject.saveProject(nwLipsum)
assert theProject.closeProject()
@pytest.mark.project
def testProjectOldFormat(nwDummy, nwOldProj):
"""Test that a project folder structure of version 1.0 can be
converted to the latest folder structure. Version 1.0 split the
documents into 'data_0' ... 'data_f' folders, which are now all
contained in a single 'content' folder.
"""
theProject = NWProject(nwDummy)
theProject.mainConf.showGUI = False
# Create dummy files for known legacy files
deleteFiles = [
os.path.join(nwOldProj, "cache", "nwProject.nwx.0"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.1"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.2"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.3"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.4"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.5"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.6"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.7"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.8"),
os.path.join(nwOldProj, "cache", "nwProject.nwx.9"),
os.path.join(nwOldProj, "meta", "mainOptions.json"),
os.path.join(nwOldProj, "meta", "exportOptions.json"),
os.path.join(nwOldProj, "meta", "outlineOptions.json"),
os.path.join(nwOldProj, "meta", "timelineOptions.json"),
os.path.join(nwOldProj, "meta", "docMergeOptions.json"),
os.path.join(nwOldProj, "meta", "sessionLogOptions.json"),
]
# Add some files that shouldn't be there
deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.nwd"))
deleteFiles.append(os.path.join(nwOldProj, "data_f", "whatnow.txt"))
# Add some folders that shouldn't be there
os.mkdir(os.path.join(nwOldProj, "stuff"))
os.mkdir(os.path.join(nwOldProj, "data_1", "stuff"))
# Create dummy files
os.mkdir(os.path.join(nwOldProj, "cache"))
for aFile in deleteFiles:
with open(aFile, mode="w+", encoding="utf8") as outFile:
outFile.write("Hi")
for aFile in deleteFiles:
assert os.path.isfile(aFile)
# Open project and check that files that are not supposed to be
# there have been removed
assert theProject.openProject(nwOldProj)
for aFile in deleteFiles:
assert not os.path.isfile(aFile)
assert not os.path.isdir(os.path.join(nwOldProj, "data_1", "stuff"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_1"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_7"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_8"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_9"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_a"))
assert not os.path.isdir(os.path.join(nwOldProj, "data_f"))
# Check stuff that has been moved
assert os.path.isdir(os.path.join(nwOldProj, "junk"))
assert os.path.isdir(os.path.join(nwOldProj, "junk", "stuff"))
assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "junk", "whatnow.txt"))
# Check that files we want to keep are in the right place
assert os.path.isdir(os.path.join(nwOldProj, "cache"))
assert os.path.isdir(os.path.join(nwOldProj, "content"))
assert os.path.isdir(os.path.join(nwOldProj, "meta"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "f528d831f5b24.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "88124a4292d8b.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "91239bf2f8b69.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "19752e7f9d8af.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "a764d5acf5a21.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "9058ae29f0dfd.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "content", "7ff63b8afc4cd.nwd"))
assert os.path.isfile(os.path.join(nwOldProj, "meta", "tagsIndex.json"))
assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionInfo.log"))
# Close the project
theProject.closeProject()
# Check that new files have been created
assert os.path.isfile(os.path.join(nwOldProj, "meta", "guiOptions.json"))
assert os.path.isfile(os.path.join(nwOldProj, "meta", "sessionStats.log"))
assert os.path.isfile(os.path.join(nwOldProj, "ToC.json"))
assert os.path.isfile(os.path.join(nwOldProj, "ToC.txt"))
@pytest.mark.project
def testProjectBackup(nwDummy, nwMinimal, nwTemp):
"""Test the automated backup feature of the project class. The test
creates a backup of the Minimal test project, and then unzips the
backupd file and checks that the project XML file is identical to
the original file.
"""
theProject = NWProject(nwDummy)
assert theProject.openProject(nwMinimal)
# Test faulty settings
# Invalid path
theProject.mainConf.backupPath = None
assert not theProject.zipIt(doNotify=False)
# Missing project name
theProject.mainConf.backupPath = nwTemp
theProject.projName = ""
assert not theProject.zipIt(doNotify=False)
# Non-existent folder
theProject.mainConf.backupPath = os.path.join(nwTemp, "nonexistent")
theProject.projName = "Test Minimal"
assert not theProject.zipIt(doNotify=False)
# Same folder as project (causes infinite loop in zipping)
theProject.mainConf.backupPath = nwMinimal
assert not theProject.zipIt(doNotify=False)
# Test correct settings
theProject.mainConf.backupPath = nwTemp
assert theProject.zipIt(doNotify=False)
theFiles = os.listdir(os.path.join(nwTemp, "Test Minimal"))
assert len(theFiles) == 1
theZip = theFiles[0]
assert theZip[:12] == "Backup from "
assert theZip[-4:] == ".zip"
# Extract the archive
with ZipFile(os.path.join(nwTemp, "Test Minimal", theZip), "r") as inZip:
inZip.extractall(os.path.join(nwTemp, "extract"))
# Check that the main project file was restored
assert cmpFiles(
os.path.join(nwMinimal, "nwProject.nwx"), os.path.join(nwTemp, "extract", "nwProject.nwx")
)
+11
View File
@@ -8,6 +8,8 @@ from nw.core.tools import countWords, numberToRoman, numberToWord
@pytest.mark.core
def testCountWords():
"""Test the word counter and the exclusion filers.
"""
testText = (
"# Heading One\n"
"## Heading Two\n"
@@ -33,6 +35,8 @@ def testCountWords():
@pytest.mark.core
def testNumberWords():
"""Test the conversion of integer to English words.
"""
assert numberToWord(0, "en") == "Zero"
assert numberToWord(1, "en") == "One"
assert numberToWord(2, "en") == "Two"
@@ -60,8 +64,15 @@ def testNumberWords():
assert numberToWord(142, "en") == "One Hundred Forty-Two"
assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine"
# Check a few with a nonsense language setting
assert numberToWord(1, "foo") == "One"
assert numberToWord(2, "foo") == "Two"
assert numberToWord(3, "foo") == "Three"
@pytest.mark.core
def testRomanNumbers():
"""Test conversion of integers to Roman numbers.
"""
assert numberToRoman(None, False) == "NAN"
assert numberToRoman(0, False) == "OOR"
assert numberToRoman(1, False) == "I"