From d7028d3c55d6d45f5d5b3a1a269bf3405e6901fc Mon Sep 17 00:00:00 2001 From: Gonzalo Bulnes Guilpain Date: Wed, 26 Oct 2022 22:35:34 -0400 Subject: [PATCH] Add conversation.Transcript --- securedrop_client/conversation/__init__.py | 1 + .../conversation/transcript/__init__.py | 1 + .../conversation/transcript/items/__init__.py | 2 + .../conversation/transcript/items/factory.py | 16 +++ .../conversation/transcript/items/file.py | 22 +++ .../conversation/transcript/items/item.py | 13 ++ .../conversation/transcript/items/message.py | 26 ++++ .../conversation/transcript/transcript.py | 44 ++++++ securedrop_client/locale/messages.pot | 9 ++ tests/test_conversation.py | 127 ++++++++++++++++++ 10 files changed, 261 insertions(+) create mode 100644 securedrop_client/conversation/__init__.py create mode 100644 securedrop_client/conversation/transcript/__init__.py create mode 100644 securedrop_client/conversation/transcript/items/__init__.py create mode 100644 securedrop_client/conversation/transcript/items/factory.py create mode 100644 securedrop_client/conversation/transcript/items/file.py create mode 100644 securedrop_client/conversation/transcript/items/item.py create mode 100644 securedrop_client/conversation/transcript/items/message.py create mode 100644 securedrop_client/conversation/transcript/transcript.py create mode 100644 tests/test_conversation.py diff --git a/securedrop_client/conversation/__init__.py b/securedrop_client/conversation/__init__.py new file mode 100644 index 0000000000..9fff1e925c --- /dev/null +++ b/securedrop_client/conversation/__init__.py @@ -0,0 +1 @@ +from .transcript import Transcript # noqa: F401 diff --git a/securedrop_client/conversation/transcript/__init__.py b/securedrop_client/conversation/transcript/__init__.py new file mode 100644 index 0000000000..9fff1e925c --- /dev/null +++ b/securedrop_client/conversation/transcript/__init__.py @@ -0,0 +1 @@ +from .transcript import Transcript # noqa: F401 diff --git a/securedrop_client/conversation/transcript/items/__init__.py b/securedrop_client/conversation/transcript/items/__init__.py new file mode 100644 index 0000000000..94c5de1180 --- /dev/null +++ b/securedrop_client/conversation/transcript/items/__init__.py @@ -0,0 +1,2 @@ +from .factory import transcribe # noqa: F401 +from .item import Item # noqa: F401 diff --git a/securedrop_client/conversation/transcript/items/factory.py b/securedrop_client/conversation/transcript/items/factory.py new file mode 100644 index 0000000000..723d68293a --- /dev/null +++ b/securedrop_client/conversation/transcript/items/factory.py @@ -0,0 +1,16 @@ +from typing import Optional + +from securedrop_client import db as database + +from .file import File +from .item import Item +from .message import Message + + +def transcribe(record: database.Base) -> Optional[Item]: + if isinstance(record, database.Message) or isinstance(record, database.Reply): + return Message(record) + if isinstance(record, database.File): + return File(record) + else: + return None diff --git a/securedrop_client/conversation/transcript/items/file.py b/securedrop_client/conversation/transcript/items/file.py new file mode 100644 index 0000000000..3276d26a27 --- /dev/null +++ b/securedrop_client/conversation/transcript/items/file.py @@ -0,0 +1,22 @@ +from gettext import gettext as _ +from typing import Optional + +from securedrop_client import db as database + +from .item import Item + + +class File(Item): + def __init__(self, record: database.File): + super().__init__() + + self.filename = record.filename + self.sender = record.source.journalist_designation + + @property + def context(self) -> Optional[str]: + return _("{username} sent:\n").format(username=self.sender) + + @property + def transcript(self) -> str: + return _("File: {filename}\n").format(filename=self.filename) diff --git a/securedrop_client/conversation/transcript/items/item.py b/securedrop_client/conversation/transcript/items/item.py new file mode 100644 index 0000000000..8a8399d2b4 --- /dev/null +++ b/securedrop_client/conversation/transcript/items/item.py @@ -0,0 +1,13 @@ +from typing import Optional + + +class Item: + @property + def transcript(self) -> str: + """A transcription of the conversation item.""" + raise NotImplementedError # pragma: nocover + + @property + def context(self) -> Optional[str]: + """Some context about the conversation item.""" + raise NotImplementedError # pragma: nocover diff --git a/securedrop_client/conversation/transcript/items/message.py b/securedrop_client/conversation/transcript/items/message.py new file mode 100644 index 0000000000..d00778d2aa --- /dev/null +++ b/securedrop_client/conversation/transcript/items/message.py @@ -0,0 +1,26 @@ +from gettext import gettext as _ +from typing import Optional, Union + +from securedrop_client import db as database + +from .item import Item + + +class Message(Item): + def __init__(self, record: Union[database.Message, database.Reply]): + super().__init__() + + self.content = record.content + + if isinstance(record, database.Message): + self.sender = record.source.journalist_designation + else: + self.sender = record.journalist.username + + @property + def context(self) -> Optional[str]: + return _("{username} wrote:\n").format(username=self.sender) + + @property + def transcript(self) -> str: + return self.content + "\n" diff --git a/securedrop_client/conversation/transcript/transcript.py b/securedrop_client/conversation/transcript/transcript.py new file mode 100644 index 0000000000..c2d826a084 --- /dev/null +++ b/securedrop_client/conversation/transcript/transcript.py @@ -0,0 +1,44 @@ +from typing import List, Optional + +from securedrop_client import db as database + +from .items import Item +from .items import transcribe as transcribe_item + + +def transcribe(record: database.Base) -> Optional[Item]: + return transcribe_item(record) + + +_ENTRY_SEPARATOR = "------\n" + + +class Transcript: + def __init__(self, conversation: database.Source) -> None: + + self._items = [transcribe(record) for record in conversation.collection] + + def __str__(self) -> str: + if len(self._items) <= 0: + return "No messages." + + entries: List[str] = [] + + context: Optional[str] = None + + for item in self._items: + if item is None: + continue + + if context is not None and context == item.context: + entry = item.transcript + elif item.context is None: + entry = item.transcript # pragma: nocover + else: + entry = f"{item.context}{item.transcript}" + + entries.append(entry) + + context = item.context + + return _ENTRY_SEPARATOR.join(entries) diff --git a/securedrop_client/locale/messages.pot b/securedrop_client/locale/messages.pot index f6cc16d8e0..fa2c4868e0 100644 --- a/securedrop_client/locale/messages.pot +++ b/securedrop_client/locale/messages.pot @@ -61,6 +61,15 @@ msgstr "" msgid "Failed to delete source at server" msgstr "" +msgid "{username} sent:\n" +msgstr "" + +msgid "File: {filename}\n" +msgstr "" + +msgid "{username} wrote:\n" +msgstr "" + msgid "Download All Files" msgstr "" diff --git a/tests/test_conversation.py b/tests/test_conversation.py new file mode 100644 index 0000000000..d03daed144 --- /dev/null +++ b/tests/test_conversation.py @@ -0,0 +1,127 @@ +import unittest +from datetime import datetime +from textwrap import dedent + +from securedrop_client import conversation +from securedrop_client import db as database + + +class TestConversationTranscript(unittest.TestCase): + def setUp(self): + + source = database.Source( + journalist_designation="happy-bird", + ) + files = [ + database.File( + filename="4-memo.pdf.gpg", + is_downloaded=True, + ), + database.File( + filename="9-memo.zip.gpg", + is_downloaded=True, + ), + ] + messages = [ + database.Message( + filename="1-message.gpg", + is_downloaded=True, + content="Hello! I think this is newsworthy: ...", + ), + database.Message( + filename="6-message.gpg", + is_downloaded=True, + content="I can send you more if you're interested.", + ), + database.Message( + filename="5-message.gpg", + is_downloaded=True, + content="Here is a document with details!", + ), + database.Message( + filename="8-message.gpg", + is_downloaded=True, + content="Sure.", + ), + ] + interested_journalist = database.User(username="interested-journalist") + other_journalist = database.User(username="other-journalist") + replies = [ + database.Reply( + journalist=interested_journalist, + filename="2-reply.gpg", + is_downloaded=True, + content=dedent( + """\ + Thank you for the tip! + + Can you tell me more about... ? + """ + ), + ), + database.Reply( + journalist=interested_journalist, + filename="3-reply.gpg", + is_downloaded=True, + content=dedent( + """\ + Do you have proof of...? + """ + ), + ), + database.Reply( + journalist=other_journalist, + filename="7-reply.gpg", + is_downloaded=True, + content=dedent("Yes, the document you sent was useful, I'd love to see more."), + ), + ] + draft_reply = database.DraftReply( + content="Let me think...", + file_counter=2, + timestamp=datetime.now(), + ) + source.files = files + source.messages = messages + source.replies = replies + source.draftreplies = [draft_reply] + + self._source = source + + def test_indicates_explicitly_absence_of_messages(self): + source = database.Source() + assert str(conversation.Transcript(source)) == "No messages." + + def test_renders_all_messages(self): + assert str(conversation.Transcript(self._source)) == dedent( + """\ + happy-bird wrote: + Hello! I think this is newsworthy: ... + ------ + interested-journalist wrote: + Thank you for the tip! + + Can you tell me more about... ? + + ------ + Do you have proof of...? + + ------ + happy-bird sent: + File: 4-memo.pdf.gpg + ------ + happy-bird wrote: + Here is a document with details! + ------ + I can send you more if you're interested. + ------ + other-journalist wrote: + Yes, the document you sent was useful, I'd love to see more. + ------ + happy-bird wrote: + Sure. + ------ + happy-bird sent: + File: 9-memo.zip.gpg + """ + )