diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 6769d83ae3..d362284bf5 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -1938,7 +1938,7 @@ def __init__( # Make sure we only allow one export or print operation at a time (workaround for frameless # modals not working as expected on QubesOS) - self.modal_in_progress = False + self.dialog_in_progress = False def eventFilter(self, obj, event): if event.type() == QEvent.MouseButtonPress: @@ -1968,10 +1968,10 @@ def _on_export_clicked(self): self.controller.sync_api() return - if not self.modal_in_progress: - self.modal_in_progress = True + if not self.dialog_in_progress: + self.dialog_in_progress = True dialog = ExportDialog(self.controller, self.file.uuid, self.file.original_filename) - dialog.modal_closing.connect(self._unset_modal_in_progress) + dialog.modal_closing.connect(self._unset_dialog_in_progress) dialog.exec() @pyqtSlot() @@ -1983,15 +1983,15 @@ def _on_print_clicked(self): self.controller.sync_api() return - if not self.modal_in_progress: - self.modal_in_progress = True + if not self.dialog_in_progress: + self.dialog_in_progress = True dialog = PrintDialog(self.controller, self.file.uuid, self.file.original_filename) - dialog.modal_closing.connect(self._unset_modal_in_progress) + dialog.modal_closing.connect(self._unset_dialog_in_progress) dialog.exec() @pyqtSlot() - def _unset_modal_in_progress(self): - self.modal_in_progress = False + def _unset_dialog_in_progress(self): + self.dialog_in_progress = False def _on_left_click(self): """ @@ -2155,15 +2155,15 @@ def __init__(self): window_buttons.setObjectName('window_buttons') button_layout = QVBoxLayout() window_buttons.setLayout(button_layout) - cancel_button = QPushButton(_('CANCEL')) - cancel_button.setAutoDefault(False) - cancel_button.clicked.connect(self.close) + self.cancel_button = QPushButton(_('CANCEL')) + self.cancel_button.setAutoDefault(False) + self.cancel_button.clicked.connect(self.close) self.continue_button = QPushButton(_('CONTINUE')) self.continue_button.setObjectName('primary_button') self.continue_button.setDefault(True) button_box = QDialogButtonBox(Qt.Horizontal) button_box.setObjectName('button_box') - button_box.addButton(cancel_button, QDialogButtonBox.ActionRole) + button_box.addButton(self.cancel_button, QDialogButtonBox.ActionRole) button_box.addButton(self.continue_button, QDialogButtonBox.ActionRole) button_layout.addWidget(button_box, alignment=Qt.AlignRight) content_layout.addWidget(header_container) @@ -2242,25 +2242,25 @@ def __init__(self, controller: Controller, file_uuid: str, file_name: str): def _show_starting_instructions(self): self.header.setText(self.starting_header) - self.error_details.hide() self.body.setText(self.starting_message) + self.error_details.hide() self.adjustSize() self.center_dialog() def _show_insert_usb_message(self): self.continue_button.clicked.connect(self._run_preflight) - self.header.setText(self.insert_usb_header) - self.error_details.hide() + self.header.setText('\n{}'.format(self.insert_usb_header)) self.body.setText(self.insert_usb_message) + self.error_details.hide() self.adjustSize() self.center_dialog() def _show_generic_error_message(self): self.continue_button.clicked.connect(self.close) self.continue_button.setText('DONE') - self.header.setText(self.error_header) - self.error_details.hide() + self.header.setText('\n{}'.format(self.error_header)) self.body.setText('{}: {}'.format(self.error_status, self.generic_error_message)) + self.error_details.hide() self.adjustSize() self.center_dialog() @@ -2349,6 +2349,7 @@ def __init__(self, controller: Controller, file_uuid: str, file_name: str): '{}'.format(self.file_name)) self.insert_usb_header = _('Insert encrypted USB drive') self.passphrase_header = _('Enter passphrase for USB drive') + self.success_header = _('Export successful') self.error_header = _('Unable to export') self.starting_message = _( '

Proceed with caution when exporting files

' @@ -2374,6 +2375,8 @@ def __init__(self, controller: Controller, file_uuid: str, file_name: str): self.generic_error_message = _('See your administrator for help.') self.continue_disabled_message = _( 'The CONTINUE button will be disabled until the Export VM is ready') + self.success_message = _( + 'Remember to be careful when working with files outside of your Workstation machine.') # Passphrase Form self.passphrase_form = QWidget() @@ -2414,57 +2417,74 @@ def _show_starting_instructions(self): def _show_passphrase_request_message(self): self.continue_button.clicked.connect(self._export_file) - self.header.setText(self.passphrase_header) + self.header.setText('\n{}'.format(self.passphrase_header)) + self.continue_button.setText('SUBMIT') self.header_line.hide() self.error_details.hide() self.body.hide() self.passphrase_form.show() - self.continue_button.setText('SUBMIT') self.adjustSize() self.center_dialog() def _show_passphrase_request_message_again(self): self.continue_button.clicked.connect(self._export_file) - self.header.setText(self.passphrase_header) - self.header_line.hide() + self.header.setText('\n{}'.format(self.passphrase_header)) self.error_details.setText(self.passphrase_error_message) - self.error_details.show() + self.continue_button.setText('SUBMIT') + self.header_line.hide() self.body.hide() + self.error_details.show() self.passphrase_form.show() - self.continue_button.setText('SUBMIT') + self.adjustSize() + self.center_dialog() + + def _show_success_message(self): + self.continue_button.clicked.connect(self.close) + self.header.setText('\n{}'.format(self.success_header)) + self.continue_button.setText('DONE') + self.body.setText(self.success_message) + self.cancel_button.hide() + self.error_details.hide() + self.passphrase_form.hide() + self.header_line.show() + self.body.show() self.adjustSize() self.center_dialog() def _show_insert_usb_message(self): self.continue_button.clicked.connect(self._run_preflight) - self.header.setText(self.insert_usb_header) - self.header_line.show() + self.header.setText('\n{}'.format(self.insert_usb_header)) + self.continue_button.setText('CONTINUE') self.body.setText(self.insert_usb_message) self.error_details.hide() self.passphrase_form.hide() - self.continue_button.setText('CONTINUE') + self.header_line.show() + self.body.show() self.adjustSize() self.center_dialog() def _show_insert_encrypted_usb_message(self): self.continue_button.clicked.connect(self._run_preflight) - self.header.setText(self.insert_usb_header) - self.header_line.show() + self.header.setText('\n{}'.format(self.insert_usb_header)) self.error_details.setText(self.usb_error_message) + self.continue_button.setText('CONTINUE') self.body.setText(self.insert_usb_message) self.passphrase_form.hide() - self.continue_button.setText('CONTINUE') + self.header_line.show() + self.error_details.show() + self.body.show() self.adjustSize() self.center_dialog() def _show_generic_error_message(self): self.continue_button.clicked.connect(self.close) self.continue_button.setText('DONE') - self.header.setText(self.error_header) - self.header_line.show() - self.error_details.hide() + self.header.setText('\n{}'.format(self.error_header)) self.body.setText('{}: {}'.format(self.error_status, self.generic_error_message)) + self.error_details.hide() self.passphrase_form.hide() + self.header_line.show() + self.body.show() self.adjustSize() self.center_dialog() @@ -2492,7 +2512,7 @@ def _on_preflight_failure(self, error: ExportError): @pyqtSlot() def _on_export_success(self): - self.close() + self._show_success_message() @pyqtSlot(object) def _on_export_failure(self, error: ExportError): diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index f49fac2078..d7a315ed32 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -1299,7 +1299,7 @@ def test_ReplyWidget_init(mocker): assert mock_failure_connected.called -def test_FileWidget__unset_modal_in_progress(mocker, source, session): +def test_FileWidget__unset_dialog_in_progress(mocker, source, session): file = factory.File(source=source['source'], is_downloaded=True) session.add(file) session.commit() @@ -1313,13 +1313,13 @@ def test_FileWidget__unset_modal_in_progress(mocker, source, session): controller.run_export_preflight_checks = mocker.MagicMock() controller.downloaded_file_exists = mocker.MagicMock(return_value=True) - assert fw.modal_in_progress is False - fw._unset_modal_in_progress() - assert fw.modal_in_progress is False + assert fw.dialog_in_progress is False + fw._unset_dialog_in_progress() + assert fw.dialog_in_progress is False fw._on_export_clicked() - assert fw.modal_in_progress is True - fw._unset_modal_in_progress() - assert fw.modal_in_progress is False + assert fw.dialog_in_progress is True + fw._unset_dialog_in_progress() + assert fw.dialog_in_progress is False def test_FileWidget_init_file_not_downloaded(mocker, source, session): @@ -1621,12 +1621,12 @@ def test_ExportDialog_close(mocker): dialog.modal_closing = mocker.MagicMock() dialog.modal_closing.emit = mocker.MagicMock() - assert dialog.isHidden() is False + assert not dialog.isHidden() dialog.close() dialog.modal_closing.emit.assert_called_once_with() - assert dialog.isHidden() is True + assert dialog.isHidden() def test_ExportDialog__show_starting_instructions(mocker): @@ -1636,7 +1636,6 @@ def test_ExportDialog__show_starting_instructions(mocker): dialog._show_starting_instructions() - assert dialog.passphrase_form.isHidden() assert dialog.header.text() == \ 'Preparing to export:' \ '
' \ @@ -1655,6 +1654,13 @@ def test_ExportDialog__show_starting_instructions(mocker): 'Documents submitted by sources may contain information or hidden metadata that ' \ 'identifies who they are. To protect your sources, please consider redacting documents ' \ 'before working with them on network-connected computers.' + assert not dialog.header.isHidden() + assert not dialog.header_line.isHidden() + assert dialog.error_details.isHidden() + assert not dialog.body.isHidden() + assert dialog.passphrase_form.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() def test_ExportDialog___show_passphrase_request_message(mocker): @@ -1664,9 +1670,14 @@ def test_ExportDialog___show_passphrase_request_message(mocker): dialog._show_passphrase_request_message() - assert not dialog.passphrase_form.isHidden() - assert dialog.header.text() == 'Enter passphrase for USB drive' + assert dialog.header.text() == '\nEnter passphrase for USB drive' + assert not dialog.header.isHidden() + assert dialog.header_line.isHidden() + assert dialog.error_details.isHidden() assert dialog.body.isHidden() + assert not dialog.passphrase_form.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() def test_ExportDialog__show_passphrase_request_message_again(mocker): @@ -1674,15 +1685,37 @@ def test_ExportDialog__show_passphrase_request_message_again(mocker): 'securedrop_client.gui.widgets.QApplication.activeWindow', return_value=QMainWindow()) dialog = ExportDialog(mocker.MagicMock(), 'mock_uuid', 'mock.jpg') - assert dialog.error_details.isHidden() is True - dialog._show_passphrase_request_message_again() - assert dialog.error_details.isHidden() is False - assert not dialog.passphrase_form.isHidden() - assert dialog.header.text() == 'Enter passphrase for USB drive' + assert dialog.header.text() == '\nEnter passphrase for USB drive' assert dialog.error_details.text() == 'The passphrase provided did not work. Please try again.' assert dialog.body.isHidden() + assert not dialog.header.isHidden() + assert dialog.header_line.isHidden() + assert not dialog.error_details.isHidden() + assert dialog.body.isHidden() + assert not dialog.passphrase_form.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() + + +def test_ExportDialog__show_success_message(mocker): + mocker.patch( + 'securedrop_client.gui.widgets.QApplication.activeWindow', return_value=QMainWindow()) + dialog = ExportDialog(mocker.MagicMock(), 'mock_uuid', 'mock.jpg') + + dialog._show_success_message() + + assert dialog.header.text() == '\nExport successful' + assert dialog.body.text() == \ + 'Remember to be careful when working with files outside of your Workstation machine.' + assert not dialog.header.isHidden() + assert not dialog.header_line.isHidden() + assert dialog.error_details.isHidden() + assert not dialog.body.isHidden() + assert dialog.passphrase_form.isHidden() + assert not dialog.continue_button.isHidden() + assert dialog.cancel_button.isHidden() def test_ExportDialog__show_insert_usb_message(mocker): @@ -1692,11 +1725,17 @@ def test_ExportDialog__show_insert_usb_message(mocker): dialog._show_insert_usb_message() - assert dialog.header.text() == 'Insert encrypted USB drive' + assert dialog.header.text() == '\nInsert encrypted USB drive' assert dialog.body.text() == \ 'Please insert one of the export drives provisioned specifically ' \ 'for the SecureDrop Workstation.' + assert not dialog.header.isHidden() + assert not dialog.header_line.isHidden() + assert dialog.error_details.isHidden() + assert not dialog.body.isHidden() assert dialog.passphrase_form.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() def test_ExportDialog__show_insert_encrypted_usb_message(mocker): @@ -1706,13 +1745,19 @@ def test_ExportDialog__show_insert_encrypted_usb_message(mocker): dialog._show_insert_encrypted_usb_message() - assert dialog.header.text() == 'Insert encrypted USB drive' + assert dialog.header.text() == '\nInsert encrypted USB drive' assert dialog.error_details.text() == \ 'Either the drive is not encrypted or there is something else wrong with it.' assert dialog.body.text() == \ 'Please insert one of the export drives provisioned specifically for the SecureDrop ' \ 'Workstation.' + assert not dialog.header.isHidden() + assert not dialog.header_line.isHidden() + assert not dialog.error_details.isHidden() + assert not dialog.body.isHidden() assert dialog.passphrase_form.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() def test_ExportDialog__show_generic_error_message(mocker): @@ -1723,10 +1768,15 @@ def test_ExportDialog__show_generic_error_message(mocker): dialog._show_generic_error_message() - assert dialog.passphrase_form.isHidden() - assert dialog.header.text() == 'Unable to export' - assert dialog.error_details.isHidden() + assert dialog.header.text() == '\nUnable to export' assert dialog.body.text() == 'mock_error_status: See your administrator for help.' + assert not dialog.header.isHidden() + assert not dialog.header_line.isHidden() + assert dialog.error_details.isHidden() + assert not dialog.body.isHidden() + assert dialog.passphrase_form.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() def test_ExportDialog__export_file(mocker): @@ -1804,11 +1854,11 @@ def test_ExportDialog__on_export_success(mocker): mocker.patch( 'securedrop_client.gui.widgets.QApplication.activeWindow', return_value=QMainWindow()) dialog = ExportDialog(mocker.MagicMock(), 'mock_uuid', 'mock.jpg') - dialog.close = mocker.MagicMock() + dialog._show_success_message = mocker.MagicMock() dialog._on_export_success() - dialog.close.assert_called_once_with() + dialog._show_success_message.assert_called_once_with() def test_ExportDialog__on_export_failure(mocker): @@ -1905,6 +1955,7 @@ def test_ExportDialog__update_dialog_when_status_is_CALLED_PROCESS_ERROR(mocker) dialog._show_generic_error_message.assert_called_once_with() assert dialog.error_status == ExportStatus.CALLED_PROCESS_ERROR.value + def test_ExportDialog__update_dialog_when_status_is_unknown(mocker): mocker.patch( 'securedrop_client.gui.widgets.QApplication.activeWindow', return_value=QMainWindow()) @@ -1963,6 +2014,12 @@ def test_PrintDialog__show_starting_instructions(mocker): 'Any part of a printed page may contain identifying information ' \ 'invisible to the naked eye, such as printer dots. Please carefully ' \ 'consider this risk when working with or publishing scanned printouts.' + assert not dialog.header.isHidden() + assert not dialog.header_line.isHidden() + assert dialog.error_details.isHidden() + assert not dialog.body.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() def test_PrintDialog__show_insert_usb_message(mocker): @@ -1972,8 +2029,14 @@ def test_PrintDialog__show_insert_usb_message(mocker): dialog._show_insert_usb_message() - assert dialog.header.text() == 'Insert USB printer' + assert dialog.header.text() == '\nInsert USB printer' assert dialog.body.text() == 'Please connect your printer to a USB port.' + assert not dialog.header.isHidden() + assert not dialog.header_line.isHidden() + assert dialog.error_details.isHidden() + assert not dialog.body.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() def test_PrintDialog__show_generic_error_message(mocker): @@ -1984,8 +2047,14 @@ def test_PrintDialog__show_generic_error_message(mocker): dialog._show_generic_error_message() - assert dialog.header.text() == 'Unable to print' + assert dialog.header.text() == '\nUnable to print' assert dialog.body.text() == 'mock_error_status: See your administrator for help.' + assert not dialog.header.isHidden() + assert not dialog.header_line.isHidden() + assert dialog.error_details.isHidden() + assert not dialog.body.isHidden() + assert not dialog.continue_button.isHidden() + assert not dialog.cancel_button.isHidden() def test_PrintDialog__print_file(mocker):