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):