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