From 105c5b8acfc10816782bddb7889eede3e2a8839b Mon Sep 17 00:00:00 2001 From: heartsucker Date: Mon, 7 Jan 2019 12:24:37 +0100 Subject: [PATCH] attempt to conect signals and slots --- securedrop_client/gui/main.py | 2 +- securedrop_client/gui/widgets.py | 62 ++++++++++++++++++++++--------- securedrop_client/logic.py | 55 ++++++++++++++++----------- securedrop_client/message_sync.py | 23 ++++++++++-- 4 files changed, 97 insertions(+), 45 deletions(-) diff --git a/securedrop_client/gui/main.py b/securedrop_client/gui/main.py index af2506269c..a412834894 100644 --- a/securedrop_client/gui/main.py +++ b/securedrop_client/gui/main.py @@ -176,7 +176,7 @@ def show_conversation_for(self, source): conversation_container = self.conversations.get(source.uuid, None) if conversation_container is None: - conversation = ConversationView(source, self.sdc_home, parent=self) + conversation = ConversationView(source, self.sdc_home, self.controller, parent=self) conversation.setup(self.controller) conversation_container = QWidget() diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index c79631125c..cd84f41e7d 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -19,12 +19,14 @@ import logging import arrow import html -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \ QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, \ QMessageBox, QToolButton +from uuid import UUID +from securedrop_client.logic import Client from securedrop_client.resources import load_svg, load_image from securedrop_client.storage import get_data from securedrop_client.utils import humanize_filesize @@ -485,15 +487,28 @@ class SpeechBubble(QWidget): css = "padding: 10px; border: 1px solid #999; border-radius: 20px;" - def __init__(self, text): + def __init__(self, message_id: UUID, text: str, update_signal) -> None: super().__init__() + self.message_id = message_id + layout = QVBoxLayout() self.setLayout(layout) - message = QLabel(html.escape(text, quote=False)) - message.setWordWrap(True) + self.message = QLabel(html.escape(text, quote=False)) + self.message.setWordWrap(True) - layout.addWidget(message) + layout.addWidget(self.message) + + update_signal.connect(self.__update_text) + + @pyqtSlot(UUID, str) + def __update_text(self, message_id: UUID, text: str) -> None: + """ + Conditionally update this SpeechBubble's text if and only if the message_id of the emitted + signal matche the message_id of this speech bubble. + """ + if message_id == self.message_id: + self.message.setText(html.escape(text, quote=False)) class ConversationWidget(QWidget): @@ -501,7 +516,11 @@ class ConversationWidget(QWidget): Draws a message onto the screen. """ - def __init__(self, message, align): + def __init__(self, + message_id: UUID, + message: str, + update_signal, + align: str) -> None: """ Initialise with the message to display and some notion of which side of the conversation ("left" or "right" [anything else]) to which the @@ -509,7 +528,7 @@ def __init__(self, message, align): """ super().__init__() layout = QHBoxLayout() - label = SpeechBubble(message) + label = SpeechBubble(message_id, message, update_signal) if align is not "left": # Float right... @@ -534,8 +553,11 @@ class MessageWidget(ConversationWidget): Represents an incoming message from the source. """ - def __init__(self, message): - super().__init__(message, align="left") + def __init__(self, message_id: UUID, message: str, controller: Client) -> None: + super().__init__(message_id, + message, + controller.message_sync.message_downloaded, + align="left") self.setStyleSheet(""" background-color: #EEE; """) @@ -546,8 +568,11 @@ class ReplyWidget(ConversationWidget): Represents a reply to a source. """ - def __init__(self, message): - super().__init__(message, align="right") + def __init__(self, message_id: UUID, message: str, controller: Client) -> None: + super().__init__(message_id, + message, + controller.reply_sync.reply_downloaded, + align="right") self.setStyleSheet(""" background-color: #2299EE; """) @@ -613,10 +638,11 @@ class ConversationView(QWidget): Renders a conversation. """ - def __init__(self, source_db_object, sdc_home: str, parent=None): + def __init__(self, source_db_object, sdc_home: str, controller, parent=None): super().__init__(parent) self.source = source_db_object self.sdc_home = sdc_home + self.controller = controller self.container = QWidget() self.conversation_layout = QVBoxLayout() @@ -667,9 +693,9 @@ def add_item_content_or(self, adder, item, default): Private helper function to add correct message to conversation widgets """ if item.is_downloaded is False: - adder(default) + adder(item.uuid, default) else: - adder(get_data(self.sdc_home, item.filename)) + adder(item.uuid, get_data(self.sdc_home, item.filename)) def setup(self, controller): """ @@ -692,17 +718,17 @@ def move_to_bottom(self, min_val, max_val): """ self.scroll.verticalScrollBar().setValue(max_val) - def add_message(self, message): + def add_message(self, message_id: UUID, message: str) -> None: """ Add a message from the source. """ - self.conversation_layout.addWidget(MessageWidget(message)) + self.conversation_layout.addWidget(MessageWidget(message_id, message, self.controller)) - def add_reply(self, reply, files=None): + def add_reply(self, message_id: UUID, reply: str, files=None) -> None: """ Add a reply from a journalist. """ - self.conversation_layout.addWidget(ReplyWidget(reply)) + self.conversation_layout.addWidget(ReplyWidget(message_id, reply, self.controller)) class DeleteSourceAction(QAction): diff --git a/securedrop_client/logic.py b/securedrop_client/logic.py index 2467c7c693..d5a0df9cfd 100644 --- a/securedrop_client/logic.py +++ b/securedrop_client/logic.py @@ -96,22 +96,42 @@ def __init__(self, hostname, gui, session, various other layers of the application: the location of the SecureDrop proxy, the user interface and SqlAlchemy local storage respectively. """ - check_dir_permissions(home) - super().__init__() - self.hostname = hostname # Location of the SecureDrop server. - self.gui = gui # Reference to the UI window. - self.api = None # Reference to the API for secure drop proxy. - self.session = session # Reference to the SqlAlchemy session. - self.message_thread = None # thread responsible for fetching messages - self.reply_thread = None # thread responsible for fetching replies - self.home = home # used for finding DB in sync thread - self.api_threads = {} # Contains active threads calling the API. - self.sync_flag = os.path.join(home, 'sync_flag') - self.data_dir = os.path.join(self.home, 'data') # File data. - self.timer = None # call timeout timer + + # used for finding DB in sync thread + self.home = home + + # boolean flag for whether or not the client is operating behind a proxy self.proxy = proxy + + # Location of the SecureDrop server. + self.hostname = hostname + + # Reference to the UI window. + self.gui = gui + + # Reference to the API for secure drop proxy. + self.api = None + # Contains active threads calling the API. + self.api_threads = {} + + # Reference to the SqlAlchemy session. + self.session = session + + # thread responsible for fetching messages + self.message_thread = None + self.message_sync = MessageSync(self.api, self.home, self.proxy) + + # thread responsible for fetching replies + self.reply_thread = None + self.reply_sync = ReplySync(self.api, self.home, self.proxy) + + self.sync_flag = os.path.join(home, 'sync_flag') + + # File data. + self.data_dir = os.path.join(self.home, 'data') + self.gpg = GpgHelper(home, proxy) def setup(self): @@ -143,13 +163,6 @@ def setup(self): self.sync_update.timeout.connect(self.sync_api) self.sync_update.start(1000 * 60 * 5) # every 5 minutes. - # Use a QTimer to update the current conversation view such - # that as downloads/decryption occur, the messages and replies - # populate the view. - self.conv_view_update = QTimer() - self.conv_view_update.timeout.connect(self.update_conversation_views) - self.conv_view_update.start(1000 * 6) # every 6 seconds - def call_api(self, function, callback, timeout, *args, current_object=None, **kwargs): """ @@ -233,7 +246,6 @@ def start_message_thread(self): """ if not self.message_thread: self.message_thread = QThread() - self.message_sync = MessageSync(self.api, self.home, self.proxy) self.message_sync.moveToThread(self.message_thread) self.message_thread.started.connect(self.message_sync.run) self.message_thread.start() @@ -246,7 +258,6 @@ def start_reply_thread(self): """ if not self.reply_thread: self.reply_thread = QThread() - self.reply_sync = ReplySync(self.api, self.home, self.proxy) self.reply_sync.moveToThread(self.reply_thread) self.reply_thread.started.connect(self.reply_sync.run) self.reply_thread.start() diff --git a/securedrop_client/message_sync.py b/securedrop_client/message_sync.py index b4afb13a45..67698ea647 100644 --- a/securedrop_client/message_sync.py +++ b/securedrop_client/message_sync.py @@ -22,12 +22,13 @@ import logging import sdclientapi.sdlocalobjects as sdkobjects -from PyQt5.QtCore import QObject +from PyQt5.QtCore import QObject, pyqtSignal from securedrop_client import storage from securedrop_client.crypto import GpgHelper from securedrop_client.db import make_engine - +from securedrop_client.storage import get_data from sqlalchemy.orm import sessionmaker +from uuid import UUID logger = logging.getLogger(__name__) @@ -58,6 +59,12 @@ class MessageSync(APISyncObject): Runs in the background, finding messages to download and downloading them. """ + """ + Signal emitted notifying that a message has been downloaded. The signal is a tuple of + (UUID, str) containing the message's UUID and the content of the message. + """ + message_downloaded = pyqtSignal([UUID, str]) + def __init__(self, api, home, is_qubes): super().__init__(api, home, is_qubes) @@ -79,6 +86,8 @@ def run(self, loop=True): db_submission, self.api.download_submission, storage.mark_file_as_downloaded) + self.message_downloaded.emit(db_submission.uuid, + get_data(self.home, db_submission.filename)) except Exception as e: logger.critical( @@ -96,6 +105,12 @@ class ReplySync(APISyncObject): Runs in the background, finding replies to download and downloading them. """ + """ + Signal emitted notifying that a reply has been downloaded. The signal is a tuple of + (UUID, str) containing the message's UUID and the content of the reply. + """ + reply_downloaded = pyqtSignal([UUID, str]) + def __init__(self, api, home, is_qubes): super().__init__(api, home, is_qubes) @@ -105,7 +120,6 @@ def run(self, loop=True): for db_reply in replies: try: - # the API wants API objects. here in the client, # we have client objects. let's take care of that # here @@ -121,7 +135,8 @@ def run(self, loop=True): db_reply, self.api.download_reply, storage.mark_reply_as_downloaded) - + self.reply_downloaded.emit(db_reply.uuid, + get_data(self.home, db_reply.filename)) except Exception as e: logger.critical( "Exception while downloading reply! {}".format(e)