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