Skip to content

Commit

Permalink
resolve conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
ntoll committed Oct 22, 2018
2 parents f23d1c3 + cb14171 commit 9f78c31
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 94 deletions.
16 changes: 15 additions & 1 deletion securedrop_client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
import logging
import pathlib
import os
import signal
import sys
from sqlalchemy.orm import sessionmaker
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QTimer
from logging.handlers import TimedRotatingFileHandler
from securedrop_client import __version__
from securedrop_client.logic import Client
Expand Down Expand Up @@ -91,11 +92,24 @@ def run():
app.setDesktopFileName('org.freedomofthepress.securedrop.client')
app.setApplicationVersion(__version__)
app.setAttribute(Qt.AA_UseHighDpiPixmaps)

gui = Window()
app.setWindowIcon(load_icon(gui.icon))
app.setStyleSheet(load_css('sdclient.css'))

Session = sessionmaker(bind=engine)
session = Session()

client = Client("http://localhost:8081/", gui, session)
client.setup()

def signal_handler(*nargs) -> None:
app.quit()

for sig in [signal.SIGINT, signal.SIGTERM]:
signal.signal(sig, signal_handler)
timer = QTimer()
timer.start(500)
timer.timeout.connect(lambda: None)

sys.exit(app.exec_())
45 changes: 34 additions & 11 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QDesktopWidget
from PyQt5.QtCore import Qt
from securedrop_client import __version__
from securedrop_client.gui.widgets import (ToolBar, MainView, LoginView,
from securedrop_client.gui.widgets import (ToolBar, MainView, LoginDialog,
ConversationView)
from securedrop_client.resources import load_icon

Expand Down Expand Up @@ -55,6 +55,8 @@ def __init__(self):
self.widget.setLayout(widget_layout)
self.tool_bar = ToolBar(self.widget)
self.main_view = MainView(self.widget)
self.main_view.source_list.itemSelectionChanged.\
connect(self.on_source_changed)
widget_layout.addWidget(self.tool_bar, 1)
widget_layout.addWidget(self.main_view, 6)
self.setCentralWidget(self.widget)
Expand All @@ -68,8 +70,6 @@ def setup(self, controller):
"""
self.controller = controller # Reference the Client logic instance.
self.tool_bar.setup(self, controller)
self.login_view = LoginView(self, self.controller)
self.main_view.setup(self.controller)

def autosize_window(self):
"""
Expand All @@ -79,15 +79,28 @@ def autosize_window(self):
screen = QDesktopWidget().screenGeometry()
self.resize(screen.width(), screen.height())

def show_login(self, error=None):
def show_login(self):
"""
Show the login form. If an error message is passed in, the login
form will display this too.
Show the login form.
"""
self.login_view.reset()
if error:
self.login_view.error(error)
self.main_view.update_view(self.login_view)
self.login_dialog = LoginDialog(self)
self.login_dialog.setup(self.controller)
self.login_dialog.reset()
self.login_dialog.exec()

def show_login_error(self, error):
"""
Display an error in the login dialog.
"""
if self.login_dialog and error:
self.login_dialog.error(error)

def hide_login(self):
"""
Kill the login dialog.
"""
self.login_dialog.accept()
self.login_dialog = None

def update_error_status(self, error=None):
"""
Expand Down Expand Up @@ -124,11 +137,21 @@ def logout(self):
"""
self.tool_bar.set_logged_out()

def show_conversation_for(self, source=None):
def on_source_changed(self):
"""
React to when the selected source has changed.
"""
source_item = self.main_view.source_list.currentItem()
source_widget = self.main_view.source_list.itemWidget(source_item)
self.show_conversation_for(source_widget.source)

def show_conversation_for(self, source):
"""
TODO: Finish this...
"""
conversation = ConversationView(self)
conversation.add_message('Source name: {}'.format(
source.journalist_designation))
conversation.add_message('Hello, hello, is this thing switched on?')
conversation.add_reply('Yes, I can hear you loud and clear!')
conversation.add_reply('How can I help?')
Expand Down
18 changes: 13 additions & 5 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from PyQt5.QtWidgets import (QListWidget, QTextEdit, QLabel, QToolBar, QAction,
QWidget, QListWidgetItem, QHBoxLayout,
QPushButton, QVBoxLayout, QLineEdit, QScrollArea,
QPlainTextEdit, QSpacerItem, QSizePolicy)
QPlainTextEdit, QSpacerItem, QSizePolicy, QDialog)
from securedrop_client.resources import load_svg, load_image


Expand Down Expand Up @@ -142,7 +142,10 @@ def update_view(self, widget):
"""
Update the view holder to contain the referenced widget.
"""
self.view_layout.takeAt(0)
old_widget = self.view_layout.takeAt(0)
if old_widget:
old_widget.widget().setVisible(False)
widget.setVisible(True)
self.view_layout.addWidget(widget)


Expand Down Expand Up @@ -246,14 +249,18 @@ def toggle_star(self, event):
self.controller.update_star(self.source)


class LoginView(QWidget):
class LoginDialog(QDialog):
"""
A widget to display the login form.
A dialog to display the login form.
"""

def __init__(self, parent, controller):
def __init__(self, parent):
super().__init__(parent)

