diff --git a/src/sakia/gui/component/controller.py b/src/sakia/gui/component/controller.py index 95689da4ced45e75952fed2498d9e75f551bdb80..aa1726fa0c118604e7b1981f7e811d595da9a65d 100644 --- a/src/sakia/gui/component/controller.py +++ b/src/sakia/gui/component/controller.py @@ -11,7 +11,7 @@ class ComponentController(QObject): Constructor of the navigation component :param PyQt5.QtWidgets.QWidget view: the presentation - :param sakia.core.gui.navigation.model.NavigationModel model: the model + :param sakia.gui.component.model.ComponentModel model: the model """ super().__init__(parent) self.view = view diff --git a/src/sakia/gui/component/model.py b/src/sakia/gui/component/model.py index 3ca16fdfd455cbe4c599e158ca6921f8d17debfa..1ba59950387acd0596711b5650c42f27b5e5f380 100644 --- a/src/sakia/gui/component/model.py +++ b/src/sakia/gui/component/model.py @@ -10,6 +10,6 @@ class ComponentModel(QObject): """ Constructor of an component - :param sakia.core.gui.component.controller.AbstractAgentController controller: the controller + :param sakia.gui.component.controller.ComponentController parent: the controller """ super().__init__(parent) diff --git a/src/sakia/gui/homescreen/controller.py b/src/sakia/gui/homescreen/controller.py index bad5bec1620d460b85468a7130bc3059ddb5b4a8..ae546f527badd438b55fc435720f4e8731d4d9d1 100644 --- a/src/sakia/gui/homescreen/controller.py +++ b/src/sakia/gui/homescreen/controller.py @@ -13,7 +13,7 @@ class HomeScreenController(ComponentController): Constructor of the navigation component :param PyQt5.QtWidgets.QWidget presentation: the presentation - :param sakia.core.gui.navigation.model.NavigationModel model: the model + :param sakia.gui.navigation.model.NavigationModel model: the model """ super().__init__(parent, view, model) diff --git a/src/sakia/gui/identities/__init__.py b/src/sakia/gui/identities/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/sakia/gui/identities/controller.py b/src/sakia/gui/identities/controller.py new file mode 100644 index 0000000000000000000000000000000000000000..74f425a68cf512e09749a57a986c5ef143dedf62 --- /dev/null +++ b/src/sakia/gui/identities/controller.py @@ -0,0 +1,114 @@ +from ..component.controller import ComponentController +from .model import IdentitiesModel +from .view import IdentitiesView +from sakia.tools.decorators import once_at_a_time, asyncify +from ..widgets.context_menu import ContextMenu +from PyQt5.QtGui import QCursor +from sakia.core.registry import Identity, BlockchainState +from duniterpy.documents.block import BlockUID +from duniterpy.api import bma, errors +from sakia.tools.exceptions import NoPeerAvailable +import logging + + +class IdentitiesController(ComponentController): + """ + The navigation panel + """ + + def __init__(self, parent, view, model, password_asker=None): + """ + Constructor of the navigation component + + :param sakia.gui.identities.view.IdentitiesView view: the view + :param sakia.gui.identities.model.IdentitiesModel model: the model + """ + super().__init__(parent, view, model) + self.password_asker = password_asker + self.view.search_by_text_requested.connect(self.search_text) + self.view.search_directly_connected_requested.connect(self.search_direct_connections) + self.view.table_identities.customContextMenuRequested.connect(self.identity_context_menu) + table_model = self.model.init_table_model() + self.view.set_table_identities_model(table_model) + + @property + def app(self): + return self.model.app + + @property + def community(self): + return self.model.community + + @property + def account(self): + return self.model.account + + @classmethod + def create(cls, parent, app, **kwargs): + account = kwargs['account'] + community = kwargs['community'] + + view = IdentitiesView(parent.view) + model = IdentitiesModel(None, app, account, community) + txhistory = cls(parent, view, model) + model.setParent(txhistory) + return txhistory + + @once_at_a_time + @asyncify + async def identity_context_menu(self, point): + index = self.view.table_identities.indexAt(point) + valid, identity = await self.model.table_data(index) + if valid: + menu = ContextMenu.from_data(self.view, self.app, self.account, self.community, self.password_asker, + (identity,)) + menu.view_identity_in_wot.connect(self.view_in_wot) + + # Show the context menu. + menu.qmenu.popup(QCursor.pos()) + + @once_at_a_time + @asyncify + async def search_text(self, text): + """ + Search identities using given text + :param str text: text to search + :return: + """ + try: + response = await self.community.bma_access.future_request(bma.wot.Lookup, {'search': text}) + identities = [] + for identity_data in response['results']: + for uid_data in identity_data['uids']: + identity = Identity.from_handled_data(uid_data['uid'], + identity_data['pubkey'], + BlockUID.from_str(uid_data['meta']['timestamp']), + BlockchainState.BUFFERED) + identities.append(identity) + await self.model.refresh_identities(identities) + except errors.DuniterError as e: + if e.ucode == errors.BLOCK_NOT_FOUND: + logging.debug(str(e)) + except NoPeerAvailable as e: + logging.debug(str(e)) + + @once_at_a_time + @asyncify + async def search_direct_connections(self): + """ + Search identities directly connected to account + :return: + """ + self_identity = await self.account.identity(self.community) + account_connections = [] + certs_of = await self_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) + for p in certs_of: + account_connections.append(p['identity']) + certifiers_of = [p for p in account_connections] + certs_by = await self_identity.unique_valid_certified_by(self.app.identities_registry, self.community) + for p in certs_by: + account_connections.append(p['identity']) + certified_by = [p for p in account_connections + if p.pubkey not in [i.pubkey for i in certifiers_of]] + identities = certifiers_of + certified_by + await self.model.refresh_identities(identities) diff --git a/res/ui/identities_tab.ui b/src/sakia/gui/identities/identities.ui similarity index 96% rename from res/ui/identities_tab.ui rename to src/sakia/gui/identities/identities.ui index 0dab4a2d41ed7e07fe2da897b2dc8bad20aa7496..7d21354e111e0506633e54f3c3577e9b0d840007 100644 --- a/res/ui/identities_tab.ui +++ b/src/sakia/gui/identities/identities.ui @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>IdentitiesTab</class> - <widget class="QWidget" name="IdentitiesTab"> + <class>IdentitiesWidget</class> + <widget class="QWidget" name="IdentitiesWidget"> <property name="geometry"> <rect> <x>0</x> diff --git a/src/sakia/gui/identities/model.py b/src/sakia/gui/identities/model.py new file mode 100644 index 0000000000000000000000000000000000000000..a81bdc50d3e42f8b0a551bde013808e4938c9f36 --- /dev/null +++ b/src/sakia/gui/identities/model.py @@ -0,0 +1,55 @@ +from PyQt5.QtCore import Qt +from ..component.model import ComponentModel +from .table_model import IdentitiesFilterProxyModel, IdentitiesTableModel + + +class IdentitiesModel(ComponentModel): + """ + The model of the identities component + """ + + def __init__(self, parent, app, account, community): + """ + Constructor of a model of Identities component + + :param sakia.gui.identities.controller.IdentitiesController parent: the controller + """ + super().__init__(parent) + self.app = app + self.account = account + self.community = community + + self.table_model = None + + def init_table_model(self): + """ + Instanciate the table model of the view + """ + identities_model = IdentitiesTableModel(self, self.community) + proxy = IdentitiesFilterProxyModel() + proxy.setSourceModel(identities_model) + self.table_model = proxy + return self.table_model + + async def table_data(self, index): + """ + Get table data at given point + :param PyQt5.QtCore.QModelIndex index: + :return: a tuple containing information of the table + """ + if index.isValid() and index.row() < self.table_model.rowCount(): + source_index = self.table_model.mapToSource(index) + pubkey_col = self.table_model.sourceModel().columns_ids.index('pubkey') + pubkey_index = self.table_model.sourceModel().index(source_index.row(), + pubkey_col) + pubkey = self.table_model.sourceModel().data(pubkey_index, Qt.DisplayRole) + identity = await self.app.identities_registry.future_find(pubkey, self.community) + return True, identity + return False, None + + async def refresh_identities(self, identities): + """ + Refresh the table with specified identities. + If no identities is passed, use the account connections. + """ + await self.table_model.sourceModel().refresh_identities(identities) diff --git a/src/sakia/models/identities.py b/src/sakia/gui/identities/table_model.py similarity index 97% rename from src/sakia/models/identities.py rename to src/sakia/gui/identities/table_model.py index b6fea90292dbf9c5b1df10e4f31576a538f0e830..a340b2229a957122f9d084fbd507e8dfd1de67cf 100644 --- a/src/sakia/models/identities.py +++ b/src/sakia/gui/identities/table_model.py @@ -4,7 +4,7 @@ Created on 5 févr. 2014 @author: inso """ -from ..tools.exceptions import NoPeerAvailable, MembershipNotFoundError +from sakia.tools.exceptions import NoPeerAvailable, MembershipNotFoundError from PyQt5.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt, \ QDateTime, QModelIndex, QLocale, QEvent from PyQt5.QtGui import QColor, QIcon @@ -107,12 +107,12 @@ class IdentitiesTableModel(QAbstractTableModel): A Qt abstract item model to display communities in a tree """ - def __init__(self, parent=None): + def __init__(self, parent, community): """ Constructor """ super().__init__(parent) - self.community = None + self.community = community self.columns_titles = {'uid': lambda: self.tr('UID'), 'pubkey': lambda: self.tr('Pubkey'), 'renewed': lambda: self.tr('Renewed'), @@ -123,9 +123,6 @@ class IdentitiesTableModel(QAbstractTableModel): self.identities_data = [] self._sig_validity = 0 - def change_community(self, community): - self.community = community - def sig_validity(self): return self._sig_validity diff --git a/src/sakia/gui/identities/view.py b/src/sakia/gui/identities/view.py new file mode 100644 index 0000000000000000000000000000000000000000..cbb4ce5b425bb4297a815115191b348788c56e71 --- /dev/null +++ b/src/sakia/gui/identities/view.py @@ -0,0 +1,73 @@ +from PyQt5.QtCore import pyqtSignal, QT_TRANSLATE_NOOP, Qt, QEvent +from PyQt5.QtWidgets import QWidget, QAbstractItemView, QAction +from .identities_uic import Ui_IdentitiesWidget + + +class IdentitiesView(QWidget, Ui_IdentitiesWidget): + """ + View of the Identities component + """ + view_in_wot = pyqtSignal(object) + money_sent = pyqtSignal() + search_by_text_requested = pyqtSignal(str) + search_directly_connected_requested = pyqtSignal() + + _direct_connections_text = QT_TRANSLATE_NOOP("IdentitiesTabWidget", "Search direct certifications") + _search_placeholder = QT_TRANSLATE_NOOP("IdentitiesTabWidget", "Research a pubkey, an uid...") + + def __init__(self, parent): + super().__init__(parent) + + self.direct_connections = QAction(self.tr(IdentitiesView._direct_connections_text), self) + self.direct_connections.triggered.connect(self.request_search_direct_connections) + self.setupUi(self) + + self.table_identities.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table_identities.sortByColumn(0, Qt.AscendingOrder) + self.table_identities.resizeColumnsToContents() + self.edit_textsearch.setPlaceholderText(self.tr(IdentitiesView._search_placeholder)) + self.button_search.addAction(self.direct_connections) + self.button_search.clicked.connect(self.request_search_by_text) + + def set_table_identities_model(self, model): + """ + Set the model of the table view + :param PyQt5.QtCore.QAbstractItemModel model: the model of the table view + """ + self.table_identities.setModel(model) + model.modelAboutToBeReset.connect(lambda: self.table_identities.setEnabled(False)) + model.modelReset.connect(lambda: self.table_identities.setEnabled(True)) + + def request_search_by_text(self): + text = self.ui.edit_textsearch.text() + if len(text) < 2: + return + self.ui.edit_textsearch.clear() + self.ui.edit_textsearch.setPlaceholderText(text) + self.search_by_text_requested.emit(text) + + def request_search_direct_connections(self): + """ + Search members of community and display found members + """ + self.edit_textsearch.setPlaceholderText(self.tr(IdentitiesView._search_placeholder)) + self.search_directly_connected_requested.emit() + + def retranslateUi(self, widget): + self.direct_connections.setText(self.tr(IdentitiesView._direct_connections_text)) + super().retranslateUi(self) + + def resizeEvent(self, event): + self.busy.resize(event.size()) + super().resizeEvent(event) + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + return super().changeEvent(event) + diff --git a/src/sakia/gui/identities_tab.py b/src/sakia/gui/identities_tab.py deleted file mode 100644 index 10b0f0cc4afee5cb276f3d9532c84b52ef83451e..0000000000000000000000000000000000000000 --- a/src/sakia/gui/identities_tab.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Created on 2 févr. 2014 - -@author: inso -""" - -import logging - -from PyQt5.QtCore import Qt, pyqtSignal, QEvent, QT_TRANSLATE_NOOP, QObject -from PyQt5.QtGui import QCursor -from PyQt5.QtWidgets import QWidget, QAction, QMenu, QDialog, \ - QAbstractItemView -from duniterpy.api import bma, errors -from duniterpy.documents import BlockUID - -from ..models.identities import IdentitiesFilterProxyModel, IdentitiesTableModel -from ..presentation.identities_tab_uic import Ui_IdentitiesTab -from ..core.registry import Identity, BlockchainState -from ..tools.exceptions import NoPeerAvailable -from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task -from .widgets.context_menu import ContextMenu - - -class IdentitiesTabWidget(QObject): - - """ - classdocs - """ - view_in_wot = pyqtSignal(object) - money_sent = pyqtSignal() - - _direct_connections_text = QT_TRANSLATE_NOOP("IdentitiesTabWidget", "Search direct certifications") - _search_placeholder = QT_TRANSLATE_NOOP("IdentitiesTabWidget", "Research a pubkey, an uid...") - - def __init__(self, app, account=None, community=None, password_asker=None, - widget=QWidget, view=Ui_IdentitiesTab): - """ - Init - - :param sakia.core.app.Application app: Application instance - :param sakia.core.Account account: The account displayed in the widget - :param sakia.core.Community community: The community displayed in the widget - :param sakia.gui.Password_Asker: password_asker: The widget to ask for passwords - :param class widget: The class of the PyQt5 widget used for this tab - :param class view: The class of the UI View for this tab - """ - super().__init__() - self.widget = widget() - self.ui = view() - self.ui.setupUi(self.widget) - - self.app = app - self.community = community - self.account = account - self.password_asker = password_asker - - self.direct_connections = QAction(self.tr(IdentitiesTabWidget._direct_connections_text), self) - self.ui.edit_textsearch.setPlaceholderText(self.tr(IdentitiesTabWidget._search_placeholder)) - - identities_model = IdentitiesTableModel() - proxy = IdentitiesFilterProxyModel() - proxy.setSourceModel(identities_model) - self.ui.table_identities.setModel(proxy) - self.ui.table_identities.setSelectionBehavior(QAbstractItemView.SelectRows) - self.ui.table_identities.customContextMenuRequested.connect(self.identity_context_menu) - self.ui.table_identities.sortByColumn(0, Qt.AscendingOrder) - self.ui.table_identities.resizeColumnsToContents() - identities_model.modelAboutToBeReset.connect(lambda: self.ui.table_identities.setEnabled(False)) - identities_model.modelReset.connect(lambda: self.ui.table_identities.setEnabled(True)) - - self.direct_connections.triggered.connect(self._async_search_direct_connections) - self.ui.button_search.addAction(self.direct_connections) - self.ui.button_search.clicked.connect(self._async_execute_search_text) - - def cancel_once_tasks(self): - cancel_once_task(self, self.identity_context_menu) - cancel_once_task(self, self._async_execute_search_text) - cancel_once_task(self, self._async_search_direct_connections) - cancel_once_task(self, self.refresh_identities) - - def change_account(self, account, password_asker): - self.cancel_once_tasks() - self.account = account - self.password_asker = password_asker - if self.account is None: - self.community = None - - def change_community(self, community): - self.cancel_once_tasks() - self.community = community - self.ui.table_identities.model().change_community(community) - self._async_search_direct_connections() - - @once_at_a_time - @asyncify - async def identity_context_menu(self, point): - index = self.ui.table_identities.indexAt(point) - model = self.ui.table_identities.model() - if index.isValid() and index.row() < model.rowCount(): - source_index = model.mapToSource(index) - pubkey_col = model.sourceModel().columns_ids.index('pubkey') - pubkey_index = model.sourceModel().index(source_index.row(), - pubkey_col) - pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) - identity = await self.app.identities_registry.future_find(pubkey, self.community) - menu = ContextMenu.from_data(self.widget, self.app, self.account, self.community, self.password_asker, - (identity,)) - menu.view_identity_in_wot.connect(self.view_in_wot) - - # Show the context menu. - menu.qmenu.popup(QCursor.pos()) - - @once_at_a_time - @asyncify - async def _async_execute_search_text(self, checked): - cancel_once_task(self, self._async_search_direct_connections) - - self.ui.busy.show() - text = self.ui.edit_textsearch.text() - if len(text) < 2: - return - try: - response = await self.community.bma_access.future_request(bma.wot.Lookup, {'search': text}) - identities = [] - for identity_data in response['results']: - for uid_data in identity_data['uids']: - identity = Identity.from_handled_data(uid_data['uid'], - identity_data['pubkey'], - BlockUID.from_str(uid_data['meta']['timestamp']), - BlockchainState.BUFFERED) - identities.append(identity) - - self.ui.edit_textsearch.clear() - self.ui.edit_textsearch.setPlaceholderText(text) - await self.refresh_identities(identities) - except errors.DuniterError as e: - if e.ucode == errors.BLOCK_NOT_FOUND: - logging.debug(str(e)) - except NoPeerAvailable as e: - logging.debug(str(e)) - finally: - self.ui.busy.hide() - - @once_at_a_time - @asyncify - async def _async_search_direct_connections(self, checked=False): - """ - Search members of community and display found members - """ - cancel_once_task(self, self._async_execute_search_text) - - if self.account and self.community: - try: - self.ui.edit_textsearch.setPlaceholderText(self.tr(IdentitiesTabWidget._search_placeholder)) - await self.refresh_identities([]) - self.ui.busy.show() - self_identity = await self.account.identity(self.community) - account_connections = [] - certs_of = await self_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) - for p in certs_of: - account_connections.append(p['identity']) - certifiers_of = [p for p in account_connections] - certs_by = await self_identity.unique_valid_certified_by(self.app.identities_registry, self.community) - for p in certs_by: - account_connections.append(p['identity']) - certified_by = [p for p in account_connections - if p.pubkey not in [i.pubkey for i in certifiers_of]] - identities = certifiers_of + certified_by - self.ui.busy.hide() - await self.refresh_identities(identities) - except NoPeerAvailable: - self.ui.busy.hide() - - async def refresh_identities(self, identities): - """ - Refresh the table with specified identities. - If no identities is passed, use the account connections. - """ - await self.ui.table_identities.model().sourceModel().refresh_identities(identities) - self.ui.table_identities.resizeColumnsToContents() - - def retranslateUi(self, widget): - self.direct_connections.setText(self.tr(IdentitiesTabWidget._direct_connections_text)) - super().retranslateUi(self) - - def resizeEvent(self, event): - self.ui.busy.resize(event.size()) - super().resizeEvent(event) - - def changeEvent(self, event): - """ - Intercepte LanguageChange event to translate UI - :param QEvent QEvent: Event - :return: - """ - if event.type() == QEvent.LanguageChange: - self.retranslateUi(self) - return super(IdentitiesTabWidget, self).changeEvent(event) - diff --git a/src/sakia/gui/navigation/controller.py b/src/sakia/gui/navigation/controller.py index 350cad60a5a524d06679cfb6d6323ecaf0b8b92d..f2ce02b6e9e282e9498fe9b234ae8f2691b41e22 100644 --- a/src/sakia/gui/navigation/controller.py +++ b/src/sakia/gui/navigation/controller.py @@ -4,6 +4,7 @@ from .view import NavigationView from ..txhistory.controller import TxHistoryController from ..homescreen.controller import HomeScreenController from ..network.controller import NetworkController +from ..identities.controller import IdentitiesController class NavigationController(ComponentController): @@ -23,7 +24,7 @@ class NavigationController(ComponentController): 'TxHistory': TxHistoryController, 'HomeScreen': HomeScreenController, 'Network': NetworkController, - 'Identities': TxHistoryController + 'Identities': IdentitiesController } @classmethod diff --git a/src/sakia/gui/navigation/view.py b/src/sakia/gui/navigation/view.py index 63c01cf5c90566fa145f5379810df3eebebb4d08..178e1dabb484e87b76125926f570c84d11d7feb1 100644 --- a/src/sakia/gui/navigation/view.py +++ b/src/sakia/gui/navigation/view.py @@ -37,6 +37,7 @@ class NavigationView(QFrame, Ui_Navigation): """ if index.isValid(): raw_data = self.tree_view.model().data(index, GenericTreeModel.ROLE_RAW_DATA) - widget = raw_data['widget'] - if self.stacked_widget.indexOf(widget): - self.stacked_widget.setCurrentWidget(widget) \ No newline at end of file + if 'widget' in raw_data: + widget = raw_data['widget'] + if self.stacked_widget.indexOf(widget): + self.stacked_widget.setCurrentWidget(widget) \ No newline at end of file diff --git a/src/sakia/gui/network/model.py b/src/sakia/gui/network/model.py index 1acbc14f91fbcc5b09558153ddadb16132d1e582..443839dc2774519769a023e17f23549ab1d42964 100644 --- a/src/sakia/gui/network/model.py +++ b/src/sakia/gui/network/model.py @@ -12,7 +12,7 @@ class NetworkModel(ComponentModel): """ Constructor of an network model - :param sakia.core.gui.component.controller.NetworkController parent: the controller + :param sakia.gui.component.controller.NetworkController parent: the controller :param sakia.core.Application app: the app :param sakia.core.Account account: the account :param sakia.core.Community community: the community