Skip to content

Commit

Permalink
Merge pull request #1394 from freedomofpress/refactor-move-source-del…
Browse files Browse the repository at this point in the history
…etion-dialog-to-namespace

Refactor move DeleteSourceDialog to 'gui.source' namespace
  • Loading branch information
sssoleileraaa authored Jan 25, 2022
2 parents 8b2f0d4 + f3f0160 commit 6c48d41
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 131 deletions.
20 changes: 20 additions & 0 deletions securedrop_client/gui/source/__init__.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
"""
# Import classes here to make possible to import them from securedrop_client.gui.source
from securedrop_client.gui.source.delete import DeleteSourceDialog # noqa: F401
20 changes: 20 additions & 0 deletions securedrop_client/gui/source/delete/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Everything necessary for a journalist to delete a source.
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 <http://www.gnu.org/licenses/>.
"""
# Import classes here to make possible to import them from securedrop_client.gui.source.delete
from securedrop_client.gui.source.delete.dialog import DeleteSourceDialog # noqa: F401
73 changes: 73 additions & 0 deletions securedrop_client/gui/source/delete/dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Source deletion dialog.
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 <http://www.gnu.org/licenses/>.
"""
from gettext import gettext as _

from PyQt5.QtCore import pyqtSlot

from securedrop_client.db import Source
from securedrop_client.gui.base import ModalDialog
from securedrop_client.logic import Controller


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 = (
"<style>",
"p {{white-space: nowrap;}}",
"</style>",
"<p><b>",
_("When the entire account for a source is deleted:"),
"</b></p>",
"<p><b>\u2219</b>&nbsp;",
_("The source will not be able to log in with their codename again."),
"</p>",
"<p><b>\u2219</b>&nbsp;",
_("Your organization will not be able to send them replies."),
"</p>",
"<p><b>\u2219</b>&nbsp;",
_("All files and messages from that source will also be destroyed."),
"</p>",
"<p>&nbsp;</p>",
)

return "".join(message_tuple).format(
source="<b>{}</b>".format(self.source.journalist_designation)
)

@pyqtSlot()
def delete_source(self) -> None:
self.controller.delete_source(self.source)
self.close()
49 changes: 1 addition & 48 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = (
"<style>",
"p {{white-space: nowrap;}}",
"</style>",
"<p><b>",
_("When the entire account for a source is deleted:"),
"</b></p>",
"<p><b>\u2219</b>&nbsp;",
_("The source will not be able to log in with their codename again."),
"</p>",
"<p><b>\u2219</b>&nbsp;",
_("Your organization will not be able to send them replies."),
"</p>",
"<p><b>\u2219</b>&nbsp;",
_("All files and messages from that source will also be destroyed."),
"</p>",
"<p>&nbsp;</p>",
)

return "".join(message_tuple).format(
source="<b>{}</b>".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
Expand Down
36 changes: 18 additions & 18 deletions securedrop_client/locale/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""

Expand Down Expand Up @@ -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 ""

Empty file added tests/gui/source/__init__.py
Empty file.
Empty file.
72 changes: 72 additions & 0 deletions tests/gui/source/delete/test_dialog.py
Original file line number Diff line number Diff line change
@@ -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(
(
"<style>",
"p {{white-space: nowrap;}}",
"</style>",
"<p><b>",
_("When the entire account for a source is deleted:"),
"</b></p>",
"<p><b>\u2219</b>&nbsp;",
_("The source will not be able to log in with their codename again."),
"</p>",
"<p><b>\u2219</b>&nbsp;",
_("Your organization will not be able to send them replies."),
"</p>",
"<p><b>\u2219</b>&nbsp;",
_("All files and messages from that source will also be destroyed."),
"</p>",
"<p>&nbsp;</p>",
)
).format(source=source.journalist_designation)
assert message == expected_message
66 changes: 1 addition & 65 deletions tests/gui/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
(
"<style>",
"p {{white-space: nowrap;}}",
"</style>",
"<p><b>",
_("When the entire account for a source is deleted:"),
"</b></p>",
"<p><b>\u2219</b>&nbsp;",
_("The source will not be able to log in with their codename again."),
"</p>",
"<p><b>\u2219</b>&nbsp;",
_("Your organization will not be able to send them replies."),
"</p>",
"<p><b>\u2219</b>&nbsp;",
_("All files and messages from that source will also be destroyed."),
"</p>",
"<p>&nbsp;</p>",
)
).format(source=source.journalist_designation)
assert message == expected_message


def test_DeleteSourceAction_init(mocker):
mock_controller = mocker.MagicMock()
mock_source = mocker.MagicMock()
Expand Down

0 comments on commit 6c48d41

Please sign in to comment.