Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor move DeleteSourceDialog to 'gui.source' namespace #1394

Merged
merged 2 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so you are hiding the dialog module from the path so that you can from securedrop_client.gui.source.delete import DeleteSourceDialog when you actually need to import DeleteSourceDialog vs from securedrop_client.gui.source.delete.dialog import DeleteSourceDialog. Either way works for me, but I tend to try to keep my __init__.py empty and kind of prefer the simplicity and explicit-y when importing classes, functions, and variables from somewhere.

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