# -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from PyQt5.QtWidgets import QDialog, QTreeWidgetItem, QLabel, QFormLayout, \
QMessageBox, QHeaderView, \
QListWidgetItem, QVBoxLayout, \
QDialogButtonBox, QProgressDialog
from PyQt5.QtGui import QTextDocument, QPixmap, QImage, QIcon
from PyQt5.QtCore import Qt, QVariant, QUrl, QThread
from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot
from collections import OrderedDict
import re
import os
from logging import warning
from weboob.core.repositories import IProgress
from weboob.core.backendscfg import BackendAlreadyExists
from weboob.capabilities.account import CapAccount, Account, AccountRegisterError
from weboob.exceptions import ModuleInstallError, ModuleLoadError
from .backendcfg_ui import Ui_BackendCfg
from .reposdlg_ui import Ui_RepositoriesDlg
from weboob.tools.misc import to_unicode
from weboob.tools.compat import unicode
from .qt import QtValue
class RepositoriesDialog(QDialog):
def __init__(self, filename, parent=None):
super(RepositoriesDialog, self).__init__(parent)
self.filename = filename
self.ui = Ui_RepositoriesDlg()
self.ui.setupUi(self)
self.ui.buttonBox.accepted.connect(self.save)
with open(self.filename, 'r') as fp:
self.ui.reposEdit.setPlainText(fp.read())
@Slot()
def save(self):
with open(self.filename, 'w') as fp:
fp.write(self.ui.reposEdit.toPlainText())
self.accept()
class IconFetcher(QThread):
retrieved = Signal()
def __init__(self, weboob, item, minfo):
super(IconFetcher, self).__init__()
self.weboob = weboob
self.items = [item]
self.minfo = minfo
def run(self):
self.weboob.repositories.retrieve_icon(self.minfo)
self.retrieved.emit()
class ProgressDialog(IProgress, QProgressDialog):
def __init__(self, *args, **kwargs):
super(ProgressDialog, self).__init__(*args, **kwargs)
def progress(self, percent, message):
self.setValue(int(percent * 100))
self.setLabelText(message)
def error(self, message):
QMessageBox.critical(self, self.tr('Error'), '%s' % message, QMessageBox.Ok)
def prompt(self, message):
reply = QMessageBox.question(self, '', unicode(message), QMessageBox.Yes|QMessageBox.No)
return reply == QMessageBox.Yes
class BackendCfg(QDialog):
def __init__(self, weboob, caps=None, parent=None):
super(BackendCfg, self).__init__(parent)
self.ui = Ui_BackendCfg()
self.ui.setupUi(self)
self.ui.backendsList.sortByColumn(0, Qt.AscendingOrder)
self.to_unload = set()
self.to_load = set()
self.weboob = weboob
self.caps = caps
self.config_widgets = {}
# This attribute is set when itemChanged it called, because when
# a backend is enabled/disabled, we don't want to display its config
# frame, and the itemClicked event is always emit just after a
# itemChanged event.
# is_enabling is a counter to prevent race conditions.
self.is_enabling = 0
self.ui.backendsList.header().setSectionResizeMode(QHeaderView.ResizeToContents)
self.ui.configFrame.hide()
self.icon_cache = {}
self.icon_threads = {}
self.loadModules()
self.loadBackendsList()
self.ui.updateButton.clicked.connect(self.updateModules)
self.ui.repositoriesButton.clicked.connect(self.editRepositories)
self.ui.backendsList.itemClicked.connect(self.backendClicked)
self.ui.backendsList.itemChanged.connect(self.backendEnabled)
self.ui.modulesList.itemSelectionChanged.connect(self.moduleSelectionChanged)
self.ui.proxyBox.toggled.connect(self.proxyEditEnabled)
self.ui.addButton.clicked.connect(self.addEvent)
self.ui.removeButton.clicked.connect(self.removeEvent)
self.ui.registerButton.clicked.connect(self.registerEvent)
self.ui.configButtonBox.accepted.connect(self.acceptBackend)
self.ui.configButtonBox.rejected.connect(self.rejectBackend)
def get_icon_cache(self, path):
if path not in self.icon_cache:
img = QImage(path)
self.icon_cache[path] = QIcon(QPixmap.fromImage(img))
return self.icon_cache[path]
def set_icon(self, item, minfo):
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
icon = self.icon_cache.get(icon_path, None)
if icon is None and not os.path.exists(icon_path):
if minfo.name in self.icon_threads:
self.icon_threads[minfo.name].items.append(item)
else:
thread = IconFetcher(self.weboob, item, minfo)
thread.retrieved.connect(self._set_icon_slot)
self.icon_threads[minfo.name] = thread
thread.start()
return
self._set_icon([item], minfo)
@Slot()
def _set_icon_slot(self):
thread = self.sender()
self._set_icon(thread.items, thread.minfo)
def _set_icon(self, items, minfo):
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
icon = self.get_icon_cache(icon_path)
if icon is None:
return
for item in items:
try:
item.setIcon(icon)
except TypeError:
item.setIcon(0, icon)
self.icon_threads.pop(minfo.name, None)
@Slot()
def updateModules(self):
self.ui.configFrame.hide()
pd = ProgressDialog('Update of modules', "Cancel", 0, 100, self)
pd.setWindowModality(Qt.WindowModal)
try:
self.weboob.repositories.update(pd)
except ModuleInstallError as err:
QMessageBox.critical(self, self.tr('Update error'),
self.tr('Unable to update modules: %s' % (err)),
QMessageBox.Ok)
pd.setValue(100)
self.loadModules()
QMessageBox.information(self, self.tr('Update of modules'),
self.tr('Modules updated!'), QMessageBox.Ok)
@Slot()
def editRepositories(self):
if RepositoriesDialog(self.weboob.repositories.sources_list).exec_():
self.updateModules()
def loadModules(self):
self.ui.modulesList.clear()
for name, module in sorted(self.weboob.repositories.get_all_modules_info(self.caps).items()):
item = QListWidgetItem(name.capitalize())
self.set_icon(item, module)
self.ui.modulesList.addItem(item)
def askInstallModule(self, minfo):
reply = QMessageBox.question(self, self.tr('Install a module'),
self.tr("Module %s is not installed. Do you want to install it?") % minfo.name,
QMessageBox.Yes|QMessageBox.No)
if reply != QMessageBox.Yes:
return False
return self.installModule(minfo)
def installModule(self, minfo):
pd = ProgressDialog('Installation of %s' % minfo.name, "Cancel", 0, 100, self)
pd.setWindowModality(Qt.WindowModal)
try:
self.weboob.repositories.install(minfo, pd)
except ModuleInstallError as err:
QMessageBox.critical(self, self.tr('Install error'),
self.tr('Unable to install module %s: %s' % (minfo.name, err)),
QMessageBox.Ok)
pd.setValue(100)
return True
def loadBackendsList(self):
self.ui.backendsList.clear()
for backend_name, module_name, params in self.weboob.backends_config.iter_backends():
info = self.weboob.repositories.get_module_info(module_name)
if not info or (self.caps and not info.has_caps(self.caps)):
continue
item = QTreeWidgetItem(None, [backend_name, module_name])
item.setCheckState(0, Qt.Checked if params.get('_enabled', '1').lower() in ('1', 'y', 'true', 'on', 'yes')
else Qt.Unchecked)
self.set_icon(item, info)
self.ui.backendsList.addTopLevelItem(item)
@Slot(QTreeWidgetItem, int)
def backendEnabled(self, item, col):
self.is_enabling += 1
backend_name = item.text(0)
if item.checkState(0) == Qt.Checked:
self.to_load.add(backend_name)
enabled = 'true'
else:
self.to_unload.add(backend_name)
try:
self.to_load.remove(backend_name)
except KeyError:
pass
enabled = 'false'
self.weboob.backends_config.edit_backend(backend_name, {'_enabled': enabled})
@Slot(QTreeWidgetItem, int)
def backendClicked(self, item, col):
if self.is_enabling:
self.is_enabling -= 1
return
backend_name = item.text(0)
self.editBackend(backend_name)
@Slot()
def addEvent(self):
self.editBackend()
@Slot()
def removeEvent(self):
item = self.ui.backendsList.currentItem()
if not item:
return
backend_name = item.text(0)
reply = QMessageBox.question(self, self.tr('Remove a backend'),
self.tr("Are you sure you want to remove the backend '%s'?") % backend_name,
QMessageBox.Yes|QMessageBox.No)
if reply != QMessageBox.Yes:
return
self.weboob.backends_config.remove_backend(backend_name)
self.to_unload.add(backend_name)
try:
self.to_load.remove(backend_name)
except KeyError:
pass
self.ui.configFrame.hide()
self.loadBackendsList()
def editBackend(self, backend_name=None):
self.ui.registerButton.hide()
self.ui.configFrame.show()
if backend_name is not None:
module_name, params = self.weboob.backends_config.get_backend(backend_name)
items = self.ui.modulesList.findItems(module_name, Qt.MatchFixedString)
if not items:
warning('Backend not found')
else:
self.ui.modulesList.setCurrentItem(items[0])
self.ui.modulesList.setEnabled(False)
self.ui.nameEdit.setText(backend_name)
self.ui.nameEdit.setEnabled(False)
if '_proxy' in params:
self.ui.proxyBox.setChecked(True)
self.ui.proxyEdit.setText(params.pop('_proxy'))
else:
self.ui.proxyBox.setChecked(False)
self.ui.proxyEdit.clear()
params.pop('_enabled', None)
info = self.weboob.repositories.get_module_info(module_name)
if info and (info.is_installed() or self.installModule(info)):
module = self.weboob.modules_loader.get_or_load_module(module_name)
for key, value in module.config.load(self.weboob, module_name, backend_name, params, nofail=True).items():
try:
l, widget = self.config_widgets[key]
except KeyError:
warning('Key "%s" is not found' % key)
else:
# Do not prompt user for value (for example a password if it is empty).
value.noprompt = True
widget.set_value(value)
return
self.ui.nameEdit.clear()
self.ui.nameEdit.setEnabled(True)
self.ui.proxyBox.setChecked(False)
self.ui.proxyEdit.clear()
self.ui.modulesList.setEnabled(True)
self.ui.modulesList.setCurrentRow(-1)
@Slot()
def moduleSelectionChanged(self):
for key, (label, value) in self.config_widgets.items():
label.hide()
value.hide()
self.ui.configLayout.removeWidget(label)
self.ui.configLayout.removeWidget(value)
label.deleteLater()
value.deleteLater()
self.config_widgets = {}
self.ui.moduleInfo.clear()
selection = self.ui.modulesList.selectedItems()
if not selection:
return
minfo = self.weboob.repositories.get_module_info(selection[0].text().lower())
if not minfo:
warning('Module not found')
return
if not minfo.is_installed() and not self.installModule(minfo):
self.editBackend(None)
return
module = self.weboob.modules_loader.get_or_load_module(minfo.name)
icon_path = os.path.join(self.weboob.repositories.icons_dir, '%s.png' % minfo.name)
img = QImage(icon_path)
self.ui.moduleInfo.document().addResource(QTextDocument.ImageResource, QUrl('mydata://logo.png'),
QVariant(img))
if module.name not in [n for n, ign, ign2 in self.weboob.backends_config.iter_backends()]:
self.ui.nameEdit.setText(module.name)
else:
self.ui.nameEdit.setText('')
self.ui.moduleInfo.setText(to_unicode(self.tr(
u'
%s Module %s
'
'Version: %s
'
'Maintainer: %s
'
'License: %s
'
'%s'
'Description: %s
'
'Capabilities: %s
'))
% ('',
module.name.capitalize(),
module.version,
to_unicode(module.maintainer).replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>'),
module.license,
(self.tr('Website: %s
') % module.website) if module.website else '',
module.description,
', '.join(sorted(cap.__name__.replace('Cap', '') for cap in module.iter_caps()))))
if module.has_caps(CapAccount) and self.ui.nameEdit.isEnabled() and \
module.klass.ACCOUNT_REGISTER_PROPERTIES is not None:
self.ui.registerButton.show()
else:
self.ui.registerButton.hide()
for key, field in module.config.items():
label = QLabel(u'%s:' % field.label)
qvalue = QtValue(field)
self.ui.configLayout.addRow(label, qvalue)
self.config_widgets[key] = (label, qvalue)
@Slot(bool)
def proxyEditEnabled(self, state):
self.ui.proxyEdit.setEnabled(state)
@Slot()
def acceptBackend(self):
backend_name = self.ui.nameEdit.text()
selection = self.ui.modulesList.selectedItems()
if not selection:
QMessageBox.critical(self, self.tr('Unable to add a backend'),
self.tr('Please select a module'))
return
try:
module = self.weboob.modules_loader.get_or_load_module(selection[0].text().lower())
except ModuleLoadError:
module = None
if not module:
QMessageBox.critical(self, self.tr('Unable to add a backend'),
self.tr('The selected module does not exist.'))
return
params = {}
if not backend_name:
QMessageBox.critical(self, self.tr('Missing field'), self.tr('Please specify a backend name'))
return
if self.ui.nameEdit.isEnabled():
if not re.match(r'^[\w\-_]+$', backend_name):
QMessageBox.critical(self, self.tr('Invalid value'),
self.tr('The backend name can only contain letters and digits'))
return
if self.weboob.backends_config.backend_exists(backend_name):
QMessageBox.critical(self, self.tr('Unable to create backend'),
self.tr('Unable to create backend "%s": it already exists') % backend_name)
return
if self.ui.proxyBox.isChecked():
params['_proxy'] = self.ui.proxyEdit.text()
if not params['_proxy']:
QMessageBox.critical(self, self.tr('Missing field'), self.tr('Please specify a proxy URL'))
return
config = module.config.load(self.weboob, module.name, backend_name, {}, nofail=True)
for key, field in config.items():
label, qtvalue = self.config_widgets[key]
try:
value = qtvalue.get_value()
except ValueError as e:
QMessageBox.critical(self, self.tr('Invalid value'),
self.tr('Invalid value for field "%s":
%s') % (field.label, e))
return
field.set(value.get())
try:
config.save(edit=not self.ui.nameEdit.isEnabled(), params=params)
except BackendAlreadyExists:
QMessageBox.critical(self, self.tr('Unable to create backend'),
self.tr('Unable to create backend "%s": it already exists') % backend_name)
return
self.to_load.add(backend_name)
self.ui.configFrame.hide()
self.loadBackendsList()
@Slot()
def rejectBackend(self):
self.ui.configFrame.hide()
@Slot()
def registerEvent(self):
selection = self.ui.modulesList.selectedItems()
if not selection:
return
try:
module = self.weboob.modules_loader.get_or_load_module(selection[0].text().lower())
except ModuleLoadError:
module = None
if not module:
return
dialog = QDialog(self)
vbox = QVBoxLayout(dialog)
if module.website:
website = 'on the website %s' % module.website
else:
website = 'with the module %s' % module.name
vbox.addWidget(QLabel('To create an account %s, please provide this information:' % website))
formlayout = QFormLayout()
props_widgets = OrderedDict()
for key, prop in module.klass.ACCOUNT_REGISTER_PROPERTIES.items():
widget = QtValue(prop)
formlayout.addRow(QLabel(u'%s:' % prop.label), widget)
props_widgets[prop.id] = widget
vbox.addLayout(formlayout)
buttonBox = QDialogButtonBox(dialog)
buttonBox.setStandardButtons(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
buttonBox.accepted.connect(dialog.accept)
buttonBox.rejected.connect(dialog.reject)
vbox.addWidget(buttonBox)
end = False
while not end:
end = True
if dialog.exec_():
account = Account()
account.properties = {}
for key, widget in props_widgets.items():
try:
v = widget.get_value()
except ValueError as e:
QMessageBox.critical(self, self.tr('Invalid value'),
self.tr('Invalid value for field "%s":
%s') % (key, e))
end = False
break
else:
account.properties[key] = v
if end:
try:
module.klass.register_account(account)
except AccountRegisterError as e:
QMessageBox.critical(self, self.tr('Error during register'),
self.tr('Unable to register account %s:
%s') % (website, e))
end = False
else:
for key, value in account.properties.items():
if key in self.config_widgets:
self.config_widgets[key][1].set_value(value)
def run(self):
self.exec_()
ret = (len(self.to_load) > 0 or len(self.to_unload) > 0)
self.weboob.unload_backends(self.to_unload)
self.weboob.load_backends(names=self.to_load)
return ret