From fb0d3b3a69f351747bbc0f71f3774868750d762c Mon Sep 17 00:00:00 2001 From: inso <insomniak.fr@gmaiL.com> Date: Sun, 21 Aug 2016 00:22:04 +0200 Subject: [PATCH] Community config component --- src/sakia/core/account.py | 2 +- .../gui/dialogs/account_cfg/controller.py | 21 +- .../gui/dialogs/community_cfg/__init__.py | 0 .../dialogs/community_cfg/community_cfg.ui | 268 +++++++++++++++ .../gui/dialogs/community_cfg/controller.py | 237 +++++++++++++ src/sakia/gui/dialogs/community_cfg/model.py | 54 +++ .../community_cfg/process_cfg_community.py | 321 ++++++++++++++++++ src/sakia/gui/dialogs/community_cfg/view.py | 65 ++++ src/sakia/models/peering.py | 3 + 9 files changed, 967 insertions(+), 4 deletions(-) create mode 100644 src/sakia/gui/dialogs/community_cfg/__init__.py create mode 100644 src/sakia/gui/dialogs/community_cfg/community_cfg.ui create mode 100644 src/sakia/gui/dialogs/community_cfg/controller.py create mode 100644 src/sakia/gui/dialogs/community_cfg/model.py create mode 100644 src/sakia/gui/dialogs/community_cfg/process_cfg_community.py create mode 100644 src/sakia/gui/dialogs/community_cfg/view.py diff --git a/src/sakia/core/account.py b/src/sakia/core/account.py index a0db953f..cbb395e8 100644 --- a/src/sakia/core/account.py +++ b/src/sakia/core/account.py @@ -370,7 +370,7 @@ class Account(QObject): uids = result['uids'] for uid_data in uids: if BlockUID.from_str(uid_data["meta"]["timestamp"]) >= timestamp: - timestamp = uid_data["meta"]["timestamp"] + timestamp = BlockUID.from_str(uid_data["meta"]["timestamp"]) found_uid = uid_data["uid"] if found_uid == self.name: found_result = result['pubkey'], found_uid diff --git a/src/sakia/gui/dialogs/account_cfg/controller.py b/src/sakia/gui/dialogs/account_cfg/controller.py index e70b13e7..0083b7c6 100644 --- a/src/sakia/gui/dialogs/account_cfg/controller.py +++ b/src/sakia/gui/dialogs/account_cfg/controller.py @@ -2,8 +2,12 @@ from PyQt5.QtWidgets import QDialog from sakia.gui.password_asker import PasswordAskerDialog, detect_non_printable from sakia.gui.component.controller import ComponentController +from ..community_cfg.controller import CommunityConfigController from .view import AccountConfigView from .model import AccountConfigModel +from sakia.tools.decorators import asyncify + +import logging class AccountConfigController(ComponentController): @@ -15,8 +19,8 @@ class AccountConfigController(ComponentController): """ Constructor of the AccountConfigController component - :param sakia.gui.AccountConfigController.view.AccountConfigControllerView: the view - :param sakia.gui.AccountConfigController.model.AccountConfigControllerModel model: the model + :param sakia.gui.account_cfg.view.AccountConfigCView: the view + :param sakia.gui.account_cfg.model.AccountConfigModel model: the model """ super().__init__(parent, view, model) @@ -53,7 +57,7 @@ class AccountConfigController(ComponentController): :param sakia.gui.component.controller.ComponentController parent: :param sakia.core.Application app: :return: a new AccountConfigController controller - :rtype: AccountConfigControllerController + :rtype: AccountConfigController """ view = AccountConfigView(parent.view) model = AccountConfigModel(None, app, None) @@ -157,6 +161,7 @@ class AccountConfigController(ComponentController): return True def init_communities(self): + self.view.button_add_community.clicked.connect(self.open_process_add_community) self.view.button_previous.setEnabled(False) self.view.button_next.setText("Ok") list_model = self.model.communities_list_model() @@ -170,6 +175,16 @@ class AccountConfigController(ComponentController): self._steps[self._current_step]['init']() self.view.stacked_pages.setCurrentWidget(self._steps[self._current_step]['page']) + @asyncify + async def open_process_add_community(self, checked=False): + logging.debug("Opening configure community dialog") + logging.debug(self.password_asker) + await CommunityConfigController.create_community(self, + self.model.app, + account=self.model.account, + password_asker=self.password_asker) + + def accept(self): if self.password_asker.result() == QDialog.Rejected: return diff --git a/src/sakia/gui/dialogs/community_cfg/__init__.py b/src/sakia/gui/dialogs/community_cfg/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/sakia/gui/dialogs/community_cfg/community_cfg.ui b/src/sakia/gui/dialogs/community_cfg/community_cfg.ui new file mode 100644 index 00000000..b4a81028 --- /dev/null +++ b/src/sakia/gui/dialogs/community_cfg/community_cfg.ui @@ -0,0 +1,268 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CommunityConfigurationDialog</class> + <widget class="QDialog" name="CommunityConfigurationDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>329</height> + </rect> + </property> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="windowTitle"> + <string>Add a community</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QStackedWidget" name="stacked_pages"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="page_node"> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Please enter the address of a node :</string> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="rightMargin"> + <number>5</number> + </property> + <item> + <widget class="QLineEdit" name="lineedit_server"/> + </item> + <item> + <widget class="QLabel" name="label_double_dot"> + <property name="text"> + <string>:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="spinbox_port"> + <property name="maximum"> + <number>65535</number> + </property> + <property name="value"> + <number>8001</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_5"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <widget class="QPushButton" name="button_register"> + <property name="text"> + <string>Register your account</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/new_membership</normaloff>:/icons/new_membership</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_connect"> + <property name="text"> + <string>Connect using your account</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_guest"> + <property name="text"> + <string>Connect as a guest</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/guest_icon</normaloff>:/icons/guest_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_error"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <widget class="QWidget" name="page_add_nodes"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Communities nodes</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTreeView" name="tree_peers"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLineEdit" name="lineedit_add_address"> + <property name="text"> + <string/> + </property> + <property name="placeholderText"> + <string>Server</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="spinbox_add_port"> + <property name="minimum"> + <number>0</number> + </property> + <property name="maximum"> + <number>65535</number> + </property> + <property name="singleStep"> + <number>1</number> + </property> + <property name="value"> + <number>8081</number> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_add"> + <property name="text"> + <string>Add</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="layout_previous_next"> + <item> + <widget class="QPushButton" name="button_previous"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Previous</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="button_next"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Next</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources> + <include location="../../../../../res/icons/icons.qrc"/> + </resources> + <connections/> + <slots> + <slot>add_node()</slot> + <slot>showContextMenu(QPoint)</slot> + <slot>check()</slot> + <slot>next()</slot> + <slot>previous()</slot> + <slot>current_wallet_changed(int)</slot> + <slot>remove_node()</slot> + </slots> +</ui> diff --git a/src/sakia/gui/dialogs/community_cfg/controller.py b/src/sakia/gui/dialogs/community_cfg/controller.py new file mode 100644 index 00000000..0ff5b795 --- /dev/null +++ b/src/sakia/gui/dialogs/community_cfg/controller.py @@ -0,0 +1,237 @@ +from PyQt5.QtWidgets import QDialog, QApplication, QMenu +from PyQt5.QtGui import QCursor +from sakia.gui.component.controller import ComponentController +from .view import CommunityConfigView +from .model import CommunityConfigModel +from sakia.tools.decorators import asyncify +from aiohttp.errors import DisconnectedError, ClientError, TimeoutError +from duniterpy.documents import MalformedDocumentError +from sakia.tools.exceptions import NoPeerAvailable + +import logging + + +class CommunityConfigController(ComponentController): + """ + The CommunityConfigController view + """ + + def __init__(self, parent, view, model): + """ + Constructor of the CommunityConfigController component + + :param sakia.gui.community_cfg.view.CommunityConfigView: the view + :param sakia.gui.community_cfg.model.CommunityConfigModel model: the model + """ + super().__init__(parent, view, model) + + self._current_step = 0 + self.view.button_next.clicked.connect(lambda checked: self.handle_next_step(False)) + self._steps = ( + { + 'page': self.view.page_node, + 'init': self.init_connect_page, + 'next': lambda: True + }, + { + 'page': self.view.page_add_nodes, + 'init': self.init_nodes_page, + 'next': self.accept + } + ) + self.handle_next_step(init=True) + self.password_asker = None + + self.view.button_connect.clicked.connect(self.check_connect) + self.view.button_register.clicked.connect(self.check_register) + self.view.button_guest.clicked.connect(self.check_guest) + + @classmethod + def create(cls, parent, app, **kwargs): + """ + Instanciate a CommunityConfigController component + :param sakia.gui.component.controller.ComponentController parent: + :param sakia.core.Application app: + :return: a new CommunityConfigController controller + :rtype: CommunityConfigController + """ + account = kwargs['account'] + community = kwargs['community'] + password_asker = kwargs['password_asker'] + view = CommunityConfigView(parent.view) + model = CommunityConfigModel(None, app, account, community) + community_cfg = cls(parent, view, model) + model.setParent(community_cfg) + community_cfg.password_asker = password_asker + return community_cfg + + @classmethod + @asyncify + def create_community(cls, parent, app, account, password_asker): + """ + Open a dialog to create a new Community + :param parent: + :param app: + :param account: + :return: + """ + community_cfg = cls.create(parent, app, account=account, community=None, password_asker=password_asker) + community_cfg.view.set_creation_layout() + return community_cfg.view.async_exec() + + @classmethod + @asyncify + def modify_community(cls, parent, app, account, community, password_asker): + """ + Open a dialog to modify an existing Community + :param parent: + :param app: + :param account: + :param community: + :return: + """ + community_cfg = cls.create(parent, app, account=account, + community=community, password_asker=password_asker) + community_cfg.view.set_modification_layout(community.name) + community_cfg._current_step = 1 + return community_cfg.view.async_exec() + + def handle_next_step(self, init=False): + if self._current_step < len(self._steps) - 1: + if not init: + self._steps[self._current_step]['next']() + self._current_step += 1 + self._steps[self._current_step]['init']() + self.view.stacked_pages.setCurrentWidget(self._steps[self._current_step]['page']) + + def init_connect_page(self): + pass + + def init_nodes_page(self): + self.view.set_steps_buttons_visible(True) + model = self.model.init_nodes_model() + self.view.tree_peers.customContextMenuRequested(self.show_context_menu) + + self.view.set_nodes_model(model) + self.view.button_previous.setEnabled(False) + self.view.button_next.setText(self.config_dialog.tr("Ok")) + + @asyncify + async def check_guest(self, checked=False): + server, port = self.view.node_parameters() + logging.debug("Is valid ? ") + self.view.display_info(self.tr("connecting...")) + try: + await self.model.create_community(server, port) + self.view.button_connect.setEnabled(False) + self.view.button_register.setEnabled(False) + self._steps[self._current_step]['next']() + except (DisconnectedError, ClientError, MalformedDocumentError, ValueError) as e: + self.view.display_info(str(e)) + except TimeoutError: + self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port")) + + @asyncify + async def check_connect(self, checked=False): + server, port = self.view.node_parameters() + logging.debug("Is valid ? ") + self.view.display_info.setText(self.tr("connecting...")) + try: + await self.model.create_community(server, port) + self.view.button_connect.setEnabled(False) + self.view.button_register.setEnabled(False) + registered = await self.model.check_registered() + self.view.button_connect.setEnabled(True) + self.view.button_register.setEnabled(True) + if registered[0] is False and registered[2] is None: + self.view.display_info(self.tr("Could not find your identity on the network.")) + elif registered[0] is False and registered[2]: + self.view.display_info(self.tr("""Your pubkey or UID is different on the network. +Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self._steps[self._current_step]['next']() + except (DisconnectedError, ClientError, MalformedDocumentError, ValueError) as e: + self.view.display_info(str(e)) + except TimeoutError: + self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port")) + except NoPeerAvailable: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) + + @asyncify + async def check_register(self, checked=False): + server, port = self.view.node_parameters() + logging.debug("Is valid ? ") + self.view.display_info(self.tr("connecting...")) + try: + await self.model.create_community(server, port) + self.view.button_connect.setEnabled(False) + self.view.button_register.setEnabled(False) + registered = await self.model.check_registered() + self.view.button_connect.setEnabled(True) + self.view.button_register.setEnabled(True) + if registered[0] is False and registered[2] is None: + password = await self.password_asker.async_exec() + if self.password_asker.result() == QDialog.Rejected: + return + self.view.display_info(self.tr("Broadcasting identity...")) + result = await self.model.publish_selfcert(password) + if result[0]: + self.view.show_success() + QApplication.restoreOverrideCursor() + self._steps[self._current_step]['next']() + else: + self.view.show_error(self.model.notification(), result[1]) + QApplication.restoreOverrideCursor() + elif registered[0] is False and registered[2]: + self.view.display_info(self.tr("""Your pubkey or UID was already found on the network. +Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self.display_info("Your account already exists on the network") + except (DisconnectedError, ClientError, MalformedDocumentError, ValueError) as e: + self.view.display_info(str(e)) + except NoPeerAvailable: + self.view.display_info(self.tr("Could not connect. Check node peering entry")) + except TimeoutError: + self.view.display_info(self.tr("Could not connect. Check hostname, ip address or port")) + + def show_context_menu(self, point): + if self.view.stacked_pages.currentWidget() == self.steps[1]['widget']: + menu = QMenu() + index = self.model.nodes_tree_model.indexAt(point) + action = menu.addAction(self.tr("Delete"), self.remove_node) + action.setData(index.row()) + if len(self.nodes) == 1: + action.setEnabled(False) + menu.exec_(QCursor.pos()) + + @asyncify + async def add_node(self, checked=False): + """ + Add node slot + """ + server, port = self.view.add_node_parameters() + try: + await self.model.add_node(server, port) + except Exception as e: + self.view.show_error(self.model.notification(), str(e)) + + def remove_node(self): + """ + Remove node slot + """ + logging.debug("Remove node") + index = self.sender().data() + self.model.remove_node(index) + + def accept(self): + if self.community not in self.account.communities: + self.account.add_community(self.community) + self.view.accept() + + @property + def view(self) -> CommunityConfigView: + return self._view + + @property + def model(self) -> CommunityConfigModel: + return self._model \ No newline at end of file diff --git a/src/sakia/gui/dialogs/community_cfg/model.py b/src/sakia/gui/dialogs/community_cfg/model.py new file mode 100644 index 00000000..c50262f2 --- /dev/null +++ b/src/sakia/gui/dialogs/community_cfg/model.py @@ -0,0 +1,54 @@ +from sakia.gui.component.model import ComponentModel +from sakia.core.net import Node +from sakia.core import Community +import aiohttp +from sakia.models.peering import PeeringTreeModel + + +class CommunityConfigModel(ComponentModel): + """ + The model of CommunityConfig component + """ + + def __init__(self, parent, app, account, community): + """ + + :param sakia.gui.dialogs.Community_cfg.controller.CommunityConfigController parent: + :param sakia.core.Application app: + :param sakia.core.Account account: + :param sakia.core.Community community: + """ + super().__init__(parent) + self.app = app + self.account = account + self.community = community + self.nodes = [] + self.nodes_tree_model = None + + async def create_community(self, server, port): + node = await Node.from_address(None, server, port, session=aiohttp.ClientSession()) + self.community = Community.create(node) + + async def add_node(self, server, port): + node = await Node.from_address(self.community.currency, server, port, session=self.community.network.session) + self.community.add_node(node) + self.nodes_tree_model.refresh_tree() + + def remove_node(self, index): + self.community.remove_node(index) + self.nodes_tree_model.refresh_tree() + + async def check_registered(self): + return await self.account.check_registered(self.community) + + async def publish_selfcert(self, password): + return await self.account.send_selfcert(password, self.community) + + def init_nodes_model(self): + # We add already known peers to the displayed list + self.nodes = self.community.network.root_nodes + self.nodes_tree_model = PeeringTreeModel(self.community) + + def notification(self): + self.app.preferences['notifications'] + diff --git a/src/sakia/gui/dialogs/community_cfg/process_cfg_community.py b/src/sakia/gui/dialogs/community_cfg/process_cfg_community.py new file mode 100644 index 00000000..4f437e38 --- /dev/null +++ b/src/sakia/gui/dialogs/community_cfg/process_cfg_community.py @@ -0,0 +1,321 @@ +""" +Created on 8 mars 2014 + +@author: inso +""" + +import logging +import asyncio + +import aiohttp + +from duniterpy.api import errors +from duniterpy.documents import MalformedDocumentError +from PyQt5.QtWidgets import QDialog, QMenu, QApplication +from PyQt5.QtGui import QCursor +from PyQt5.QtCore import pyqtSignal, QObject + +from ..presentation.community_cfg_uic import Ui_CommunityConfigurationDialog +from ..models.peering import PeeringTreeModel +from ..core import Community +from ..core.net import Node +from .widgets import toast +from .widgets.dialogs import QAsyncMessageBox +from ..tools.decorators import asyncify +from ..tools.exceptions import NoPeerAvailable + + +class Step(QObject): + def __init__(self, config_dialog, previous_step=None, next_step=None): + super().__init__() + self.previous_step = previous_step + self.next_step = next_step + self.config_dialog = config_dialog + + +class StepPageInit(Step): + """ + First step when adding a community + """ + def __init__(self, config_dialog): + super().__init__(config_dialog) + self.node = None + logging.debug("Init") + self.config_dialog.button_connect.clicked.connect(self.check_connect) + self.config_dialog.button_register.clicked.connect(self.check_register) + self.config_dialog.button_guest.clicked.connect(self.check_guest) + + @property + def app(self): + return self.config_dialog.app + + @property + def account(self): + return self.config_dialog.account + + @property + def community(self): + return self.config_dialog.community + + @property + def password_asker(self): + return self.config_dialog.password_asker + + @asyncify + async def check_guest(self, checked=False): + server = self.config_dialog.lineedit_server.text() + port = self.config_dialog.spinbox_port.value() + logging.debug("Is valid ? ") + self.config_dialog.label_error.setText(self.tr("connecting...")) + try: + self.node = await Node.from_address(None, server, port, session=aiohttp.ClientSession()) + community = Community.create(self.node) + self.config_dialog.button_connect.setEnabled(False) + self.config_dialog.button_register.setEnabled(False) + self.config_dialog.community = community + self.config_dialog.next() + except aiohttp.errors.DisconnectedError as e: + self.config_dialog.label_error.setText(str(e)) + except aiohttp.errors.ClientError as e: + self.config_dialog.label_error.setText(str(e)) + except (MalformedDocumentError, ValueError) as e: + self.config_dialog.label_error.setText(str(e)) + except aiohttp.errors.TimeoutError: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check hostname, ip address or port")) + + @asyncify + async def check_connect(self, checked=False): + server = self.config_dialog.lineedit_server.text() + port = self.config_dialog.spinbox_port.value() + logging.debug("Is valid ? ") + self.config_dialog.label_error.setText(self.tr("connecting...")) + try: + self.node = await Node.from_address(None, server, port, session=aiohttp.ClientSession()) + community = Community.create(self.node) + self.config_dialog.button_connect.setEnabled(False) + self.config_dialog.button_register.setEnabled(False) + registered = await self.account.check_registered(community) + self.config_dialog.button_connect.setEnabled(True) + self.config_dialog.button_register.setEnabled(True) + if registered[0] is False and registered[2] is None: + self.config_dialog.label_error.setText(self.tr("Could not find your identity on the network.")) + elif registered[0] is False and registered[2]: + self.config_dialog.label_error.setText(self.tr("""Your pubkey or UID is different on the network. +Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self.config_dialog.community = community + self.config_dialog.next() + except aiohttp.errors.DisconnectedError as e: + self.config_dialog.label_error.setText(str(e)) + except aiohttp.errors.ClientError as e: + self.config_dialog.label_error.setText(str(e)) + except (MalformedDocumentError, ValueError) as e: + self.config_dialog.label_error.setText(str(e)) + except NoPeerAvailable: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) + except aiohttp.errors.TimeoutError: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check hostname, ip address or port")) + + @asyncify + async def check_register(self, checked=False): + server = self.config_dialog.lineedit_server.text() + port = self.config_dialog.spinbox_port.value() + logging.debug("Is valid ? ") + self.config_dialog.label_error.setText(self.tr("connecting...")) + try: + session = aiohttp.ClientSession() + self.node = await Node.from_address(None, server, port, session=session) + community = Community.create(self.node) + self.config_dialog.button_connect.setEnabled(False) + self.config_dialog.button_register.setEnabled(False) + registered = await self.account.check_registered(community) + self.config_dialog.button_connect.setEnabled(True) + self.config_dialog.button_register.setEnabled(True) + if registered[0] is False and registered[2] is None: + password = await self.password_asker.async_exec() + if self.password_asker.result() == QDialog.Rejected: + return + self.config_dialog.label_error.setText(self.tr("Broadcasting identity...")) + result = await self.account.send_selfcert(password, community) + if result[0]: + if self.app.preferences['notifications']: + toast.display(self.tr("UID broadcast"), self.tr("Identity broadcasted to the network")) + QApplication.restoreOverrideCursor() + self.config_dialog.next() + else: + self.config_dialog.label_error.setText(self.tr("Error") + " " + \ + self.tr("{0}".format(result[1]))) + if self.app.preferences['notifications']: + toast.display(self.tr("Error"), self.tr("{0}".format(result[1]))) + QApplication.restoreOverrideCursor() + self.config_dialog.community = community + elif registered[0] is False and registered[2]: + self.config_dialog.label_error.setText(self.tr("""Your pubkey or UID was already found on the network. +Yours : {0}, the network : {1}""".format(registered[1], registered[2]))) + else: + self.config_dialog.label_error.setText(self.tr("Your account already exists on the network")) + except (MalformedDocumentError, ValueError, errors.DuniterError, + aiohttp.errors.ClientError, aiohttp.errors.DisconnectedError) as e: + session.close() + self.config_dialog.label_error.setText(str(e)) + except NoPeerAvailable: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check node peering entry")) + except aiohttp.errors.TimeoutError: + self.config_dialog.label_error.setText(self.tr("Could not connect. Check hostname, ip address or port")) + + def is_valid(self): + return self.node is not None + + def process_next(self): + """ + We create the community + """ + account = self.config_dialog.account + logging.debug("Account : {0}".format(account)) + self.config_dialog.community = Community.create(self.node) + + def display_page(self): + self.config_dialog.button_next.hide() + self.config_dialog.button_previous.hide() + + +class StepPageAddpeers(Step): + """ + The step where the user add peers + """ + def __init__(self, config_dialog): + super().__init__(config_dialog) + + def is_valid(self): + return True + + def process_next(self): + pass + + def display_page(self): + self.config_dialog.button_next.show() + self.config_dialog.button_previous.show() + # We add already known peers to the displayed list + self.config_dialog.nodes = self.config_dialog.community.network.root_nodes + tree_model = PeeringTreeModel(self.config_dialog.community) + + self.config_dialog.tree_peers.setModel(tree_model) + self.config_dialog.button_previous.setEnabled(False) + self.config_dialog.button_next.setText(self.config_dialog.tr("Ok")) + + +class ProcessConfigureCommunity(QDialog, Ui_CommunityConfigurationDialog): + """ + Dialog to configure or add a community + """ + community_added = pyqtSignal() + + def __init__(self, app, account, community, password_asker): + """ + Constructor + + :param sakia.core.Application app: The application + :param sakia.core.Account account: The configured account + :param sakia.core.Community community: The configured community + :param sakia.gui.password_asker.Password_Asker password_asker: The password asker + """ + super().__init__() + self.setupUi(self) + self.app = app + self.community = community + self.account = account + self.password_asker = password_asker + self.step = None + self.nodes = [] + + self.community_added.connect(self.add_community_and_close) + self._step_init = StepPageInit(self) + step_add_peers = StepPageAddpeers(self) + + self._step_init.next_step = step_add_peers + + if self.community is not None: + self.stacked_pages.removeWidget(self.page_node) + self.step = step_add_peers + self.setWindowTitle(self.tr("Configure community {0}").format(self.community.currency)) + else: + self.step = self._step_init + self.setWindowTitle(self.tr("Add a community")) + + self.step.display_page() + + def next(self): + if self.step.next_step is not None: + if self.step.is_valid(): + self.step.process_next() + self.step = self.step.next_step + next_index = self.stacked_pages.currentIndex() + 1 + self.stacked_pages.setCurrentIndex(next_index) + self.step.display_page() + else: + self.add_community_and_close() + + def previous(self): + if self.step.previous_step is not None: + self.step = self.step.previous_step + previous_index = self.stacked_pages.currentIndex() - 1 + self.stacked_pages.setCurrentIndex(previous_index) + self.step.display_page() + + async def start_add_node(self): + """ + Add node slot + """ + server = self.lineedit_add_address.text() + port = self.spinbox_add_port.value() + + try: + node = await Node.from_address(self.community.currency, server, port, session=self.community.network.session) + self.community.add_node(node) + except Exception as e: + await QAsyncMessageBox.critical(self, self.tr("Error"), + str(e)) + self.tree_peers.setModel(PeeringTreeModel(self.community)) + + def add_node(self): + asyncio.ensure_future(self.start_add_node()) + + def remove_node(self): + """ + Remove node slot + """ + logging.debug("Remove node") + index = self.sender().data() + self.community.remove_node(index) + self.tree_peers.setModel(PeeringTreeModel(self.community)) + + @property + def nb_steps(self): + s = self.step + nb_steps = 1 + while s.next_step != None: + s = s.next_step + nb_steps = nb_steps + 1 + return nb_steps + + def showContextMenu(self, point): + if self.stacked_pages.currentIndex() == self.nb_steps - 1: + menu = QMenu() + index = self.tree_peers.indexAt(point) + action = menu.addAction(self.tr("Delete"), self.remove_node) + action.setData(index.row()) + if self.community is not None: + if len(self.nodes) == 1: + action.setEnabled(False) + menu.exec_(QCursor.pos()) + + def async_exec(self): + future = asyncio.Future() + self.finished.connect(lambda r: future.set_result(r)) + self.open() + return future + + def add_community_and_close(self): + if self.community not in self.account.communities: + self.account.add_community(self.community) + self.accept() diff --git a/src/sakia/gui/dialogs/community_cfg/view.py b/src/sakia/gui/dialogs/community_cfg/view.py new file mode 100644 index 00000000..cd4baaa2 --- /dev/null +++ b/src/sakia/gui/dialogs/community_cfg/view.py @@ -0,0 +1,65 @@ +from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import pyqtSignal +from .community_cfg_uic import Ui_CommunityConfigurationDialog +from sakia.gui.widgets import toast +from sakia.gui.widgets.dialogs import QAsyncMessageBox +import asyncio + + +class CommunityConfigView(QDialog, Ui_CommunityConfigurationDialog): + """ + community config view + """ + + def __init__(self, parent): + """ + Constructor + """ + super().__init__(parent) + self.setupUi(self) + self.set_steps_buttons_visible(False) + + def set_creation_layout(self): + self.setWindowTitle(self.tr("Add a community")) + + def set_edition_layout(self, name): + self.stacked_pages.removeWidget(self.page_node) + self.setWindowTitle(self.tr("Configure community {0}").format(name)) + + def display_info(self, info): + self.label_error.setText(info) + + def node_parameters(self): + server = self.lineedit_server.text() + port = self.spinbox_port.value() + return server, port + + def add_node_parameters(self): + server = self.lineedit_add_address.text() + port = self.spinbox_add_port.value() + return server, port + + async def show_success(self, notification): + if notification: + toast.display(self.tr("UID broadcast"), self.tr("Identity broadcasted to the network")) + else: + await QAsyncMessageBox.information(self, self.tr("UID broadcast"), + self.tr("Identity broadcasted to the network")) + + def show_error(self, notification, error_txt): + if notification: + toast.display(self.tr("UID broadcast"), error_txt) + self.label_error.setText(self.tr("Error") + " " + error_txt) + + def set_steps_buttons_visible(self, visible): + self.button_next.setVisible(visible) + self.button_previous.setVisible(visible) + + def set_nodes_model(self, model): + self.tree_peers.setModel(model) + + def async_exec(self): + future = asyncio.Future() + self.finished.connect(lambda r: future.set_result(r)) + self.open() + return future diff --git a/src/sakia/models/peering.py b/src/sakia/models/peering.py index 4b2e48c2..7c34da76 100644 --- a/src/sakia/models/peering.py +++ b/src/sakia/models/peering.py @@ -167,7 +167,10 @@ class PeeringTreeModel(QAbstractItemModel): return True def refresh_tree(self): + self.beginResetModel() logging.debug("root : " + self.root_item.data(0)) + self.root_item.node_items = [] for node in self.nodes: node_item = NodeItem(node, self.root_item) self.root_item.appendChild(node_item) + self.endResetModel() -- GitLab