diff --git a/borg_qt/borg_qt.py b/borg_qt/borg_qt.py new file mode 100755 index 0000000..9a23c45 --- /dev/null +++ b/borg_qt/borg_qt.py @@ -0,0 +1,19 @@ +#!/usr/bin/python3 + +from PyQt5.QtWidgets import QApplication +import sys + +from main_window import MainWindow + +if __name__ == "__main__": + # creates the main application, only one of these is every needed + # in an application + app = QApplication(sys.argv) + # creates a new window, you can have multiple of these + window = MainWindow() + # show the window, they are hidden by default + window.show() + window.config.read() + + # start the application + sys.exit(app.exec_()) diff --git a/borg_qt/config.py b/borg_qt/config.py index 3a329ed..5d5130f 100644 --- a/borg_qt/config.py +++ b/borg_qt/config.py @@ -2,12 +2,83 @@ import os import configparser import json +from PyQt5.QtWidgets import QDialog, QFileDialog +from PyQt5 import uic + from helper import BorgException -class Config(): +class Config(QDialog): def __init__(self): - self.list_values = ['excludes', 'includes'] + # Setting all the PyQt relevant parts + super(QDialog, self).__init__() + dir_path = os.path.dirname(os.path.realpath(__file__)) + ui_path = os.path.join(dir_path + '/static/UI/Settings.ui') + uic.loadUi(ui_path, self) + + self.button_box.accepted.connect(self.accept) + self.button_include_file.clicked.connect(self.include_file) + self.button_include_directory.clicked.connect(self.include_directory) + self.button_exclude_file.clicked.connect(self.exclude_file) + self.button_exclude_directory.clicked.connect(self.exclude_directory) + self.button_remove_include.clicked.connect(self.remove_include) + self.button_remove_exclude.clicked.connect(self.remove_exclude) + self.button_restore_exclude_defaults.clicked.connect( + self.restore_exclude_defaults) + + @property + def full_path(self): + if 'repository_path' in self.config['borgqt']: + if self.config['borgqt']['server']: + return self._create_server_path() + else: + return self.config['borgqt']['repository_path'] + else: + return "" + + @property + def repository_path(self): + return self._return_single_option('repository_path') + + @property + def password(self): + return self._return_single_option('password') + + @property + def includes(self): + return self._return_list_option('includes') + + @property + def excludes(self): + return self._return_list_option('excludes') + + @property + def server(self): + return self._return_single_option('server') + + @property + def port(self): + return self._return_single_option('port') + + @property + def user(self): + return self._return_single_option('user') + + @property + def prefix(self): + return self._return_single_option('prefix') + + def _return_single_option(self, option): + if option in self.config['borgqt']: + return self.config['borgqt'][option] + else: + return "" + + def _return_list_option(self, option): + if option in self.config['borgqt']: + return json.loads(self.config['borgqt'][option]) + else: + return [] def _get_path(self): home = os.environ['HOME'] @@ -21,8 +92,8 @@ class Config(): raise BorgException("Configuration file not found!") def _set_environment_variables(self): - os.environ['BORG_REPO'] = str(self.repository_path) - os.environ['BORG_PASSPHRASE'] = str(self.password) + os.environ['BORG_REPO'] = self.full_path + os.environ['BORG_PASSPHRASE'] = self.password def _create_server_path(self): if not self.config['borgqt']['user']: @@ -37,6 +108,50 @@ class Config(): + self.config['borgqt']['repository_path']) return server_path + def _select_file(self): + dialog = QFileDialog + dialog.ExistingFile + file_path, ignore = dialog.getOpenFileName( + self, "Select Directory", os.getenv('HOME'), "All Files (*)") + return file_path + + def _select_directory(self): + dialog = QFileDialog + dialog.DirectoryOnly + return dialog.getExistingDirectory( + self, "Select Directory", os.getenv('HOME')) + + def include_file(self): + file_path = self._select_file() + if file_path: + self.list_include.addItem(file_path) + + def include_directory(self): + directory_path = self._select_directory() + if directory_path: + self.list_include.addItem(directory_path) + + def exclude_file(self): + file_path = self._select_file() + if file_path: + self.list_exclude.addItem(file_path) + + def exclude_directory(self): + directory_path = self._select_directory() + if directory_path: + self.list_exclude.addItem(directory_path) + + def remove_include(self): + self.list_include.takeItem(self.list_include.currentRow()) + + def remove_exclude(self): + self.list_exclude.takeItem(self.list_exclude.currentRow()) + + def restore_exclude_defaults(self): + self.list_exclude.clear() + default_excludes = json.loads(self.config['DEFAULT']['excludes']) + self.list_exclude.addItems(default_excludes) + def read(self): """Reads the config file """ @@ -44,28 +159,47 @@ class Config(): self.config = configparser.ConfigParser() self.config.read(self.path) - def apply(self): - for option, value in self.config.items('borgqt'): - setattr(self, option, value) + def set_form_values(self): + self.line_edit_repository_path.setText(self.repository_path) + self.line_edit_password.setText(self.password) + self.line_edit_prefix.setText(self.prefix) + self.line_edit_server.setText(self.server) + self.line_edit_port.setText(self.port) + self.line_edit_user.setText(self.user) + self.list_include.clear() + self.list_include.addItems(self.includes) + self.list_exclude.clear() + self.list_exclude.addItems(self.excludes) - for item in self.list_values: - setattr(self, item, json.loads( - self.config['borgqt'].get(item, '[]'))) + def apply_options(self): + self.config['borgqt']['repository_path'] = self.line_edit_repository_path.text() + self.config['borgqt']['password'] = self.line_edit_password.text() + self.config['borgqt']['prefix'] = self.line_edit_prefix.text() + self.config['borgqt']['server'] = self.line_edit_server.text() + self.config['borgqt']['port'] = self.line_edit_port.text() + self.config['borgqt']['user'] = self.line_edit_user.text() - if self.config['borgqt']['server']: - self.repository_path = self._create_server_path() + excludes = [] + for index in range(self.list_exclude.count()): + excludes.append(self.list_exclude.item(index).text()) + + includes = [] + for index in range(self.list_include.count()): + includes.append(self.list_include.item(index).text()) + + self.config['borgqt']['includes'] = json.dumps(includes, + indent=4, + sort_keys=True) + self.config['borgqt']['excludes'] = json.dumps(excludes, + indent=4, + sort_keys=True) self._set_environment_variables() def write(self): - if self.server: - self.config['borgqt']['port'] = self.port - self.config['borgqt']['user'] = self.user - self.config['borgqt']['server'] = self.server - - for item in self.list_values: - self.config['borgqt'][item] = json.dumps( - getattr(self, item), indent=4, sort_keys=True) - - self.config['borgqt']['password'] = self.password with open(self.path, 'w+') as configfile: self.config.write(configfile) + + def accept(self): + super().accept() + self.apply_options() + self.write() diff --git a/borg_qt/main_window.py b/borg_qt/main_window.py new file mode 100644 index 0000000..4afe169 --- /dev/null +++ b/borg_qt/main_window.py @@ -0,0 +1,26 @@ +import os + +from PyQt5 import uic +from PyQt5.QtCore import QCoreApplication +from PyQt5.QtWidgets import QMainWindow + +from settings import Settings +from config import Config + + +class MainWindow(QMainWindow): + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + self.setWindowTitle("Borg Interface") + QCoreApplication.setApplicationName("borg-qt") + dir_path = os.path.dirname(os.path.realpath(__file__)) + ui_path = os.path.join(dir_path + '/static/UI/MainWindow.ui') + uic.loadUi(ui_path, self) + + self.config = Config() + + self.action_settings.triggered.connect(self.show_settings) + + def show_settings(self): + self.config.set_form_values() + self.config.exec_() diff --git a/borg_qt/settings.py b/borg_qt/settings.py new file mode 100644 index 0000000..22945cf --- /dev/null +++ b/borg_qt/settings.py @@ -0,0 +1,67 @@ +import os + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QDialog +from PyQt5 import uic + + +class Settings(QDialog): + def __init__(self, config): + super(QDialog, self).__init__() + dir_path = os.path.dirname(os.path.realpath(__file__)) + ui_path = os.path.join(dir_path + '/static/UI/Settings.ui') + uic.loadUi(ui_path, self) + self.config = config + + self.read_config() + self.button_box.accepted.connect(self.read_inputs) + self.show() + + def read_config(self): + try: + self.line_edit_repository_path.setText(self.config.repository_path) + self.line_edit_password.setText(self.config.password) + except AttributeError: + pass + try: + self.line_edit_prefix.setText(self.config.prefix) + except AttributeError: + pass + try: + self.line_edit_server.setText(self.config.server) + self.line_edit_port.setText(self.config.port) + self.line_edit_user.setText(self.config.user) + except AttributeError: + pass + + try: + self.list_include.clear() + self.list_include.addItems(self.config.includes) + except AttributeError: + pass + try: + self.list_exclude.clear() + self.list_exclude.addItems(self.config.excludes) + except AttributeError: + pass + + def read_inputs(self): + self.config.repository_path = self.line_edit_repository_path.text() + self.config.password = self.line_edit_password.text() + self.config.prefix = self.line_edit_prefix.text() + self.config.server = self.line_edit_server.text() + self.config.port = self.line_edit_port.text() + self.config.user = self.line_edit_user.text() + + excludes = [] + for index in range(self.list_exclude.count()): + excludes.append(self.list_exclude.item(index).text()) + + includes = [] + for index in range(self.list_include.count()): + includes.append(self.list_include.item(index).text()) + + self.config.includes = includes + self.config.excludes = excludes + self.config.write() + self.config.apply() diff --git a/borg_qt/static/UI/Settings.ui b/borg_qt/static/UI/Settings.ui index 188fd35..7d69b70 100644 --- a/borg_qt/static/UI/Settings.ui +++ b/borg_qt/static/UI/Settings.ui @@ -54,27 +54,11 @@ 10 10 541 - 251 + 296 - - - - - - - - 0 - 0 - - - - Password: - - - - + @@ -88,46 +72,10 @@ - + - - - - Qt::ImhHiddenText|Qt::ImhNoAutoUppercase|Qt::ImhNoPredictiveText|Qt::ImhSensitiveData - - - QLineEdit::Password - - - false - - - - - - - - 0 - 0 - - - - Port: - - - - - - - - 0 - 0 - - - - Username: - - + + @@ -142,13 +90,23 @@ - + - - + + + + + 0 + 0 + + + + Port: + + - + @@ -161,9 +119,108 @@ - + + + + + 0 + 0 + + + + Username: + + + + + + + + 0 + 0 + + + + Password: + + + + + + + Qt::ImhNone + + + QLineEdit::Normal + + + false + + + + + + + + + + + + + Include + + + + + 10 + 10 + 541 + 331 + + + + + + + + + + + + Add file + + + + ../icons/plus.svg../icons/plus.svg + + + + + + + Add folder + + + + ../icons/plus.svg../icons/plus.svg + + + + + + + Remove + + + + ../icons/minus.svg../icons/minus.svg + + + + + @@ -182,12 +239,12 @@ - + - + Add file @@ -198,7 +255,7 @@ - + Add folder @@ -209,14 +266,14 @@ - + - Add default + Restore defaults - + Remove diff --git a/tests/test_config.py b/tests/test_config.py index 7a6003d..c44603a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,78 +1,139 @@ +import os +import sys +import configparser import unittest from unittest.mock import MagicMock, patch +import warnings -import os -import configparser +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QApplication +from PyQt5.QtTest import QTest import context +from main_window import MainWindow from config import Config from helper import BorgException -class TestConfiguration(unittest.TestCase): +app = QApplication(sys.argv) + + +def fxn(): + warnings.warn("deprecated", DeprecationWarning) + + +class BorgQtConfigTestCase(unittest.TestCase): def setUp(self): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + fxn() + self.form = MainWindow() + self.dir_path = os.path.dirname(os.path.realpath(__file__)) self.config_path = os.path.join(self.dir_path, '../docs/borg_qt.conf.example') + +class TestConfiguration(BorgQtConfigTestCase): def test_read_configuration(self): - config = Config() - config._get_path = MagicMock(return_value=self.config_path) - config.read() + self.form.config._get_path = MagicMock(return_value=self.config_path) + self.form.config.read() parser = configparser.ConfigParser() parser.read(self.config_path) - self.assertEqual(parser, config.config) + self.assertEqual(parser, self.form.config.config) @patch('config.os.path') def test_absent_config_file(self, mock_path): mock_path.exists.return_value = False with self.assertRaises(BorgException): - config = Config() - config._get_path() + self.form.config._get_path() def test_absent_port(self): - config = Config() - config._get_path = MagicMock(return_value=self.config_path) - config.read() - config.config['borgqt']['port'] = "" + self.form.config._get_path = MagicMock(return_value=self.config_path) + self.form.config.read() + self.form.config.config['borgqt']['port'] = "" with self.assertRaises(BorgException): - config._create_server_path() + self.form.config._create_server_path() - def test_absent_port(self): - config = Config() - config._get_path = MagicMock(return_value=self.config_path) - config.read() - config.config['borgqt']['user'] = "" + def test_absent_user(self): + self.form.config._get_path = MagicMock(return_value=self.config_path) + self.form.config.read() + self.form.config.config['borgqt']['user'] = "" with self.assertRaises(BorgException): - config._create_server_path() + self.form.config._create_server_path() def test_apply_settings(self): - config = Config() - config._get_path = MagicMock(return_value=self.config_path) - config.read() - config.apply() - self.assertIs(type(config.excludes), list) - self.assertEqual(str(config.repository_path), os.environ['BORG_REPO']) - self.assertEqual(str(config.password), os.environ['BORG_PASSPHRASE']) + self.form.config._get_path = MagicMock(return_value=self.config_path) + self.form.config.read() + self.form.config.apply_options() + self.assertIs(type(self.form.config.excludes), list) + self.assertEqual(self.form.config.full_path, + os.environ['BORG_REPO']) + self.assertEqual(self.form.config.password, + os.environ['BORG_PASSPHRASE']) -class TestWriteConfiguration(unittest.TestCase): - def setUp(self): - self.dir_path = os.path.dirname(os.path.realpath(__file__)) - self.config_path = os.path.join(self.dir_path, - '../docs/borg_qt.conf.example') - self.config = Config() - self.config._get_path = MagicMock(return_value=self.config_path) - self.config.read() - self.config.apply() - self.config.path = '/tmp/test.conf' - +class TestWriteConfiguration(BorgQtConfigTestCase): def tearDown(self): - os.remove(self.config.path) + if os.path.exists(self.form.config.path): + os.remove(self.form.config.path) def test_write_config(self): - self.config.write() + self.form.config._get_path = MagicMock(return_value=self.config_path) + self.form.config.read() + self.form.config.path = '/tmp/test.conf' + self.form.config.write() config = configparser.ConfigParser() - config.read(self.config.path) - self.assertEqual(self.config.config['borgqt']['password'], + config.read(self.form.config.path) + self.assertEqual(self.form.config.config['borgqt']['password'], config['borgqt']['password']) + + +class TestGuiConfiguration(BorgQtConfigTestCase): + def setUp(self): + super().setUp() + self.form.config.read() + self.form.config.path = '/tmp/test.conf' + + def tearDown(self): + if os.path.exists(self.form.config.path): + os.remove(self.form.config.path) + + def test_set_form_values(self): + self.form.config.set_form_values() + self.assertEqual(self.form.config.password, + self.form.config.line_edit_password.text()) + + def test_cancel_settings(self): + self.form.config.line_edit_password.clear() + QTest.keyClicks(self.form.config.line_edit_password, "bar") + cancel_button = self.form.config.button_box.button( + self.form.config.button_box.Cancel) + QTest.mouseClick(cancel_button, Qt.LeftButton) + self.assertEqual(self.form.config.password, + self.form.config.config['borgqt']['password']) + + def test_ok_settings(self): + self.form.config.line_edit_password.clear() + QTest.keyClicks(self.form.config.line_edit_password, "bar") + ok_button = self.form.config.button_box.button( + self.form.config.button_box.Ok) + QTest.mouseClick(ok_button, Qt.LeftButton) + parser = configparser.ConfigParser() + parser.read(self.form.config.path) + self.assertEqual(self.form.config.line_edit_password.text(), + parser['borgqt']['password']) + + def test_include_remove(self): + self.form.config.set_form_values() + counter = self.form.config.list_include.count() + self.form.config.list_include.setCurrentRow(0) + self.form.config.remove_include() + self.assertGreaterEqual(counter, self.form.config.list_include.count()) + + def test_exclude_remove(self): + self.form.config.set_form_values() + counter = self.form.config.list_exclude.count() + self.form.config.list_exclude.setCurrentRow(0) + self.form.config.remove_exclude() + self.assertGreaterEqual(counter, self.form.config.list_exclude.count())