diff --git a/res/ui/transactions_tab.ui b/res/ui/transactions_tab.ui index f32e7ebae129d4805b8fd1d023e548617972a9bb..737ecfa4aa93d0e094da40eed917ae4b71e16457 100644 --- a/res/ui/transactions_tab.ui +++ b/res/ui/transactions_tab.ui @@ -37,6 +37,9 @@ </property> </widget> </item> + <item> + <widget class="Busy" name="busy_balance" native="true"/> + </item> </layout> </widget> </item> @@ -105,61 +108,16 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>Busy</class> + <extends>QWidget</extends> + <header>sakia.gui.widgets</header> + <container>1</container> + </customwidget> + </customwidgets> <resources> <include location="../icons/icons.qrc"/> </resources> - <connections> - <connection> - <sender>table_history</sender> - <signal>customContextMenuRequested(QPoint)</signal> - <receiver>transactionsTabWidget</receiver> - <slot>history_context_menu()</slot> - <hints> - <hint type="sourcelabel"> - <x>273</x> - <y>183</y> - </hint> - <hint type="destinationlabel"> - <x>830</x> - <y>802</y> - </hint> - </hints> - </connection> - <connection> - <sender>date_from</sender> - <signal>dateChanged(QDate)</signal> - <receiver>transactionsTabWidget</receiver> - <slot>dates_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>102</x> - <y>28</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - <connection> - <sender>date_to</sender> - <signal>dateChanged(QDate)</signal> - <receiver>transactionsTabWidget</receiver> - <slot>dates_changed()</slot> - <hints> - <hint type="sourcelabel"> - <x>297</x> - <y>28</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>149</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>history_context_menu()</slot> - <slot>dates_changed()</slot> - </slots> + <connections/> </ui> diff --git a/src/sakia/core/transfer.py b/src/sakia/core/transfer.py index 53d130111371bfb547180b3d6cf434365f569d5f..f86bb625ed444ed3671f5ef16eee2f9124dd3b9d 100644 --- a/src/sakia/core/transfer.py +++ b/src/sakia/core/transfer.py @@ -352,5 +352,16 @@ class Transfer(QObject): else: await r.text() self.run_state_transitions(([r.status for r in responses], block_doc)) - self.run_state_transitions(([r.status for r in responses])) + self.run_state_transitions(([r.status for r in responses], )) return result + + async def get_raw_document(self, community): + """ + Get the raw documents of this transfer + """ + block = await community.get_block(self.blockid.number) + if block: + block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) + for tx in block_doc.transactions: + if tx.sha_hash == self.sha_hash: + return tx diff --git a/src/sakia/gui/community_view.py b/src/sakia/gui/community_view.py index 5db08b0bb898ec57593cb1ac92cd9937ce34e607..2da8ca3130b3ee465a6179406d774cca2fdd6963 100644 --- a/src/sakia/gui/community_view.py +++ b/src/sakia/gui/community_view.py @@ -77,7 +77,7 @@ class CommunityWidget(QWidget, Ui_CommunityWidget): self.tab_identities.money_sent.connect(lambda: self.tab_history.table_history.model().sourceModel().refresh_transfers()) self.tab_wot.money_sent.connect(lambda: self.tab_history.table_history.model().sourceModel().refresh_transfers()) - self.tabs.addTab(self.tab_history, + self.tabs.addTab(self.tab_history.widget, QIcon(':/icons/tx_icon'), self.tr(CommunityWidget._tab_history_label)) @@ -413,7 +413,7 @@ The process to join back the community later will have to be done again.""") self.tabs.setTabText(self.tabs.indexOf(self.tab_wot), self.tr(CommunityWidget._tab_wot_label)) self.tabs.setTabText(self.tabs.indexOf(self.tab_network), self.tr(CommunityWidget._tab_network_label)) self.tabs.setTabText(self.tabs.indexOf(self.tab_informations), self.tr(CommunityWidget._tab_informations_label)) - self.tabs.setTabText(self.tabs.indexOf(self.tab_history), self.tr(CommunityWidget._tab_history_label)) + self.tabs.setTabText(self.tabs.indexOf(self.tab_history.widget), self.tr(CommunityWidget._tab_history_label)) self.tabs.setTabText(self.tabs.indexOf(self.tab_identities), self.tr(CommunityWidget._tab_identities_label)) self.action_publish_uid.setText(self.tr(CommunityWidget._action_publish_uid_text)) self.action_revoke_uid.setText(self.tr(CommunityWidget._action_revoke_uid_text)) diff --git a/src/sakia/gui/transactions_tab.py b/src/sakia/gui/transactions_tab.py index b35cf48c9538690e6699c68cfce87addf051c401..29e630a5358132cc8191a69baf8570ec5fd7cc0a 100644 --- a/src/sakia/gui/transactions_tab.py +++ b/src/sakia/gui/transactions_tab.py @@ -3,9 +3,9 @@ import asyncio from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView, QDialog, \ QMenu, QAction, QApplication, QMessageBox -from PyQt5.QtCore import Qt, QDateTime, QTime, QModelIndex, pyqtSignal, pyqtSlot, QEvent - +from PyQt5.QtCore import Qt, QObject, QDateTime, QTime, QModelIndex, pyqtSignal, pyqtSlot, QEvent from PyQt5.QtGui import QCursor +from ucoinpy.documents import Block from ..gen_resources.transactions_tab_uic import Ui_transactionsTabWidget from ..models.txhistory import HistoryTableModel, TxFilterProxyModel @@ -22,13 +22,13 @@ from sakia.gui.widgets import toast from sakia.gui.widgets.busy import Busy -class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): +class TransactionsTabWidget(QObject): """ classdocs """ - view_in_wot = pyqtSignal(Identity) + view_in_wot = pyqtSignal(object) - def __init__(self, app): + def __init__(self, app, widget=QWidget, view=Ui_transactionsTabWidget): """ Init @@ -37,32 +37,37 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): """ super().__init__() - self.setupUi(self) + self.widget = widget() + self.ui = view() + self.ui.setupUi(self.widget) self.app = app self.account = None self.community = None self.password_asker = None - self.busy_balance = Busy(self.groupbox_balance) - self.busy_balance.hide() + self.ui.busy_balance.hide() - ts_from = self.date_from.dateTime().toTime_t() - ts_to = self.date_to.dateTime().toTime_t() + ts_from = self.ui.date_from.dateTime().toTime_t() + ts_to = self.ui.date_to.dateTime().toTime_t() model = HistoryTableModel(self.app, self.account, self.community) proxy = TxFilterProxyModel(ts_from, ts_to) proxy.setSourceModel(model) proxy.setDynamicSortFilter(True) proxy.setSortRole(Qt.DisplayRole) - self.table_history.setModel(proxy) - self.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) - self.table_history.setSortingEnabled(True) - self.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) - self.table_history.resizeColumnsToContents() + self.ui.table_history.setModel(proxy) + self.ui.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) + self.ui.table_history.setSortingEnabled(True) + self.ui.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) + self.ui.table_history.resizeColumnsToContents() + + self.ui.table_history.customContextMenuRequested['QPoint'].connect(self.history_context_menu) + self.ui.date_from.dateChanged['QDate'].connect(self.dates_changed) + self.ui.date_to.dateChanged['QDate'].connect(self.dates_changed) - model.modelAboutToBeReset.connect(lambda: self.table_history.setEnabled(False)) - model.modelReset.connect(lambda: self.table_history.setEnabled(True)) + model.modelAboutToBeReset.connect(lambda: self.ui.table_history.setEnabled(False)) + model.modelReset.connect(lambda: self.ui.table_history.setEnabled(True)) - self.progressbar.hide() + self.ui.progressbar.hide() self.refresh() def cancel_once_tasks(self): @@ -74,15 +79,15 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): self.cancel_once_tasks() self.account = account self.password_asker = password_asker - self.table_history.model().sourceModel().change_account(account) + self.ui.table_history.model().sourceModel().change_account(account) if account: self.connect_progress() def change_community(self, community): self.cancel_once_tasks() self.community = community - self.progressbar.hide() - self.table_history.model().sourceModel().change_community(self.community) + self.ui.progressbar.hide() + self.ui.table_history.model().sourceModel().change_community(self.community) self.refresh() @once_at_a_time @@ -94,14 +99,14 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): minimum_datetime.setTime_t(block['medianTime']) minimum_datetime.setTime(QTime(0, 0)) - self.date_from.setMinimumDateTime(minimum_datetime) - self.date_from.setDateTime(minimum_datetime) - self.date_from.setMaximumDateTime(QDateTime().currentDateTime()) + self.ui.date_from.setMinimumDateTime(minimum_datetime) + self.ui.date_from.setDateTime(minimum_datetime) + self.ui.date_from.setMaximumDateTime(QDateTime().currentDateTime()) - self.date_to.setMinimumDateTime(minimum_datetime) + self.ui.date_to.setMinimumDateTime(minimum_datetime) tomorrow_datetime = QDateTime().currentDateTime().addDays(1) - self.date_to.setDateTime(tomorrow_datetime) - self.date_to.setMaximumDateTime(tomorrow_datetime) + self.ui.date_to.setDateTime(tomorrow_datetime) + self.ui.date_to.setMaximumDateTime(tomorrow_datetime) except NoPeerAvailable as e: logging.debug(str(e)) except ValueError as e: @@ -109,25 +114,25 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): def refresh(self): if self.community: - self.table_history.model().sourceModel().refresh_transfers() - self.table_history.resizeColumnsToContents() + self.ui.table_history.model().sourceModel().refresh_transfers() + self.ui.table_history.resizeColumnsToContents() self.refresh_minimum_maximum() self.refresh_balance() def connect_progress(self): def progressing(community, value, maximum): if community == self.community: - self.progressbar.show() - self.progressbar.setValue(value) - self.progressbar.setMaximum(maximum) + self.ui.progressbar.show() + self.ui.progressbar.setValue(value) + self.ui.progressbar.setMaximum(maximum) self.account.loading_progressed.connect(progressing) self.account.loading_finished.connect(self.stop_progress) def stop_progress(self, community, received_list): if community == self.community: - self.progressbar.hide() - self.table_history.model().sourceModel().refresh_transfers() - self.table_history.resizeColumnsToContents() + self.ui.progressbar.hide() + self.ui.table_history.model().sourceModel().refresh_transfers() + self.ui.table_history.resizeColumnsToContents() self.notification_reception(received_list) @asyncify @@ -148,28 +153,28 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): @once_at_a_time @asyncify async def refresh_balance(self): - self.busy_balance.show() + self.ui.busy_balance.show() amount = await self.app.current_account.amount(self.community) localized_amount = await self.app.current_account.current_ref(amount, self.community, self.app).localized(units=True, international_system=self.app.preferences['international_system_of_units']) # set infos in label - self.label_balance.setText( + self.ui.label_balance.setText( self.tr("{:}") .format( localized_amount ) ) - self.busy_balance.hide() + self.ui.busy_balance.hide() @once_at_a_time @asyncify async def history_context_menu(self, point): - index = self.table_history.indexAt(point) - model = self.table_history.model() + index = self.ui.table_history.indexAt(point) + model = self.ui.table_history.model() if index.isValid() and index.row() < model.rowCount(QModelIndex()): - menu = QMenu(self.tr("Actions"), self) + menu = QMenu(self.tr("Actions"), self.widget) source_index = model.mapToSource(index) state_col = model.sourceModel().columns_types.index('state') state_index = model.sourceModel().index(source_index.row(), @@ -180,103 +185,107 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): pubkey_index = model.sourceModel().index(source_index.row(), pubkey_col) pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) + + block_number_col = model.sourceModel().columns_types.index('block_number') + block_number_index = model.sourceModel().index(source_index.row(), + block_number_col) + block_number = model.sourceModel().data(block_number_index, Qt.DisplayRole) + identity = await self.app.identities_registry.future_find(pubkey, self.community) transfer = model.sourceModel().transfers()[source_index.row()] if state_data == TransferState.REFUSED or state_data == TransferState.TO_SEND: - send_back = QAction(self.tr("Send again"), self) + send_back = QAction(self.tr("Send again"), self.widget) send_back.triggered.connect(lambda checked, tr=transfer: self.send_again(checked, tr)) - send_back.setData(transfer) menu.addAction(send_back) - cancel = QAction(self.tr("Cancel"), self) - cancel.triggered.connect(self.cancel_transfer) - cancel.setData(transfer) + cancel = QAction(self.tr("Cancel"), self.widget) + cancel.triggered.connect(lambda checked, tr=transfer: self.cancel_transfer(tr)) menu.addAction(cancel) else: if isinstance(identity, Identity): - informations = QAction(self.tr("Informations"), self) - informations.triggered.connect(self.menu_informations) - informations.setData(identity) + informations = QAction(self.tr("Informations"), self.widget) + informations.triggered.connect(lambda checked, i=identity: self.menu_informations(i)) menu.addAction(informations) - add_as_contact = QAction(self.tr("Add as contact"), self) - add_as_contact.triggered.connect(self.menu_add_as_contact) - add_as_contact.setData(identity) + add_as_contact = QAction(self.tr("Add as contact"), self.widget) + add_as_contact.triggered.connect(lambda checked,i=identity: self.menu_add_as_contact(i)) menu.addAction(add_as_contact) - send_money = QAction(self.tr("Send money"), self) - send_money.triggered.connect(self.menu_send_money) - send_money.setData(identity) + send_money = QAction(self.tr("Send money"), self.widget) + send_money.triggered.connect(lambda checked, i=identity: self.menu_send_money(identity)) menu.addAction(send_money) if isinstance(identity, Identity): - view_wot = QAction(self.tr("View in Web of Trust"), self) - view_wot.triggered.connect(self.view_wot) - view_wot.setData(identity) + view_wot = QAction(self.tr("View in Web of Trust"), self.widget) + view_wot.triggered.connect(lambda checked, i=identity: self.view_wot(i)) menu.addAction(view_wot) - copy_pubkey = QAction(self.tr("Copy pubkey to clipboard"), self) - copy_pubkey.triggered.connect(self.copy_pubkey_to_clipboard) - copy_pubkey.setData(identity) + copy_pubkey = QAction(self.tr("Copy pubkey to clipboard"), self.widget) + copy_pubkey.triggered.connect(lambda checked, i=identity: self.copy_pubkey_to_clipboard(i)) menu.addAction(copy_pubkey) + if self.app.preferences['expert_mode']: + if type(transfer) is Transfer: + copy_doc = QAction(self.tr("Copy raw transaction to clipboard"), self.widget) + copy_doc.triggered.connect(lambda checked, tx=transfer: self.copy_transaction_to_clipboard(tx)) + menu.addAction(copy_doc) + + copy_doc = QAction(self.tr("Copy block to clipboard"), self.widget) + copy_doc.triggered.connect(lambda checked, number=block_number: self.copy_block_to_clipboard(number)) + menu.addAction(copy_doc) + # Show the context menu. menu.popup(QCursor.pos()) - def copy_pubkey_to_clipboard(self): - data = self.sender().data() + def copy_pubkey_to_clipboard(self, identity): clipboard = QApplication.clipboard() - if data.__class__ is Wallet: - clipboard.setText(data.pubkey) - elif data.__class__ is Identity: - clipboard.setText(data.pubkey) - elif data.__class__ is str: - clipboard.setText(data) - - def menu_informations(self): - person = self.sender().data() - self.identity_informations(person) - - def menu_add_as_contact(self): - person = self.sender().data() - self.add_identity_as_contact({'name': person.uid, - 'pubkey': person.pubkey}) - - def menu_send_money(self): - identity = self.sender().data() - self.send_money_to_identity(identity) + clipboard.setText(identity.pubkey) - def identity_informations(self, person): - dialog = MemberDialog(self.app, self.account, self.community, person) + @asyncify + async def copy_transaction_to_clipboard(self, tx): + clipboard = QApplication.clipboard() + raw_doc = await tx.get_raw_document(self.community) + clipboard.setText(raw_doc.signed_raw()) + + @asyncify + async def copy_block_to_clipboard(self, number): + clipboard = QApplication.clipboard() + block = await self.community.get_block(number) + if block: + block_doc = Block.from_signed_raw("{0}{1}\n".format(block['raw'], block['signature'])) + clipboard.setText(block_doc.signed_raw()) + + def menu_informations(self, identity): + dialog = MemberDialog(self.app, self.account, self.community, identity) dialog.exec_() - def add_identity_as_contact(self, person): - dialog = ConfigureContactDialog(self.account, self.window(), person) + def menu_add_as_contact(self, identity): + dialog = ConfigureContactDialog(self.account, self.window(), identity) result = dialog.exec_() if result == QDialog.Accepted: self.window().refresh_contacts() @asyncify - async def send_money_to_identity(self, identity): + async def menu_send_money(self, identity): + self.send_money_to_identity(identity) await TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker, self.community, identity) - self.table_history.model().sourceModel().refresh_transfers() + self.ui.table_history.model().sourceModel().refresh_transfers() @asyncify async def certify_identity(self, identity): await CertificationDialog.certify_identity(self.app, self.account, self.password_asker, self.community, identity) - def view_wot(self): - identity = self.sender().data() + def view_wot(self, identity): self.view_in_wot.emit(identity) @asyncify async def send_again(self, checked=False, transfer=None): result = await TransferMoneyDialog.send_transfer_again(self.app, self.app.current_account, self.password_asker, self.community, transfer) - self.table_history.model().sourceModel().refresh_transfers() + self.ui.table_history.model().sourceModel().refresh_transfers() def cancel_transfer(self): reply = QMessageBox.warning(self, self.tr("Warning"), @@ -286,24 +295,24 @@ QMessageBox.Ok | QMessageBox.Cancel) if reply == QMessageBox.Ok: transfer = self.sender().data() transfer.cancel() - self.table_history.model().sourceModel().refresh_transfers() + self.ui.table_history.model().sourceModel().refresh_transfers() def dates_changed(self): logging.debug("Changed dates") - if self.table_history.model(): - qdate_from = self.date_from + if self.ui.table_history.model(): + qdate_from = self.ui.date_from qdate_from.setTime(QTime(0, 0, 0)) - qdate_to = self.date_to + qdate_to = self.ui.date_to qdate_to.setTime(QTime(0, 0, 0)) ts_from = qdate_from.dateTime().toTime_t() ts_to = qdate_to.dateTime().toTime_t() - self.table_history.model().set_period(ts_from, ts_to) + self.ui.table_history.model().set_period(ts_from, ts_to) self.refresh_balance() def resizeEvent(self, event): - self.busy_balance.resize(event.size()) + self.ui.busy_balance.resize(event.size()) super().resizeEvent(event) def changeEvent(self, event): diff --git a/src/sakia/tests/unit/gui/test_transactions_tab.py b/src/sakia/tests/unit/gui/test_transactions_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..9efecc709dc68280c08595819aa94cc0c554e36e --- /dev/null +++ b/src/sakia/tests/unit/gui/test_transactions_tab.py @@ -0,0 +1,68 @@ +import unittest +from unittest.mock import patch, MagicMock, Mock +from asynctest.mock import CoroutineMock +from asynctest.mock import MagicMock as AsyncMagicMock +from PyQt5.QtCore import QLocale, pyqtSignal +from sakia.tests import QuamashTest +from sakia.tests.mocks.bma import nice_blockchain +from sakia.gui.transactions_tab import TransactionsTabWidget, Ui_transactionsTabWidget, QWidget + + +class TestTransactionTab(unittest.TestCase, QuamashTest): + def setUp(self): + self.setUpQuamash() + QLocale.setDefault(QLocale("en_GB")) + + self.identity = Mock(specs='core.registry.Identity') + self.identity.pubkey = "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" + self.identity.uid = "A" + + def tearDown(self): + self.tearDownQuamash() + + @patch('sakia.gen_resources.transactions_tab_uic.Ui_transactionsTabWidget', create=True) + @patch('PyQt5.QtWidgets.QWidget', create=True) + def test_view_in_wot(self, widget, view): + wot_refreshed = False + + def refresh_wot(identity): + nonlocal wot_refreshed + self.assertEqual(identity, self.identity) + wot_refreshed = True + + app = Mock('sakia.core.Application') + async def exec_test(): + transaction_tab = TransactionsTabWidget(app, widget, view) + transaction_tab.view_in_wot.connect(refresh_wot) + transaction_tab.view_wot(self.identity) + + self.lp.run_until_complete(exec_test()) + self.assertTrue(wot_refreshed) + + @patch('sakia.gen_resources.transactions_tab_uic.Ui_transactionsTabWidget', create=True) + @patch('PyQt5.QtWidgets.QWidget', create=True) + def copy_pubkey_to_clipboard(self, widget, view): + app = Mock('sakia.core.Application') + async def exec_test(): + transaction_tab = TransactionsTabWidget(app, widget, view) + transaction_tab.copy_pubkey_to_clipboard(self.identity) + self.lp.run_until_complete(exec_test()) + self.assertEqual(self.qapplication.clipboard().text(), "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk") + + @patch('sakia.gen_resources.transactions_tab_uic.Ui_transactionsTabWidget', create=True) + @patch('PyQt5.QtWidgets.QWidget', create=True) + def test_copy_block_to_clipboard(self, widget, view): + app = AsyncMagicMock('sakia.core.Application') + community = Mock('sakia.core.Community') + community.get_block = CoroutineMock(side_effect=lambda n: nice_blockchain.bma_blockchain_current if n == 15 \ + else nice_blockchain.bma_blockchain_0) + self.qapplication.clipboard().clear() + async def exec_test(): + transaction_tab = TransactionsTabWidget(app, widget, view) + transaction_tab.community = community + transaction_tab.copy_block_to_clipboard(15) + + self.lp.run_until_complete(exec_test()) + raw_block = "{0}{1}\n".format(nice_blockchain.bma_blockchain_current["raw"], + nice_blockchain.bma_blockchain_current["signature"]) + self.assertEqual(self.qapplication.clipboard().text(), raw_block)