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

no longer access source db obj in snippet #964

Merged
merged 2 commits into from
Mar 21, 2020
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
77 changes: 44 additions & 33 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import sys

from gettext import gettext as _
from typing import Dict, List, Union # noqa: F401
from typing import Dict, List, Union, Optional # noqa: F401
from uuid import uuid4
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QEvent, QTimer, QSize, pyqtBoundSignal, \
QObject, QPoint
Expand Down Expand Up @@ -927,12 +927,12 @@ def update(self, sources: List[Source]) -> List[str]:

try:
del self.source_widgets[list_widget.source_uuid]
deleted_uuids.append(list_widget.source_uuid)
except KeyError:
pass
finally:
self.takeItem(i)
list_widget.deleteLater()

self.takeItem(i)
deleted_uuids.append(list_widget.source_uuid)
list_widget.deleteLater()

# Create new widgets for new sources
widget_uuids = [self.itemWidget(self.item(i)).source_uuid for i in range(self.count())]
Expand Down Expand Up @@ -966,15 +966,35 @@ def get_current_source(self):
if source_widget and source_exists(self.controller.session, source_widget.source_uuid):
return source_widget.source

def set_snippet(self, source_uuid, message_uuid, content):
"""
Given a UUID of a source, if the referenced message is the latest
message, then update the source's preview snippet to the referenced
content.
"""
source_widget = self.source_widgets.get(source_uuid)
def get_source_widget(self, source_uuid: str) -> Optional[QListWidget]:
'''
First try to get the source widget from the cache, then look for it in the SourceList.
'''
try:
source_widget = self.source_widgets[source_uuid]
return source_widget
except KeyError:
pass

for i in range(self.count()):
list_item = self.item(i)
source_widget = self.itemWidget(list_item)
if source_widget and source_widget.source_uuid == source_uuid:
return source_widget

return None

@pyqtSlot(str, str, str)
def set_snippet(self, source_uuid: str, collection_item_uuid: str, content: str) -> None:
'''
Set the source widget's preview snippet with the supplied content.

Note: The signal's `collection_item_uuid` is not needed for setting the preview snippet. It
is used by other signal handlers.
'''
source_widget = self.get_source_widget(source_uuid)
if source_widget:
source_widget.set_snippet(source_uuid, message_uuid, content)
source_widget.set_snippet(source_uuid, content)


class SourceWidget(QWidget):
Expand Down Expand Up @@ -1145,37 +1165,28 @@ def update(self):
self.controller.session.refresh(self.source)
self.timestamp.setText(_(arrow.get(self.source.last_updated).format('DD MMM')))
self.name.setText(self.source.journalist_designation)
self.set_snippet(self.source.uuid)

if not self.source.collection:
self.set_snippet(self.source_uuid, '')
else:
last_collection_obj = self.source.collection[-1]
self.set_snippet(self.source_uuid, str(last_collection_obj))

if self.source.document_count == 0:
self.paperclip.hide()
self.star.update()
except sqlalchemy.exc.InvalidRequestError as e:
logger.error(f"Could not update SourceWidget for source {self.source_uuid}: {e}")
raise

def set_snippet(self, source: str, uuid: str = None, content: str = None):
def set_snippet(self, source_uuid: str, content: str):
"""
Update the preview snippet only if the new message is for the
referenced source and there's a source collection. If a uuid and
content are passed then use these, otherwise default to whatever the
latest item in the conversation might be.
Update the preview snippet if the source_uuid matches our own.
"""
if source != self.source_uuid:
if source_uuid != self.source_uuid:
return

self.preview.setText("")

try:
self.controller.session.refresh(self.source)
if not self.source.collection:
return
last_collection_object = self.source.collection[-1]
if uuid == last_collection_object.uuid and content:
self.preview.setText(content)
else:
self.preview.setText(str(last_collection_object))
except sqlalchemy.exc.InvalidRequestError as e:
logger.error(f"Could not update snippet for source {self.source_uuid}: {e}")
self.preview.setText(content)

def delete_source(self, event):
if self.controller.api is None:
Expand Down
54 changes: 38 additions & 16 deletions tests/gui/test_widgets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
Make sure the UI widgets are configured correctly and work as expected.
"""
import re
import pytest
import arrow
from datetime import datetime
Expand Down Expand Up @@ -950,7 +949,40 @@ def test_SourceList_set_snippet(mocker):
"a_uuid": mock_widget,
}
sl.set_snippet("a_uuid", "msg_uuid", "msg_content")
mock_widget.set_snippet.assert_called_once_with("a_uuid", "msg_uuid", "msg_content")
mock_widget.set_snippet.assert_called_once_with("a_uuid", "msg_content")


def test_SourceList_get_source_widget(mocker):
sl = SourceList()
sl.controller = mocker.MagicMock()
mock_source = factory.Source(uuid='mock_uuid')
sl.update([mock_source])
sl.source_widgets = {}

source_widget = sl.get_source_widget('mock_uuid')

assert source_widget.source_uuid == 'mock_uuid'
assert source_widget == sl.itemWidget(sl.item(0))


def test_SourceList_get_source_widget_does_not_exist(mocker):
sl = SourceList()
sl.controller = mocker.MagicMock()
mock_source = factory.Source(uuid='mock_uuid')
sl.update([mock_source])
sl.source_widgets = {}

source_widget = sl.get_source_widget('uuid_for_source_not_in_list')

assert source_widget is None


def test_SourceList_get_source_widget_if_one_exists_in_cache(mocker):
sl = SourceList()
mock_source_widget = factory.Source(uuid='mock_uuid')
sl.source_widgets = {'mock_uuid': mock_source_widget}
source_widget = sl.get_source_widget('mock_uuid')
assert source_widget == mock_source_widget


def test_SourceWidget_init(mocker):
Expand Down Expand Up @@ -1013,29 +1045,19 @@ def test_SourceWidget_set_snippet(mocker, session_maker, session, homedir):
session.commit()

sw = SourceWidget(controller, source)
sw.set_snippet(source.uuid, f.uuid, f.filename)
sw.set_snippet(source.uuid, f.filename)
assert sw.preview.text() == f.filename

# check when a different source is specified
sw.set_snippet("not-the-source-uuid", f.uuid, "something new")
sw.set_snippet("not-the-source-uuid", "something new")
assert sw.preview.text() == f.filename

# check when the source has been deleted
source_uuid = source.uuid
session.delete(source)
session.commit()
error_logger = mocker.patch("securedrop_client.gui.widgets.logger.error")
sw.set_snippet(source_uuid, "some-uuid", "something new")
error_logger.assert_called_once()
assert re.match(
(
f"Could not update snippet for source {source_uuid}: "
"Instance '<Source at 0x[0-9a-f]+>' is not persistent within this Session"
),
error_logger.call_args_list[0][0][0],
)

assert sw.preview.text() == ""
# check when the source has been deleted that it catches sqlalchemy.exc.InvalidRequestError
sw.set_snippet(source_uuid, "something new")


def test_SourceWidget_update_truncate_latest_msg(mocker):
Expand Down