Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

save in-progress replies via persisting SourceConversationWrapper #431

Merged
merged 2 commits into from
Jun 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import sys

from gettext import gettext as _
from typing import List
from typing import Dict, List # noqa: F401
from uuid import uuid4
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QTimer, QSize, pyqtBoundSignal, QObject
from PyQt5.QtGui import QIcon, QPalette, QBrush, QColor, QFont, QLinearGradient
Expand Down Expand Up @@ -609,6 +609,8 @@ def __init__(self, parent: QObject):
self.layout.addWidget(self.source_list)
self.layout.addWidget(self.view_holder)

self.source_conversations = {} # type: Dict[Source, SourceConversationWrapper]

def setup(self, controller):
"""
Pass through the controller object to this widget.
Expand All @@ -631,7 +633,14 @@ def on_source_changed(self):
source = self.source_list.get_current_source()

if source:
conversation_wrapper = SourceConversationWrapper(source, self.controller)
# Try to get the SourceConversationWrapper from the persistent dict,
# else we create it.
try:
conversation_wrapper = self.source_conversations[source]
except KeyError:
conversation_wrapper = SourceConversationWrapper(source, self.controller)
self.source_conversations[source] = conversation_wrapper

self.set_conversation(conversation_wrapper)
else:
self.clear_conversation()
Expand Down
49 changes: 49 additions & 0 deletions tests/gui/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,55 @@ def test_MainView_on_source_changed_updates_conversation_view(mocker, session):
assert add_file_fn.call_count == 1


def test_MainView_on_source_changed_SourceConversationWrapper_is_preserved(mocker, session):
"""
SourceConversationWrapper contains ReplyBoxWidget - this tests that we do not recreate
SourceConversationWrapper when we click away from a given source. We should create it the
first time, and then it should persist.
"""
mv = MainView(None)
mv.source_list = mocker.MagicMock()
mv.set_conversation = mocker.MagicMock()
mv.controller = mocker.MagicMock(is_authenticated=True)
source = factory.Source()
source2 = factory.Source()
session.add(source)
session.add(source2)
session.commit()

source_conversation_init = mocker.patch(
'securedrop_client.gui.widgets.SourceConversationWrapper.__init__', return_value=None)

# We expect on the first call, SourceConversationWrapper.__init__ should be called.
mv.source_list.get_current_source = mocker.MagicMock(return_value=source)
mv.on_source_changed()
assert mv.set_conversation.call_count == 1
assert source_conversation_init.call_count == 1

# Reset mock call counts for next call of on_source_changed.
source_conversation_init.reset_mock()
mv.set_conversation.reset_mock()

# Now click on another source (source2). Since this is the first time we have clicked
# on source2, we expect on the first call, SourceConversationWrapper.__init__ should be
# called.
mv.source_list.get_current_source = mocker.MagicMock(return_value=source2)
mv.on_source_changed()
assert mv.set_conversation.call_count == 1
assert source_conversation_init.call_count == 1

# Reset mock call counts for next call of on_source_changed.
source_conversation_init.reset_mock()
mv.set_conversation.reset_mock()

# But if we click back (call on_source_changed again) to the source,
# its SourceConversationWrapper should _not_ be recreated.
mv.source_list.get_current_source = mocker.MagicMock(return_value=source)
mv.on_source_changed()
assert mv.set_conversation.call_count == 1
assert source_conversation_init.call_count == 0


def test_MainView_set_conversation(mocker):
"""
Ensure the passed-in widget is added to the layout of the main view holder
Expand Down