From 772c828062087c34e96a049ff65f5ae3b2ae9cf2 Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Wed, 24 Apr 2019 08:54:52 -0700 Subject: [PATCH 1/2] section layouts inside source widget layout --- securedrop_client/gui/widgets.py | 236 ++++++++++++++++++------------- tests/gui/test_widgets.py | 20 +-- 2 files changed, 151 insertions(+), 105 deletions(-) diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 3b7846e56..570ce403a 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -33,7 +33,7 @@ from securedrop_client.db import Source, Message, File, Reply from securedrop_client.gui import SvgLabel, SvgPushButton, SvgToggleButton from securedrop_client.logic import Controller -from securedrop_client.resources import load_svg, load_icon, load_image +from securedrop_client.resources import load_icon, load_image from securedrop_client.utils import humanize_filesize logger = logging.getLogger(__name__) @@ -589,7 +589,6 @@ class MainView(QWidget): def __init__(self, parent): super().__init__(parent) - # Set styles self.setStyleSheet(self.CSS) self.layout = QHBoxLayout(self) @@ -597,15 +596,7 @@ def __init__(self, parent): self.layout.setSpacing(0) self.setLayout(self.layout) - left_column = QWidget(parent=self) - left_layout = QVBoxLayout() - left_layout.setContentsMargins(0, 0, 0, 0) - left_column.setLayout(left_layout) - - self.source_list = SourceList(left_column) - left_layout.addWidget(self.source_list) - - self.layout.addWidget(left_column, 4) + self.source_list = SourceList() self.view_layout = QVBoxLayout() self.view_layout.setContentsMargins(0, 0, 0, 0) @@ -613,6 +604,7 @@ def __init__(self, parent): self.view_holder.setObjectName('view_holder') # Set css id self.view_holder.setLayout(self.view_layout) + self.layout.addWidget(self.source_list, 4) self.layout.addWidget(self.view_holder, 6) def setup(self, controller): @@ -647,24 +639,34 @@ class SourceList(QListWidget): """ CSS = ''' + QListWidget { + show-decoration-selected: 0; + } QListWidget::item:selected { background: #efeef7; } ''' - def __init__(self, parent): - super().__init__(parent) + def __init__(self): + super().__init__() # Set css id - self.setObjectName('source_list') + self.setObjectName('sourcelist') # Set styles self.setStyleSheet(self.CSS) + self.setMinimumWidth(445) + self.setMaximumWidth(565) + + # Set layout + layout = QVBoxLayout(self) + self.setLayout(layout) + + # Remove margins + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) def setup(self, controller): - """ - Pass through the controller object to this widget. - """ self.controller = controller def update(self, sources: List[Source]): @@ -676,7 +678,7 @@ def update(self, sources: List[Source]): new_current_maybe = None for source in sources: - new_source = SourceWidget(self, source) + new_source = SourceWidget(source) new_source.setup(self.controller) list_item = QListWidgetItem(self) @@ -692,52 +694,6 @@ def update(self, sources: List[Source]): self.setCurrentItem(new_current_maybe) -class DeleteSourceMessageBox: - """Use this to display operation details and confirm user choice.""" - - def __init__(self, parent, source, controller): - self.parent = parent - self.source = source - self.controller = controller - - def launch(self): - """It will launch the message box. - - The Message box will warns the user regarding the severity of the - operation. It will confirm the desire to delete the source. On positive - answer, it will delete the record of source both from SecureDrop server - and local state. - """ - message = self._construct_message(self.source) - reply = QMessageBox.question( - self.parent, "", _(message), QMessageBox.Cancel | QMessageBox.Yes, QMessageBox.Cancel) - - if reply == QMessageBox.Yes: - logger.debug("Deleting source %s" % (self.source.uuid,)) - self.controller.delete_source(self.source) - - def _construct_message(self, source: Source) -> str: - files = 0 - messages = 0 - replies = 0 - for submission in source.collection: - if isinstance(submission, Message): - messages += 1 - if isinstance(submission, Reply): - replies += 1 - elif isinstance(submission, File): - files += 1 - - message = ("Deleting the Source account for " - "{} will also " - "delete {} files, {} replies, and {} messages." - "
" - "This Source will no longer be able to correspond " - "through the log-in tied to this account.").format( - source.journalist_designation, files, replies, messages) - return message - - class SourceWidget(QWidget): """ Used to display summary information about a source in the list view. @@ -747,13 +703,24 @@ class SourceWidget(QWidget): QWidget#color_bar { background-color: #9211ff; } + QLabel#source_name { + font-family: Open Sans; + font-size: 16px; + font-weight: bold; + color: #000; + } + QLabel#timestamp { + font-family: Open Sans; + font-size: 12px; + color: #000; + } ''' - def __init__(self, parent: QWidget, source: Source): - """ - Set up the child widgets. - """ - super().__init__(parent) + def __init__(self, source: Source): + super().__init__() + + # Store source + self.source = source # Set css id self.setObjectName('source_widget') @@ -761,38 +728,69 @@ def __init__(self, parent: QWidget, source: Source): # Set styles self.setStyleSheet(self.CSS) - self.source = source + # Set layout + layout = QVBoxLayout(self) + self.setLayout(layout) + + # Remove margins and spacing + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(0) + # Set up gutter + self.gutter = QWidget() + self.gutter.setObjectName('gutter') + self.gutter.setFixedWidth(30) + gutter_layout = QVBoxLayout(self.gutter) + gutter_layout.setContentsMargins(0, 0, 0, 0) + gutter_layout.setSpacing(0) self.star = StarToggleButton(self.source) + spacer = QWidget() + gutter_layout.addWidget(self.star) + gutter_layout.addWidget(spacer) + # Set up summary + self.summary = QWidget() + self.summary.setObjectName('summary') + summary_layout = QVBoxLayout(self.summary) + summary_layout.setContentsMargins(0, 0, 0, 0) + summary_layout.setSpacing(0) self.name = QLabel() - self.name.setFont(QFont("Open Sans", 16)) + self.name.setObjectName('source_name') + self.preview = QLabel('') + self.preview.setObjectName('preview') + self.preview.setWordWrap(True) + summary_layout.addWidget(self.name) + summary_layout.addWidget(self.preview) + + # Set up metadata + self.metadata = QWidget() + self.metadata.setObjectName('metadata') + metadata_layout = QVBoxLayout(self.metadata) + metadata_layout.setContentsMargins(0, 0, 0, 0) + metadata_layout.setSpacing(0) + self.attached = SvgLabel('paperclip.svg', QSize(16, 16)) + self.attached.setObjectName('paperclip') + self.attached.setFixedSize(QSize(20, 20)) + spacer = QWidget() + metadata_layout.addWidget(self.attached, 1, Qt.AlignRight) + metadata_layout.addWidget(spacer, 1) + + # Set up source row + self.source_row = QWidget() + source_row_layout = QHBoxLayout(self.source_row) + source_row_layout.setContentsMargins(0, 0, 0, 0) + source_row_layout.setSpacing(0) + source_row_layout.addWidget(self.gutter, 1) + source_row_layout.addWidget(self.summary, 1) + source_row_layout.addWidget(self.metadata, 1) + + # Set up timestamp self.updated = QLabel() - self.updated.setFont(QFont("Open Sans", 10)) + self.updated.setObjectName('timestamp') - layout = QVBoxLayout() - self.setLayout(layout) - - self.summary = QWidget(self) - self.summary.setObjectName('summary') - self.summary_layout = QHBoxLayout() - self.summary.setLayout(self.summary_layout) - - self.attached = load_svg('paperclip.svg') - self.attached.setMaximumSize(16, 16) - - self.summary_layout.addWidget(self.name) - self.summary_layout.addWidget(self.attached) - - layout.addWidget(self.summary) - layout.addWidget(self.updated) - - self.delete = load_svg('cross.svg') - self.delete.setMaximumSize(16, 16) - self.delete.mouseReleaseEvent = self.delete_source - - self.summary_layout.addWidget(self.delete) - self.summary_layout.addWidget(self.star) + # Add widgets to main layout + layout.addWidget(self.source_row, 1) + layout.addWidget(self.updated, 1, Qt.AlignRight) self.update() @@ -881,6 +879,54 @@ def on_toggle_offline(self): self.set_icon(on='star_on.svg', off='star_on.svg') +class DeleteSourceMessageBox: + """Use this to display operation details and confirm user choice.""" + + def __init__(self, parent, source, controller): + self.parent = parent + self.source = source + self.controller = controller + + def launch(self): + """It will launch the message box. + + The Message box will warns the user regarding the severity of the + operation. It will confirm the desire to delete the source. On positive + answer, it will delete the record of source both from SecureDrop server + and local state. + """ + message = self._construct_message(self.source) + reply = QMessageBox.question( + self.parent, "", _(message), QMessageBox.Cancel | QMessageBox.Yes, QMessageBox.Cancel) + + if reply == QMessageBox.Yes: + logger.debug("Deleting source %s" % (self.source.uuid,)) + self.controller.delete_source(self.source) + + def _construct_message(self, source: Source) -> str: + files = 0 + messages = 0 + replies = 0 + for submission in source.collection: + if isinstance(submission, Message): + messages += 1 + if isinstance(submission, Reply): + replies += 1 + elif isinstance(submission, File): + files += 1 + + message = ( + "Deleting the Source account for", + "{} will also".format(source.journalist_designation,), + "delete {} files, {} replies, and {} messages.".format(files, replies, messages), + "
", + "This Source will no longer be able to correspond", + "through the log-in tied to this account.", + ) + message = ' '.join(message) + return message + + class LoginDialog(QDialog): """ A dialog to display the login form. diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index eb0aa5b65..fdf5541c3 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -427,7 +427,7 @@ def test_SourceList_update(mocker): Check the items in the source list are cleared and a new SourceWidget for each passed-in source is created along with an associated QListWidgetItem. """ - sl = SourceList(None) + sl = SourceList() sl.clear = mocker.MagicMock() sl.addItem = mocker.MagicMock() @@ -453,7 +453,7 @@ def test_SourceList_maintains_selection(mocker): """ Maintains the selected item if present in new list """ - sl = SourceList(None) + sl = SourceList() sources = [factory.Source(), factory.Source()] sl.setup(mocker.MagicMock()) sl.update(sources) @@ -471,7 +471,7 @@ def test_SourceWidget_init(mocker): """ mock_source = mocker.MagicMock() mock_source.journalist_designation = 'foo bar baz' - sw = SourceWidget(None, mock_source) + sw = SourceWidget(mock_source) assert sw.source == mock_source @@ -481,7 +481,7 @@ def test_SourceWidget_setup(mocker): """ mock_controller = mocker.MagicMock() mock_source = mocker.MagicMock() - sw = SourceWidget(None, mock_source) + sw = SourceWidget(mock_source) sw.star = mocker.MagicMock() sw.setup(mock_controller) @@ -498,11 +498,11 @@ def test_SourceWidget_html_init(mocker): mock_source = mocker.MagicMock() mock_source.journalist_designation = 'foo bar baz' - sw = SourceWidget(None, mock_source) + sw = SourceWidget(mock_source) sw.name = mocker.MagicMock() sw.summary_layout = mocker.MagicMock() - mocker.patch('securedrop_client.gui.widgets.load_svg') + mocker.patch('securedrop_client.gui.SvgLabel') sw.update() sw.name.setText.assert_called_once_with('foo <b>bar</b> baz') @@ -513,7 +513,7 @@ def test_SourceWidget_update_attachment_icon(): Attachment icon identicates document count """ source = factory.Source(document_count=1) - sw = SourceWidget(None, source) + sw = SourceWidget(source) sw.update() assert not sw.attached.isHidden() @@ -530,7 +530,7 @@ def test_SourceWidget_delete_source(mocker, session, source): mock_delete_source_message = mocker.MagicMock( return_value=mock_delete_source_message_box_object) - sw = SourceWidget(None, source['source']) + sw = SourceWidget(source['source']) sw.controller = mock_controller mocker.patch( @@ -554,7 +554,7 @@ def test_SourceWidget_delete_source_when_user_chooses_cancel(mocker, session, so mock_message_box_question.return_value = QMessageBox.Cancel mock_controller = mocker.MagicMock() - sw = SourceWidget(None, source) + sw = SourceWidget(source) sw.controller = mock_controller mocker.patch( @@ -1585,7 +1585,7 @@ def test_DeleteSource_from_source_widget_when_user_is_loggedout(mocker): 'securedrop_client.gui.widgets.DeleteSourceMessageBox', mock_delete_source_message_box ): - source_widget = SourceWidget(None, mock_source) + source_widget = SourceWidget(mock_source) source_widget.setup(mock_controller) source_widget.delete_source(mock_event) mock_delete_source_message_box_obj.launch.assert_not_called() From 40c1f3ec7f45a1af1f1be0ed8a8e37b44b29699c Mon Sep 17 00:00:00 2001 From: Allie Crevier Date: Mon, 29 Apr 2019 11:01:01 -0700 Subject: [PATCH 2/2] fix mypy errors --- securedrop_client/gui/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 570ce403a..6ccb9cee1 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -915,7 +915,7 @@ def _construct_message(self, source: Source) -> str: elif isinstance(submission, File): files += 1 - message = ( + message_tuple = ( "Deleting the Source account for", "{} will also".format(source.journalist_designation,), "delete {} files, {} replies, and {} messages.".format(files, replies, messages), @@ -923,7 +923,7 @@ def _construct_message(self, source: Source) -> str: "This Source will no longer be able to correspond", "through the log-in tied to this account.", ) - message = ' '.join(message) + message = ' '.join(message_tuple) return message