Skip to content

Commit

Permalink
Adding functionality to delete Source
Browse files Browse the repository at this point in the history
A Source Profile Short menu, which is responsible for displaying Source
name and Hamburger Button Menu. Hamburger button menu contains single
operation to delete the Source.

Resolves: #18
  • Loading branch information
ultimatecoder committed Nov 20, 2018
1 parent 267957d commit 447b3f3
Show file tree
Hide file tree
Showing 8 changed files with 406 additions and 9 deletions.
13 changes: 11 additions & 2 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import logging
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QDesktopWidget, QStatusBar
from securedrop_client import __version__
from securedrop_client.gui.widgets import ToolBar, MainView, LoginDialog, ConversationView
from securedrop_client.gui.widgets import (ToolBar, MainView, LoginDialog,
ConversationView,
SourceProfileShortWidget)
from securedrop_client.resources import load_icon

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -186,7 +188,14 @@ def show_conversation_for(self, source):
else:
conversation.add_file(source, conversation_item)

self.main_view.update_view(conversation)
container = QWidget()
layout = QVBoxLayout()
container.setLayout(layout)
source_profile = SourceProfileShortWidget(source, self.controller)

layout.addWidget(source_profile)
layout.addWidget(conversation)
self.main_view.update_view(container)

def set_status(self, message, duration=5000):
"""
Expand Down
143 changes: 142 additions & 1 deletion securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
import arrow
import html
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, \
QMessageBox, QToolButton

from securedrop_client.resources import load_svg, load_image
from securedrop_client.utils import humanize_filesize

Expand Down Expand Up @@ -195,6 +198,49 @@ def update(self, sources):
self.setCurrentItem(new_current_maybe)


class DeleteSourceMessageBox:
"""Use this to display operation details and confirm user choice."""

def __init__(self, parent, source, controller):
self.parent = parent
self.source = source
self.controller = controller

def launch(self):
"""It will launch the message box.
The Message box will warns the user regarding the severity of the
operation. It will confirm the desire to delete the source. On positive
answer, it will delete the record of source both from SecureDrop server
and local state.
"""
message = self._construct_message(self.source)
reply = QMessageBox.question(
self.parent,
"",
_(message),
QMessageBox.Cancel | QMessageBox.Yes,
QMessageBox.Cancel
)
if reply == QMessageBox.Yes:
logger.debug("Deleting source %s" % (self.source.uuid,))
self.controller.delete_source(self.source)

def _construct_message(self, source):
message = (
"<big>Deleting the Source account for",
"<b>%s</b> will also" % (source.journalist_designation,),
"delete %d files and %d messages.</big>" % (
len(source.submissions), len(source.replies)
),
"<br>",
"<small>This Source will no longer be able to correspond",
"through the log-in tied to this account.</small>",
)
message = ' '.join(message)
return message


class SourceWidget(QWidget):
"""
Used to display summary information about a source in the list view.
Expand All @@ -220,6 +266,10 @@ def __init__(self, parent, source):
layout.addWidget(self.summary)
self.updated = QLabel()
layout.addWidget(self.updated)
self.delete = load_svg('cross.svg')
self.delete.setMaximumSize(16, 16)
self.delete.mouseReleaseEvent = self.delete_source
self.summary_layout.addWidget(self.delete)
self.update()

