diff --git a/res/icons/icons.qrc b/res/icons/icons.qrc index a69d4de1e76266589697e0d723451ce6091c867d..48290d17ebf2ea171c645444a8127d97a8df77e1 100644 --- a/res/icons/icons.qrc +++ b/res/icons/icons.qrc @@ -1,5 +1,6 @@ <RCC> <qresource prefix="icons"> + <file alias="member_icon">iconmonstr-user-icon.svg</file> <file alias="informations_icon">iconmonstr-info-2-icon.svg</file> <file alias="community_icon">noun_22441_cc.svg</file> <file alias="wot_icon">noun_2651_cc.svg</file> diff --git a/src/cutecoin/core/graph.py b/src/cutecoin/core/graph.py new file mode 100644 index 0000000000000000000000000000000000000000..ef2e9db39b72a6355f559220c63daa808c2f7bad --- /dev/null +++ b/src/cutecoin/core/graph.py @@ -0,0 +1,185 @@ +import logging +import copy +import time +import datetime +from cutecoin.core.person import Person +from cutecoin.gui.views.wot import NODE_STATUS_HIGHLIGHTED, NODE_STATUS_OUT, ARC_STATUS_STRONG, ARC_STATUS_WEAK + + +class Graph(dict): + + def __init__(self, community): + self.community = community + self.signature_validity = self.community.get_parameters()['sigValidity'] + # arc considered strong during 75% of signature validity time + self.ARC_STATUS_STRONG_time = int(self.signature_validity * 0.75) + + def get_shortest_path_between_members(self, from_person, to_person): + path = list() + graph_tmp = copy.deepcopy(self) + + if to_person.pubkey not in graph_tmp.keys(): + # recursively feed graph searching for account node... + graph_tmp.explore_to_find_member(to_person, graph_tmp[from_person.pubkey]['nodes'], list()) + if len(graph_tmp[from_person.pubkey]['nodes']) > 0: + # calculate path of nodes between person and to_person + path = graph_tmp.find_shortest_path(graph_tmp[from_person.pubkey], graph_tmp[to_person.pubkey]) + + if path: + logging.debug([node['text'] for node in path]) + else: + logging.debug('no wot path') + + return path + + def explore_to_find_member(self, person, nodes=list(), done=list()): + for node in tuple(nodes): + if node['id'] in tuple(done): + continue + person_selected = Person(node['text'], node['id']) + certifier_list = person_selected.certifiers_of(self.community) + self.add_certifier_list(certifier_list, person_selected, person) + if person.pubkey in tuple(self.keys()): + return False + certified_list = person_selected.certified_by(self.community) + self.add_certified_list(certified_list, person_selected, person) + if person.pubkey in tuple(self.keys()): + return False + if node['id'] not in tuple(done): + done.append(node['id']) + if len(done) >= len(self): + return True + result = self.explore_to_find_member(person, self[person_selected.pubkey]['nodes'], done) + if not result: + return False + + return True + + def find_shortest_path(self, start, end, path=list()): + path = path + [start] + if start['id'] == end['id']: + return path + if start['id'] not in self.keys(): + return None + shortest = None + for node in tuple(self[start['id']]['nodes']): + if node not in path: + newpath = self.find_shortest_path(node, end, path) + if newpath: + if not shortest or len(newpath) < len(shortest): + shortest = newpath + return shortest + + def add_certifier_list(self, certifiers, person, person_account): + # add certifiers of uid + for certifier in tuple(certifiers): + # add only valid certification... + if (time.time() - certifier['cert_time']['medianTime']) > self.signature_validity: + continue + # new node + if certifier['pubkey'] not in self.keys(): + node_status = 0 + if certifier['pubkey'] == person_account.pubkey: + node_status += NODE_STATUS_HIGHLIGHTED + if certifier['isMember'] is False: + node_status += NODE_STATUS_OUT + self[certifier['pubkey']] = { + 'id': certifier['pubkey'], + 'arcs': list(), + 'text': certifier['uid'], + 'tooltip': certifier['pubkey'], + 'status': node_status, + 'nodes': [self[person.pubkey]] + } + + # keep only the latest certification + if self[certifier['pubkey']]['arcs']: + if certifier['cert_time']['medianTime'] < self[certifier['pubkey']]['arcs'][0]['cert_time']: + continue + # display validity status + if (time.time() - certifier['cert_time']['medianTime']) > self.ARC_STATUS_STRONG_time: + arc_status = ARC_STATUS_WEAK + else: + arc_status = ARC_STATUS_STRONG + arc = { + 'id': person.pubkey, + 'status': arc_status, + 'tooltip': datetime.datetime.fromtimestamp( + certifier['cert_time']['medianTime'] + self.signature_validity + ).strftime("%d/%m/%Y"), + 'cert_time': certifier['cert_time']['medianTime'] + } + # add arc to certifier + self[certifier['pubkey']]['arcs'].append(arc) + # if certifier node not in person nodes + if self[certifier['pubkey']] not in tuple(self[person.pubkey]['nodes']): + # add certifier node to person node + self[person.pubkey]['nodes'].append(self[certifier['pubkey']]) + + def add_certified_list(self, certified_list, person, person_account): + # add certified by uid + for certified in tuple(certified_list): + # add only valid certification... + if (time.time() - certified['cert_time']['medianTime']) > self.signature_validity: + continue + if certified['pubkey'] not in self.keys(): + node_status = 0 + if certified['pubkey'] == person_account.pubkey: + node_status += NODE_STATUS_HIGHLIGHTED + if certified['isMember'] is False: + node_status += NODE_STATUS_OUT + self[certified['pubkey']] = { + 'id': certified['pubkey'], + 'arcs': list(), + 'text': certified['uid'], + 'tooltip': certified['pubkey'], + 'status': node_status, + 'nodes': [self[person.pubkey]] + } + # display validity status + if (time.time() - certified['cert_time']['medianTime']) > self.ARC_STATUS_STRONG_time: + arc_status = ARC_STATUS_WEAK + else: + arc_status = ARC_STATUS_STRONG + arc = { + 'id': certified['pubkey'], + 'status': arc_status, + 'tooltip': datetime.datetime.fromtimestamp( + certified['cert_time']['medianTime'] + self.signature_validity + ).strftime("%d/%m/%Y"), + 'cert_time': certified['cert_time']['medianTime'] + } + + # replace old arc if this one is more recent + new_arc = True + index = 0 + for a in self[person.pubkey]['arcs']: + # if same arc already exists... + if a['id'] == arc['id']: + # if arc more recent, dont keep old one... + if arc['cert_time'] >= a['cert_time']: + self[person.pubkey]['arcs'][index] = arc + new_arc = False + index += 1 + + # if arc not in graph... + if new_arc: + # add arc in graph + self[person.pubkey]['arcs'].append(arc) + # if certified node not in person nodes + if self[certified['pubkey']] not in tuple(self[person.pubkey]['nodes']): + # add certified node to person node + self[person.pubkey]['nodes'].append(self[certified['pubkey']]) + + def add_person(self, person, status=0, arcs=None, nodes=None): + # functions keywords args are persistent... Need to reset it with None trick + arcs = list() and (arcs is None) + nodes = list() and (nodes is None) + self[person.pubkey] = { + 'id': person.pubkey, + 'arcs': arcs, + 'text': person.name, + 'tooltip': person.pubkey, + 'status': status, + 'nodes': nodes + } diff --git a/src/cutecoin/core/person.py b/src/cutecoin/core/person.py index 57c6e7695ecc224cbcca7141eeb9678ae9c534da..8709f460f404c2c9eaaccb5ba2ef397557be4c5d 100644 --- a/src/cutecoin/core/person.py +++ b/src/cutecoin/core/person.py @@ -5,6 +5,7 @@ Created on 11 févr. 2014 ''' import logging +import datetime from ucoinpy.api import bma from ucoinpy import PROTOCOL_VERSION from ucoinpy.documents.certification import SelfCertification @@ -78,13 +79,27 @@ class Person(object): signature) raise PersonNotFoundError(self.pubkey, community.name()) + def get_join_date(self, community): + try: + search = community.request(bma.blockchain.Membership, {'search': self.pubkey}) + membership_data = None + if len(search['memberships']) > 0: + membership_data = search['memberships'][0] + return datetime.datetime.fromtimestamp(community.get_block(membership_data['blockNumber']).mediantime).strftime("%d/%m/%Y %I:%M") + else: + return None + except ValueError as e: + if '400' in str(e): + raise MembershipNotFoundError(self.pubkey, community.name()) + def membership(self, community): try: search = community.request(bma.blockchain.Membership, {'search': self.pubkey}) - block_number = 0 + block_number = -1 for ms in search['memberships']: - if ms['blockNumber'] >= block_number: + if ms['blockNumber'] > block_number: + block_number = ms['blockNumber'] if 'type' in ms: if ms['type'] is 'IN': membership_data = ms diff --git a/src/cutecoin/gui/community_tab.py b/src/cutecoin/gui/community_tab.py index 3016af98064ef644b08b979416c88a719e659c38..8d00628740fcd628563fd4cad339b8e2b6a3f036 100644 --- a/src/cutecoin/gui/community_tab.py +++ b/src/cutecoin/gui/community_tab.py @@ -12,6 +12,7 @@ from PyQt5.QtWidgets import QWidget, QMessageBox, QAction, QMenu, QDialog, \ from ..models.members import MembersFilterProxyModel, MembersTableModel from ..gen_resources.community_tab_uic import Ui_CommunityTabWidget from cutecoin.gui.contact import ConfigureContactDialog +from cutecoin.gui.member import MemberDialog from .wot_tab import WotTabWidget from .transfer import TransferMoneyDialog from .password_asker import PasswordAskerDialog @@ -100,6 +101,10 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): person = self.sender().data() self.certify_member(person) + def show_member(self, person): + dialog = MemberDialog(self.account, self.community, person) + dialog.exec_() + def add_member_as_contact(self, person): dialog = ConfigureContactDialog(self.account, self.window(), person) result = dialog.exec_() diff --git a/src/cutecoin/gui/member.py b/src/cutecoin/gui/member.py new file mode 100644 index 0000000000000000000000000000000000000000..fbb38d31462b70f006b5390633c0a29794a6f611 --- /dev/null +++ b/src/cutecoin/gui/member.py @@ -0,0 +1,42 @@ +import logging +import datetime +import math +from PyQt5.QtWidgets import QDialog +from ..gen_resources.member_uic import Ui_DialogMember + + +class MemberDialog(QDialog, Ui_DialogMember): + """ + classdocs + """ + + def __init__(self, account, community, person): + """ + Constructor + """ + super().__init__() + self.setupUi(self) + self.community = community + self.account = account + self.person = person + self.label_uid.setText(person.name) + + join_date = self.person.get_join_date(self.community) + if join_date is None: + join_date = 'not a member' + + # set infos in label + self.label_properties.setText( + """ + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr> + </table> + """.format( + 'Public key', + self.person.pubkey, + 'Join date', + join_date + ) + ) + diff --git a/src/cutecoin/gui/views/wot.py b/src/cutecoin/gui/views/wot.py index ccc6c1627dbd878afd0fdc80971270e9a4810add..e30690786536a60075bfa1640e07e83d679ba3fa 100644 --- a/src/cutecoin/gui/views/wot.py +++ b/src/cutecoin/gui/views/wot.py @@ -60,6 +60,7 @@ class Scene(QGraphicsScene): node_signed = pyqtSignal(dict, name='nodeSigned') node_transaction = pyqtSignal(dict, name='nodeTransaction') node_contact = pyqtSignal(dict, name='nodeContact') + node_member = pyqtSignal(dict, name='nodeMember') def __init__(self, parent=None): """ @@ -110,7 +111,7 @@ class Scene(QGraphicsScene): """ draw community graph - :param dict graph: graph to draw + :param cutecoin.core.graph.Graph graph: graph to draw """ #  clear scene self.clear() @@ -204,6 +205,7 @@ class Node(QGraphicsEllipseItem): self.action_sign = None self.action_transaction = None self.action_contact = None + self.action_show_member = None # color around ellipse outline_color = QColor('grey') @@ -236,7 +238,6 @@ class Node(QGraphicsEllipseItem): self.setTransform( QTransform().translate(-self.boundingRect().width() / 2.0, -self.boundingRect().height() / 2.0)) self.setPos(x, y) - #print(x, y) # center text in ellipse self.text_item.setPos(self.boundingRect().width() / 4.0, self.boundingRect().height() / 4.0) @@ -280,6 +281,10 @@ class Node(QGraphicsEllipseItem): return None # create node context menus self.menu = QMenu() + # action show member + self.action_show_member = QAction('Show member', self.scene()) + self.menu.addAction(self.action_show_member) + self.action_show_member.triggered.connect(self.member_action) # action add identity as contact self.action_contact = QAction('Add as contact', self.scene()) self.menu.addAction(self.action_contact) @@ -303,26 +308,33 @@ class Node(QGraphicsEllipseItem): """ self.arcs.append(arc) - def sign_action(self): + def member_action(self): """ - Sign identity node + Transaction action to identity node """ # trigger scene signal - self.scene().node_signed.emit(self.metadata) + self.scene().node_member.emit(self.metadata) - def transaction_action(self): + def contact_action(self): """ Transaction action to identity node """ # trigger scene signal - self.scene().node_transaction.emit(self.metadata) + self.scene().node_contact.emit(self.metadata) - def contact_action(self): + def sign_action(self): + """ + Sign identity node + """ + # trigger scene signal + self.scene().node_signed.emit(self.metadata) + + def transaction_action(self): """ Transaction action to identity node """ # trigger scene signal - self.scene().node_contact.emit(self.metadata) + self.scene().node_transaction.emit(self.metadata) class Arc(QGraphicsLineItem): diff --git a/src/cutecoin/gui/wot_tab.py b/src/cutecoin/gui/wot_tab.py index da2b697bb08e27a36d151ae147dfe6ab7ce3b237..64f1515ca2c321af310f187b0db6872e176ad398 100644 --- a/src/cutecoin/gui/wot_tab.py +++ b/src/cutecoin/gui/wot_tab.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -import time -import datetime import logging -import copy +from cutecoin.core.graph import Graph from PyQt5.QtWidgets import QWidget, QComboBox from ..gen_resources.wot_tab_uic import Ui_WotTabWidget from cutecoin.gui.views.wot import NODE_STATUS_HIGHLIGHTED, NODE_STATUS_SELECTED, NODE_STATUS_OUT, ARC_STATUS_STRONG, ARC_STATUS_WEAK @@ -41,6 +39,7 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): 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.show_member) self.account = account self.community = community @@ -48,9 +47,6 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): # nodes list for menu from search self.nodes = list() - self.signature_validity = self.community.get_parameters()['sigValidity'] - # arc considered strong during 75% of signature validity time - self.ARC_STATUS_STRONG_time = int(self.signature_validity * 0.75) # create node metadata from account metadata = {'text': self.account.name, 'id': self.account.pubkey} @@ -70,8 +66,8 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): certifier_list = person.certifiers_of(self.community) certified_list = person.certified_by(self.community) - # reset graph - graph = dict() + # create empty graph instance + graph = Graph(self.community) # add wallet node node_status = 0 @@ -80,21 +76,12 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): if person.is_member(self.community) is False: node_status += NODE_STATUS_OUT node_status += NODE_STATUS_SELECTED - - # center node - graph[person.pubkey] = { - 'id': person.pubkey, - 'arcs': list(), - 'text': person.name, - 'tooltip': person.pubkey, - 'status': node_status, - 'nodes': list() - } + graph.add_person(person, node_status) # populate graph with certifiers-of - self.add_certifier_list_to_graph(graph, certifier_list, person, person_account) + graph.add_certifier_list(certifier_list, person, person_account) # populate graph with certified-by - self.add_certified_list_to_graph(graph, certified_list, person, person_account) + graph.add_certified_list(certified_list, person, person_account) # draw graph in qt scene self.graphicsView.scene().update_wot(graph) @@ -102,7 +89,7 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): # if selected member is not the account member... if person.pubkey != person_account.pubkey: # add path from selected member to account member - path = self.get_path_from_member(graph, person, person_account) + path = graph.get_shortest_path_between_members(person, person_account) if path: self.graphicsView.scene().update_path(path) @@ -154,6 +141,10 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): metadata ) + def show_member(self, metadata): + person = get_person_from_metadata(metadata) + self.parent.show_member(person) + def sign_node(self, metadata): person = get_person_from_metadata(metadata) self.parent.certify_member(person) @@ -176,161 +167,3 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): logging.debug('community.get_block request error : ' + str(e)) return False return block.mediantime - - def get_path_from_member(self, graph, person_selected, person_account): - path = list() - graph_tmp = copy.deepcopy(graph) - - if person_account.pubkey not in graph_tmp.keys(): - # recursively feed graph searching for account node... - self.feed_graph_to_find_account(graph_tmp, graph_tmp[person_selected.pubkey]['nodes'], person_account, list()) - if len(graph_tmp[person_selected.pubkey]['nodes']) > 0: - # calculate path of nodes between person and person_account - path = self.find_shortest_path(graph_tmp, graph_tmp[person_selected.pubkey], graph_tmp[person_account.pubkey]) - - if path: - logging.debug([node['text'] for node in path]) - else: - logging.debug('no wot path') - - return path - - def feed_graph_to_find_account(self, graph, nodes, person_account, done=list()): - for node in tuple(nodes): - if node['id'] in tuple(done): - continue - person_selected = Person(node['text'], node['id']) - certifier_list = person_selected.certifiers_of(self.community) - self.add_certifier_list_to_graph(graph, certifier_list, person_selected, person_account) - if person_account.pubkey in tuple(graph.keys()): - return False - certified_list = person_selected.certified_by(self.community) - self.add_certified_list_to_graph(graph, certified_list, person_selected, person_account) - if person_account.pubkey in tuple(graph.keys()): - return False - if node['id'] not in tuple(done): - done.append(node['id']) - if len(done) >= len(graph): - return True - result = self.feed_graph_to_find_account(graph, graph[person_selected.pubkey]['nodes'], person_account, done) - if not result: - return False - - return True - - def find_shortest_path(self, graph, start, end, path=list()): - path = path + [start] - if start['id'] == end['id']: - return path - if start['id'] not in graph.keys(): - return None - shortest = None - for node in tuple(graph[start['id']]['nodes']): - if node not in path: - newpath = self.find_shortest_path(graph, node, end, path) - if newpath: - if not shortest or len(newpath) < len(shortest): - shortest = newpath - return shortest - - def add_certifier_list_to_graph(self, graph, certifiers, person, person_account): - - # add certifiers of uid - for certifier in tuple(certifiers): - # add only valid certification... - if (time.time() - certifier['cert_time']['medianTime']) > self.signature_validity: - continue - # new node - if certifier['pubkey'] not in graph.keys(): - node_status = 0 - if certifier['pubkey'] == person_account.pubkey: - node_status += NODE_STATUS_HIGHLIGHTED - if certifier['isMember'] is False: - node_status += NODE_STATUS_OUT - graph[certifier['pubkey']] = { - 'id': certifier['pubkey'], - 'arcs': list(), - 'text': certifier['uid'], - 'tooltip': certifier['pubkey'], - 'status': node_status, - 'nodes': [graph[person.pubkey]] - } - - # keep only the latest certification - if graph[certifier['pubkey']]['arcs']: - if certifier['cert_time']['medianTime'] < graph[certifier['pubkey']]['arcs'][0]['cert_time']: - continue - # display validity status - if (time.time() - certifier['cert_time']['medianTime']) > self.ARC_STATUS_STRONG_time: - arc_status = ARC_STATUS_WEAK - else: - arc_status = ARC_STATUS_STRONG - arc = { - 'id': person.pubkey, - 'status': arc_status, - 'tooltip': datetime.datetime.fromtimestamp( - certifier['cert_time']['medianTime'] + self.signature_validity - ).strftime("%d/%m/%Y"), - 'cert_time': certifier['cert_time']['medianTime'] - } - # add arc to certifier - graph[certifier['pubkey']]['arcs'].append(arc) - # if certifier node not in person nodes - if graph[certifier['pubkey']] not in tuple(graph[person.pubkey]['nodes']): - # add certifier node to person node - graph[person.pubkey]['nodes'].append(graph[certifier['pubkey']]) - - def add_certified_list_to_graph(self, graph, certified_list, person, person_account): - # add certified by uid - for certified in tuple(certified_list): - # add only valid certification... - if (time.time() - certified['cert_time']['medianTime']) > self.signature_validity: - continue - if certified['pubkey'] not in graph.keys(): - node_status = 0 - if certified['pubkey'] == person_account.pubkey: - node_status += NODE_STATUS_HIGHLIGHTED - if certified['isMember'] is False: - node_status += NODE_STATUS_OUT - graph[certified['pubkey']] = { - 'id': certified['pubkey'], - 'arcs': list(), - 'text': certified['uid'], - 'tooltip': certified['pubkey'], - 'status': node_status, - 'nodes': [graph[person.pubkey]] - } - # display validity status - if (time.time() - certified['cert_time']['medianTime']) > self.ARC_STATUS_STRONG_time: - arc_status = ARC_STATUS_WEAK - else: - arc_status = ARC_STATUS_STRONG - arc = { - 'id': certified['pubkey'], - 'status': arc_status, - 'tooltip': datetime.datetime.fromtimestamp( - certified['cert_time']['medianTime'] + self.signature_validity - ).strftime("%d/%m/%Y"), - 'cert_time': certified['cert_time']['medianTime'] - } - - # replace old arc if this one is more recent - new_arc = True - index = 0 - for a in graph[person.pubkey]['arcs']: - # if same arc already exists... - if a['id'] == arc['id']: - # if arc more recent, dont keep old one... - if arc['cert_time'] >= a['cert_time']: - graph[person.pubkey]['arcs'][index] = arc - new_arc = False - index += 1 - - # if arc not in graph... - if new_arc: - # add arc in graph - graph[person.pubkey]['arcs'].append(arc) - # if certified node not in person nodes - if graph[certified['pubkey']] not in tuple(graph[person.pubkey]['nodes']): - # add certified node to person node - graph[person.pubkey]['nodes'].append(graph[certified['pubkey']])