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: freedomofpress#18
  • Loading branch information
ultimatecoder committed Nov 7, 2018
1 parent 5fcf0cd commit 016f412
Show file tree
Hide file tree
Showing 8 changed files with 415 additions and 7 deletions.
58 changes: 56 additions & 2 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
from PyQt5.QtCore import Qt
from securedrop_client import __version__
from securedrop_client.gui.widgets import (ToolBar, MainView, LoginDialog,
ConversationView)
ConversationView,
SourceProfileShortWidget)
from securedrop_client.resources import load_icon
import os

Expand Down Expand Up @@ -192,7 +193,60 @@ def show_conversation_for(self, source):
else:
conversation.add_file(source, conversation_item)

self.main_view.update_view(conversation)
conversation.add_message('Hello, hello, is this thing switched on?')
conversation.add_reply('Yes, I can hear you loud and clear!')
conversation.add_reply('How can I help?')
conversation.add_message('I have top secret documents relating to '
'a massive technical scandal at the heart '
' of the Freedom of the Press Foundation. '
'In a shocking turn of events, it appears '
'they give away all their software for FREE.')
conversation.add_message("Hello: I’m a nurse at one of the trauma "
"centers in town. We've had many patients in "
"the last several months, all with "
"similar/mysterious respiratory issues. My "
"staff has noticed that most live down-wind "
"from the Dole fields West of 696. Some of "
"the patients say they have complained to "
"local authorities about sewage smells. One "
"said she's spotted a truck spraying a "
"sludge of some kind, on the fields at "
"night. I'm attaching a video from the "
"patient who taped the trucks, and a PDF of "
"redacted police reports that other patients "
"shared. I don’t know if there's much you "
"can do, but if there is I would be happy "
"to help.")
conversation.add_message("I work at the City Water Department, and a "
"man named Reggie Esters is one of our board "
"directors. I believe Reggie is related to "
"Rep Monica Conyers. He's literally never "
"here, and the resume on file for him makes "
"no sense. I have a hunch he is not in his "
"job legitimately, and think you should look "
"into this. Also: someone I work with heard "
"him on the phone once, talking about his "
"'time' at Jackson—that contradicts his "
"resume. It really seems fishy.")
conversation.add_reply("THIS IS IT THIS IS THE TAPE EVERYONE'S "
"LOOKING FOR!!!")
conversation.add_reply("Hello: I read your story on Sally Dale, and "
"her lawsuit against the St. Joseph's "
"Orphanage. My great-aunt was one of the nuns "
"there. She is willing to be interviewed, but "
"does not want her name, location, or any "
"identity details released. She feels "
"horrible. She wants the children who survived "
"to find peace. Thanks.")

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
148 changes: 146 additions & 2 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
import logging
import arrow
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtGui import QPainter, QIcon
from PyQt5.QtWidgets import (QListWidget, QTextEdit, QLabel, QToolBar, QAction,
QWidget, QListWidgetItem, QHBoxLayout,
QPushButton, QVBoxLayout, QLineEdit, QScrollArea,
QPlainTextEdit, QSpacerItem, QSizePolicy, QDialog)
QPlainTextEdit, QSpacerItem, QSizePolicy, QDialog,
QMenu, QMessageBox, QToolButton)

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

Expand Down Expand Up @@ -210,6 +212,10 @@ def __init__(self, parent, source):
self.details = QLabel()
self.details.setWordWrap(True)
layout.addWidget(self.details)
self.delete = load_svg('cross.svg')
self.delete.setMaximumSize(16, 16)
self.delete.mousePressEvent = self.delete_source
self.summary_layout.addWidget(self.delete)
self.update()

def setup(self, controller):
Expand Down Expand Up @@ -253,6 +259,36 @@ def toggle_star(self, event):
"""
self.controller.update_star(self.source)

def delete_source(self, event):
"""It will launch the message box.
The Message box will warns the user regarding the seniority of the
operation. It will re-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 = (
"<big>Deleting the Source account for",
"<b>%s</b> will also" % (self.source.journalist_designation,),
"delete %d files and %d messages.</big>" % (
len(self.source.submissions), len(self.source.replies)
),
"",
"<small>This Source will no longer be able to correspond",
"through the log-in tied to this account.</small>",
)
message = ' <br> '.join(message)
reply = QMessageBox.question(
self,
"",
_(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)


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


class ExportSourceMessagesAndFilesAction(QAction):
"""Use this action to export messages and files of source."""

def __init__(self, source, parent, controller):
self.source = source
self.controller = controller
self.text = _("Export Source messages & files")
super().__init__(self.text, parent)
self.triggered.connect(self._export_messages_and_files)

def _export_messages_and_files(self):
#TODO: Implement functionality of exporting files and messages.
pass


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.triggered.connect(self._delete_source)

def _delete_source(self):
self.controller.delete_source(self.source)


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

def __init__(self, source, controller):
super().__init__()
self.source = source
self.controller = controller
actions = (
ExportSourceMessagesAndFilesAction(
self.source,
self,
self.controller
),
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 @@ -535,3 +535,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.
4 changes: 3 additions & 1 deletion tests/gui/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ def test_conversation_for():
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):
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
Loading

0 comments on commit 016f412

Please sign in to comment.