Skip to content

Commit

Permalink
updated tests for signals/slots
Browse files Browse the repository at this point in the history
  • Loading branch information
heartsucker committed Jan 9, 2019
1 parent 4dfdafc commit d53c47a
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 69 deletions.
4 changes: 1 addition & 3 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
ConversationView,
SourceProfileShortWidget)
from securedrop_client.resources import load_icon
from securedrop_client.storage import get_data

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -177,8 +176,7 @@ def show_conversation_for(self, source):
conversation_container = self.conversations.get(source.uuid, None)

if conversation_container is None:
conversation = ConversationView(source, parent=self)
conversation.setup(self.controller)
conversation = ConversationView(source, self.sdc_home, self.controller, parent=self)

conversation_container = QWidget()
layout = QVBoxLayout()
Expand Down
73 changes: 48 additions & 25 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
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 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

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -484,31 +486,48 @@ class SpeechBubble(QWidget):

css = "padding: 10px; border: 1px solid #999; border-radius: 20px;"

def __init__(self, text):
def __init__(self, message_id: str, 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(str, str)
def _update_text(self, message_id: str, 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):
"""
Draws a message onto the screen.
"""

def __init__(self, message, align):
def __init__(self,
message_id: str,
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
widget should belong.
"""
super().__init__()
layout = QHBoxLayout()
label = SpeechBubble(message)
label = SpeechBubble(message_id, message, update_signal)

if align is not "left":
# Float right...
Expand All @@ -533,8 +552,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: str, message: str, update_signal) -> None:
super().__init__(message_id,
message,
update_signal,
align="left")
self.setStyleSheet("""
background-color: #EEE;
""")
Expand All @@ -545,8 +567,11 @@ class ReplyWidget(ConversationWidget):
Represents a reply to a source.
"""

def __init__(self, message):
super().__init__(message, align="right")
def __init__(self, message_id: str, message: str, update_signal) -> None:
super().__init__(message_id,
message,
update_signal,
align="right")
self.setStyleSheet("""
background-color: #2299EE;
""")
Expand Down Expand Up @@ -612,9 +637,11 @@ class ConversationView(QWidget):
Renders a conversation.
"""

def __init__(self, source_db_object, parent=None):
def __init__(self, source_db_object, sdc_home: str, controller: Client, 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()
Expand Down Expand Up @@ -642,7 +669,7 @@ def update_conversation(self, collection: list) -> None:
# clear all old items
while True:
w = self.conversation_layout.takeAt(0)
if w:
if w: # pragma: no cover
del w
else:
break
Expand All @@ -665,15 +692,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(item.content)

def setup(self, controller):
"""
Ensure there's a reference to program logic.
"""
self.controller = controller
adder(item.uuid, get_data(self.sdc_home, item.filename))

def add_file(self, source_db_object, submission_db_object):
"""
Expand All @@ -690,17 +711,19 @@ 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: str, 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.message_sync.message_downloaded))

def add_reply(self, reply, files=None):
def add_reply(self, message_id: str, 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.reply_sync.reply_downloaded))


class DeleteSourceAction(QAction):
Expand Down
81 changes: 45 additions & 36 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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_view)
self.conv_view_update.start(1000 * 60 * 0.10) # every 6 seconds

def call_api(self, function, callback, timeout, *args, current_object=None,
**kwargs):
"""
Expand Down Expand Up @@ -232,8 +245,8 @@ def start_message_thread(self):
Starts the message-fetching thread in the background.
"""
if not self.message_thread:
self.message_sync.api = self.api
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()
Expand All @@ -245,8 +258,8 @@ def start_reply_thread(self):
Starts the reply-fetching thread in the background.
"""
if not self.reply_thread:
self.reply_sync.api = self.api
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()
Expand Down Expand Up @@ -406,8 +419,7 @@ def on_synced(self, result):
except CryptoError:
logger.warning('Failed to import key for source {}'.format(source.uuid))

# TODO: show something in the conversation view?
# self.gui.show_conversation_for()
self.update_conversation_views()
else:
# How to handle a failure? Exceptions are already logged. Perhaps
# a message in the UI?
Expand All @@ -431,16 +443,14 @@ def update_sources(self):
self.gui.show_sources(sources)
self.update_sync()

def update_conversation_view(self):
def update_conversation_views(self):
"""
Updates the conversation view to reflect progress
of the download and decryption of messages and replies.
"""
# Redraw the conversation view if we have clicked on a source
# and the source has not been deleted.
if self.gui.current_source and self.gui.current_source in self.session:
self.session.refresh(self.gui.current_source)
self.gui.show_conversation_for(self.gui.current_source)
for conversation in self.gui.conversations.values():
self.session.refresh(conversation.source)
conversation.update_conversation(conversation.source.collection)

def on_update_star_complete(self, result):
"""
Expand Down Expand Up @@ -569,7 +579,9 @@ def on_file_downloaded(self, result, current_object):
# Attempt to decrypt the file.
self.gpg.decrypt_submission_or_reply(
filepath_in_datadir, server_filename, is_doc=True)
except CryptoError:
except CryptoError as e:
logger.debug('Failed to decrypt file {}: {}'.format(server_filename, e))

self.set_status("Failed to download and decrypt file, "
"please try again.")
# TODO: We should save the downloaded content, and just
Expand All @@ -579,12 +591,9 @@ def on_file_downloaded(self, result, current_object):
# Now that download and decrypt are done, mark the file as such.
storage.mark_file_as_downloaded(file_uuid, self.session)

# Refresh the current source conversation, bearing in mind
# that the user may have navigated to another source.
self.gui.show_conversation_for(self.gui.current_source)
self.set_status(
'Finished downloading {}'.format(current_object.filename))
self.set_status('Finished downloading {}'.format(current_object.filename))
else: # The file did not download properly.
logger.debug('Failed to download file {}'.format(server_filename))
# Update the UI in some way to indicate a failure state.
self.set_status("The file download failed. Please try again.")

Expand Down
Loading

0 comments on commit d53c47a

Please sign in to comment.