Skip to content

Commit

Permalink
WiP for file download. Behaviour for decryption needs to be decided f…
Browse files Browse the repository at this point in the history
…or further progress.
  • Loading branch information
ntoll authored and redshiftzero committed Oct 25, 2018
1 parent 456f248 commit 3846610
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 20 deletions.
23 changes: 9 additions & 14 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions securedrop_client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

def init(sdc_home: str) -> None:
safe_mkdir(sdc_home)
safe_mkdir(sdc_home, 'data')


def excepthook(*exc_args):
Expand Down
1 change: 1 addition & 0 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def show_conversation_for(self, source):
TODO: Finish this...
"""
conversation = ConversationView(self)
conversation.setup(self.controller)
conversation.add_message('Source name: {}'.format(
source.journalist_designation))
conversation.add_message('Hello, hello, is this thing switched on?')
Expand Down
24 changes: 21 additions & 3 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,10 +429,14 @@ class FileWidget(QWidget):
Represents a file attached to a message.
"""

def __init__(self, text, align):
def __init__(self, text, align, message, controller):
"""
Given some text, an indication of alignment ('left' or 'right') and
a reference to the controller, make something to display a file.
"""
super().__init__()
self.message = message
self.controller = controller
layout = QHBoxLayout()
icon = QLabel()
icon.setPixmap(load_image('file.png'))
Expand All @@ -447,6 +451,12 @@ def __init__(self, text, align):
layout.addStretch(5)
self.setLayout(layout)

def mouseDoubleClickEvent(self, e):
"""
Handle a double-click via the program logic.
"""
self.controller.on_file_click(self.message)


class ConversationView(QWidget):
"""
Expand All @@ -470,14 +480,21 @@ def __init__(self, parent):
main_layout.addWidget(scroll)
self.setLayout(main_layout)

def setup(self, controller):
"""
Ensure there's a reference to program logic.
"""
self.controller = controller

def add_message(self, message, files=None):
"""
Add a message from the source.
"""
self.conversation_layout.addWidget(MessageWidget(message))
if files:
for f in files:
self.conversation_layout.addWidget(FileWidget(f, 'left'))
self.conversation_layout.addWidget(FileWidget(f, 'left',
message, self.controller))

def add_reply(self, reply, files=None):
"""
Expand All @@ -486,4 +503,5 @@ def add_reply(self, reply, files=None):
self.conversation_layout.addWidget(ReplyWidget(reply))
if files:
for f in files:
self.conversation_layout.addWidget(FileWidget(f, 'right'))
self.conversation_layout.addWidget(FileWidget(f, 'right',
reply, self.controller))
35 changes: 35 additions & 0 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ def __init__(self, hostname, gui, session, home: str) -> None:
self.session = session # Reference to the SqlAlchemy session.
self.api_thread = None # Currently active API call thread.
self.sync_flag = os.path.join(home, 'sync_flag')
self.home = home # The "home" directory for client files.
self.data_dir = os.path.join(self.home, 'data') # File data.

def setup(self):
"""
Expand Down Expand Up @@ -313,3 +315,36 @@ def set_status(self, message, duration=5000):
duration.
"""
self.gui.set_status(message, duration)

def on_file_click(self, message):
"""
Download the file associated with the associated message (which may
be a Submission or Reply).
"""
if isinstance(message, sdclientapi.Submission):
# Handle sources.
func = self.api.download_submission
else:
# Handle journalist's replies.
func = self.api.download_reply
self.call_api(func, self.on_file_download,
self.on_download_timeout, message, self.data_dir)

def on_file_download(self):
"""
Called when a file has downloaded. Cause a refresh to the conversation
view to display the contents of the new file.
"""
if result: # pragma: no cover
# Refresh the conversation with the content of the downloaded file.
pass
else: # pragma: no cover
# Update the UI in some way to indicate a failure state.
pass

def on_download_timeout(self):
"""
Called when downloading a file has timed out.
"""
# Update the UI in some way to indicate a failure state.
pass # pragma: no cover
2 changes: 1 addition & 1 deletion securedrop_client/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os


