From 1ca74229ed7a1a325f03364e5691bb5fb0a70db6 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Tue, 28 Jan 2020 16:07:28 +0000 Subject: [PATCH] Potential fix. TODO: test. --- securedrop_client/api_jobs/downloads.py | 2 +- securedrop_client/gui/widgets.py | 34 ++++++++++++++---- securedrop_client/logic.py | 13 +++++-- securedrop_client/queue.py | 11 ++++-- .../resources/images/sync_animation.gif | Bin 0 -> 935 bytes 5 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 securedrop_client/resources/images/sync_animation.gif diff --git a/securedrop_client/api_jobs/downloads.py b/securedrop_client/api_jobs/downloads.py index 419aacb163..343dbaa1bd 100644 --- a/securedrop_client/api_jobs/downloads.py +++ b/securedrop_client/api_jobs/downloads.py @@ -53,7 +53,7 @@ def call_api(self, api_client: API, session: Session) -> Any: # TODO: Once https://github.com/freedomofpress/securedrop-client/issues/648, we will want to # pass the default request timeout to api calls instead of setting it on the api object # directly. - api_client.default_request_timeout = 20 + api_client.default_request_timeout = 120 remote_sources, remote_submissions, remote_replies = \ get_remote_data(api_client) diff --git a/securedrop_client/gui/widgets.py b/securedrop_client/gui/widgets.py index 9be4e78a1f..1edac7cd90 100644 --- a/securedrop_client/gui/widgets.py +++ b/securedrop_client/gui/widgets.py @@ -215,6 +215,9 @@ def __init__(self): self.active = False + self.download_animation = load_movie("sync_animation.gif") + self.download_animation.frameChanged.connect(self.set_animation_frame) + # Set css id self.setObjectName('refresh_button') @@ -230,7 +233,7 @@ def setup(self, controller): Assign a controller object (containing the application logic). """ self.controller = controller - self.controller.sync_events.connect(self._on_refresh_complete) + self.controller.sync_events.connect(self._on_sync) def _on_clicked(self): if self.active: @@ -242,21 +245,40 @@ def _on_clicked(self): # refresh, rather than for just the duration of a click. The icon image will be replaced # when the controller tells us the refresh has finished. A cleaner solution would be to # store and update our own icon mode so we don't have to reload any images. - self.setIcon(load_icon(normal='refresh_active.svg', disabled='refresh_offline.svg')) + QTimer.singleShot(300, self.start_animation) self.active = True - def _on_refresh_complete(self, data): - if (data == 'synced'): + def start_animation(self): + """ + Starts the rotating spinner animation to indicate ongoing activity. + """ + self.download_animation.start() + + def set_animation_frame(self, frame_number): + """ + Set the current frame of the animation as the current icon for the + button. + """ + self.setIcon(QIcon(self.download_animation.currentPixmap())) + + def _on_sync(self, data): + """ + Ensure the animation is stopped and the icon reset to something static. + """ + if data == "syncing": + QTimer.singleShot(300, self.start_animation) + self.active = True + elif (data == 'synced'): self.setIcon(load_icon( normal='refresh.svg', disabled='refresh_offline.svg', active='refresh_active.svg', selected='refresh.svg')) - self.active = False + self.download_animation.stop() + self.active = False def enable(self): self.setEnabled(True) - self.active is False def disable(self): self.setEnabled(False) diff --git a/securedrop_client/logic.py b/securedrop_client/logic.py index 483ad92c4d..0a50a8836a 100644 --- a/securedrop_client/logic.py +++ b/securedrop_client/logic.py @@ -189,6 +189,9 @@ def __init__(self, hostname: str, gui, session_maker: sessionmaker, # File data. self.data_dir = os.path.join(self.home, 'data') + # Flag to indicate if sync in flight. + self.syncing = False + @property def is_authenticated(self) -> bool: return self.__is_authenticated @@ -332,7 +335,6 @@ def on_authenticate_success(self, result): self.api.journalist_last_name, self.session) self.gui.show_main_window(user) - self.update_sources() self.api_job_queue.login(self.api) self.sync_api() self.is_authenticated = True @@ -375,6 +377,8 @@ def sync_api(self, manual_refresh: bool = False): """ logger.debug("In sync_api on thread {}".format(self.thread().currentThreadId())) self.sync_events.emit('syncing') + self.syncing = True + self.set_status(_('Syncing with server...'), 60000) if self.authenticated(): logger.debug("You are authenticated, going to make your call") @@ -419,6 +423,7 @@ def on_sync_success(self) -> None: * Update missing files so that they can be re-downloaded """ self.gui.clear_error_status() # remove any permanent error status message + self.syncing = False with open(self.sync_flag, 'w') as f: f.write(arrow.now().format()) @@ -436,6 +441,7 @@ def on_sync_failure(self, result: Exception) -> None: queues so that we continue to retry syncing with the server in the background. """ logger.debug('The SecureDrop server cannot be reached due to Error: {}'.format(result)) + self.syncing = False self.gui.update_error_status( _('The SecureDrop server cannot be reached.'), duration=0, @@ -446,6 +452,7 @@ def on_refresh_failure(self, result: Exception) -> None: Called when syncronisation of data via the API fails after a user manual clicks refresh. """ logger.debug('The SecureDrop server cannot be reached due to Error: {}'.format(result)) + self.syncing = False self.gui.update_error_status( _('The SecureDrop server cannot be reached.'), duration=0, @@ -455,7 +462,8 @@ def update_sync(self): """ Updates the UI to show human time of last sync. """ - self.gui.show_sync(self.last_sync()) + if not self.syncing: + self.gui.show_sync(self.last_sync()) def update_sources(self): """ @@ -732,7 +740,6 @@ def on_file_download_success(self, result: Any) -> None: """ self.gui.clear_error_status() # remove any permanent error status message self.file_ready.emit(result) - self.update_sources() def on_file_download_failure(self, exception: Exception) -> None: """ diff --git a/securedrop_client/queue.py b/securedrop_client/queue.py index a52ce45176..536d2f6765 100644 --- a/securedrop_client/queue.py +++ b/securedrop_client/queue.py @@ -103,7 +103,12 @@ def re_add_job(self, job: ApiJob) -> None: ''' job.remaining_attempts = DEFAULT_NUM_ATTEMPTS priority = self.JOB_PRIORITIES[type(job)] - self.queue.put_nowait((priority, job)) + try: + self.queue.put_nowait((priority, job)) + except Full: + # Pass silently if the queue is full. For use with MetadataSyncJob. + # See #652. + pass @pyqtSlot() def process(self) -> None: @@ -216,7 +221,9 @@ def resume_queues(self) -> None: def enqueue(self, job: ApiJob) -> None: # Prevent api jobs being added to the queue when not logged in. - if not self.main_queue.api_client or not self.download_file_queue.api_client: + if (not self.main_queue.api_client or + not self.download_file_queue.api_client or + not self.metadata_queue.api_client): logger.info('Not adding job, we are not logged in') return diff --git a/securedrop_client/resources/images/sync_animation.gif b/securedrop_client/resources/images/sync_animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..228c59cab28754113235de7417fccb258f7ae671 GIT binary patch literal 935 zcmZ?wbhEHbRA5kGXkcLY^Y739|Nj;LbNji51UowhxEkphFf%eRFev_H;pAjsWY7V! zfl3t^7??tP`d6NQ%fEQe7O#fQxsuxZOXr7mda?&-Bs-;?YVv0~Yj_v9a~SgFdjwuy7{k^%>G_0Pt6cK;=U&M4 z{S=`nl9$H!{lqG*u%&tXZ-4o<5UXv$P~Y_=+{zNYVezJ>;L#1=Cy_x)MYCj#z4ZU= z{5ols<%}~nb-y#4wA%gEm%N>Hd`aTe$f^#3tQ9vq7}t8O3TsnpJaj?BP3MW#^*7sY z2Vu3x8y;V`n2mOLRi9Xj=Dj zErl(f`G?&;d)?Y-J?}#wvt6Fee4iVRMpegBE>DcoS{lq;_=9KLoXF6;?4+>%Ghre< zeygv)VZxTE!{C-gu^8>}&EBVa&wS!(j^4!dP4_PEinKbsLU?}Zlw|Aw!Czu_EXZBz zd})f4^2QS)Y;hNsZkGHSxbSB4s_Y%A2N$isvgnb)wpYMJ-G5q;mBG3sB&RsS(912r zEyu~o%_Bc8)x;t+y(-Dwr#LLEOh4btE5b4$z%;TXEHuKks3_btIK literal 0 HcmV?d00001