Skip to content

Commit

Permalink
WIP everything esle all at once
Browse files Browse the repository at this point in the history
  • Loading branch information
gonzalo-bulnes committed Dec 4, 2022
1 parent f5ffdb9 commit 217d38d
Show file tree
Hide file tree
Showing 20 changed files with 437 additions and 23 deletions.
2 changes: 1 addition & 1 deletion securedrop_client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def start_app(args, qt_args) -> NoReturn: # type: ignore [no-untyped-def]
export_service.moveToThread(export_service_thread)
export_service_thread.start()

gui = Window(app_state, export_service)
gui = Window(app_state)

controller = Controller(
"http://localhost:8081/",
Expand Down
4 changes: 3 additions & 1 deletion securedrop_client/conversation/transcript/items/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ def context(self) -> Optional[str]:

@property
def transcript(self) -> str:
return self.content + "\n"
if self.content:
return self.content + "\n"
return ""
Empty file.
4 changes: 4 additions & 0 deletions securedrop_client/export/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,15 @@ def run_printer_preflight(self) -> None: # DEPRECATED
"""
Make sure the Export VM is started.
"""

with TemporaryDirectory() as temp_dir:
try:
self._run_printer_preflight(temp_dir)
self.printer_found_ready.emit()
except CLIError as e:
# HACK
# self.printer_preflight_success.emit()
# return
logger.error("Export failed")
logger.debug(f"Export failed: {e}")
self.printer_not_found_ready.emit(e)
Expand Down
141 changes: 139 additions & 2 deletions securedrop_client/gui/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@
Over time, this module could become the interface between
the GUI and the controller.
"""
import logging
from gettext import gettext as _
from pathlib import Path
from typing import Callable, Optional

from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QAction, QDialog, QMenu

from securedrop_client import state
from securedrop_client import conversation, export, state
from securedrop_client.db import Source
from securedrop_client.logic import Controller
from securedrop_client.utils import safe_mkdir

logger = logging.getLogger(__name__)


class DownloadConversation(QAction):
Expand Down Expand Up @@ -126,3 +131,135 @@ def _on_confirmation_dialog_accepted(self) -> None:
return
self.controller.delete_conversation(self.source)
self._state.remove_conversation_files(id)


class PrintConversation(QAction):
"""Use this action to print a transcript of the messages, replies, etc.
The transcript includes references to any attached files.
"""

# printer_job_enqueued = pyqtSignal(str, str)
printer_job_enqueued = pyqtSignal(list)

_SUPPORT_FOR_CONCURRENT_PRINTING_JOBS_ENABLED = False

def __init__(
self,
source: Source,
parent: QMenu,
controller: Controller,
confirmation_dialog: Callable[[export.Printer, str], QDialog],
error_dialog: Callable[[str, str], QDialog],
) -> None:
title = _("Print Conversation")
super().__init__(title, parent)

self._controller = controller
self._source = source
printing_service = export.getService()
self._printer = export.getPrinter(printing_service)
self._create_error_dialog = error_dialog
self._create_confirmation_dialog = confirmation_dialog
self._printing_job_id = self._source.journalist_designation
self._printing_job_failure_notification_in_progress = False

self.setShortcut(Qt.CTRL + Qt.Key_P)

self._printer.job_failed.connect(self._on_printing_job_failed)
self._printer.job_done.connect(self._on_printing_job_done)
self.triggered.connect(self.trigger)

self._printer.enqueue_job_on(self.printer_job_enqueued)

@pyqtSlot()
def trigger(self) -> None:
if self._controller.api is None:
self._controller.on_action_requiring_login()
else:
self._printer.connect()
self._confirmation_dialog = self._create_confirmation_dialog(
self._printer, self._transcript_display_name
)
print("CREATED", self._confirmation_dialog)
print(" PRINTER", self._printer.status, self._printer)
self._confirmation_dialog.accepted.connect(self._on_confirmation_dialog_accepted)
self._confirmation_dialog.rejected.connect(self._on_confirmation_dialog_rejected)
self._confirmation_dialog.finished.connect(self._printer.disconnect)
self._confirmation_dialog.show()

@pyqtSlot()
def _on_confirmation_dialog_rejected(self) -> None:
self.setEnabled(True)
self._confirmation_dialog.deleteLater()

@pyqtSlot()
def _on_confirmation_dialog_accepted(self) -> None:
self.setEnabled(False)
self._enqueue_printing_job(self._transcript_path)
self._confirmation_dialog.deleteLater()

@pyqtSlot(str)
def _on_printing_job_done(self, job_id: str) -> None:
if self._SUPPORT_FOR_CONCURRENT_PRINTING_JOBS_ENABLED and job_id != self._printing_job_id:
return

self.setEnabled(True)

@pyqtSlot(str, str)
def _on_printing_job_failed(self, job_id: str, reason: str) -> None:
if self._SUPPORT_FOR_CONCURRENT_PRINTING_JOBS_ENABLED and job_id != self._printing_job_id:
return

# The lack of meaningful job_id means we can only print one conversation at a time.
# Enabling _SUPPORT_FOR_CONCURRENT_PRINTING_JOBS_ENABLED removes the need for the following
# flag, but requires the export service to associate a job_id to its responses.
# Note that such job_id can (and should) be opaque to the export service.
if not self._printing_job_failure_notification_in_progress:
self._printing_job_failure_notification_in_progress = True

self._error_dialog = self._create_error_dialog(job_id, reason)
self._error_dialog.finished.connect(self._on_error_dialog_finished)
self._error_dialog.finished.connect(self._printer.disconnect)
self._error_dialog.show()

@pyqtSlot(int)
def _on_error_dialog_finished(self, _result: int) -> None:
self.setEnabled(True)
self._error_dialog.deleteLater()
self._printing_job_failure_notification_in_progress = False

def _start_printer(self) -> None:
"""Start the printer in a thread-safe manner."""
self.printer_start_requested.emit()

def _enqueue_printing_job(self, file_path: Path) -> None:
"""Enqueue a printing job in a thread-safe manner."""

transcript = conversation.Transcript(self._source)
safe_mkdir(file_path.parent)

with open(file_path, "w") as f:
f.write(str(transcript))

logger.info(f"Printing transcript of conversation: ({self._transcript_display_name})")

# self.printer_job_enqueued.emit(self._printing_job_id, str(file_path))
self.printer_job_enqueued.emit([str(file_path)])

@property
def _transcript_path(self) -> Path:
"""The transcript path. This is te source of truth for this data."""
return (
Path(self._controller.data_dir)
.joinpath(self._source.journalist_filename)
.joinpath("conversation.txt")
)

@property
def _transcript_display_name(self) -> str:
"""The transcript name for display purposes.
Example: wonderful_source/conversation.txt
"""
return str(self._transcript_path.relative_to(self._transcript_path.parents[1]))
2 changes: 2 additions & 0 deletions securedrop_client/gui/conversation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
from .export import Device as ExportDevice # noqa: F401
from .export import Dialog as ExportFileDialog # noqa: F401
from .export import PrintDialog as PrintFileDialog # noqa: F401
from .print import ConfirmationDialog as PrintConfirmationDialog # noqa: F401
from .print import ErrorDialog as PrintErrorDialog # noqa: F401
2 changes: 2 additions & 0 deletions securedrop_client/gui/conversation/print/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .confirmation_dialog import ConfirmationDialog # noqa: F401
from .error_dialog import ErrorDialog # noqa: F401
84 changes: 84 additions & 0 deletions securedrop_client/gui/conversation/print/confirmation_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from gettext import gettext as _

from PyQt5.QtCore import QSize, pyqtSlot

from securedrop_client.export import Printer
from securedrop_client.gui.base import ModalDialog, SecureQLabel


class ConfirmationDialog(ModalDialog):

FILENAME_WIDTH_PX = 260

def __init__(self, printer: Printer, file_name: str) -> None:
super().__init__()

self._printer = printer
print("when creating the dialog", self._printer.status)
self._printer.status_changed.connect(self._on_printer_status_changed)

self.continue_button.setText("PRINT")
self.continue_button.clicked.connect(
self.accept
) # FIXME The ModalDialog is more complex than needed to do this.

file_name = SecureQLabel(
file_name, wordwrap=False, max_length=self.FILENAME_WIDTH_PX
).text() # FIXME This seems like a heavy way to sanitize a string.
header = _("Print:<br />" '<span style="font-weight:normal">{}</span>').format(file_name)
body = _(
"<h2>Managing printout risks</h2>"
"<b>QR codes and web addresses</b>"
"<br />"
"Never type in and open web addresses or scan QR codes contained in printed "
"documents without taking security precautions. If you are unsure how to "
"manage this risk, please contact your administrator."
"<br /><br />"
"<b>Printer dots</b>"
"<br />"
"Any part of a printed page may contain identifying information "
"invisible to the naked eye, such as printer dots. Please carefully "
"consider this risk when working with or publishing scanned printouts."
)

self.header.setText(header)
self.header_icon.update_image("printer.svg", svg_size=QSize(64, 64))
self.body.setText(body)
self.adjustSize()

self._body = body

self._on_printer_status_changed()

@pyqtSlot()
def _on_printer_status_changed(self) -> None:
printer_status = self._printer.status
if printer_status == Printer.StatusUnknown:
print("printer status became unknown")
self._on_printer_status_unknown()
elif printer_status == Printer.StatusReady:
print("printer status ready")
self._on_printer_ready()
elif printer_status == Printer.StatusUnreachable:
print("printer status unreachable")
self._on_printer_unreachable()

def _on_printer_status_unknown(self) -> None:
self.continue_button.setEnabled(False)

status = "<i>Waiting for printer status to be known...</i>"
self.body.setText("<br /><br />".join([self._body, status]))
self.adjustSize()

def _on_printer_ready(self) -> None:
self.continue_button.setEnabled(True)

self.body.setText(self._body)
self.adjustSize()

def _on_printer_unreachable(self) -> None:
self.continue_button.setEnabled(False)

status = "<i>Printer unreachable, please verify it's connected.</i>"
self.body.setText("<br /><br />".join([self._body, status]))
self.adjustSize()
32 changes: 32 additions & 0 deletions securedrop_client/gui/conversation/print/error_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from gettext import gettext as _

from PyQt5.QtCore import QSize

from securedrop_client.gui.base import ModalDialog, SecureQLabel


class ErrorDialog(ModalDialog):

FILENAME_WIDTH_PX = 260

def __init__(self, file_name: str, reason: str) -> None:
super().__init__()

self.continue_button.clicked.connect(
self.accept
) # FIXME The ModalDialog is more complex than needed to do this.

file_name = SecureQLabel(
file_name, wordwrap=False, max_length=self.FILENAME_WIDTH_PX
).text() # FIXME This seems like a heavy way to sanitize a string.
reason = SecureQLabel(
reason, wordwrap=False, max_length=self.FILENAME_WIDTH_PX
).text() # FIXME This seems like a heavy way to sanitize a string.
header = _("Printing failed<br />" '<span style="font-weight:normal">{}</span>').format(
file_name
)

self.header.setText(header)
self.header_icon.update_image("printer.svg", svg_size=QSize(64, 64))
self.body.setText(reason)
self.adjustSize()
5 changes: 2 additions & 3 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from PyQt5.QtGui import QClipboard, QGuiApplication, QIcon, QKeySequence
from PyQt5.QtWidgets import QAction, QApplication, QHBoxLayout, QMainWindow, QVBoxLayout, QWidget

from securedrop_client import __version__, export, state
from securedrop_client import __version__, state
from securedrop_client.db import Source, User
from securedrop_client.gui.auth import LoginDialog
from securedrop_client.gui.widgets import LeftPane, MainView, TopPane
Expand All @@ -48,7 +48,6 @@ class Window(QMainWindow):
def __init__(
self,
app_state: Optional[state.State] = None,
export_service: Optional[export.Service] = None,
) -> None:
"""
Create the default start state. The window contains a root widget into
Expand Down Expand Up @@ -77,7 +76,7 @@ def __init__(
layout.setSpacing(0)
self.main_pane.setLayout(layout)
self.left_pane = LeftPane()
self.main_view = MainView(self.main_pane, app_state, export_service)
self.main_view = MainView(self.main_pane, app_state)
layout.addWidget(self.left_pane)
layout.addWidget(self.main_view)

Expand Down
Loading

0 comments on commit 217d38d

Please sign in to comment.