def safe_mkdir(sdc_home: str, relative_path: str=None) -> None:
def safe_mkdir(sdc_home: str, relative_path: str = None) -> None:
'''
Safely create directories while checking permissions along the way.
'''
Expand Down
1 change: 1 addition & 0 deletions tests/gui/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ def test_conversation_for():
is called in the expected manner with dummy data.
"""
w = Window()
w.controller = mock.MagicMock()
w.main_view = mock.MagicMock()
mock_conview = mock.MagicMock()
mock_source = mock.MagicMock()
Expand Down
35 changes: 33 additions & 2 deletions tests/gui/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,22 +362,41 @@ def test_FileWidget_init_left():
"""
Check the FileWidget is configured correctly for align-left.
"""
fw = FileWidget('hello', 'left')
mock_message = mock.MagicMock()
mock_controller = mock.MagicMock()
fw = FileWidget('hello', 'left', mock_message, mock_controller)
layout = fw.layout()
assert isinstance(layout.takeAt(0), QWidgetItem)
assert isinstance(layout.takeAt(0), QWidgetItem)
assert isinstance(layout.takeAt(0), QSpacerItem)
assert fw.message == mock_message
assert fw.controller == mock_controller


def test_FileWidget_init_right():
"""
Check the FileWidget is configured correctly for align-right.
"""
fw = FileWidget('hello', 'right')
mock_message = mock.MagicMock()
mock_controller = mock.MagicMock()
fw = FileWidget('hello', 'right', mock_message, mock_controller)
layout = fw.layout()
assert isinstance(layout.takeAt(0), QSpacerItem)
assert isinstance(layout.takeAt(0), QWidgetItem)
assert isinstance(layout.takeAt(0), QWidgetItem)
assert fw.message == mock_message
assert fw.controller == mock_controller


def test_FileWidget_mouseDoubleClickEvent():
"""
Should fire the expected event handler in the logic layer.
"""
mock_message = mock.MagicMock()
mock_controller = mock.MagicMock()
fw = FileWidget('hello', 'right', mock_message, mock_controller)
fw.mouseDoubleClickEvent(None)
fw.controller.on_file_click.assert_called_once_with(mock_message)


def test_ConversationView_init():
Expand All @@ -388,12 +407,23 @@ def test_ConversationView_init():
assert isinstance(cv.conversation_layout, QVBoxLayout)


def test_ConversationView_setup():
"""
Ensure the controller is set
"""
cv = ConversationView(None)
mock_controller = mock.MagicMock()
cv.setup(mock_controller)
assert cv.controller == mock_controller


def test_ConversationView_add_message():
"""
Adding a message results in a new MessageWidget added to the layout. Any
associated files are added as FileWidgets.
"""
cv = ConversationView(None)
cv.controller = mock.MagicMock()
cv.conversation_layout = mock.MagicMock()
cv.add_message('hello', ['file1.pdf', ])
assert cv.conversation_layout.addWidget.call_count == 2
Expand All @@ -408,6 +438,7 @@ def test_ConversationView_add_reply():
associated files are added as FileWidgets.
"""
cv = ConversationView(None)
cv.controller = mock.MagicMock()
cv.conversation_layout = mock.MagicMock()
cv.add_reply('hello', ['file1.pdf', ])
assert cv.conversation_layout.addWidget.call_count == 2
Expand Down
36 changes: 36 additions & 0 deletions tests/test_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,3 +566,39 @@ def func() -> None:
else:
with pytest.raises(RuntimeError):
func()


def test_Client_on_file_click_Submission(safe_tmpdir):
"""
If the handler is passed a submission, check the download_submission
function is the one called against the API.
"""
mock_gui = mock.MagicMock()
mock_session = mock.MagicMock()
cl = Client('http://localhost', mock_gui, mock_session, str(safe_tmpdir))
s = sdclientapi.Submission(download_url='foo', filename='test',
is_read=True, size=123, source_url='foo/bar',
submission_url='bar', uuid='test')
cl.call_api = mock.MagicMock()
cl.api = mock.MagicMock()
cl.on_file_click(s)
cl.call_api.assert_called_once_with(cl.api.download_submission,
cl.on_file_download,
cl.on_download_timeout, s, cl.data_dir)


def test_Client_on_file_click_Reply(safe_tmpdir):
"""
If the handler is passed a reply, check the download_reply
function is the one called against the API.
"""
mock_gui = mock.MagicMock()
mock_session = mock.MagicMock()
cl = Client('http://localhost', mock_gui, mock_session, str(safe_tmpdir))
r = mock.MagicMock() # Not a sdclientapi.Submission
cl.call_api = mock.MagicMock()
cl.api = mock.MagicMock()
cl.on_file_click(r)
cl.call_api.assert_called_once_with(cl.api.download_reply,
cl.on_file_download,
cl.on_download_timeout, r, cl.data_dir)

0 comments on commit 3846610

Please sign in to comment.