def setup(self, controller):
Expand Down Expand Up @@ -262,6 +312,10 @@ def toggle_star(self, event):
"""
self.controller.update_star(self.source)

def delete_source(self, event):
messagebox = DeleteSourceMessageBox(self, self.source, self.controller)
messagebox.launch()


class LoginDialog(QDialog):
"""
Expand Down Expand Up @@ -552,3 +606,90 @@ def add_reply(self, reply, files=None):
Add a reply from a journalist.
"""
self.conversation_layout.addWidget(ReplyWidget(reply))


class DeleteSourceAction(QAction):
"""Use this action to delete the source record."""

def __init__(self, source, parent, controller):
self.source = source
self.controller = controller
self.text = _("Delete source account")
super().__init__(self.text, parent)
self.messagebox = DeleteSourceMessageBox(
parent, self.source, self.controller
)
self.triggered.connect(self.messagebox.launch)


class SourceMenu(QMenu):
"""Renders menu having various operations.
This menu provides below functionality via menu actions:
1. Delete source
Note: At present this only supports "delete" operation.
"""

def __init__(self, source, controller):
super().__init__()
self.source = source
self.controller = controller
actions = (
DeleteSourceAction(
self.source,
self,
self.controller
),
)
for action in actions:
self.addAction(action)


class SourceMenuButton(QToolButton):
"""An ellipse based source menu button.
This button is responsible for launching menu on click.
"""

def __init__(self, source, controller):
super().__init__()
self.controller = controller
self.source = source
ellipsis_icon = load_image("ellipsis.svg")
self.setIcon(QIcon(ellipsis_icon))
self.menu = SourceMenu(self.source, self.controller)
self.setMenu(self.menu)
self.setPopupMode(QToolButton.InstantPopup)


class TitleLabel(QLabel):
"""Centered aligned, HTML heading level 3 label."""

def __init__(self, text):
html_text = "<h3>%s</h3>" % (text,)
super().__init__(_(html_text))
self.setAlignment(Qt.AlignCenter)


class SourceProfileShortWidget(QWidget):
"""A widget for displaying short view for Source.
It contains below information.
1. Journalist designation
2. A menu to perform various operations on Source.
"""

def __init__(self, source, controller):
super().__init__()
self.source = source
self.controller = controller
self.layout = QHBoxLayout()
self.setLayout(self.layout)
widgets = (
TitleLabel(self.source.journalist_designation),
SourceMenuButton(self.source, self.controller)
)
for widget in widgets:
self.layout.addWidget(widget)
30 changes: 30 additions & 0 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,3 +582,33 @@ def on_download_timeout(self, current_object):
# Update the status bar to indicate a failure state.
self.set_status("The connection to the SecureDrop server timed out. "
"Please try again.")

def _on_delete_source_complete(self, result):
"""Trigger this when delete operation on source is completed."""
if result:
self.sync_api()
self.gui.update_error_status("")
else:
logging.info("failed to delete source at server")
error = _('Failed to delete source at server')
self.gui.update_error_status(error)

def _on_delete_action_timeout(self):
"""Trigger this when delete operation on source of is timeout."""
error = _('The connection to SecureDrop timed out. Please try again.')
self.gui.update_error_status(error)

def delete_source(self, source):
"""Performs a delete operation on source record.
This method will first request server to delete the source record. If
the process of deleting record at server is successful, it will sync
the server records with the local state. On failure, it will display an
error.
"""
self.call_api(
self.api.delete_source,
self._on_delete_source_complete,
self._on_delete_action_timeout,
source
)
14 changes: 14 additions & 0 deletions securedrop_client/resources/images/cross.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions securedrop_client/resources/images/ellipsis.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 10 additions & 4 deletions tests/gui/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,11 @@ def test_conversation_for():
mock_reply = mock.MagicMock()
mock_reply.filename = '3-my-source-reply.gpg'
mock_source.collection = [mock_file, mock_message, mock_reply]
with mock.patch('securedrop_client.gui.main.ConversationView',
mock_conview):
with mock.patch(
'securedrop_client.gui.main.ConversationView', mock_conview
), \
mock.patch('securedrop_client.gui.main.QVBoxLayout'), \
mock.patch('securedrop_client.gui.main.QWidget'):
w.show_conversation_for(mock_source)
conv = mock_conview()
assert conv.add_message.call_count > 0
Expand Down Expand Up @@ -224,8 +227,11 @@ def test_conversation_pending_message():

mock_source.collection = [submission]

with mock.patch('securedrop_client.gui.main.ConversationView',
mock_conview):
with mock.patch(
'securedrop_client.gui.main.ConversationView', mock_conview
), \
mock.patch('securedrop_client.gui.main.QVBoxLayout'), \
mock.patch('securedrop_client.gui.main.QWidget'):
w.show_conversation_for(mock_source)
conv = mock_conview()

Expand Down
Loading

0 comments on commit 447b3f3

Please sign in to comment.