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

GUI scaffolding and core functions #29

Merged
merged 10 commits into from
Oct 10, 2018
Merged

Conversation

ntoll
Copy link
Contributor

@ntoll ntoll commented Sep 26, 2018

This is currently a work in progress. This PR is simply a place for discussion and to give more visibility to the work being done to initially address #15 and #14.

Things to note:

  • The application is started/configured in app.py.
  • Program logic is in the Client class in logic.py.
  • UI related code is in the gui namespace. All interaction with the UI is via the Window class in gui.main.
  • Custom widgets for the client app are in gui.widgets. These should only be used by the afore mentioned Window class.
  • A resources namespace containing some utility functions and CSS/image assets has been created.
  • Currently contains a "spike" for non-blocking API calls (see inside logic module for the rough draft which is, at this stage, very much a work in progress).

Right now, it looks like this:

sd_client

@redshiftzero
Copy link
Contributor

Looking awesome. Btw a few of us ran into mu-editor/mu#502 (a cool project! ;)) while testing this out today, so heads up to other reviewers to run:

pipenv install pyqt5==5.10.1

and running the application here should occur without issue

@ntoll
Copy link
Contributor Author

ntoll commented Oct 3, 2018

This PR is ready for review. I've added a bunch of generic code scaffolding for interacting with the remote API, I've also refactored the storage module so API calls and local database calls are done in separate functions (because you can't use SqlAlchemy on multiple threads). Tests have 100% coverage. The code is copiously commented too.

Happy to answer questions..!

@ntoll
Copy link
Contributor Author

ntoll commented Oct 3, 2018

No idea why CI is failing. Help!?!?!

@redshiftzero redshiftzero self-requested a review October 4, 2018 17:09
@redshiftzero
Copy link
Contributor

Hm, yeah that is indeed a mysterious CI failure. Will SSH in and take a look...

@redshiftzero
Copy link
Contributor

Ah, so it looks like the issue is occurring during the loading of resources:

circleci@114e47a8e7c3:~/project$ pipenv run pytest -v tests/test_resources.py 
=================================================== test session starts ====================================================
platform linux -- Python 3.5.6, pytest-3.8.2, py-1.6.0, pluggy-0.7.1 -- /home/circleci/.local/share/virtualenvs/project-zxI9dQ-Q/bin/python3.5m
cachedir: .pytest_cache
Using --random-order-bucket=module
Using --random-order-seed=716462

rootdir: /home/circleci/project, inifile:
plugins: random-order-0.8.0, cov-2.6.0
collected 5 items                                                                                                          

tests/test_resources.py::test_load_svg Aborted

Will dig further

@redshiftzero
Copy link
Contributor

Hey @ntoll, so - while I'm not sure why there is divergence between CI and the local dev env (all tests pass locally for me), the issue causing the fatal error here in CI is QPixmap: Must construct a QGuiApplication before a QPixmap:

(project-zxI9dQ-Q) circleci@cb55424a6d9a:~/project$ xvfb-run python
Python 3.5.6 (default, Sep  5 2018, 04:03:52) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import securedrop_client.resources
>>> securedrop_client.resources.load_icon('icon')
QPixmap: Must construct a QGuiApplication before a QPixmap
Aborted

Instantiating a QApplication object before creating the QPixmap resolves:

(project-zxI9dQ-Q) circleci@5b60e947b3af:~/project$ xvfb-run python
Python 3.5.6 (default, Sep  5 2018, 04:03:52) 
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from PyQt5.QtWidgets import QApplication
>>> app = QApplication([])
QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-circleci'
>>> import securedrop_client.resources
>>> securedrop_client.resources.load_icon('icon')
<PyQt5.QtGui.QIcon object at 0x7f96e597d048>

have you seen this before?

language_code = 'en'
# DEBUG/TRANSLATE: override the language code here (e.g. to Chinese).
# language_code = 'zh'
gettext.translation('mu', localedir=localedir,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mu -> securedrop_client

for the domain here

@@ -178,7 +214,7 @@ def find_or_create_user(username, session):
Returns a user object representing the referenced username. If the username
does not already exist in the data, a new instance is created.
"""
user = session.query(User).filter_by(username=username)
user = list(session.query(User).filter_by(username=username))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the following about the sync logic when logging in using the client GUI: journalist users can change their journalist username so we should be using the journalist UUID as a unique identifier (journalist UUID will not change). Unfortunately the SDK doesn't expose this right now on its Reply objects (only the journalist username), so I made freedomofpress/securedrop-sdk#19 to resolve. Then we can do something like the following for the find_or_create_user logic:

diff --git a/securedrop_client/storage.py b/securedrop_client/storage.py
index 59e9a9c..7e4ee28 100644
--- a/securedrop_client/storage.py
+++ b/securedrop_client/storage.py
@@ -187,7 +187,8 @@ def update_replies(remote_replies, local_replies, session):
         if reply.uuid in local_uuids:
             # Update an existing record.
             local_reply = [r for r in local_replies if r.uuid == reply.uuid][0]
-            user = find_or_create_user(reply.journalist_username, session)
+            user = find_or_create_user(reply.journalist_uuid,
+                                       reply.journalist_username, session)
             local_reply.journalist_id = user.id
             local_reply.filename = reply.filename
             local_reply.size = reply.size
@@ -197,7 +198,8 @@ def update_replies(remote_replies, local_replies, session):
             # A new reply to be added to the database.
             source_uuid = reply.source_uuid
             source = session.query(Source).filter_by(uuid=source_uuid)[0]
-            user = find_or_create_user(reply.journalist_username, session)
+            user = find_or_create_user(reply.journalist_uuid,
+                                       reply.journalist_username, session)
             nr = Reply(reply.uuid, user, source, reply.filename, reply.size)
             session.add(nr)
             logger.info('Added new reply {}'.format(reply.uuid))
@@ -209,15 +211,27 @@ def update_replies(remote_replies, local_replies, session):
     session.commit()
 
 
-def find_or_create_user(username, session):
-    """
-    Returns a user object representing the referenced username. If the username
-    does not already exist in the data, a new instance is created.
-    """
-    user = list(session.query(User).filter_by(username=username))
-    if user:
-        return user[0]
-    new_user = User(username)
-    session.add(new_user)
-    session.commit()
-    return new_user
+def find_or_create_user(uuid, username, session):
+    """
+    Returns a user object representing the referenced journalist UUID.
+    If the user does not already exist in the data, a new instance is created.
+    If the user exists but the username has changed, the username is updated.
+    """
+    user = session.query(User).filter_by(uuid=uuid).one()
+
+    if user and user.username == username:
+        # User exists in the local database and the username is unchanged.
+        return user
+    elif user and user.username != username:
+        # User exists in the local database but the username is changed.
+        user.username = username
+        session.add(user)
+        session.commit()
+        return user
+    else:
+        # User does not exist in the local database.
+        new_user = User(username)
+        new_user.uuid = uuid
+        session.add(new_user)
+        session.commit()
+        return new_user

Let me know if you notice an error with my logic here

@redshiftzero
Copy link
Contributor

redshiftzero commented Oct 10, 2018

@ntoll and I just had a quick chat about this PR, which is looking great! The final steps prior to merge are:

This gives us a nice foundation in master for further work. We can add the conversations view and breaking the login page into its own window in followup PRs

Copy link
Contributor

@redshiftzero redshiftzero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for addressing my comments, looks good! mergin'

@redshiftzero redshiftzero merged commit f12019a into freedomofpress:master Oct 10, 2018
@eloquence eloquence mentioned this pull request Nov 8, 2022
10 tasks
legoktm pushed a commit that referenced this pull request Dec 11, 2023
legoktm pushed a commit that referenced this pull request Dec 11, 2023
Have CI test and build on bullseye too
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants