From 630561f508c4143e9f931e99bdfe23a74aaf321a Mon Sep 17 00:00:00 2001 From: inso <insomniak.fr@gmaiL.com> Date: Wed, 26 Apr 2017 21:27:54 +0200 Subject: [PATCH] Add table of certifiers --- .../gui/navigation/identities/table_model.py | 8 +- .../gui/navigation/identity/controller.py | 35 +++- src/sakia/gui/navigation/identity/identity.ui | 16 +- src/sakia/gui/navigation/identity/model.py | 34 +++- .../gui/navigation/identity/table_model.py | 162 ++++++++++++++++++ src/sakia/gui/navigation/identity/view.py | 17 +- src/sakia/gui/navigation/model.py | 6 +- 7 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 src/sakia/gui/navigation/identity/table_model.py diff --git a/src/sakia/gui/navigation/identities/table_model.py b/src/sakia/gui/navigation/identities/table_model.py index de482f27..c3aa5991 100644 --- a/src/sakia/gui/navigation/identities/table_model.py +++ b/src/sakia/gui/navigation/identities/table_model.py @@ -173,12 +173,8 @@ class IdentitiesTableModel(QAbstractTableModel): identities_data.append(self.identity_data(identity)) if len(identities) > 0: - try: - parameters = self.blockchain_service.parameters() - self._sig_validity = parameters.sig_validity - except NoPeerAvailable as e: - logging.debug(str(e)) - self._sig_validity = 0 + parameters = self.blockchain_service.parameters() + self._sig_validity = parameters.sig_validity self.identities_data = identities_data self.endResetModel() diff --git a/src/sakia/gui/navigation/identity/controller.py b/src/sakia/gui/navigation/identity/controller.py index 4dc80e67..e8508152 100644 --- a/src/sakia/gui/navigation/identity/controller.py +++ b/src/sakia/gui/navigation/identity/controller.py @@ -1,16 +1,20 @@ import logging -from PyQt5.QtCore import QObject +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QAction +from PyQt5.QtCore import QObject, pyqtSignal from sakia.errors import NoPeerAvailable from sakia.constants import ROOT_SERVERS +from sakia.data.entities import Identity from duniterpy.api import errors from .model import IdentityModel from .view import IdentityView -from sakia.decorators import asyncify +from sakia.decorators import asyncify, once_at_a_time from sakia.gui.sub.certification.controller import CertificationController from sakia.gui.sub.password_input import PasswordInputController from sakia.gui.widgets import toast +from sakia.gui.widgets.context_menu import ContextMenu from sakia.gui.widgets.dialogs import QAsyncMessageBox, QMessageBox @@ -18,6 +22,7 @@ class IdentityController(QObject): """ The informations component """ + view_in_wot = pyqtSignal(Identity) def __init__(self, parent, view, model, certification): """ @@ -52,8 +57,27 @@ class IdentityController(QObject): certification.accepted.connect(view.clear) certification.rejected.connect(view.clear) identity.refresh_localized_data() + table_model = model.init_table_model() + view.set_table_identities_model(table_model) + view.table_certifiers.customContextMenuRequested['QPoint'].connect(identity.identity_context_menu) + identity.view_in_wot.connect(app.view_in_wot) return identity + def identity_context_menu(self, point): + index = self.view.table_certifiers.indexAt(point) + valid, identity = self.model.table_data(index) + if valid: + menu = ContextMenu.from_data(self.view, self.model.app, None, (identity,)) + menu.view_identity_in_wot.connect(self.view_in_wot) + + menu.qmenu.addSeparator().setText("Certifications") + refresh_certs = QAction(menu.qmenu.tr("Refresh"), menu.qmenu.parent()) + refresh_certs.triggered.connect(self.refresh_certs) + menu.qmenu.addAction(refresh_certs) + + # Show the context menu. + menu.qmenu.popup(QCursor.pos()) + @asyncify async def init_view_text(self): """ @@ -68,6 +92,13 @@ class IdentityController(QObject): if identity.pubkey == self.model.connection.pubkey and identity.uid == self.model.connection.uid: self.refresh_localized_data() + @once_at_a_time + @asyncify + async def refresh_certs(self, checked=False): + self.view.table_certifiers.setEnabled(False) + await self.model.refresh_certifications() + self.view.table_certifiers.setEnabled(True) + def refresh_localized_data(self): """ Refresh localized data in view diff --git a/src/sakia/gui/navigation/identity/identity.ui b/src/sakia/gui/navigation/identity/identity.ui index 0b15bbfc..90d2d5db 100644 --- a/src/sakia/gui/navigation/identity/identity.ui +++ b/src/sakia/gui/navigation/identity/identity.ui @@ -34,7 +34,21 @@ QGroupBox::title { <number>0</number> </property> <widget class="QWidget" name="page_empty"> - <layout class="QVBoxLayout" name="verticalLayout_3"/> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTableView" name="table_certifiers"> + <attribute name="horizontalHeaderShowSortIndicator" stdset="0"> + <bool>true</bool> + </attribute> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> </widget> </widget> </item> diff --git a/src/sakia/gui/navigation/identity/model.py b/src/sakia/gui/navigation/identity/model.py index ffc8d356..7b23c014 100644 --- a/src/sakia/gui/navigation/identity/model.py +++ b/src/sakia/gui/navigation/identity/model.py @@ -1,10 +1,10 @@ import logging import math -from PyQt5.QtCore import QLocale, QDateTime, pyqtSignal, QObject +from PyQt5.QtCore import QLocale, QDateTime, pyqtSignal, QObject, QModelIndex, Qt from sakia.errors import NoPeerAvailable from sakia.constants import ROOT_SERVERS -from sakia.money import Referentials +from .table_model import CertifiersTableModel, CertifiersFilterProxyModel from sakia.data.processors import BlockchainProcessor from duniterpy.api import errors @@ -33,8 +33,38 @@ class IdentityModel(QObject): self.blockchain_service = blockchain_service self.identities_service = identities_service self.sources_service = sources_service + self.table_model = None + self.proxy_model = None self._logger = logging.getLogger('sakia') + def init_table_model(self): + """ + Instanciate the table model of the view + """ + certifiers_model = CertifiersTableModel(self, self.connection, self.blockchain_service, self.identities_service) + proxy = CertifiersFilterProxyModel(self.app) + proxy.setSourceModel(certifiers_model) + + self.table_model = certifiers_model + self.proxy_model = proxy + self.table_model.init_certifiers() + return self.proxy_model + + async def refresh_certifications(self): + identity = self.identities_service.get_identity(self.connection.pubkey, self.connection.uid) + certifiers = await self.identities_service.load_certifiers_of(identity) + await self.identities_service.load_certs_in_lookup(identity, certifiers, []) + self.table_model.init_certifiers() + + def table_data(self, index): + if index.isValid() and index.row() < self.table_model.rowCount(QModelIndex()): + source_index = self.proxy_model.mapToSource(index) + identity_col = self.table_model.columns_ids.index('identity') + identity_index = self.table_model.index(source_index.row(), identity_col) + identity = self.table_model.data(identity_index, Qt.DisplayRole) + return True, identity + return False, None + def get_localized_data(self): localized_data = {} # try to request money parameters diff --git a/src/sakia/gui/navigation/identity/table_model.py b/src/sakia/gui/navigation/identity/table_model.py new file mode 100644 index 00000000..ace96e29 --- /dev/null +++ b/src/sakia/gui/navigation/identity/table_model.py @@ -0,0 +1,162 @@ +from sakia.errors import NoPeerAvailable +from sakia.data.processors import BlockchainProcessor +from PyQt5.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt, \ + QDateTime, QModelIndex, QLocale, QT_TRANSLATE_NOOP +from PyQt5.QtGui import QColor, QIcon, QFont +import logging +import asyncio + + +class CertifiersFilterProxyModel(QSortFilterProxyModel): + def __init__(self, app, parent=None): + super().__init__(parent) + self.app = app + self.blockchain_processor = BlockchainProcessor.instanciate(app) + + def columnCount(self, parent): + return len(CertifiersTableModel.columns_ids) - 2 + + def lessThan(self, left, right): + """ + Sort table by given column number. + """ + source_model = self.sourceModel() + left_data = source_model.data(left, Qt.DisplayRole) + right_data = source_model.data(right, Qt.DisplayRole) + left_data = 0 if left_data is None else left_data + right_data = 0 if right_data is None else right_data + return left_data < right_data + + def data(self, index, role): + source_index = self.mapToSource(index) + if source_index.isValid(): + source_data = self.sourceModel().data(source_index, role) + publication_col = CertifiersTableModel.columns_ids.index('publication') + publication_index = self.sourceModel().index(source_index.row(), publication_col) + expiration_col = CertifiersTableModel.columns_ids.index('expiration') + expiration_index = self.sourceModel().index(source_index.row(), expiration_col) + written_col = CertifiersTableModel.columns_ids.index('written') + written_index = self.sourceModel().index(source_index.row(), written_col) + + publication_data = self.sourceModel().data(publication_index, Qt.DisplayRole) + expiration_data = self.sourceModel().data(expiration_index, Qt.DisplayRole) + written_data = self.sourceModel().data(written_index, Qt.DisplayRole) + current_time = QDateTime().currentDateTime().toMSecsSinceEpoch() + warning_expiration_time = int((expiration_data - publication_data) / 3) + #logging.debug("{0} > {1}".format(current_time, expiration_data)) + + if role == Qt.DisplayRole: + if source_index.column() == CertifiersTableModel.columns_ids.index('expiration'): + if source_data: + ts = self.blockchain_processor.adjusted_ts(self.app.currency, source_data) + return QLocale.toString( + QLocale(), + QDateTime.fromTime_t(ts), + QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) + ) + else: + return "" + if source_index.column() == CertifiersTableModel.columns_ids.index('publication'): + if source_data: + ts = self.blockchain_processor.adjusted_ts(self.app.currency, source_data) + return QLocale.toString( + QLocale(), + QDateTime.fromTime_t(ts), + QLocale.dateTimeFormat(QLocale(), QLocale.ShortFormat) + ) + else: + return "" + if source_index.column() == CertifiersTableModel.columns_ids.index('pubkey'): + return source_data + + if role == Qt.FontRole: + font = QFont() + if not written_data: + font.setItalic(True) + return font + + if role == Qt.ForegroundRole: + if current_time > ((expiration_data*1000) - (warning_expiration_time*1000)): + return QColor("darkorange").darker(120) + + return source_data + + +class CertifiersTableModel(QAbstractTableModel): + + """ + A Qt abstract item model to display communities in a tree + """ + + columns_titles = {'uid': lambda: QT_TRANSLATE_NOOP("CertifiersTableModel", 'UID'), + 'pubkey': lambda: QT_TRANSLATE_NOOP("CertifiersTableModel", 'Pubkey'), + 'publication': lambda: QT_TRANSLATE_NOOP("CertifiersTableModel", 'Publication Date'), + 'expiration': lambda: QT_TRANSLATE_NOOP("CertifiersTableModel", 'Expiration'), + 'available': lambda: QT_TRANSLATE_NOOP("CertifiersTableModel"), } + columns_ids = ('uid', 'pubkey', 'publication', 'expiration', 'written', 'identity') + + def __init__(self, parent, connection, blockchain_service, identities_service): + """ + Constructor + :param parent: + :param sakia.data.entities.Connection connection: + :param sakia.services.BlockchainService blockchain_service: the blockchain service + :param sakia.services.IdentitiesService identities_service: the identities service + """ + super().__init__(parent) + self.connection = connection + self.blockchain_service = blockchain_service + self.identities_service = identities_service + self._certifiers_data = [] + + def certifier_data(self, certification): + """ + Return the identity in the form a tuple to display + :param sakia.data.entities.Certification certification: The certification to get data from + :return: The certification data in the form of a tuple + :rtype: tuple + """ + parameters = self.blockchain_service.parameters() + publication_date = certification.timestamp + identity = self.identities_service.get_identity(certification.certifier) + written = certification.written_on >= 0 + if written: + expiration_date = publication_date + parameters.sig_validity + else: + expiration_date = publication_date + parameters.sig_window + return identity.uid, identity.pubkey, publication_date, expiration_date, written, identity + + def init_certifiers(self): + """ + Change the identities to display + """ + self.beginResetModel() + certifications = self.identities_service.certifications_received(self.connection.pubkey) + logging.debug("Refresh {0} certifiers".format(len(certifications))) + certifiers_data = [] + for certifier in certifications: + certifiers_data.append(self.certifier_data(certifier)) + + self._certifiers_data = certifiers_data + self.endResetModel() + + def rowCount(self, parent): + return len(self._certifiers_data) + + def columnCount(self, parent): + return len(CertifiersTableModel.columns_ids) + + def headerData(self, section, orientation, role): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + col_id = CertifiersTableModel.columns_ids[section] + return CertifiersTableModel.columns_titles[col_id]() + + def data(self, index, role): + if index.isValid() and role == Qt.DisplayRole: + row = index.row() + col = index.column() + identity_data = self._certifiers_data[row] + return identity_data[col] + + def flags(self, index): + return Qt.ItemIsSelectable | Qt.ItemIsEnabled diff --git a/src/sakia/gui/navigation/identity/view.py b/src/sakia/gui/navigation/identity/view.py index 055b635b..7cd6d5c0 100644 --- a/src/sakia/gui/navigation/identity/view.py +++ b/src/sakia/gui/navigation/identity/view.py @@ -1,5 +1,5 @@ -from PyQt5.QtWidgets import QWidget, QMessageBox -from PyQt5.QtCore import QEvent, QLocale, pyqtSignal +from PyQt5.QtWidgets import QWidget, QMessageBox, QAbstractItemView, QHeaderView +from PyQt5.QtCore import QEvent, QLocale, pyqtSignal, Qt from .identity_uic import Ui_IdentityWidget from enum import Enum from sakia.helpers import timestamp_to_dhms @@ -25,6 +25,19 @@ class IdentityView(QWidget, Ui_IdentityWidget): self.stacked_widget.insertWidget(1, certification_view) self.button_certify.clicked.connect(lambda c: self.stacked_widget.setCurrentWidget(self.certification_view)) + 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_certifiers.setModel(model) + self.table_certifiers.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table_certifiers.setSortingEnabled(True) + self.table_certifiers.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) + self.table_certifiers.resizeRowsToContents() + self.table_certifiers.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) + self.table_certifiers.setContextMenuPolicy(Qt.CustomContextMenu) + def clear(self): self.stacked_widget.setCurrentWidget(self.page_empty) diff --git a/src/sakia/gui/navigation/model.py b/src/sakia/gui/navigation/model.py index 2c8e96cd..dbea2036 100644 --- a/src/sakia/gui/navigation/model.py +++ b/src/sakia/gui/navigation/model.py @@ -79,10 +79,14 @@ class NavigationModel(QObject): else: title = connection.title() if connection.uid: + if self.identity_is_member(connection): + icon = ':/icons/member' + else: + icon = ':/icons/not_member' node = { 'title': title, 'component': "Informations", - 'icon': ':/icons/member', + 'icon': icon, 'dependencies': { 'blockchain_service': self.app.blockchain_service, 'identities_service': self.app.identities_service, -- GitLab