diff --git a/src/sakia/data/processors/identities.py b/src/sakia/data/processors/identities.py index 414385605f25db4f7f6cd955f41713bf3e4b2edf..1c5844a08d71aad3abb88d47d0e3d7416a243723 100644 --- a/src/sakia/data/processors/identities.py +++ b/src/sakia/data/processors/identities.py @@ -139,6 +139,7 @@ class IdentitiesProcessor: if ms['written'] and ms['written'] > identity.membership_written_on: identity.membership_buid = BlockUID(ms['blockNumber'], ms['blockHash']) identity.membership_type = ms['membership'] + identity.membership_written_on = ms['written'] if identity.membership_buid: log_stream("Requesting membership timestamp") diff --git a/src/sakia/gui/dialogs/connection_cfg/controller.py b/src/sakia/gui/dialogs/connection_cfg/controller.py index b1993af403a65aa17b9f7f49cf0e958d2045e4c8..23765e1c6f8b2f5250d70bfdfe9766bd42ac7f8b 100644 --- a/src/sakia/gui/dialogs/connection_cfg/controller.py +++ b/src/sakia/gui/dialogs/connection_cfg/controller.py @@ -183,6 +183,8 @@ class ConnectionConfigController(QObject): self._logger.debug("Validate changes") self.model.insert_or_update_connection() self.model.app.db.commit() + + await self.view.show_register_message(self.model.blockchain_parameters()) except (NoPeerAvailable, DuniterError, StopIteration) as e: if not isinstance(e, StopIteration): self.view.show_error(self.model.notification(), str(e)) diff --git a/src/sakia/gui/dialogs/connection_cfg/model.py b/src/sakia/gui/dialogs/connection_cfg/model.py index 317334086540730b4242d55ea061b645c6af6b3d..98fda10327cd26fe8022040cca0303cce11e0ad3 100644 --- a/src/sakia/gui/dialogs/connection_cfg/model.py +++ b/src/sakia/gui/dialogs/connection_cfg/model.py @@ -120,3 +120,7 @@ class ConnectionConfigModel(QObject): def key_exists(self): return self.connection.pubkey in ConnectionsProcessor.instanciate(self.app).pubkeys() + + def blockchain_parameters(self): + blockchain_processor = BlockchainProcessor.instanciate(self.app) + return blockchain_processor.parameters(self.app.currency) diff --git a/src/sakia/gui/dialogs/connection_cfg/view.py b/src/sakia/gui/dialogs/connection_cfg/view.py index 82ada7449678d22b2f4feaba47541dc1a672dd41..ea3a26940960550663de7788f0a0bb176c1f38c7 100644 --- a/src/sakia/gui/dialogs/connection_cfg/view.py +++ b/src/sakia/gui/dialogs/connection_cfg/view.py @@ -3,8 +3,9 @@ from PyQt5.QtCore import pyqtSignal, Qt from .connection_cfg_uic import Ui_ConnectionConfigurationDialog from duniterpy.key import SigningKey, ScryptParams from math import ceil, log -from ...widgets import toast -from ...widgets.dialogs import QAsyncMessageBox +from sakia.gui.widgets import toast +from sakia.helpers import timestamp_to_dhms +from sakia.gui.widgets.dialogs import QAsyncMessageBox class ConnectionConfigView(QDialog, Ui_ConnectionConfigurationDialog): @@ -132,3 +133,25 @@ class ConnectionConfigView(QDialog, Ui_ConnectionConfigurationDialog): :param str log: """ self.plain_text_edit.insertPlainText("\n" + log) + + async def show_register_message(self, blockchain_parameters): + """ + + :param sakia.data.entities.BlockchainParameters blockchain_parameters: + :return: + """ + days, hours, minutes, seconds = timestamp_to_dhms(blockchain_parameters.idty_window) + expiration_time_str = self.tr("{days} days, {hours}h and {min}min").format(days=days, + hours=hours, + min=minutes) + + await QAsyncMessageBox.information(self, self.tr("Registration"), self.tr(""" +<b>Congratulations !</b><br> +<br> +You just published your identity to the network.<br> +For your identity to be registered, you will need <b>{certs} certifications</b> from members.<br> +Once you got the required certifications, you will be able to validate your registration +by <b>publishing your membership request !</b><br> +Please notice that your identity document <b>will expire in {expiration_time_str}.</b> If you failed +to get {certs} certifications before this time, the process will have to be restarted from scratch. +""".format(certs=blockchain_parameters.sig_qty, expiration_time_str=expiration_time_str))) diff --git a/src/sakia/gui/main_window/toolbar/controller.py b/src/sakia/gui/main_window/toolbar/controller.py index e07bff18c05156de650b4be26b74cd1b4792960b..cb82a27d78c78ea9b3a8e6c30c5d98ff019dd535 100644 --- a/src/sakia/gui/main_window/toolbar/controller.py +++ b/src/sakia/gui/main_window/toolbar/controller.py @@ -1,15 +1,10 @@ from PyQt5.QtCore import QObject from PyQt5.QtWidgets import QDialog - -from sakia.decorators import asyncify from sakia.gui.dialogs.certification.controller import CertificationController from sakia.gui.dialogs.connection_cfg.controller import ConnectionConfigController from sakia.gui.dialogs.revocation.controller import RevocationController from sakia.gui.dialogs.transfer.controller import TransferController from sakia.gui.preferences import PreferencesDialog -from sakia.gui.sub.password_input import PasswordInputController -from sakia.gui.widgets import toast -from sakia.gui.widgets.dialogs import QAsyncMessageBox from .model import ToolbarModel from .view import ToolbarView @@ -30,7 +25,6 @@ class ToolbarController(QObject): self.model = model self.view.button_certification.clicked.connect(self.open_certification_dialog) self.view.button_send_money.clicked.connect(self.open_transfer_money_dialog) - self.view.button_membership.clicked.connect(self.send_join_demand) self.view.action_add_connection.triggered.connect(self.open_add_connection_dialog) self.view.action_parameters.triggered.connect(self.open_settings_dialog) self.view.action_about.triggered.connect(self.open_about_dialog) @@ -53,29 +47,6 @@ class ToolbarController(QObject): def enable_actions(self, enabled): self.view.button_certification.setEnabled(enabled) self.view.button_send_money.setEnabled(enabled) - self.view.button_membership.setEnabled(enabled) - - @asyncify - async def send_join_demand(self, checked=False): - connection = await self.view.ask_for_connection(self.model.connections_with_uids()) - if not connection: - return - secret_key, password = await PasswordInputController.open_dialog(self, connection) - if not password or not secret_key: - return - result = await self.model.send_join(connection, secret_key, password) - if result[0]: - if self.model.notifications(): - toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) - else: - await QAsyncMessageBox.information(self.view, self.tr("Membership"), - self.tr("Success sending Membership demand")) - else: - if self.model.notifications(): - toast.display(self.tr("Membership"), result[1]) - else: - await QAsyncMessageBox.critical(self.view, self.tr("Membership"), - result[1]) def open_certification_dialog(self): CertificationController.open_dialog(self, self.model.app, diff --git a/src/sakia/gui/main_window/toolbar/toolbar.ui b/src/sakia/gui/main_window/toolbar/toolbar.ui index 39850025c90911ad23c57e76d28c2c2c7c1480a8..e17acceb550e3606871d5207dd04ee94b01bc558 100644 --- a/src/sakia/gui/main_window/toolbar/toolbar.ui +++ b/src/sakia/gui/main_window/toolbar/toolbar.ui @@ -60,23 +60,6 @@ </property> </widget> </item> - <item> - <widget class="QPushButton" name="button_membership"> - <property name="text"> - <string>Renew membership</string> - </property> - <property name="icon"> - <iconset resource="../../../../../res/icons/icons.qrc"> - <normaloff>:/icons/renew_membership</normaloff>:/icons/renew_membership</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - </widget> - </item> <item> <spacer name="horizontalSpacer"> <property name="orientation"> diff --git a/src/sakia/gui/navigation/informations/controller.py b/src/sakia/gui/navigation/informations/controller.py index f2609d23054bbc849719b7057caf5ae6783dda68..ceec658d40c0b2cb4f39e094551cf7f1d17f9455 100644 --- a/src/sakia/gui/navigation/informations/controller.py +++ b/src/sakia/gui/navigation/informations/controller.py @@ -8,6 +8,11 @@ from sakia.decorators import asyncify from .model import InformationsModel from .view import InformationsView +from sakia.decorators import asyncify +from sakia.gui.sub.password_input import PasswordInputController +from sakia.gui.widgets import toast +from sakia.gui.widgets.dialogs import QAsyncMessageBox + class InformationsController(QObject): """ @@ -25,6 +30,7 @@ class InformationsController(QObject): self.view = view self.model = model self._logger = logging.getLogger('sakia') + self.view.button_membership.clicked.connect(self.send_join_demand) @property def informations_view(self): @@ -98,3 +104,24 @@ class InformationsController(QObject): self.view.set_general_text(localized_data) self.view.set_rules_text(localized_data) + @asyncify + async def send_join_demand(self, checked=False): + connection = await self.view.ask_for_connection(self.model.connections_with_uids()) + if not connection: + return + secret_key, password = await PasswordInputController.open_dialog(self, connection) + if not password or not secret_key: + return + result = await self.model.send_join(connection, secret_key, password) + if result[0]: + if self.model.notifications(): + toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) + else: + await QAsyncMessageBox.information(self.view, self.tr("Membership"), + self.tr("Success sending Membership demand")) + else: + if self.model.notifications(): + toast.display(self.tr("Membership"), result[1]) + else: + await QAsyncMessageBox.critical(self.view, self.tr("Membership"), + result[1]) diff --git a/src/sakia/gui/navigation/informations/informations.ui b/src/sakia/gui/navigation/informations/informations.ui index 920da0a0356356bd02d576bc429b1841c01ac67e..ba3f34f1c501d941bd96cea513d764b5fefd5d5a 100644 --- a/src/sakia/gui/navigation/informations/informations.ui +++ b/src/sakia/gui/navigation/informations/informations.ui @@ -42,7 +42,7 @@ QGroupBox::title { <x>0</x> <y>0</y> <width>522</width> - <height>308</height> + <height>272</height> </rect> </property> <layout class="QVBoxLayout" name="verticalLayout_5"> @@ -188,12 +188,69 @@ QGroupBox::title { </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <widget class="QLabel" name="label_simple"> + <widget class="QLabel" name="label_currency"> <property name="text"> <string/> </property> </widget> </item> + <item> + <widget class="QLabel" name="label_identity"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="topMargin"> + <number>6</number> + </property> + <item> + <widget class="QLabel" name="label_membership"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_membership"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Renew membership</string> + </property> + <property name="icon"> + <iconset resource="../../../../../res/icons/icons.qrc"> + <normaloff>:/icons/renew_membership</normaloff>:/icons/renew_membership</iconset> + </property> + <property name="iconSize"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> </layout> </widget> </item> @@ -213,7 +270,7 @@ QGroupBox::title { </layout> </widget> <resources> - <include location="../../../../res/icons/icons.qrc"/> + <include location="../../../../../res/icons/icons.qrc"/> </resources> <connections/> </ui> diff --git a/src/sakia/gui/navigation/informations/model.py b/src/sakia/gui/navigation/informations/model.py index 4a7a538819f9a2d55afc57961ed158aa03d07f14..c99e904b81065aa3fc28cdf14c02917f88ddc232 100644 --- a/src/sakia/gui/navigation/informations/model.py +++ b/src/sakia/gui/navigation/informations/model.py @@ -115,12 +115,15 @@ class InformationsModel(QObject): localized_amount = self.app.current_ref.instance(amount, self.connection.currency, self.app).localized(False, True) - mstime_remaining_text = self.tr("Expired or never published") outdistanced_text = self.tr("Outdistanced") + is_identity = False is_member = False nb_certs = 0 + mstime_remaining = 0 + nb_certs_required = self.blockchain_service.parameters().sig_qty if self.connection.uid: + is_identity = True try: identity = self.identities_service.get_identity(self.connection.pubkey, self.connection.uid) if identity: @@ -129,15 +132,6 @@ class InformationsModel(QObject): nb_certs = len(self.identities_service.certifications_received(identity.pubkey)) if not identity.outdistanced: outdistanced_text = self.tr("In WoT range") - - if mstime_remaining > 0: - days, hours, minutes, seconds = timestamp_to_dhms(mstime_remaining) - mstime_remaining_text = self.tr("Expires in ") - if days > 0: - mstime_remaining_text += "{days} days".format(days=days) - else: - mstime_remaining_text += "{hours} hours and {min} min.".format(hours=hours, - min=minutes) except errors.DuniterError as e: if e.ucode == errors.NO_MEMBER_MATCHING_PUB_OR_UID: pass @@ -148,8 +142,10 @@ class InformationsModel(QObject): 'amount': localized_amount, 'outdistanced': outdistanced_text, 'nb_certs': nb_certs, - 'mstime': mstime_remaining_text, - 'membership_state': is_member + 'nb_certs_required': nb_certs_required, + 'mstime': mstime_remaining, + 'membership_state': is_member, + 'is_identity': is_identity } def parameters(self): diff --git a/src/sakia/gui/navigation/informations/view.py b/src/sakia/gui/navigation/informations/view.py index 72af6972db67dc0825964737b869b967e872f28b..549a6767ca2429cc086913bf551212f0e71ff2ba 100644 --- a/src/sakia/gui/navigation/informations/view.py +++ b/src/sakia/gui/navigation/informations/view.py @@ -30,7 +30,7 @@ class InformationsView(QWidget, Ui_InformationsWidget): def set_simple_informations(self, data, state): if state in (InformationsView.CommunityState.NOT_INIT, InformationsView.CommunityState.OFFLINE): - self.label_simple.setText("""<html> + self.label_currency.setText("""<html> <body> <p> <span style=" font-size:16pt; font-weight:600;">{currency}</span> @@ -39,37 +39,88 @@ class InformationsView(QWidget, Ui_InformationsWidget): </body> </html>""".format(currency=data['currency'], message=InformationsView.simple_message[state])) + self.button_membership.hide() else: status_value = self.tr("Member") if data['membership_state'] else self.tr("Non-Member") + if data['mstime'] > 0: + membership_action_value = self.tr("Renew membership") + status_info = "" + membership_action_enabled = True + elif data['membership_state']: + membership_action_value = self.tr("Renew membership") + status_info = "Your membership expired" + membership_action_enabled = True + else: + membership_action_value = self.tr("Request membership") + if data['nb_certs'] > data['nb_certs_required']: + status_info = self.tr("Registration ready") + membership_action_enabled = True + else: + status_info = self.tr("{0} more certifications required")\ + .format(data['nb_certs_required'] - data['nb_certs']) + membership_action_enabled = True + + if data['mstime'] > 0: + days, hours, minutes, seconds = timestamp_to_dhms(data['mstime']) + mstime_remaining_text = self.tr("Expires in ") + if days > 0: + mstime_remaining_text += "{days} days".format(days=days) + else: + mstime_remaining_text += "{hours} hours and {min} min.".format(hours=hours, + min=minutes) + else: + mstime_remaining_text = self.tr("Expired or never published") + status_color = '#00AA00' if data['membership_state'] else self.tr('#FF0000') - description = """<html> - <body> - <p> - <span style=" font-size:16pt; font-weight:600;">{currency}</span> - </p> - <p>{nb_members} {members_label}</p> - <p><span style="font-weight:600;">{monetary_mass_label}</span> : {monetary_mass}</p> - <p><span style="font-weight:600;">{status_label}</span> : <span style="color:{status_color};">{status}</span></p> - <p><span style="font-weight:600;">{nb_certs_label}</span> : {nb_certs} ({outdistanced_text})</p> - <p><span style="font-weight:600;">{mstime_remaining_label}</span> : {mstime_remaining}</p> - <p><span style="font-weight:600;">{balance_label}</span> : {balance}</p> - </body> - </html>""".format(currency=data['units'], - nb_members=data['members_count'], - members_label=self.tr("members"), - monetary_mass_label=self.tr("Monetary mass"), - monetary_mass=data['mass'], - status_color=status_color, - status_label=self.tr("Status"), - status=status_value, - nb_certs_label=self.tr("Certs. received"), - nb_certs=data['nb_certs'], - outdistanced_text=data['outdistanced'], - mstime_remaining_label=self.tr("Membership"), - mstime_remaining=data['mstime'], - balance_label=self.tr("Balance"), - balance=data['amount']) - self.label_simple.setText(description) + description_currency = """<html> +<body> + <p> + <span style=" font-size:16pt; font-weight:600;">{currency}</span> + </p> + <p>{nb_members} {members_label}</p> + <p><span style="font-weight:600;">{monetary_mass_label}</span> : {monetary_mass}</p> + <p><span style="font-weight:600;">{balance_label}</span> : {balance}</p> +</body> +</html>""".format(currency=data['units'], + nb_members=data['members_count'], + members_label=self.tr("members"), + monetary_mass_label=self.tr("Monetary mass"), + monetary_mass=data['mass'], + balance_label=self.tr("Balance"), + balance=data['amount']) + + description_membership = """<html> +<body> + <p><span style="font-weight:600;">{status_label}</span> + : <span style="color:{status_color};">{status}</span> + - <span>{status_info}</span></p> +</body> +</html>""".format(status_color=status_color, + status_label=self.tr("Status"), + status=status_value, + status_info=status_info) + description_identity = """<html> +<body> + <p><span style="font-weight:600;">{nb_certs_label}</span> : {nb_certs} ({outdistanced_text})</p> + <p><span style="font-weight:600;">{mstime_remaining_label}</span> : {mstime_remaining}</p> +</body> +</html>""".format(nb_certs_label=self.tr("Certs. received"), + nb_certs=data['nb_certs'], + outdistanced_text=data['outdistanced'], + mstime_remaining_label=self.tr("Membership"), + mstime_remaining=mstime_remaining_text) + + self.label_currency.setText(description_currency) + + if data['is_identity']: + self.label_membership.setText(description_membership) + self.label_identity.setText(description_identity) + self.button_membership.setText(membership_action_value) + self.button_membership.setEnabled(membership_action_enabled) + else: + self.label_membership.hide() + self.label_identity.hide() + self.button_membership.hide() def set_general_text_no_dividend(self): """ diff --git a/tests/functional/test_connection_cfg_dialog.py b/tests/functional/test_connection_cfg_dialog.py index 9cc0a8797f683371bdce451f0542aaabb50a4b79..2a2778d3b947c300e66fcfc19ae8058443761b50 100644 --- a/tests/functional/test_connection_cfg_dialog.py +++ b/tests/functional/test_connection_cfg_dialog.py @@ -1,11 +1,19 @@ import asyncio import pytest +from PyQt5.QtWidgets import QApplication, QMessageBox from PyQt5.QtCore import Qt from PyQt5.QtTest import QTest from sakia.data.processors import ConnectionsProcessor from sakia.gui.dialogs.connection_cfg import ConnectionConfigController +def click_on_top_message_box(): + topWidgets = QApplication.topLevelWidgets() + for w in topWidgets: + if type(w) is QMessageBox: + QTest.keyClick(w, Qt.Key_Enter) + + def assert_key_parameters_behaviour(connection_config_dialog, user): QTest.keyClicks(connection_config_dialog.view.edit_uid, user.uid) QTest.keyClicks(connection_config_dialog.view.edit_salt, user.salt) @@ -42,9 +50,10 @@ async def test_register_empty_blockchain(application, fake_server, bob): assert_key_parameters_behaviour(connection_config_dialog, bob) QTest.mouseClick(connection_config_dialog.view.button_next, Qt.LeftButton) connection_config_dialog.model.connection.password = bob.password - await asyncio.sleep(10) + await asyncio.sleep(1) assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_services - assert len(ConnectionsProcessor.instanciate(application).connections(fake_server.forge.currency)) == 1 + assert len(ConnectionsProcessor.instanciate(application).connections()) == 1 + click_on_top_message_box() application.loop.call_later(10, close_dialog) asyncio.ensure_future(exec_test()) @@ -70,7 +79,8 @@ async def test_connect(application, simple_fake_server, bob): await asyncio.sleep(1) assert connection_config_dialog.view.stacked_pages.currentWidget() == connection_config_dialog.view.page_services - assert len(ConnectionsProcessor.instanciate(application).connections(simple_fake_server.forge.currency)) == 1 + assert len(ConnectionsProcessor.instanciate(application).connections()) == 1 + click_on_top_message_box() application.loop.call_later(10, close_dialog) asyncio.ensure_future(exec_test())