diff --git a/securedrop_client/gui/source/__init__.py b/securedrop_client/gui/source/__init__.py
new file mode 100644
index 000000000..ac92bbc95
--- /dev/null
+++ b/securedrop_client/gui/source/__init__.py
@@ -0,0 +1,20 @@
+"""
+A source who interacts with journalists.
+
+Copyright (C) 2021 The Freedom of the Press Foundation.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see
", + _("When the entire account for a source is deleted:"), + "
", + "\u2219 ", + _("The source will not be able to log in with their codename again."), + "
", + "\u2219 ", + _("Your organization will not be able to send them replies."), + "
", + "\u2219 ", + _("All files and messages from that source will also be destroyed."), + "
", + "", + ) + + return "".join(message_tuple).format( + source="{}".format(self.source.journalist_designation) + ) + + @pyqtSlot() + def delete_source(self) -> None: + self.controller.delete_source(self.source) + self.close() diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index c89ab85ee..7dc44b504 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -79,6 +79,7 @@ SvgToggleButton, ) from securedrop_client.gui.conversation import DeleteConversationDialog +from securedrop_client.gui.source import DeleteSourceDialog from securedrop_client.logic import Controller from securedrop_client.resources import load_css, load_icon, load_image, load_movie from securedrop_client.storage import source_exists @@ -2774,54 +2775,6 @@ def _update_dialog(self, error_status: str) -> None: self._show_generic_error_message() -class DeleteSourceDialog(ModalDialog): - """Used to confirm deletion of source accounts.""" - - def __init__(self, source: Source, controller: Controller) -> None: - super().__init__(show_header=False, dangerous=True) - - self.source = source - self.controller = controller - - self.body.setText(self.make_body_text()) - - self.continue_button.setText(_("YES, DELETE ENTIRE SOURCE ACCOUNT")) - self.continue_button.clicked.connect(self.delete_source) - - self.confirmation_label.setText(_("Are you sure this is what you want?")) - - self.adjustSize() - - def make_body_text(self) -> str: - message_tuple = ( - "", - "
", - _("When the entire account for a source is deleted:"), - "
", - "\u2219 ", - _("The source will not be able to log in with their codename again."), - "
", - "\u2219 ", - _("Your organization will not be able to send them replies."), - "
", - "\u2219 ", - _("All files and messages from that source will also be destroyed."), - "
", - "", - ) - - return "".join(message_tuple).format( - source="{}".format(self.source.journalist_designation) - ) - - @pyqtSlot() - def delete_source(self) -> None: - self.controller.delete_source(self.source) - self.close() - - class ConversationScrollArea(QScrollArea): MARGIN_BOTTOM = 28 diff --git a/securedrop_client/locale/messages.pot b/securedrop_client/locale/messages.pot index 8aa0a1c88..8c2e5178c 100644 --- a/securedrop_client/locale/messages.pot +++ b/securedrop_client/locale/messages.pot @@ -203,24 +203,6 @@ msgstr "" msgid "CONTINUE" msgstr "" -msgid "YES, DELETE ENTIRE SOURCE ACCOUNT" -msgstr "" - -msgid "Are you sure this is what you want?" -msgstr "" - -msgid "When the entire account for a source is deleted:" -msgstr "" - -msgid "The source will not be able to log in with their codename again." -msgstr "" - -msgid "Your organization will not be able to send them replies." -msgstr "" - -msgid "All files and messages from that source will also be destroyed." -msgstr "" - msgid "Earlier files and messages deleted." msgstr "" @@ -313,3 +295,21 @@ msgid_plural "{message_count} messages" msgstr[0] "" msgstr[1] "" +msgid "YES, DELETE ENTIRE SOURCE ACCOUNT" +msgstr "" + +msgid "Are you sure this is what you want?" +msgstr "" + +msgid "When the entire account for a source is deleted:" +msgstr "" + +msgid "The source will not be able to log in with their codename again." +msgstr "" + +msgid "Your organization will not be able to send them replies." +msgstr "" + +msgid "All files and messages from that source will also be destroyed." +msgstr "" + diff --git a/tests/gui/source/__init__.py b/tests/gui/source/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/gui/source/delete/__init__.py b/tests/gui/source/delete/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/gui/source/delete/test_dialog.py b/tests/gui/source/delete/test_dialog.py new file mode 100644 index 000000000..57206d74c --- /dev/null +++ b/tests/gui/source/delete/test_dialog.py @@ -0,0 +1,72 @@ +from gettext import gettext as _ + +from PyQt5.QtWidgets import QApplication + +from securedrop_client.gui.source import DeleteSourceDialog +from tests import factory + +app = QApplication([]) + + +def test_DeleteSourceDialog_init(mocker, source): + mock_controller = mocker.MagicMock() + DeleteSourceDialog(source["source"], mock_controller) + + +def test_DeleteSourceDialog_cancel(mocker, source): + source = source["source"] # to get the Source object + + mock_controller = mocker.MagicMock() + delete_source_dialog = DeleteSourceDialog(source, mock_controller) + delete_source_dialog.cancel_button.click() + mock_controller.delete_source.assert_not_called() + + +def test_DeleteSourceDialog_continue(mocker, source, session): + source = source["source"] # to get the Source object + + mock_controller = mocker.MagicMock() + delete_source_dialog = DeleteSourceDialog(source, mock_controller) + delete_source_dialog.continue_button.click() + mock_controller.delete_source.assert_called_once_with(source) + + +def test_DeleteSourceDialog_make_body_text(mocker, source, session): + source = source["source"] # to get the Source object + file_ = factory.File(source=source) + session.add(file_) + message = factory.Message(source=source) + session.add(message) + message = factory.Message(source=source) + session.add(message) + reply = factory.Reply(source=source) + session.add(reply) + session.commit() + + mock_controller = mocker.MagicMock() + + delete_source_message_box = DeleteSourceDialog(source, mock_controller) + + message = delete_source_message_box.make_body_text() + + expected_message = "".join( + ( + "", + "
", + _("When the entire account for a source is deleted:"), + "
", + "\u2219 ", + _("The source will not be able to log in with their codename again."), + "
", + "\u2219 ", + _("Your organization will not be able to send them replies."), + "
", + "\u2219 ", + _("All files and messages from that source will also be destroyed."), + "
", + "", + ) + ).format(source=source.journalist_designation) + assert message == expected_message diff --git a/tests/gui/test_widgets.py b/tests/gui/test_widgets.py index f3752c0cd..2c5a38e7d 100644 --- a/tests/gui/test_widgets.py +++ b/tests/gui/test_widgets.py @@ -18,12 +18,12 @@ from securedrop_client import db, logic, storage from securedrop_client.export import ExportError, ExportStatus +from securedrop_client.gui.source import DeleteSourceDialog from securedrop_client.gui.widgets import ( ActivityStatusBar, ConversationView, DeleteConversationAction, DeleteSourceAction, - DeleteSourceDialog, EmptyConversationView, ErrorStatusBar, ExportDialog, @@ -4829,70 +4829,6 @@ def test_ConversationView_add_not_downloaded_file(mocker, homedir, source, sessi assert isinstance(file_widget, FileWidget) -def test_DeleteSourceDialog_init(mocker, source): - mock_controller = mocker.MagicMock() - DeleteSourceDialog(source["source"], mock_controller) - - -def test_DeleteSourceDialog_cancel(mocker, source): - source = source["source"] # to get the Source object - - mock_controller = mocker.MagicMock() - delete_source_dialog = DeleteSourceDialog(source, mock_controller) - delete_source_dialog.cancel_button.click() - mock_controller.delete_source.assert_not_called() - - -def test_DeleteSourceDialog_continue(mocker, source, session): - source = source["source"] # to get the Source object - - mock_controller = mocker.MagicMock() - delete_source_dialog = DeleteSourceDialog(source, mock_controller) - delete_source_dialog.continue_button.click() - mock_controller.delete_source.assert_called_once_with(source) - - -def test_DeleteSourceDialog_make_body_text(mocker, source, session): - source = source["source"] # to get the Source object - file_ = factory.File(source=source) - session.add(file_) - message = factory.Message(source=source) - session.add(message) - message = factory.Message(source=source) - session.add(message) - reply = factory.Reply(source=source) - session.add(reply) - session.commit() - - mock_controller = mocker.MagicMock() - - delete_source_message_box = DeleteSourceDialog(source, mock_controller) - - message = delete_source_message_box.make_body_text() - - expected_message = "".join( - ( - "", - "
", - _("When the entire account for a source is deleted:"), - "
", - "\u2219 ", - _("The source will not be able to log in with their codename again."), - "
", - "\u2219 ", - _("Your organization will not be able to send them replies."), - "
", - "\u2219 ", - _("All files and messages from that source will also be destroyed."), - "
", - "", - ) - ).format(source=source.journalist_designation) - assert message == expected_message - - def test_DeleteSourceAction_init(mocker): mock_controller = mocker.MagicMock() mock_source = mocker.MagicMock()