def setup(self, controller):
self.controller = controller
self.setMinimumSize(600, 400)
self.setWindowTitle(_('Login to SecureDrop'))
main_layout = QHBoxLayout()
main_layout.addStretch()
self.setLayout(main_layout)
Expand Down Expand Up @@ -318,6 +325,7 @@ def error(self, message):
"""
Ensures the passed in message is displayed as an error message.
"""
self.setDisabled(False)
self.error_label.setText(message)

def validate(self):
Expand Down
12 changes: 7 additions & 5 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ def setup(self):
self.gui.setup(self)
# If possible, update the UI with available sources.
self.update_sources()
# Show the login view.
self.gui.show_conversation_for()
# Show the login dialog.
self.gui.show_login()
# Create a timer to check for sync status every 30 seconds.
self.sync_timer = QTimer()
self.sync_timer.timeout.connect(self.update_sync)
Expand Down Expand Up @@ -161,6 +161,7 @@ def on_authenticate(self, result):
self.call_reset()
if result:
# It worked! Sync with the API and update the UI.
self.gui.hide_login()
self.sync_api()
self.gui.set_logged_in_as(self.api.username)
# Clear the sidebar error status bar if a message was shown
Expand All @@ -170,7 +171,7 @@ def on_authenticate(self, result):
# Failed to authenticate. Reset state with failure message.
self.api = None
error = _('There was a problem logging in. Please try again.')
self.gui.show_login(error=error)
self.gui.show_login_error(error=error)

def on_login_timeout(self):
"""
Expand All @@ -179,7 +180,7 @@ def on_login_timeout(self):
self.call_reset()
self.api = None
error = _('The connection to SecureDrop timed out. Please try again.')
self.gui.show_login(error=error)
self.gui.show_login_error(error=error)

def on_action_requiring_login(self):
"""
Expand Down Expand Up @@ -235,7 +236,8 @@ def on_synced(self, result):
# Set last sync flag.
with open(self.sync_flag, 'w') as f:
f.write(arrow.now().format())
self.gui.show_conversation_for()
# TODO: show something in the conversation view?
# self.gui.show_conversation_for()
else:
# How to handle a failure? Exceptions are already logged. Perhaps
# a message in the UI?
Expand Down
61 changes: 51 additions & 10 deletions tests/gui/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
Check the core Window UI class works as expected.
"""
from PyQt5.QtWidgets import QApplication, QVBoxLayout
from PyQt5.QtCore import QTimer
from securedrop_client.gui.main import Window
from securedrop_client.gui.widgets import LoginView
from securedrop_client.gui.widgets import LoginDialog
from securedrop_client.resources import load_icon
from unittest import mock

Expand Down Expand Up @@ -39,7 +40,6 @@ def test_setup():
mock_controller = mock.MagicMock()
w.setup(mock_controller)
assert w.controller == mock_controller
assert isinstance(w.login_view, LoginView)


def test_autosize_window():
Expand All @@ -61,17 +61,42 @@ def test_autosize_window():

def test_show_login():
"""
Ensures the update_view is called with a LoginView instance.
The login dialog is displayed with a clean state.
"""
mock_controller = mock.MagicMock()
w = Window()
w.setup(mock_controller)
w.login_view = mock.MagicMock()
w.main_view = mock.MagicMock()
w.show_login("error message")
w.login_view.reset.assert_called_once_with()
w.login_view.error.assert_called_once_with("error message")
w.main_view.update_view.assert_called_once_with(w.login_view)
with mock.patch('securedrop_client.gui.main.LoginDialog') as mock_ld:
w.show_login()
mock_ld.assert_called_once_with(w)
w.login_dialog.reset.assert_called_once_with()
w.login_dialog.exec.assert_called_once_with()


def test_show_login_error():
"""
Ensures that an error message is displayed in the login dialog.
"""
mock_controller = mock.MagicMock()
w = Window()
w.setup(mock_controller)
w.login_dialog = mock.MagicMock()
w.show_login_error('boom')
w.login_dialog.error.assert_called_once_with('boom')


def test_hide_login():
"""
Ensure the login dialog is closed and hidden.
"""
mock_controller = mock.MagicMock()
w = Window()
w.setup(mock_controller)
ld = mock.MagicMock()
w.login_dialog = ld
w.hide_login()
ld.accept.assert_called_once_with()
assert w.login_dialog is None


def test_show_sources():
Expand Down Expand Up @@ -139,6 +164,20 @@ def test_logout():
w.tool_bar.set_logged_out.assert_called_once_with()


def test_on_source_changed():
"""
Ensure the event handler for when a source is changed calls the
show_conversation_for method with the expected source object.
"""
w = Window()
w.main_view = mock.MagicMock()
mock_si = w.main_view.source_list.currentItem()
mock_sw = w.main_view.source_list.itemWidget()
w.show_conversation_for = mock.MagicMock()
w.on_source_changed()
w.show_conversation_for.assert_called_once_with(mock_sw.source)


def test_conversation_for():
"""
TODO: Finish this once we have data. Currently checks only the GUI layer
Expand All @@ -147,9 +186,11 @@ def test_conversation_for():
w = Window()
w.main_view = mock.MagicMock()
mock_conview = mock.MagicMock()
mock_source = mock.MagicMock()
mock_source.journalistic_designation = 'Testy McTestface'
with mock.patch('securedrop_client.gui.main.ConversationView',
mock_conview):
w.show_conversation_for()
w.show_conversation_for(mock_source)
conv = mock_conview()
assert conv.add_message.call_count > 0
assert conv.add_reply.call_count > 0
Loading

0 comments on commit 9f78c31

Please sign in to comment.