Skip to content
Snippets Groups Projects
Commit 896313f4 authored by inso's avatar inso
Browse files

Implementating explorer tab

parent e9b5389f
No related branches found
No related tags found
No related merge requests found
......@@ -28,6 +28,7 @@
<file alias="settings_display_icon">noun_38960_cc.svg</file>
<file alias="settings_app_icon">noun_42425_cc.svg</file>
<file alias="settings_network_icon">noun_62146_cc.svg</file>
<file alias="explorer_icon">noun_101791_cc.svg</file>
<file alias="connected">connected.svg</file>
<file alias="weak_connect">weak_connect.svg</file>
<file alias="disconnected">disconnected.svg</file>
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 100 125"
enable-background="new 0 0 100 100"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="noun_101791_cc.svg"><metadata
id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs12" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="777"
inkscape:window-height="480"
id="namedview10"
showgrid="false"
inkscape:zoom="1.888"
inkscape:cx="50"
inkscape:cy="62.5"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><path
d="M 77.891855,62.286039 C 83.30181,41.246432 70.585007,19.72692 49.546735,14.316965 28.504455,8.9056726 6.9862794,21.622477 1.5749866,42.662085 -3.8349685,63.703028 8.8804994,85.219867 29.921443,90.632496 50.959714,96.042452 72.480563,83.326984 77.891855,62.286039 Z M 32.585649,80.269764 C 17.258111,76.328931 7.9968867,60.651155 11.937719,45.324953 15.879889,29.998751 31.554991,20.736191 46.879857,24.677023 62.207396,28.617856 71.469956,44.294295 67.527786,59.620497 63.585617,74.946699 47.913188,84.210597 32.585649,80.269764 Z m 64.940859,27.323826 c 0.0021,-0.001 0.0043,-0.002 0.0054,-0.002 -0.01741,0.0241 -0.04009,0.0494 -0.0588,0.0722 -0.01602,0.0227 -0.03074,0.0414 -0.04676,0.0642 -0.0022,0.001 -0.0043,0.001 -0.0064,0.004 -0.201856,0.24597 -0.458522,0.54942 -0.660378,0.75528 l -0.815433,0.83148 c -2.364767,2.41156 -5.464764,3.2056 -6.927202,1.77257 L 65.622872,88.163459 c 1.263259,-1.046699 2.932899,-2.573304 3.693528,-3.272441 l -0.0027,-0.0013 c 2.116125,-1.914271 2.855365,-3.03182 3.265757,-3.420823 0.818111,-0.772661 2.018541,-2.439628 2.418239,-2.870071 l 23.393699,22.925756 c 1.285982,1.26326 0.864889,3.82855 -0.8649,6.069 z M 30.801046,36.40862 c -5.951352,4.030396 -10.322628,11.609948 -8.509952,18.799161 0.661707,2.618755 -3.222981,3.823195 -3.883351,1.209787 -2.344715,-9.29598 2.997065,-18.605328 10.394814,-23.616923 2.211037,-1.497195 4.201505,2.116126 1.998489,3.607975 z"
id="path4"
inkscape:connector-curvature="0" /></svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ExplorerTabWidget</class>
<widget class="QWidget" name="ExplorerTabWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>442</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="ExplorerView" name="graphicsView">
<property name="viewportUpdateMode">
<enum>QGraphicsView::BoundingRectViewportUpdate</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>6</number>
</property>
<item>
<spacer name="horizontalSpacer">
<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="QLabel" name="label">
<property name="text">
<string>Steps</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="steps_slider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_go">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ExplorerView</class>
<extends>QGraphicsView</extends>
<header>sakia.gui.views</header>
</customwidget>
</customwidgets>
<resources>
<include location="../icons/icons.qrc"/>
</resources>
<connections/>
<slots>
<slot>reset()</slot>
<slot>search()</slot>
<slot>select_node()</slot>
</slots>
</ui>
......@@ -4,6 +4,7 @@ from enum import Enum
class ArcStatus(Enum):
WEAK = 0
STRONG = 1
ON_PATH = 2
class NodeStatus:
......
import logging
import networkx
import asyncio
from PyQt5.QtCore import pyqtSignal
from .base_graph import BaseGraph
from sakia.core.graph.constants import ArcStatus, NodeStatus
class ExplorerGraph(BaseGraph):
graph_changed = pyqtSignal()
def __init__(self, app, community, nx_graph=None):
"""
Init ExplorerGraph instance
:param sakia.core.app.Application app: Application instance
:param sakia.core.community.Community community: Community instance
:param networkx.Graph nx_graph: The networkx graph
:return:
"""
super().__init__(app, community, nx_graph)
self.exploration_task = None
self.explored_identity = None
self.steps = 0
def start_exploration(self, identity, steps):
"""
Start exploration of the wot from given identity
:param sakia.core.registry.Identity identity: The identity source of exploration
:param int steps: The number of steps from identity to explore
"""
if self.exploration_task:
if self.explored_identity is not identity or steps != self.steps:
self.exploration_task.cancel()
else:
return
self.explored_identity = identity
self.steps = steps
self.exploration_task = asyncio.ensure_future(self._explore(identity, steps))
def stop_exploration(self):
"""
Stop current exploration task, if present.
"""
if self.exploration_task:
self.exploration_task.cancel()
self.exploration_task = None
async def _explore(self, identity, steps):
"""
Scan graph recursively
:param sakia.core.registry.Identity identity: identity instance from where we start
:param int steps: The number of steps from given identity to explore
:return: False when the identity is added in the graph
"""
# functions keywords args are persistent... Need to reset it with None trick
logging.debug("search %s in " % identity.uid)
explored = []
explorable = {0: [identity]}
current_identity = identity
self.nx_graph.clear()
self.add_identity(current_identity, NodeStatus.HIGHLIGHTED)
self.graph_changed.emit()
for step in range(1, steps + 1):
explorable[step] = []
for step in range(0, steps):
while len(explorable[step]) > 0:
# for each pubkey connected...
if current_identity not in explored:
self.add_identity(current_identity, NodeStatus.NEUTRAL)
logging.debug("New identity explored : {pubkey}".format(pubkey=current_identity.pubkey[:5]))
self.graph_changed.emit()
certifier_list = await current_identity.unique_valid_certifiers_of(self.app.identities_registry,
self.community)
await self.add_certifier_list(certifier_list, current_identity, identity)
logging.debug("New identity certifiers : {pubkey}".format(pubkey=current_identity.pubkey[:5]))
self.graph_changed.emit()
certified_list = await current_identity.unique_valid_certified_by(self.app.identities_registry,
self.community)
await self.add_certified_list(certified_list, current_identity, identity)
logging.debug("New identity certified : {pubkey}".format(pubkey=current_identity.pubkey[:5]))
self.graph_changed.emit()
for cert in certified_list + certifier_list:
if cert['identity'] not in explorable[step + 1]:
explorable[step + 1].append(cert['identity'])
explored.append(current_identity)
logging.debug("New identity explored : {pubkey}".format(pubkey=current_identity.pubkey[:5]))
self.graph_changed.emit()
current_identity = explorable[step].pop()
\ No newline at end of file
......@@ -11,14 +11,15 @@ from PyQt5.QtCore import pyqtSlot, QDateTime, QLocale, QEvent, QT_TRANSLATE_NOOP
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QMessageBox, QDialog, QPushButton, QTabBar, QAction
from sakia.gui.graphs.wot_tab import WotTabWidget
from sakia.gui.widgets import toast
from sakia.gui.widgets.dialogs import QAsyncMessageBox
from .graphs.wot_tab import WotTabWidget
from .widgets import toast
from .widgets.dialogs import QAsyncMessageBox
from .certifications_tab import CertificationsTabWidget
from .identities_tab import IdentitiesTabWidget
from .informations_tab import InformationsTabWidget
from .network_tab import NetworkTabWidget
from .transactions_tab import TransactionsTabWidget
from .graphs.explorer_tab import ExplorerTabWidget
from ..gen_resources.community_view_uic import Ui_CommunityWidget
from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task
from ..tools.exceptions import MembershipNotFoundError, LookupFailureError, NoPeerAvailable
......@@ -36,6 +37,7 @@ class CommunityWidget(QWidget, Ui_CommunityWidget):
_tab_network_label = QT_TRANSLATE_NOOP("CommunityWidget", "Network")
_tab_informations_label = QT_TRANSLATE_NOOP("CommunityWidget", "Informations")
_action_showinfo_text = QT_TRANSLATE_NOOP("CommunityWidget", "Show informations")
_action_explore_text = QT_TRANSLATE_NOOP("CommunityWidget", "Explore the Web of Trust")
_action_publish_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Publish UID")
_action_revoke_uid_text = QT_TRANSLATE_NOOP("CommunityWidget", "Revoke UID")
......@@ -59,10 +61,12 @@ class CommunityWidget(QWidget, Ui_CommunityWidget):
self.tab_informations = InformationsTabWidget(self.app)
self.tab_certifications = CertificationsTabWidget(self.app)
self.tab_network = NetworkTabWidget(self.app)
self.tab_explorer = ExplorerTabWidget(self.app)
self.action_publish_uid = QAction(self.tr(CommunityWidget._action_publish_uid_text), self)
self.action_revoke_uid = QAction(self.tr(CommunityWidget._action_revoke_uid_text), self)
self.action_showinfo = QAction(self.tr(CommunityWidget._action_showinfo_text), self)
self.action_explorer = QAction(self.tr(CommunityWidget._action_explore_text), self)
super().setupUi(self)
......@@ -94,6 +98,11 @@ class CommunityWidget(QWidget, Ui_CommunityWidget):
QIcon(":/icons/informations_icon"), self.tr("Informations")))
self.toolbutton_menu.addAction(action_showinfo)
action_showexplorer = QAction(self.tr("Show explorer"), self.toolbutton_menu)
action_showexplorer.triggered.connect(lambda : self.show_closable_tab(self.tab_explorer,
QIcon(":/icons/explorer_icon"), self.tr("Explorer")))
self.toolbutton_menu.addAction(action_showexplorer)
self.action_publish_uid.triggered.connect(self.publish_uid)
self.toolbutton_menu.addAction(self.action_publish_uid)
......@@ -127,6 +136,7 @@ class CommunityWidget(QWidget, Ui_CommunityWidget):
self.tab_identities.change_account(account, self.password_asker)
self.tab_history.change_account(account, self.password_asker)
self.tab_informations.change_account(account)
self.tab_explorer.change_account(account, self.password_asker)
def change_community(self, community):
self.cancel_once_tasks()
......@@ -136,6 +146,7 @@ class CommunityWidget(QWidget, Ui_CommunityWidget):
self.tab_history.change_community(community)
self.tab_identities.change_community(community)
self.tab_informations.change_community(community)
self.tab_explorer.change_community(community)
if self.community:
self.community.network.new_block_mined.disconnect(self.refresh_block)
......
import logging
from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP
from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task
from ...core.graph import ExplorerGraph
from .graph_tab import GraphTabWidget
from ...gen_resources.explorer_tab_uic import Ui_ExplorerTabWidget
class ExplorerTabWidget(GraphTabWidget, Ui_ExplorerTabWidget):
money_sent = pyqtSignal()
_search_placeholder = QT_TRANSLATE_NOOP("ExplorerTabWidget", "Research a pubkey, an uid...")
def __init__(self, app):
"""
:param sakia.core.app.Application app: Application instance
"""
# construct from qtDesigner
super().__init__(app)
self.setupUi(self)
self.set_scene(self.graphicsView.scene())
self.account = None
self.community = None
self.password_asker = None
self.graph = None
self.app = app
self.draw_task = None
# nodes list for menu from search
self.nodes = list()
# create node metadata from account
self._current_identity = None
self.button_go.clicked.connect(self.go_clicked)
def cancel_once_tasks(self):
cancel_once_task(self, self.refresh_informations_frame)
cancel_once_task(self, self.reset)
def change_account(self, account, password_asker):
self.account = account
self.password_asker = password_asker
def change_community(self, community):
self.community = community
if self.graph:
self.graph.stop_exploration()
self.graph = ExplorerGraph(self.app, self.community)
self.graph.graph_changed.connect(self.refresh)
self.reset()
def go_clicked(self):
if self.graph:
self.graph.stop_exploration()
self.draw_graph(self._current_identity)
def draw_graph(self, identity):
"""
Draw community graph centered on the identity
:param sakia.core.registry.Identity identity: Graph node identity
"""
logging.debug("Draw graph - " + identity.uid)
if self.community:
#Connect new identity
if self._current_identity != identity:
self._current_identity = identity
self.graph.start_exploration(identity, self.steps_slider.value())
# draw graph in qt scene
self.graphicsView.scene().update_wot(self.graph.nx_graph, identity, self.steps_slider.maximum())
def refresh(self):
"""
Refresh graph scene to current metadata
"""
if self._current_identity:
# draw graph in qt scene
self.graphicsView.scene().update_wot(self.graph.nx_graph, self._current_identity, self.steps_slider.maximum())
else:
self.reset()
@once_at_a_time
@asyncify
async def reset(self, checked=False):
"""
Reset graph scene to wallet identity
"""
if self.account and self.community:
parameters = await self.community.parameters()
self.steps_slider.setMaximum(parameters['stepMax'])
self.steps_slider.setValue(int(0.33 * parameters['stepMax']))
identity = await self.account.identity(self.community)
self.draw_graph(identity)
def changeEvent(self, event):
"""
Intercepte LanguageChange event to translate UI
:param QEvent QEvent: Event
:return:
"""
if event.type() == QEvent.LanguageChange:
self.retranslateUi(self)
self.refresh()
return super().changeEvent(event)
from PyQt5.QtWidgets import QWidget, QDialog
from PyQt5.QtCore import pyqtSlot, QEvent, QLocale, QDateTime, pyqtSignal
from ...tools.exceptions import MembershipNotFoundError
from ...tools.decorators import asyncify, once_at_a_time
from ...core.registry import BlockchainState
from ...gui.member import MemberDialog
from ...gui.certification import CertificationDialog
from ...gui.transfer import TransferMoneyDialog
from ...gui.contact import ConfigureContactDialog
class GraphTabWidget(QWidget):
money_sent = pyqtSignal()
def __init__(self, app):
"""
:param sakia.core.app.Application app: Application instance
"""
super().__init__()
self.password_asker = None
self.app = app
def set_scene(self, scene):
# add scene events
scene.node_clicked.connect(self.handle_node_click)
scene.node_signed.connect(self.sign_node)
scene.node_transaction.connect(self.send_money_to_node)
scene.node_contact.connect(self.add_node_as_contact)
scene.node_member.connect(self.identity_informations)
scene.node_copy_pubkey.connect(self.copy_node_pubkey)
@once_at_a_time
@asyncify
async def refresh_informations_frame(self):
parameters = self.community.parameters
try:
identity = await self.account.identity(self.community)
membership = identity.membership(self.community)
renew_block = membership['blockNumber']
last_renewal = self.community.get_block(renew_block)['medianTime']
expiration = last_renewal + parameters['sigValidity']
except MembershipNotFoundError:
last_renewal = None
expiration = None
certified = await identity.unique_valid_certified_by(self.app.identities_registry, self.community)
certifiers = await identity.unique_valid_certifiers_of(self.app.identities_registry, self.community)
if last_renewal and expiration:
date_renewal = QLocale.toString(
QLocale(),
QDateTime.fromTime_t(last_renewal).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat)
)
date_expiration = QLocale.toString(
QLocale(),
QDateTime.fromTime_t(expiration).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat)
)
if self.account.pubkey in self.community.members_pubkeys():
# set infos in label
self.label_general.setText(
self.tr("""
<table cellpadding="5">
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
</table>
""").format(
self.account.name, self.account.pubkey,
self.tr("Membership"),
self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration),
self.tr("Your web of trust"),
self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers),
len(certified))
)
)
else:
# set infos in label
self.label_general.setText(
self.tr("""
<table cellpadding="5">
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
</table>
""").format(
self.account.name, self.account.pubkey,
self.tr("Not a member"),
self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration),
self.tr("Your web of trust"),
self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers),
len(certified))
)
)
else:
# set infos in label
self.label_general.setText(
self.tr("""
<table cellpadding="5">
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
</table>
""").format(
self.account.name, self.account.pubkey,
self.tr("Not a member"),
self.tr("Your web of trust"),
self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers),
len(certified))
)
)
@pyqtSlot(str, dict)
def handle_node_click(self, pubkey, metadata):
self.draw_graph(
self.app.identities_registry.from_handled_data(
metadata['text'],
pubkey,
None,
BlockchainState.VALIDATED,
self.community
)
)
@once_at_a_time
@asyncify
async def draw_graph(self, identity):
"""
Draw community graph centered on the identity
:param sakia.core.registry.Identity identity: Graph node identity
"""
pass
@once_at_a_time
@asyncify
async def reset(self, checked=False):
"""
Reset graph scene to wallet identity
"""
pass
def refresh(self):
"""
Refresh graph scene to current metadata
"""
pass
def select_node(self, index):
"""
Select node in graph when item is selected in combobox
"""
if index < 0 or index >= len(self.nodes):
return False
node = self.nodes[index]
metadata = {'id': node['pubkey'], 'text': node['uid']}
self.draw_graph(
self.app.identities_registry.from_handled_data(
metadata['text'],
metadata['id'],
None,
BlockchainState.VALIDATED,
self.community
)
)
def identity_informations(self, pubkey, metadata):
identity = self.app.identities_registry.from_handled_data(
metadata['text'],
pubkey,
None,
BlockchainState.VALIDATED,
self.community
)
dialog = MemberDialog(self.app, self.account, self.community, identity)
dialog.exec_()
@asyncify
async def sign_node(self, pubkey, metadata):
identity = self.app.identities_registry.from_handled_data(
metadata['text'],
pubkey,
None,
BlockchainState.VALIDATED,
self.community
)
await CertificationDialog.certify_identity(self.app, self.account, self.password_asker,
self.community, identity)
@asyncify
async def send_money_to_node(self, pubkey, metadata):
identity = self.app.identities_registry.from_handled_data(
metadata['text'],
pubkey,
None,
BlockchainState.VALIDATED,
self.community
)
result = await TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker,
self.community, identity)
if result == QDialog.Accepted:
self.money_sent.emit()
def copy_node_pubkey(self, pubkey):
cb = self.app.qapp.clipboard()
cb.clear(mode=cb.Clipboard)
cb.setText(pubkey, mode=cb.Clipboard)
def add_node_as_contact(self, pubkey, metadata):
# check if contact already exists...
if pubkey == self.account.pubkey \
or pubkey in [contact['pubkey'] for contact in self.account.contacts]:
return False
dialog = ConfigureContactDialog(self.account, self.window(), {'name': metadata['text'],
'pubkey': pubkey,
})
result = dialog.exec_()
if result == QDialog.Accepted:
self.window().refresh_contacts()
def changeEvent(self, event):
"""
Intercepte LanguageChange event to translate UI
:param QEvent QEvent: Event
:return:
"""
if event.type() == QEvent.LanguageChange:
self.retranslateUi(self)
self._auto_refresh(None)
self.refresh()
return super().changeEvent(event)
import logging
import asyncio
from PyQt5.QtWidgets import QWidget, QComboBox, QDialog
from PyQt5.QtCore import pyqtSlot, QEvent, QLocale, QDateTime, pyqtSignal, QT_TRANSLATE_NOOP
from PyQt5.QtWidgets import QComboBox
from PyQt5.QtCore import QEvent, pyqtSignal, QT_TRANSLATE_NOOP
from ucoinpy.api import bma
from sakia.tools.exceptions import MembershipNotFoundError
from sakia.tools.decorators import asyncify, once_at_a_time, cancel_once_task
from sakia.core.graph import WoTGraph
from sakia.core.registry import BlockchainState
from sakia.gui.member import MemberDialog
from sakia.gui.certification import CertificationDialog
from sakia.gui.transfer import TransferMoneyDialog
from sakia.gui.contact import ConfigureContactDialog
from sakia.gen_resources.wot_tab_uic import Ui_WotTabWidget
from sakia.gui.widgets.busy import Busy
from sakia.tools.exceptions import NoPeerAvailable
from ...tools.decorators import asyncify, once_at_a_time, cancel_once_task
from ...core.graph import WoTGraph
from ...gen_resources.wot_tab_uic import Ui_WotTabWidget
from ...gui.widgets.busy import Busy
from ...tools.exceptions import NoPeerAvailable
from .graph_tab import GraphTabWidget
class WotTabWidget(QWidget, Ui_WotTabWidget):
class WotTabWidget(GraphTabWidget, Ui_WotTabWidget):
money_sent = pyqtSignal()
_search_placeholder = QT_TRANSLATE_NOOP("WotTabWidget", "Research a pubkey, an uid...")
......@@ -27,7 +22,7 @@ class WotTabWidget(QWidget, Ui_WotTabWidget):
"""
:param sakia.core.app.Application app: Application instance
"""
super().__init__()
super().__init__(app)
# construct from qtDesigner
self.setupUi(self)
......@@ -42,13 +37,7 @@ class WotTabWidget(QWidget, Ui_WotTabWidget):
self.busy = Busy(self.graphicsView)
self.busy.hide()
# add scene events
self.graphicsView.scene().node_clicked.connect(self.handle_node_click)
self.graphicsView.scene().node_signed.connect(self.sign_node)
self.graphicsView.scene().node_transaction.connect(self.send_money_to_node)
self.graphicsView.scene().node_contact.connect(self.add_node_as_contact)
self.graphicsView.scene().node_member.connect(self.identity_informations)
self.graphicsView.scene().node_copy_pubkey.connect(self.copy_node_pubkey)
self.set_scene(self.graphicsView.scene())
self.account = None
self.community = None
......@@ -91,105 +80,13 @@ class WotTabWidget(QWidget, Ui_WotTabWidget):
elif self.community:
self.community.network.new_block_mined.connect(self.refresh)
@once_at_a_time
@asyncify
async def refresh_informations_frame(self):
parameters = self.community.parameters
try:
identity = await self.account.identity(self.community)
membership = identity.membership(self.community)
renew_block = membership['blockNumber']
last_renewal = self.community.get_block(renew_block)['medianTime']
expiration = last_renewal + parameters['sigValidity']
except MembershipNotFoundError:
last_renewal = None
expiration = None
certified = await identity.unique_valid_certified_by(self.app.identities_registry, self.community)
certifiers = await identity.unique_valid_certifiers_of(self.app.identities_registry, self.community)
if last_renewal and expiration:
date_renewal = QLocale.toString(
QLocale(),
QDateTime.fromTime_t(last_renewal).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat)
)
date_expiration = QLocale.toString(
QLocale(),
QDateTime.fromTime_t(expiration).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat)
)
if self.account.pubkey in self.community.members_pubkeys():
# set infos in label
self.label_general.setText(
self.tr("""
<table cellpadding="5">
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
</table>
""").format(
self.account.name, self.account.pubkey,
self.tr("Membership"),
self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration),
self.tr("Your web of trust"),
self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers),
len(certified))
)
)
else:
# set infos in label
self.label_general.setText(
self.tr("""
<table cellpadding="5">
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
</table>
""").format(
self.account.name, self.account.pubkey,
self.tr("Not a member"),
self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration),
self.tr("Your web of trust"),
self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers),
len(certified))
)
)
else:
# set infos in label
self.label_general.setText(
self.tr("""
<table cellpadding="5">
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
<tr><td align="right"><b>{:}</b></td></tr>
<tr><td align="right"><b>{:}</b></td><td>{:}</td></tr>
</table>
""").format(
self.account.name, self.account.pubkey,
self.tr("Not a member"),
self.tr("Your web of trust"),
self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers),
len(certified))
)
)
@pyqtSlot(dict)
def handle_node_click(self, metadata):
self.draw_graph(
self.app.identities_registry.from_handled_data(
metadata['text'],
metadata['id'],
None,
BlockchainState.VALIDATED,
self.community
)
)
@once_at_a_time
@asyncify
async def draw_graph(self, identity):
"""
Draw community graph centered on the identity
:param sakia.core.registry.Identity identity: Graph node identity
:param sakia.core.registry.Identity identity: Center identity
"""
logging.debug("Draw graph - " + identity.uid)
self.busy.show()
......@@ -199,7 +96,7 @@ class WotTabWidget(QWidget, Ui_WotTabWidget):
# create empty graph instance
graph = WoTGraph(self.app, self.community)
await graph.initialize(identity_account, identity_account)
await graph.initialize(identity, identity_account)
# draw graph in qt scene
self.graphicsView.scene().update_wot(graph.nx_graph, identity)
......@@ -208,7 +105,7 @@ class WotTabWidget(QWidget, Ui_WotTabWidget):
# add path from selected member to account member
path = await graph.get_shortest_path_to_identity(identity_account, identity)
if path:
self.graphicsView.scene().update_path(path)
self.graphicsView.scene().update_path(graph.nx_graph, path)
self.busy.hide()
@once_at_a_time
......@@ -257,78 +154,6 @@ class WotTabWidget(QWidget, Ui_WotTabWidget):
except NoPeerAvailable:
pass
def select_node(self, index):
"""
Select node in graph when item is selected in combobox
"""
if index < 0 or index >= len(self.nodes):
return False
node = self.nodes[index]
metadata = {'id': node['pubkey'], 'text': node['uid']}
self.draw_graph(
self.app.identities_registry.from_handled_data(
metadata['text'],
metadata['id'],
None,
BlockchainState.VALIDATED,
self.community
)
)
def identity_informations(self, metadata):
identity = self.app.identities_registry.from_handled_data(
metadata['text'],
metadata['id'],
None,
BlockchainState.VALIDATED,
self.community
)
dialog = MemberDialog(self.app, self.account, self.community, identity)
dialog.exec_()
@asyncify
async def sign_node(self, metadata):
identity = self.app.identities_registry.from_handled_data(
metadata['text'],
metadata['id'],
None,
BlockchainState.VALIDATED,
self.community
)
await CertificationDialog.certify_identity(self.app, self.account, self.password_asker,
self.community, identity)
@asyncify
async def send_money_to_node(self, metadata):
identity = self.app.identities_registry.from_handled_data(
metadata['text'],
metadata['id'],
None,
BlockchainState.VALIDATED,
self.community
)
result = await TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker,
self.community, identity)
if result == QDialog.Accepted:
self.money_sent.emit()
def copy_node_pubkey(self, metadata):
cb = self.app.qapp.clipboard()
cb.clear(mode=cb.Clipboard)
cb.setText(metadata['id'], mode=cb.Clipboard)
def add_node_as_contact(self, metadata):
# check if contact already exists...
if metadata['id'] == self.account.pubkey \
or metadata['id'] in [contact['pubkey'] for contact in self.account.contacts]:
return False
dialog = ConfigureContactDialog(self.account, self.window(), {'name': metadata['text'],
'pubkey': metadata['id'],
})
result = dialog.exec_()
if result == QDialog.Accepted:
self.window().refresh_contacts()
def retranslateUi(self, widget):
"""
Retranslate missing widgets from generated code
......
from .explorer import ExplorerView
from .wot import WotView
\ No newline at end of file
......@@ -29,6 +29,8 @@ class Arc(QGraphicsLineItem):
self.setAcceptedMouseButtons(Qt.NoButton)
self.paint_pen = self.pen_from_status()
# cursor change on hover
self.setAcceptHoverEvents(True)
self.setZValue(0)
......@@ -57,6 +59,21 @@ class Arc(QGraphicsLineItem):
extra
)
def pen_from_status(self):
"""
Get a pen from current status
:return:
"""
# Draw the line itself
color = QColor()
style = Qt.SolidLine
if self.status == ArcStatus.STRONG:
color.setNamedColor('blue')
if self.status == ArcStatus.WEAK:
color.setNamedColor('salmon')
style = Qt.DashLine
return QPen(color, 1, style, Qt.RoundCap, Qt.RoundJoin)
def paint(self, painter, option, widget):
"""
Customize line adding an arrow head
......@@ -71,18 +88,9 @@ class Arc(QGraphicsLineItem):
if qFuzzyCompare(line.length(), 0):
return
# Draw the line itself
color = QColor()
style = Qt.SolidLine
if self.status == ArcStatus.STRONG:
color.setNamedColor('blue')
if self.status == ArcStatus.WEAK:
color.setNamedColor('salmon')
style = Qt.DashLine
painter.setPen(QPen(color, 1, style, Qt.RoundCap, Qt.RoundJoin))
painter.setPen(self.paint_pen)
painter.drawLine(line)
painter.setPen(QPen(color, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.setPen(QPen(self.paint_pen.color(), 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
# Draw the arrows
angle = math.acos(line.dx() / line.length())
......@@ -94,7 +102,7 @@ class Arc(QGraphicsLineItem):
hpy = line.p1().y() + (line.dy() / 2.0)
head_point = QPointF(hpx, hpy)
painter.setPen(QPen(color, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
painter.setPen(QPen(self.paint_pen.color(), 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
destination_arrow_p1 = head_point + QPointF(
math.sin(angle - math.pi / 3) * self.arrow_size,
math.cos(angle - math.pi / 3) * self.arrow_size)
......@@ -102,7 +110,7 @@ class Arc(QGraphicsLineItem):
math.sin(angle - math.pi + math.pi / 3) * self.arrow_size,
math.cos(angle - math.pi + math.pi / 3) * self.arrow_size)
painter.setBrush(color)
painter.setBrush(self.paint_pen.color())
painter.drawPolygon(QPolygonF([head_point, destination_arrow_p1, destination_arrow_p2]))
if self.metadata["confirmation_text"]:
......
import networkx
from PyQt5.QtGui import QPainter, QWheelEvent
from PyQt5.QtCore import Qt, QPoint, pyqtSignal
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene
from PyQt5.QtGui import QTransform, QColor, QPen, QBrush, QMouseEvent, QRadialGradient
from .node import Node
from .arc import Arc
import logging
class ExplorerView(QGraphicsView):
def __init__(self, parent=None):
"""
Create View to display scene
:param parent: [Optional, default=None] Parent widget
"""
super().__init__(parent)
self.setScene(Scene(self))
self.setCacheMode(QGraphicsView.CacheBackground)
self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
self.setRenderHint(QPainter.Antialiasing)
self.setRenderHint(QPainter.SmoothPixmapTransform)
def wheelEvent(self, event: QWheelEvent):
"""
Zoom in/out on the mouse cursor
"""
# zoom only when CTRL key pressed
if (event.modifiers() & Qt.ControlModifier) == Qt.ControlModifier:
steps = event.angleDelta().y() / 15 / 8
if steps == 0:
event.ignore()
return
# scale factor 1.25
sc = pow(1.25, steps)
self.scale(sc, sc)
self.centerOn(self.mapToScene(event.pos()))
event.accept()
# act normally on scrollbar
else:
# transmit event to parent class wheelevent
super(QGraphicsView, self).wheelEvent(event)
class Scene(QGraphicsScene):
# This defines signals taking string arguments
node_hovered = pyqtSignal(str, name='nodeHovered')
node_clicked = pyqtSignal(str, dict, name='nodeClicked')
node_signed = pyqtSignal(str, dict, name='nodeSigned')
node_transaction = pyqtSignal(str, dict, name='nodeTransaction')
node_contact = pyqtSignal(str, dict, name='nodeContact')
node_member = pyqtSignal(str, dict, name='nodeMember')
node_copy_pubkey = pyqtSignal(str, name='nodeCopyPubkey')
def __init__(self, parent=None):
"""
Create scene of the graph
:param parent: [Optional, default=None] Parent view
"""
super(Scene, self).__init__(parent)
self.lastDragPos = QPoint()
self.setItemIndexMethod(QGraphicsScene.NoIndex)
self.node_hovered.connect(self.display_path_to)
# list of nodes in scene
self.nodes = dict()
self.arcs = dict()
self.nx_graph = None
self.identity = None
# axis of the scene for debug purpose
# self.addLine(-100, 0, 100, 0)
# self.addLine(0, -100, 0, 100)
def update_wot(self, nx_graph, identity, dist_max):
"""
draw community graph
:param networkx.Graph nx_graph: graph to draw
:param sakia.core.registry.Identity identity: the wot of the identity
:param dist_max: the dist_max to display
"""
# clear scene
self.clear()
self.nodes.clear()
self.arcs.clear()
self.identity = identity
self.nx_graph = nx_graph
# Programs available : neato, twopi, circo, fdp,
# Nice programs : twopi
try:
graph_pos = networkx.pygraphviz_layout(nx_graph, prog="twopi")
except OSError:
logging.debug("Twopi not found, fallback mode...")
graph_pos = networkx.spring_layout(nx_graph, scale=len(nx_graph.nodes())*12)
# create networkx graph
for node in nx_graph.nodes(data=True):
v = Node(node, graph_pos, scale=2)
distance = networkx.shortest_path_length(nx_graph.to_undirected(), identity.pubkey, node[0])
factor = int((dist_max + 1) / (distance + 1) * 100)
color = QColor('light grey').darker(factor)
v.setBrush(QBrush(color))
v.text_item.setBrush(QBrush(QColor('dark grey').lighter(factor)))
self.addItem(v)
self.nodes[node[0]] = v
for edge in nx_graph.edges(data=True):
edge[2]["confirmation_text"] = ""
arc = Arc(edge[0], edge[1], edge[2], graph_pos, scale=2)
self.addItem(arc)
self.arcs[(edge[0], edge[1])] = arc
self.update()
def display_path_to(self, id):
if id != self.identity.pubkey:
path = networkx.shortest_path(self.nx_graph.to_undirected(), self.identity.pubkey, id)
for arc in self.arcs.values():
arc.paint_pen = arc.pen_from_status()
self.update(arc.boundingRect())
for node, next_node in zip(path[:-1], path[1:]):
if (node, next_node) in self.arcs:
arc = self.arcs[(node, next_node)]
elif (next_node, node) in self.arcs:
arc = self.arcs[(next_node, node)]
if arc:
arc.paint_pen = QPen(QColor('black'), 3, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
logging.debug("Update arc between {0} and {1}".format(node, next_node))
self.update(arc.boundingRect())
......@@ -99,6 +99,7 @@ class Node(QGraphicsEllipseItem):
:param event: scene hover event
"""
self.setCursor(Qt.ArrowCursor)
self.scene().node_hovered.emit(self.id)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent):
"""
......
import sys
import unittest
import asyncio
from asynctest.mock import Mock, CoroutineMock, patch
from PyQt5.QtCore import QLocale
from sakia.tests import QuamashTest
from sakia.core.graph import ExplorerGraph
class TestExplorerGraph(unittest.TestCase, QuamashTest):
def setUp(self):
self.setUpQuamash()
QLocale.setDefault(QLocale("en_GB"))
## Graph to test :
## - E
## A - B - C - D
##
## Path : Between A and C
self.idA = Mock(specs='core.registry.Identity')
self.idA.pubkey = "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk"
self.idA.uid = "A"
self.idA.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True)
self.idB = Mock(specs='core.registry.Identity')
self.idB.pubkey = "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ"
self.idB.uid = "B"
self.idB.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True)
self.idC = Mock(specs='core.registry.Identity')
self.idC.pubkey = "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn"
self.idC.uid = "C"
self.idC.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=False)
self.idD = Mock(specs='core.registry.Identity')
self.idD.pubkey = "6R11KGpG6w5Z6JfiwaPf3k4BCMY4dwhjCdmjGpvn7Gz5"
self.idD.uid = "D"
self.idD.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=True)
self.idE = Mock(specs='core.registry.Identity')
self.idE.pubkey = "CZVDEsM6pPNxhAvXApGM8MJ6ExBZVpc8PNVyDZ7hKxLu"
self.idE.uid = "E"
self.idE.is_member = CoroutineMock(spec='core.registry.Identity.is_member', return_value=False)
self.idA.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by',
return_value=[
{
'cert_time': 49800,
'identity': self.idB,
'block_number': 996
}
])
self.idA.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of',
return_value=[])
self.idB.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by',
return_value=[
{
'cert_time': 49100,
'identity': self.idC,
'block_number': 990
}
])
self.idB.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of',
return_value=[
{
'cert_time': 49800,
'identity': self.idA,
'block_number': 996
}
])
self.idC.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certifierd_by',
return_value=[
{
'cert_time': 49100,
'identity': self.idD,
'block_number': 990
},
{
'cert_time': 49110,
'identity': self.idE,
'block_number': 990
}
])
self.idC.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of',
return_value=[
{
'cert_time': 49100,
'identity': self.idB,
'block_number': 990
}
])
self.idD.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by',
return_value=[
])
self.idD.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of',
return_value=[
{
'cert_time': 49100,
'identity': self.idC,
'block_number': 990
}])
self.idE.unique_valid_certified_by = CoroutineMock(spec='core.registry.Identity.certified_by',
return_value=[
])
self.idE.unique_valid_certifiers_of = CoroutineMock(spec='core.registry.Identity.certifiers_of',
return_value=[
{
'cert_time': 49100,
'identity': self.idC,
'block_number': 990
}])
def tearDown(self):
self.tearDownQuamash()
@patch('sakia.core.Application')
@patch('sakia.core.Community')
@patch('time.time', Mock(return_value=50000))
def test_explore_full(self, app, community):
community.parameters = CoroutineMock(return_value = {'sigValidity': 1000})
community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None)
app.preferences = {'expert_mode': True}
explorer_graph = ExplorerGraph(app, community)
async def exec_test():
await explorer_graph._explore(self.idB, 5)
self.assertEqual(len(explorer_graph.nx_graph.nodes()), 5)
self.assertEqual(len(explorer_graph.nx_graph.edges()), 4)
self.lp.run_until_complete(exec_test())
@patch('sakia.core.Application')
@patch('sakia.core.Community')
@patch('time.time', Mock(return_value=50000))
def test_explore_partial(self, app, community):
community.parameters = CoroutineMock(return_value = {'sigValidity': 1000})
community.network.confirmations = Mock(side_effect=lambda n: 4 if 996 else None)
app.preferences = {'expert_mode': True}
explorer_graph = ExplorerGraph(app, community)
async def exec_test():
await explorer_graph._explore(self.idB, 1)
self.assertEqual(len(explorer_graph.nx_graph.nodes()), 3)
self.assertEqual(len(explorer_graph.nx_graph.edges()), 2)
self.lp.run_until_complete(exec_test())
@patch('sakia.core.Application')
@patch('sakia.core.Community')
@patch('time.time', Mock(return_value=50000))
def test_start_stop_exploration(self, app, community):
async def explore_mock(id, steps):
await asyncio.sleep(0.1)
await asyncio.sleep(0.1)
await asyncio.sleep(0.1)
explorer_graph = ExplorerGraph(app, community)
explorer_graph._explore = explore_mock
async def exec_test():
self.assertEqual(explorer_graph.exploration_task, None)
explorer_graph.start_exploration(self.idA, 1)
self.assertNotEqual(explorer_graph.exploration_task, None)
task = explorer_graph.exploration_task
explorer_graph.start_exploration(self.idA, 1)
self.assertEqual(task, explorer_graph.exploration_task)
explorer_graph.start_exploration(self.idB, 1)
await asyncio.sleep(0)
self.assertTrue(task.cancelled())
self.assertNotEqual(task, explorer_graph.exploration_task)
task2 = explorer_graph.exploration_task
explorer_graph.start_exploration(self.idB, 2)
await asyncio.sleep(0)
self.assertTrue(task2.cancelled())
task3 = explorer_graph.exploration_task
explorer_graph.stop_exploration()
await asyncio.sleep(0)
self.assertTrue(task2.cancelled())
self.lp.run_until_complete(exec_test())
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment