Merge pull request #37 from vkbo/backup

Backup Feature
This commit is contained in:
Veronica K. Berglyd Olsen
2019-06-27 21:56:25 +02:00
committed by GitHub
16 changed files with 264 additions and 44 deletions
+34 -16
View File
@@ -89,6 +89,11 @@ class Config:
self.spellLanguage = "en_GB"
# Backup
self.backupPath = ""
self.backupOnClose = False
self.askBeforeBackup = True
# Path
self.recentList = [""]*10
@@ -167,22 +172,28 @@ class Config:
## Editor
cnfSec = "Editor"
self.textFont = self._parseLine(cnfParse, cnfSec, "textfont", self.CNF_STR, self.textFont)
self.textSize = self._parseLine(cnfParse, cnfSec, "textsize", self.CNF_INT, self.textSize)
self.textFixedW = self._parseLine(cnfParse, cnfSec, "fixedwidth", self.CNF_BOOL, self.textFixedW)
self.textWidth = self._parseLine(cnfParse, cnfSec, "width", self.CNF_INT, self.textWidth)
self.textMargin = self._parseLine(cnfParse, cnfSec, "margin", self.CNF_INT, self.textMargin)
self.tabWidth = self._parseLine(cnfParse, cnfSec, "tabwidth", self.CNF_INT, self.tabWidth)
self.doJustify = self._parseLine(cnfParse, cnfSec, "justify", self.CNF_BOOL, self.doJustify)
self.autoSelect = self._parseLine(cnfParse, cnfSec, "autoselect", self.CNF_BOOL, self.autoSelect)
self.doReplace = self._parseLine(cnfParse, cnfSec, "autoreplace", self.CNF_BOOL, self.doReplace)
self.doReplaceSQuote = self._parseLine(cnfParse, cnfSec, "repsquotes", self.CNF_BOOL, self.doReplaceSQuote)
self.doReplaceDQuote = self._parseLine(cnfParse, cnfSec, "repdquotes", self.CNF_BOOL, self.doReplaceDQuote)
self.doReplaceDash = self._parseLine(cnfParse, cnfSec, "repdash", self.CNF_BOOL, self.doReplaceDash)
self.doReplaceDots = self._parseLine(cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots)
self.fmtSingleQuotes = self._parseLine(cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes)
self.fmtDoubleQuotes = self._parseLine(cnfParse, cnfSec, "fmtdoublequote", self.CNF_LIST, self.fmtDoubleQuotes)
self.spellLanguage = self._parseLine(cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage)
self.textFont = self._parseLine(cnfParse, cnfSec, "textfont", self.CNF_STR, self.textFont)
self.textSize = self._parseLine(cnfParse, cnfSec, "textsize", self.CNF_INT, self.textSize)
self.textFixedW = self._parseLine(cnfParse, cnfSec, "fixedwidth", self.CNF_BOOL, self.textFixedW)
self.textWidth = self._parseLine(cnfParse, cnfSec, "width", self.CNF_INT, self.textWidth)
self.textMargin = self._parseLine(cnfParse, cnfSec, "margin", self.CNF_INT, self.textMargin)
self.tabWidth = self._parseLine(cnfParse, cnfSec, "tabwidth", self.CNF_INT, self.tabWidth)
self.doJustify = self._parseLine(cnfParse, cnfSec, "justify", self.CNF_BOOL, self.doJustify)
self.autoSelect = self._parseLine(cnfParse, cnfSec, "autoselect", self.CNF_BOOL, self.autoSelect)
self.doReplace = self._parseLine(cnfParse, cnfSec, "autoreplace", self.CNF_BOOL, self.doReplace)
self.doReplaceSQuote = self._parseLine(cnfParse, cnfSec, "repsquotes", self.CNF_BOOL, self.doReplaceSQuote)
self.doReplaceDQuote = self._parseLine(cnfParse, cnfSec, "repdquotes", self.CNF_BOOL, self.doReplaceDQuote)
self.doReplaceDash = self._parseLine(cnfParse, cnfSec, "repdash", self.CNF_BOOL, self.doReplaceDash)
self.doReplaceDots = self._parseLine(cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots)
self.fmtSingleQuotes = self._parseLine(cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes)
self.fmtDoubleQuotes = self._parseLine(cnfParse, cnfSec, "fmtdoublequote", self.CNF_LIST, self.fmtDoubleQuotes)
self.spellLanguage = self._parseLine(cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage)
## Backup
cnfSec = "Backup"
self.backupPath = self._parseLine(cnfParse, cnfSec, "backuppath", self.CNF_STR, self.backupPath)
self.backupOnClose = self._parseLine(cnfParse, cnfSec, "backuponclose", self.CNF_BOOL, self.backupOnClose)
self.askBeforeBackup = self._parseLine(cnfParse, cnfSec, "askbeforebackup", self.CNF_BOOL, self.askBeforeBackup)
## Path
cnfSec = "Path"
@@ -240,6 +251,13 @@ class Config:
cnfParse.set(cnfSec,"fmtdoublequote",self._packList(self.fmtDoubleQuotes))
cnfParse.set(cnfSec,"spellcheck", str(self.spellLanguage))
## Backup
cnfSec = "Backup"
cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec,"backuppath", str(self.backupPath))
cnfParse.set(cnfSec,"backuponclose", str(self.backupOnClose))
cnfParse.set(cnfSec,"askbeforebackup",str(self.askBeforeBackup))
## Path
cnfSec = "Path"
cnfParse.add_section(cnfSec)
+77 -15
View File
@@ -21,7 +21,7 @@ from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QFormLayout, QLineEdit, QPlainTextEdit, QLabel,
QWidget, QTabWidget, QDialogButtonBox, QSpinBox, QGroupBox, QComboBox, QMessageBox,
QCheckBox, QGridLayout, QFontComboBox
QCheckBox, QGridLayout, QFontComboBox, QPushButton, QFileDialog
)
from nw.enum import nwAlert
from nw.constants import nwQuotes
@@ -183,12 +183,47 @@ class GuiConfigEditGeneral(QWidget):
self.autoSaveForm.addWidget(self.autoSaveProj, 1, 1)
self.autoSaveForm.addWidget(QLabel("seconds"), 1, 2)
# Backup
self.projBackup = QGroupBox("Backup", self)
self.projBackupForm = QGridLayout(self)
self.projBackup.setLayout(self.projBackupForm)
self.projBackupPath = QLineEdit()
if path.isdir(self.mainConf.backupPath):
self.projBackupPath.setText(self.mainConf.backupPath)
self.projBackupGetPath = QPushButton(QIcon.fromTheme("folder"),"")
self.projBackupGetPath.clicked.connect(self._backupFolder)
self.projBackupClose = QCheckBox(self)
self.projBackupClose.setToolTip("Backup automatically on project close.")
if self.mainConf.backupOnClose:
self.projBackupClose.setCheckState(Qt.Checked)
else:
self.projBackupClose.setCheckState(Qt.Unchecked)
self.projBackupAsk = QCheckBox(self)
self.projBackupAsk.setToolTip("Ask before backup.")
if self.mainConf.askBeforeBackup:
self.projBackupAsk.setCheckState(Qt.Checked)
else:
self.projBackupAsk.setCheckState(Qt.Unchecked)
self.projBackupForm.addWidget(QLabel("Backup Folder"), 0, 0)
self.projBackupForm.addWidget(self.projBackupPath, 0, 1, 1, 3)
self.projBackupForm.addWidget(self.projBackupGetPath, 0, 4)
self.projBackupForm.addWidget(QLabel("Run on Close"), 1, 0)
self.projBackupForm.addWidget(self.projBackupClose, 1, 1)
self.projBackupForm.addWidget(QLabel("Ask Before Backuo"), 1, 2)
self.projBackupForm.addWidget(self.projBackupAsk, 1, 3)
# Assemble
self.outerBox.addWidget(self.guiLook, 0, 0)
self.outerBox.addWidget(self.spellLang, 1, 0)
self.outerBox.addWidget(self.autoSave, 2, 0)
self.outerBox.addWidget(self.guiLook, 0, 0)
self.outerBox.addWidget(self.spellLang, 1, 0)
self.outerBox.addWidget(self.autoSave, 2, 0)
self.outerBox.addWidget(self.projBackup, 3, 0)
self.outerBox.setColumnStretch(2, 1)
self.outerBox.setRowStretch(3, 1)
self.outerBox.setRowStretch(4, 1)
self.setLayout(self.outerBox)
return
@@ -198,25 +233,52 @@ class GuiConfigEditGeneral(QWidget):
validEntries = True
needsRestart = False
guiTheme = self.guiLookTheme.currentData()
guiSyntax = self.guiLookSyntax.currentData()
spellLanguage = self.spellLangList.currentData()
autoSaveDoc = self.autoSaveDoc.value()
autoSaveProj = self.autoSaveProj.value()
guiTheme = self.guiLookTheme.currentData()
guiSyntax = self.guiLookSyntax.currentData()
spellLanguage = self.spellLangList.currentData()
autoSaveDoc = self.autoSaveDoc.value()
autoSaveProj = self.autoSaveProj.value()
backupPath = self.projBackupPath.text()
backupOnClose = self.projBackupClose.isChecked()
askBeforeBackup = self.projBackupAsk.isChecked()
# Check if restart is needed
needsRestart |= self.mainConf.guiTheme != guiTheme
self.mainConf.guiTheme = guiTheme
self.mainConf.guiSyntax = guiSyntax
self.mainConf.spellLanguage = spellLanguage
self.mainConf.autoSaveDoc = autoSaveDoc
self.mainConf.autoSaveProj = autoSaveProj
self.mainConf.guiTheme = guiTheme
self.mainConf.guiSyntax = guiSyntax
self.mainConf.spellLanguage = spellLanguage
self.mainConf.autoSaveDoc = autoSaveDoc
self.mainConf.autoSaveProj = autoSaveProj
self.mainConf.backupPath = backupPath
self.mainConf.backupOnClose = backupOnClose
self.mainConf.askBeforeBackup = askBeforeBackup
self.mainConf.confChanged = True
return validEntries, needsRestart
##
# Internal Functions
##
def _backupFolder(self):
currDir = self.projBackupPath.text()
if not path.isdir(currDir):
currDir = ""
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.ShowDirsOnly
dlgOpt |= QFileDialog.DontUseNativeDialog
newDir = QFileDialog.getExistingDirectory(
self,"Backup Directory",currDir,options=dlgOpt
)
if newDir:
self.projBackupPath.setText(newDir)
return True
return False
# END Class GuiConfigEditGeneral
class GuiConfigEditEditor(QWidget):
+6
View File
@@ -507,6 +507,12 @@ class GuiMainMenu(QMenuBar):
menuItem.triggered.connect(self.theParent.rebuildIndex)
self.toolsMenu.addAction(menuItem)
# Tools > Backup
menuItem = QAction(QIcon.fromTheme("drive-harddisk"), "Backup Project", self)
menuItem.setStatusTip("Backup Project")
menuItem.triggered.connect(self.theParent.backupProject)
self.toolsMenu.addAction(menuItem)
# Tools > Settings
menuItem = QAction(QIcon.fromTheme("preferences-system"), "Preferences", self)
menuItem.setStatusTip("Preferences")
+17 -4
View File
@@ -21,7 +21,7 @@ from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QFormLayout, QLineEdit, QPlainTextEdit, QLabel,
QWidget, QTabWidget, QDialogButtonBox, QListWidget, QListWidgetItem, QPushButton,
QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem
QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem, QCheckBox
)
from nw.enum import nwAlert
@@ -81,9 +81,11 @@ class GuiProjectEditor(QDialog):
projName = self.tabMain.editName.text()
bookTitle = self.tabMain.editTitle.text()
bookAuthors = self.tabMain.editAuthors.toPlainText()
doBackup = self.tabMain.doBackup.isChecked()
self.theProject.setProjectName(projName)
self.theProject.setBookTitle(bookTitle)
self.theProject.setBookAuthors(bookAuthors)
self.theProject.setProjBackup(doBackup)
if self.tabStatus.colChanged:
statusCol = self.tabStatus.getNewList()
@@ -116,13 +118,19 @@ class GuiProjectEditMain(QWidget):
self.theParent = theParent
self.theProject = theProject
self.mainForm = QFormLayout()
self.backupBox = QHBoxLayout()
self.editName = QLineEdit()
self.editTitle = QLineEdit()
self.editAuthors = QPlainTextEdit()
self.doBackup = QCheckBox(self)
self.mainForm.addRow("Working Title", self.editName)
self.mainForm.addRow("Book Title", self.editTitle)
self.mainForm.addRow("Book Authors", self.editAuthors)
self.mainForm.addRow("Working Title", self.editName)
self.mainForm.addRow("Book Title", self.editTitle)
self.mainForm.addRow("Book Authors", self.editAuthors)
self.mainForm.addRow(self.backupBox)
self.backupBox.addStretch(1)
self.backupBox.addWidget(QLabel("Backup on Close"))
self.backupBox.addWidget(self.doBackup)
self.editName.setText(self.theProject.projName)
self.editTitle.setText(self.theProject.bookTitle)
@@ -130,8 +138,13 @@ class GuiProjectEditMain(QWidget):
for bookAuthor in self.theProject.bookAuthors:
bookAuthors += bookAuthor+"\n"
self.editAuthors.setPlainText(bookAuthors)
if self.theProject.doBackup:
self.doBackup.setCheckState(Qt.Checked)
else:
self.doBackup.setCheckState(Qt.Unchecked)
self.setLayout(self.mainForm)
self.editAuthors.setMaximumHeight(120)
return
+25 -4
View File
@@ -36,6 +36,7 @@ from nw.project.project import NWProject
from nw.project.document import NWDoc
from nw.project.item import NWItem
from nw.project.index import NWIndex
from nw.project.backup import NWBackup
from nw.tools.wordcount import countWords
from nw.theme import Theme
from nw.enum import nwItemType, nwAlert
@@ -112,7 +113,7 @@ class GuiMain(QMainWindow):
# Set Main Window Elements
self.setMenuBar(self.mainMenu)
self.setStatusBar(self.statusBar)
self.statusBar.showMessage("Ready")
self.statusBar.setStatus("Ready")
# Set Up Autosaving Project Timer
self.asProjTimer = QTimer()
@@ -206,18 +207,33 @@ class GuiMain(QMainWindow):
if msgRes != QMessageBox.Yes:
return False
self.closeDocument()
if self.docEditor.docChanged:
self.saveDocument()
if self.theProject.projChanged:
saveOK = self.saveProject()
saveOK = self.saveProject()
doBackup = False
if self.theProject.doBackup and self.mainConf.backupOnClose:
doBackup = True
if self.mainConf.showGUI and self.mainConf.askBeforeBackup:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Backup Project", "Backup current project?"
)
if msgRes != QMessageBox.Yes:
doBackup = False
if doBackup:
self.backupProject()
else:
saveOK = True
if saveOK:
self.closeDocument()
self.theProject.closeProject()
self.theIndex.clearIndex()
self.clearGUI()
self.hasProject = False
return saveOK
def openProject(self, projFile=None):
@@ -279,6 +295,11 @@ class GuiMain(QMainWindow):
return True
def backupProject(self):
theBackup = NWBackup(self, self.theProject)
theBackup.zipIt()
return True
##
# Document Actions
##
+65
View File
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
"""novelWriter Project Backup
novelWriter Project Backup
==============================
Class handling project backups
File History:
Created: 2019-06-16 [0.1.5]
"""
import logging
import nw
from os import path, mkdir, listdir
from shutil import make_archive
from datetime import datetime
from nw.enum import nwAlert
logger = logging.getLogger(__name__)
class NWBackup():
def __init__(self, theParent, theProject):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
return
def zipIt(self):
if self.mainConf.backupPath is None:
self.theParent.makeAlert("Cannot backup project because no backup path is set.",nwAlert.WARN)
return False
if self.theProject.projName is None:
self.theParent.makeAlert("Cannot backup project because no project name is set.",nwAlert.WARN)
return False
logger.info("Backing up project")
self.theParent.statusBar.setStatus("Backing up project ...")
archName = ""
for c in self.theProject.projName:
if c.isalnum():
archName += c
archName = archName+"_"+datetime.now().strftime("%Y%m%d-%H%M%S")
baseName = path.join(self.mainConf.backupPath, archName)
try:
make_archive(baseName, "zip", self.theProject.projPath, ".")
except Exception as e:
self.theParent.makeAlert(["Could not write backup archive.",str(e)],nwAlert.ERROR)
return False
self.theParent.statusBar.setStatus("Project backup complete")
return True
# END Class NWBackup
+22
View File
@@ -67,6 +67,7 @@ class NWProject():
self.lastViewed = None
self.lastWCount = 0
self.currWCount = 0
self.doBackup = True
# Set Defaults
self.clearProject()
@@ -221,6 +222,8 @@ class NWProject():
elif xItem.tag == "author":
logger.verbose("Author: '%s'" % xItem.text)
self.bookAuthors.append(xItem.text)
elif xItem.tag == "backup":
self.doBackup = checkBool(xItem.text,False)
elif xChild.tag == "settings":
logger.debug("Found project settings")
for xItem in xChild:
@@ -295,6 +298,7 @@ class NWProject():
self._saveProjectValue(xProject,"name", self.projName, True)
self._saveProjectValue(xProject,"title", self.bookTitle, True)
self._saveProjectValue(xProject,"author",self.bookAuthors)
self._saveProjectValue(xProject,"backup",self.doBackup)
# Save Project Settings
xSettings = etree.SubElement(nwXML,"settings")
@@ -375,6 +379,24 @@ class NWProject():
self.setProjectChanged(True)
return True
def setProjBackup(self, doBackup):
self.doBackup = False
if doBackup:
if not path.isdir(self.mainConf.backupPath):
self.theParent.makeAlert(
"You must set a valid backup path in preferences<br>to use the automatic project backup feature.",
nwAlert.ERROR
)
return False
if self.projName == "":
self.theParent.makeAlert(
"You must set a valid project name in project settings<br>to use the automatic project backup feature.",
nwAlert.ERROR
)
return False
self.doBackup = True
return True
def setSpellCheck(self, theMode):
if self.spellCheck != theMode:
self.spellCheck = theMode
+3 -2
View File
@@ -1,10 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.5" fileVersion="1.0" timeStamp="2019-06-15 22:17:54">
<novelWriterXML appVersion="0.1.5" fileVersion="1.0" timeStamp="2019-06-26 18:11:31">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<backup>False</backup>
</project>
<settings>
<spellCheck>True</spellCheck>
@@ -80,7 +81,7 @@
<charCount>736</charCount>
<wordCount>137</wordCount>
<paraCount>6</paraCount>
<cursorPos>69</cursorPos>
<cursorPos>82</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>New File</name>
+2 -1
View File
@@ -1,8 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-08 20:45:49">
<novelWriterXML appVersion="0.1.5" fileVersion="1.0" timeStamp="2019-06-27 20:30:38">
<project>
<name></name>
<title></title>
<backup>True</backup>
</project>
<settings>
<spellCheck>False</spellCheck>
+2 -1
View File
@@ -1,8 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-08 20:46:34">
<novelWriterXML appVersion="0.1.5" fileVersion="1.0" timeStamp="2019-06-27 20:31:37">
<project>
<name></name>
<title></title>
<backup>True</backup>
</project>
<settings>
<spellCheck>True</spellCheck>
+1
View File
@@ -5,6 +5,7 @@
<title>Project Title</title>
<author>Jane Doe</author>
<author>John Doh</author>
<backup>True</backup>
</project>
<settings>
<spellCheck>False</spellCheck>
+1
View File
@@ -3,6 +3,7 @@
<project>
<name></name>
<title></title>
<backup>True</backup>
</project>
<settings>
<spellCheck>False</spellCheck>
+6 -1
View File
@@ -1,5 +1,5 @@
[Main]
timestamp = 2019-06-15 23:10:43
timestamp = 2019-06-27 20:26:43
theme = default
syntax = default_light
@@ -32,6 +32,11 @@ fmtsinglequote = ,
fmtdoublequote = “, ”
spellcheck = en_GB
[Backup]
backuppath =
backuponclose = False
askbeforebackup = True
[Path]
recent0 =
recent1 =
+1
View File
@@ -3,6 +3,7 @@
<project>
<name></name>
<title></title>
<backup>True</backup>
</project>
<settings>
<spellCheck>False</spellCheck>
+1
View File
@@ -3,6 +3,7 @@
<project>
<name></name>
<title></title>
<backup>True</backup>
</project>
<settings>
<spellCheck>False</spellCheck>
+1
View File
@@ -287,6 +287,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef):
# Create new, save, open project
nwGUI.theProject.handleSeed = 42
assert nwGUI.newProject(nwTempGUI, True)
nwGUI.mainConf.backupPath = nwTempGUI
projEdit = GuiProjectEditor(nwGUI, nwGUI.theProject)
qtbot.addWidget(projEdit)