diff --git a/src/sakia/data/entities/node.py b/src/sakia/data/entities/node.py index e3f9cac2887ecb82fc14b1fe27edf93e499b5061..adf1c4db756bbdccee694510650e5a1a35f1a375 100644 --- a/src/sakia/data/entities/node.py +++ b/src/sakia/data/entities/node.py @@ -1,5 +1,6 @@ import attr from duniterpy.documents import block_uid, endpoint +from sakia.helpers import attrs_tuple_of_str def _tuple_of_endpoints(value): @@ -18,18 +19,6 @@ def _tuple_of_endpoints(value): raise TypeError("Can't convert {0} to list of endpoints".format(value)) -def _tuple_of_hashes(ls): - if isinstance(ls, tuple): - return ls - elif isinstance(ls, list): - return tuple([str(a) for a in ls]) - elif isinstance(ls, str): - if ls: # if string is not empty - return tuple([str(a) for a in ls.split('\n')]) - else: - return tuple() - - @attr.s() class Node: """ @@ -73,7 +62,7 @@ class Node: merkle_peers_root = attr.ib(convert=str, cmp=False, default=MERKLE_EMPTY_ROOT) # Leaves of the merkle peers tree - merkle_peers_leaves = attr.ib(convert=_tuple_of_hashes, cmp=False, default=tuple()) + merkle_peers_leaves = attr.ib(convert=attrs_tuple_of_str, cmp=False, default=tuple()) # Define if this node is a root node in Sakia root = attr.ib(convert=bool, cmp=False, default=False) # If this node is a member or not diff --git a/src/sakia/data/entities/transaction.py b/src/sakia/data/entities/transaction.py index 3180f8ea34079966ccf0bffca9afebd96a8e4e27..32d264205443d5e93fe6914361010397f22daf4c 100644 --- a/src/sakia/data/entities/transaction.py +++ b/src/sakia/data/entities/transaction.py @@ -1,6 +1,7 @@ import attr from duniterpy.documents import block_uid from duniterpy.documents.transaction import reduce_base +from sakia.helpers import attrs_tuple_of_str import math @@ -57,7 +58,7 @@ def parse_transaction_doc(tx_doc, pubkey, block_number, mediantime, txid): timestamp=mediantime, signature=tx_doc.signatures[0], issuer=tx_doc.issuers[0], - receiver=receivers[0], + receivers=receivers, amount=amount, amount_base=amount_base, comment=tx_doc.comment, @@ -99,7 +100,7 @@ class Transaction: timestamp = attr.ib(convert=int, cmp=False) signature = attr.ib(convert=str, cmp=False) issuer = attr.ib(convert=str, cmp=False) - receiver = attr.ib(convert=str, cmp=False) + receivers = attr.ib(convert=attrs_tuple_of_str, cmp=False) amount = attr.ib(convert=int, cmp=False) amount_base = attr.ib(convert=int, cmp=False) comment = attr.ib(convert=str, cmp=False) diff --git a/src/sakia/data/repositories/meta.sql b/src/sakia/data/repositories/meta.sql index 3b48df09de695779ea9b52159ce4ec81ab72a6e9..63785d61d3fe911ca9c72aec2b939fbba7c7d8e4 100644 --- a/src/sakia/data/repositories/meta.sql +++ b/src/sakia/data/repositories/meta.sql @@ -75,7 +75,7 @@ CREATE TABLE IF NOT EXISTS transactions( ts INT, signature VARCHAR(100), issuer VARCHAR(50), - receiver VARCHAR(50), + receiver TEXT, amount INT, amountbase INT, comment VARCHAR(255), diff --git a/src/sakia/data/repositories/transactions.py b/src/sakia/data/repositories/transactions.py index 5f735b6b64507f1fcbd331464fbe285c64f5874a..0ca6499e970a01823b45a352ccad07ad211a1272 100644 --- a/src/sakia/data/repositories/transactions.py +++ b/src/sakia/data/repositories/transactions.py @@ -15,7 +15,10 @@ class TransactionsRepo: Commit a transaction to the database :param sakia.data.entities.Transaction transaction: the transaction to commit """ - transaction_tuple = attr.astuple(transaction) + transaction_tuple = attr.astuple(transaction, tuple_factory=list) + + transaction_tuple[7] = "\n".join([str(n) for n in transaction_tuple[7]]) + values = ",".join(['?'] * len(transaction_tuple)) self._conn.execute("INSERT INTO transactions VALUES ({0})".format(values), transaction_tuple) @@ -93,7 +96,7 @@ class TransactionsRepo: :rtype: List[sakia.data.entities.Transaction] """ request = """SELECT * FROM transactions - WHERE currency=? AND (issuer=? or receiver=?) + WHERE currency=? AND (issuer=? or receiver LIKE ?) ORDER BY {sort_by} {sort_order} LIMIT {limit} OFFSET {offset}""" \ .format(offset=offset, @@ -101,7 +104,7 @@ class TransactionsRepo: sort_by=sort_by, sort_order=sort_order ) - c = self._conn.execute(request, (currency, pubkey, pubkey)) + c = self._conn.execute(request, (currency, pubkey, "%" + pubkey + "%")) datas = c.fetchall() if datas: return [Transaction(*data) for data in datas] diff --git a/src/sakia/gui/dialogs/transfer/controller.py b/src/sakia/gui/dialogs/transfer/controller.py index 693660cd4156b88133979784a2b71f66bf80a9ef..f5f49906deb7194d8e783c85e3b857aeeb5eff67 100644 --- a/src/sakia/gui/dialogs/transfer/controller.py +++ b/src/sakia/gui/dialogs/transfer/controller.py @@ -89,7 +89,7 @@ class TransferController(QObject): def send_transfer_again(cls, parent, app, connection, resent_transfer): dialog = cls.create(parent, app) dialog.view.combo_connections.setCurrentText(connection.title()) - dialog.view.edit_pubkey.setText(resent_transfer.receiver) + dialog.view.edit_pubkey.setText(resent_transfer.receivers[0]) dialog.view.radio_pubkey.setChecked(True) dialog.refresh() @@ -105,7 +105,7 @@ class TransferController(QObject): connections_processor = ConnectionsProcessor.instanciate(app) wallet_index = connections_processor.connections().index(connection) dialog.view.combo_connections.setCurrentIndex(wallet_index) - dialog.view.edit_pubkey.setText(resent_transfer.receiver) + dialog.view.edit_pubkey.setText(resent_transfer.receivers[0]) dialog.view.radio_pubkey.setChecked(True) dialog.view.edit_message.setText(resent_transfer.comment) diff --git a/src/sakia/gui/navigation/txhistory/controller.py b/src/sakia/gui/navigation/txhistory/controller.py index a827c6861d8650e3bbf8fffeb4209c18494cbfec..ecc03fbf99d839524459f31cbeca5a4271b97f63 100644 --- a/src/sakia/gui/navigation/txhistory/controller.py +++ b/src/sakia/gui/navigation/txhistory/controller.py @@ -78,7 +78,7 @@ class TxHistoryController(QObject): if transfer.issuer != self.model.connection.pubkey: pubkey = transfer.issuer else: - pubkey = transfer.receiver + pubkey = transfer.receivers[0] identity = Identity(currency=transfer.currency, pubkey=pubkey) menu = ContextMenu.from_data(self.view, self.model.app, self.model.connection, (identity, transfer)) menu.view_identity_in_wot.connect(self.view_in_wot) diff --git a/src/sakia/gui/navigation/txhistory/delegate.py b/src/sakia/gui/navigation/txhistory/delegate.py new file mode 100644 index 0000000000000000000000000000000000000000..70c08ee03c2e347c3e862cf8ff5f80ef1b0f3ee3 --- /dev/null +++ b/src/sakia/gui/navigation/txhistory/delegate.py @@ -0,0 +1,41 @@ +from PyQt5.QtGui import QTextDocument, QAbstractTextDocumentLayout +from PyQt5.QtWidgets import QStyledItemDelegate, QApplication, QStyle +from PyQt5.QtCore import QSize +from .table_model import HistoryTableModel + + +class TxHistoryDelegate(QStyledItemDelegate): + def paint(self, painter, option, index): + self.initStyleOption(option, index) + + style = QApplication.style() + + doc = QTextDocument() + if index.column() == HistoryTableModel.columns_types.index('uid'): + doc.setHtml(option.text) + else: + doc.setPlainText(option.text) + + option.text = "" + style.drawControl(QStyle.CE_ItemViewItem, option, painter) + + ctx = QAbstractTextDocumentLayout.PaintContext() + + text_rect = style.subElementRect(QStyle.SE_ItemViewItemText, option) + painter.save() + painter.translate(text_rect.topLeft()) + painter.setClipRect(text_rect.translated(-text_rect.topLeft())) + doc.documentLayout().draw(painter, ctx) + + painter.restore() + + def sizeHint(self, option, index): + self.initStyleOption(option, index) + + doc = QTextDocument() + if index.column() == HistoryTableModel.columns_types.index('uid'): + doc.setHtml(option.text) + else: + doc.setPlainText("") + doc.setTextWidth(option.rect.width()) + return QSize(doc.idealWidth(), doc.size().height()) diff --git a/src/sakia/gui/navigation/txhistory/table_model.py b/src/sakia/gui/navigation/txhistory/table_model.py index ddd2d3cf2072467bb2560698626f2fbc3b1a88e9..534c732edc32c488dc05694aafc8e9162ec341bb 100644 --- a/src/sakia/gui/navigation/txhistory/table_model.py +++ b/src/sakia/gui/navigation/txhistory/table_model.py @@ -2,7 +2,7 @@ import datetime import logging from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \ - QDateTime, QLocale, QModelIndex + QDateTime, QLocale, QModelIndex, QT_TRANSLATE_NOOP from PyQt5.QtGui import QFont, QColor from sakia.data.entities import Transaction from sakia.constants import MAX_CONFIRMATIONS @@ -93,7 +93,7 @@ class TxFilterProxyModel(QSortFilterProxyModel): if role == Qt.DisplayRole: if source_index.column() == model.columns_types.index('uid'): - return source_data + return "<p>" + source_data.replace('\n', "<br>") + "</p>" if source_index.column() == model.columns_types.index('date'): return QLocale.toString( QLocale(), @@ -160,6 +160,26 @@ class HistoryTableModel(QAbstractTableModel): DIVIDEND = 32 + columns_types = ( + 'date', + 'uid', + 'amount', + 'comment', + 'state', + 'txid', + 'pubkey', + 'block_number', + 'txhash', + 'raw_data' + ) + + columns_headers = ( + QT_TRANSLATE_NOOP("HistoryTableModel", 'Date'), + QT_TRANSLATE_NOOP("HistoryTableModel", 'UID/Public key'), + QT_TRANSLATE_NOOP("HistoryTableModel", 'Amount'), + QT_TRANSLATE_NOOP("HistoryTableModel", 'Comment') + ) + def __init__(self, parent, app, connection, identities_service, transactions_service): """ History of all transactions @@ -177,26 +197,6 @@ class HistoryTableModel(QAbstractTableModel): self.transactions_service = transactions_service self.transfers_data = [] - self.columns_types = ( - 'date', - 'uid', - 'amount', - 'comment', - 'state', - 'txid', - 'pubkey', - 'block_number', - 'txhash', - 'raw_data' - ) - - self.column_headers = ( - lambda: self.tr('Date'), - lambda: self.tr('UID/Public key'), - lambda: self.tr('Amount'), - lambda: self.tr('Comment') - ) - def transfers(self): """ Transfer @@ -212,11 +212,11 @@ class HistoryTableModel(QAbstractTableModel): return self.transactions_service.dividends(self.connection.pubkey) def add_transfer(self, transfer): - if self.connection.pubkey in (transfer.issuer, transfer.receiver): + if self.connection.pubkey in (transfer.issuer, *transfer.receivers): self.beginInsertRows(QModelIndex(), len(self.transfers_data), len(self.transfers_data)) if transfer.issuer == self.connection.pubkey: self.transfers_data.append(self.data_sent(transfer)) - if transfer.receiver == self.connection.pubkey: + if self.connection.pubkey in transfer.receivers: self.transfers_data.append(self.data_received(transfer)) self.endInsertRows() @@ -228,7 +228,7 @@ class HistoryTableModel(QAbstractTableModel): def change_transfer(self, transfer): for i, data in enumerate(self.transfers_data): - if data[self.columns_types.index('txhash')] == transfer.sha_hash: + if data[HistoryTableModel.columns_types.index('txhash')] == transfer.sha_hash: if transfer.state == Transaction.DROPPED: self.beginRemoveRows(QModelIndex(), i, i) self.transfers_data.pop(i) @@ -236,10 +236,10 @@ class HistoryTableModel(QAbstractTableModel): else: if transfer.issuer == self.connection.pubkey: self.transfers_data[i] = self.data_sent(transfer) - self.dataChanged.emit(self.index(i, 0), self.index(i, len(self.columns_types))) - if transfer.receiver == self.connection.pubkey: + self.dataChanged.emit(self.index(i, 0), self.index(i, len(HistoryTableModel.columns_types))) + if self.connection.pubkey in transfer.receivers: self.transfers_data[i] = self.data_received(transfer) - self.dataChanged.emit(self.index(i, 0), self.index(i, len(self.columns_types))) + self.dataChanged.emit(self.index(i, 0), self.index(i, len(HistoryTableModel.columns_types))) return def data_received(self, transfer): @@ -274,16 +274,18 @@ class HistoryTableModel(QAbstractTableModel): block_number = transfer.written_block amount = transfer.amount * 10**transfer.amount_base * -1 - identity = self.identities_service.get_identity(transfer.receiver) - if identity: - receiver = identity.uid - else: - receiver = transfer.receiver + receivers = [] + for receiver in transfer.receivers: + identity = self.identities_service.get_identity(receiver) + if identity: + receivers.append(identity.uid) + else: + receivers.append(receiver) date_ts = transfer.timestamp txid = transfer.txid - return (date_ts, receiver, amount, transfer.comment, transfer.state, txid, - transfer.receiver, block_number, transfer.sha_hash, transfer) + return (date_ts, receivers, amount, transfer.comment, transfer.state, txid, + "\n".join(transfer.receivers), block_number, transfer.sha_hash, transfer) def data_dividend(self, dividend): """ @@ -312,7 +314,7 @@ class HistoryTableModel(QAbstractTableModel): if transfer.state != Transaction.DROPPED: if transfer.issuer == self.connection.pubkey: self.transfers_data.append(self.data_sent(transfer)) - if transfer.receiver == self.connection.pubkey: + if self.connection.pubkey in transfer.receivers: self.transfers_data.append(self.data_received(transfer)) dividends = self.dividends() for dividend in dividends: @@ -323,18 +325,18 @@ class HistoryTableModel(QAbstractTableModel): return len(self.transfers_data) def columnCount(self, parent): - return len(self.columns_types) + return len(HistoryTableModel.columns_types) def headerData(self, section, orientation, role): if role == Qt.DisplayRole: - if self.columns_types[section] == 'amount': + if HistoryTableModel.columns_types[section] == 'amount': dividend, base = self.blockchain_processor.last_ud(self.transactions_service.currency) - header = '{:}'.format(self.column_headers[section]()) + header = '{:}'.format(HistoryTableModel.columns_headers[section]) if self.app.current_ref.base_str(base): header += " ({:})".format(self.app.current_ref.base_str(base)) return header - return self.column_headers[section]() + return HistoryTableModel.columns_headers[section] def data(self, index, role): row = index.row() diff --git a/src/sakia/gui/navigation/txhistory/view.py b/src/sakia/gui/navigation/txhistory/view.py index f20ff94d26615eb3ca716acd236e7963001c865f..92d243ab693e7839ef29c8a1374bfd86e4bdb195 100644 --- a/src/sakia/gui/navigation/txhistory/view.py +++ b/src/sakia/gui/navigation/txhistory/view.py @@ -1,5 +1,6 @@ from PyQt5.QtCore import QDateTime, QEvent from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView +from .delegate import TxHistoryDelegate from .txhistory_uic import Ui_TxHistoryWidget @@ -30,6 +31,7 @@ class TxHistoryView(QWidget, Ui_TxHistoryWidget): self.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) self.table_history.setSortingEnabled(True) self.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) + self.table_history.setItemDelegate(TxHistoryDelegate()) model.modelReset.connect(self.table_history.resizeColumnsToContents) def set_minimum_maximum_datetime(self, minimum, maximum): diff --git a/src/sakia/helpers.py b/src/sakia/helpers.py index a12eeefa1488bc018dbabfb309f49766f7424abf..0e64a23937a01cbdc04c07e97ef311d34817863d 100644 --- a/src/sakia/helpers.py +++ b/src/sakia/helpers.py @@ -31,3 +31,15 @@ def single_instance_lock(): def cleanup_lock(lock): if lock.isAttached(): lock.detach() + + +def attrs_tuple_of_str(ls): + if isinstance(ls, tuple): + return ls + elif isinstance(ls, list): + return tuple([str(a) for a in ls]) + elif isinstance(ls, str): + if ls: # if string is not empty + return tuple([str(a) for a in ls.split('\n')]) + else: + return tuple() \ No newline at end of file diff --git a/tests/unit/data/test_transactions_repo.py b/tests/unit/data/test_transactions_repo.py index 6b2010d5c31856c2bf6cc31f4173c15f0f537e13..ea6b0fd3de1d45c444cdaeccc704d4a0c3d51961 100644 --- a/tests/unit/data/test_transactions_repo.py +++ b/tests/unit/data/test_transactions_repo.py @@ -28,7 +28,7 @@ def test_add_get_drop_transaction(meta_repo): assert transaction.amount == 1565 assert transaction.amount_base == 1 assert transaction.issuer == "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" - assert transaction.receiver == "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" + assert transaction.receivers[0] == "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" assert transaction.comment == "" assert transaction.txid == 0 transactions_repo.drop(transaction) @@ -66,7 +66,7 @@ def test_add_get_multiple_transaction(meta_repo): Transaction.TO_SEND)) transactions = transactions_repo.get_all(currency="testcurrency") assert "testcurrency" in [t.currency for t in transactions] - assert "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" in [t.receiver for t in transactions] + assert "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" in [t.receivers[0] for t in transactions] assert "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" in [t.issuer for t in transactions]