From 3ccc9e0c4f15362b25e4d5cc8658aede663a6cf9 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Tue, 14 May 2019 16:25:10 -0400 Subject: [PATCH 01/12] Transcription View: avoid updates for unmodified sources This avoids the problem in #949 where the update triggered by the reservation system would cause the transcription view to replace any unsaved text with the original starting text. Now the update method will not replace the text when the upstream value has not changed. This still requires testing around the case where someone may not have had a lock when they first clicked on the item to confirm that we correctly will reset it when the lock is obtained. --- concordia/static/js/action-app/components.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/concordia/static/js/action-app/components.js b/concordia/static/js/action-app/components.js index 4d5e573ad..8110ceddb 100644 --- a/concordia/static/js/action-app/components.js +++ b/concordia/static/js/action-app/components.js @@ -620,6 +620,23 @@ class TranscriberView { } update(asset) { + if ( + this.currentAsset && + this.currentAsset.id == asset.id && + asset.latest_transcription && + this.currentAsset.latest_transcription && + this.currentAsset.latest_transcription.id == + asset.latest_transcription.id && + this.currentAsset.latest_transcription.text == + asset.latest_transcription.text + ) { + // eslint-disable-next-line no-console + console.debug( + `Asset ${asset.id} unmodified; not resetting transcription view` + ); + return; + } + this.currentAsset = asset; let text = ''; if (asset.latest_transcription && asset.latest_transcription.text) { From 55e64c688cd109d4890a0b1cdfb05e0a1fb0c631 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Tue, 14 May 2019 17:04:25 -0400 Subject: [PATCH 02/12] Start refactoring when reservations check editability This allows the system to periodically re-check whether it makes sense to obtain a reservation. We can add additional prompts to the stream receiver as needed. --- concordia/static/js/action-app/index.js | 37 +++++++++++++++---------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/concordia/static/js/action-app/index.js b/concordia/static/js/action-app/index.js index 08ddf7edd..aeb30adb0 100644 --- a/concordia/static/js/action-app/index.js +++ b/concordia/static/js/action-app/index.js @@ -787,22 +787,15 @@ export class ActionApp { // FIXME: refactor openAssetElement into a single open asset ID property & pass it to the respective list & viewer components this.openAssetElement = assetElement; + this.assetReservationURL = this.urlTemplates.assetReservation.expand({ + assetId: encodeURIComponent(asset.id) + }); - let {canEdit} = this.canEditAsset(asset); - - if (canEdit) { - this.assetReservationURL = this.urlTemplates.assetReservation.expand( - { - assetId: encodeURIComponent(asset.id) - } - ); - - this.reserveAsset(); - this.reservationTimer = window.setInterval( - this.reserveAsset.bind(this), - 30000 - ); - } + this.reserveAsset(); + this.reservationTimer = window.setInterval( + this.reserveAsset.bind(this), + 30000 + ); this.metadataPanel = new MetadataPanel(asset); mount( @@ -881,11 +874,25 @@ export class ActionApp { } reserveAsset() { + // TODO: refactor open asset DOM/class references + if (!this.appElement.dataset.openAssetId) { + console.warn('reserveAsset called without an open asset?'); + return; + } + if (!this.assetReservationURL) { console.warn('reserveAsset called without asset reservation URL!'); return; } + let asset = this.assets.get(this.appElement.dataset.openAssetId); + let {canEdit, reason} = this.canEditAsset(asset); + + if (!canEdit) { + console.info(`Asset ${asset.id} cannot be edited: ${reason}`); + return; + } + let reservationURL = this.assetReservationURL; jQuery From 46acd99b16483d675e578e763b6af20367378f2e Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Tue, 14 May 2019 17:11:00 -0400 Subject: [PATCH 03/12] Attempt to reserve open unreserved assets If someone else releases the asset which is currently open, attempt to reserve it ahead of schedule --- concordia/static/js/action-app/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/concordia/static/js/action-app/index.js b/concordia/static/js/action-app/index.js index aeb30adb0..54c8c3e10 100644 --- a/concordia/static/js/action-app/index.js +++ b/concordia/static/js/action-app/index.js @@ -280,6 +280,13 @@ export class ActionApp { reservationToken: null }); + if ( + this.appElement.dataset.openAssetId && + this.appElement.dataset.openAssetId == assetId + ) { + this.reserveAsset(); + } + break; default: console.warn( @@ -895,6 +902,8 @@ export class ActionApp { let reservationURL = this.assetReservationURL; + // TODO: record the last asset renewal time and don't renew early unless the ID has changed + jQuery .ajax({ url: reservationURL, From dd82245de33c5ddeacc9abbc612ee501e8420602 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 15:47:49 -0400 Subject: [PATCH 04/12] Refactor asset data handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Asset data will be indexed using the type provided by the remote API and a convenience function will handle any necessary type conversion * openViewer() takes the asset ID rather than an Element * actionApp will have an openAssetId property set which can be used for all is-open tests * AssetList component no longer needs to have access to the entire asset list in the constructor — it can use a callback to get data for the tooltips and will be called with the filtered list when it’s time to update. --- concordia/static/js/action-app/components.js | 8 +- concordia/static/js/action-app/index.js | 88 ++++++++++---------- 2 files changed, 47 insertions(+), 49 deletions(-) diff --git a/concordia/static/js/action-app/components.js b/concordia/static/js/action-app/components.js index 8110ceddb..b7437e677 100644 --- a/concordia/static/js/action-app/components.js +++ b/concordia/static/js/action-app/components.js @@ -295,7 +295,7 @@ class AssetListItem { } export class AssetList extends List { - constructor(assets, callbacks) { + constructor(callbacks) { // TODO: refactor this into a utility function let assetListObserver = new IntersectionObserver(entries => { entries @@ -337,17 +337,17 @@ export class AssetList extends List { } }); - this.setupTooltip(assets); + this.setupTooltip(callbacks.getAssetData); } - setupTooltip(assets) { + setupTooltip(getAssetData) { /* Tooltips */ let tooltip = new AssetTooltip(); const handleTooltipShowEvent = event => { let target = event.target; if (target && target.classList.contains('asset')) { - const asset = assets.get(target.dataset.id); + const asset = getAssetData(target.dataset.id); tooltip.update(asset); mount(target, tooltip); } diff --git a/concordia/static/js/action-app/index.js b/concordia/static/js/action-app/index.js index 54c8c3e10..87e238396 100644 --- a/concordia/static/js/action-app/index.js +++ b/concordia/static/js/action-app/index.js @@ -34,11 +34,6 @@ export class ActionApp { /* These will store *all* metadata retrieved from the API so it can be easily queried and updated. - - Note that while the IDs returned from the server may currently be - numeric we index them as strings to avoid surprises in the future - and to avoid issues with DOM interfaces such as dataset which - convert arguments to strings. */ this.assets = new Map(); this.items = new Map(); @@ -86,9 +81,20 @@ export class ActionApp { this.serializeStateToURL(); } + getAssetData(assetId) { + // Convenience accessor which type-checks + if (typeof assetId != 'number') { + assetId = Number(assetId); + } + + return this.assets.get(assetId); + } + restoreOpenAsset() { let assetId = this.persistentState.get('asset'); - if (!assetId) return; + if (!assetId) { + return; + } let allAssetsURL = this.urlTemplates.assetData.expand({ // This is a special-case for retrieving all assets regardless of status @@ -101,7 +107,7 @@ export class ActionApp { if (!element) { console.warn('Expected to load asset with ID %s', assetId); } else { - this.openViewer(element); + this.openViewer(assetId); } }); }); @@ -188,7 +194,7 @@ export class ActionApp { .filter(button => button != clickedButton) .filter( button => - this.openAssetElement || + this.openAssetId || !('toggleableOnlyWhenOpen' in button.dataset) ) .forEach(button => { @@ -248,7 +254,7 @@ export class ActionApp { let data = JSON.parse(rawMessage.data); let message = data.message; - let assetId = message.asset_pk.toString(); + let assetId = message.asset_pk; switch (message.type) { case 'asset_update': { @@ -280,10 +286,7 @@ export class ActionApp { reservationToken: null }); - if ( - this.appElement.dataset.openAssetId && - this.appElement.dataset.openAssetId == assetId - ) { + if (this.openAssetId && this.openAssetId == assetId) { this.reserveAsset(); } @@ -298,7 +301,7 @@ export class ActionApp { if (assetListItem) { // If this is visible, we want to update the displayed asset // list icon using the current value: - assetListItem.update(this.assets.get(assetId)); + assetListItem.update(this.getAssetData(assetId)); } }); @@ -340,8 +343,9 @@ export class ActionApp { } }).observe(loadMoreButton); - this.assetList = new AssetList(this.assets, { - open: targetElement => this.openViewer(targetElement) + this.assetList = new AssetList({ + getAssetData: assetId => this.getAssetData(assetId), + open: targetElement => this.openViewer(targetElement.id) }); mount($('#asset-list-container'), this.assetList, loadMoreButton); @@ -514,7 +518,7 @@ export class ActionApp { of the fields which frequently change. */ - let oldData = this.assets.get(assetId) || {}; + let oldData = this.getAssetData(assetId) || {}; let mergedData = Object.assign({}, oldData); mergedData = Object.assign(mergedData, newData); @@ -542,10 +546,7 @@ export class ActionApp { } createAsset(assetData) { - // n.b. although we are currently using numeric keys, we're coding under - // the assumption that they will become opaque strings in the future: - let assetId = assetData.id.toString(); - + let assetId = assetData.id; this.mergeAssetUpdate(assetId, assetData); } @@ -565,7 +566,7 @@ export class ActionApp { if (typeof assetObjectOrID == 'string') { assetID = assetObjectOrID; - asset = this.assets.get(assetObjectOrID); + asset = this.getAssetData(assetObjectOrID); } else { asset = assetObjectOrID; assetID = asset.id; @@ -639,10 +640,8 @@ export class ActionApp { alwaysIncludedAssets = []; } - if (this.appElement.dataset.openAssetId) { - alwaysIncludedAssets.push( - Number(this.appElement.dataset.openAssetId) - ); + if (this.openAssetId) { + alwaysIncludedAssets.push(this.openAssetId); } window.requestIdleCallback(() => { @@ -781,19 +780,19 @@ export class ActionApp { return visibleAssets; } - openViewer(assetElement) { - if (this.openAssetElement) { + openViewer(assetId) { + if (this.openAssetId) { this.closeViewer(); } - let asset = this.assets.get(assetElement.dataset.id); + let asset = this.getAssetData(assetId); + + this.openAssetId = asset.id; this.addToState('asset', asset.id); this.updateSharing(asset.url, asset.title); - // FIXME: refactor openAssetElement into a single open asset ID property & pass it to the respective list & viewer components - this.openAssetElement = assetElement; this.assetReservationURL = this.urlTemplates.assetReservation.expand({ assetId: encodeURIComponent(asset.id) }); @@ -825,22 +824,23 @@ export class ActionApp { window.requestAnimationFrame(() => { // This will trigger the CSS which displays the viewer: - this.appElement.dataset.openAssetId = asset.id; + this.appElement.dataset.openAssetId = this.openAssetId; - this.assetList.setActiveAsset(assetElement); + this.assetList.setActiveAsset( + document.getElementById(this.openAssetId) + ); this.updateViewer(); }); } updateViewer() { - if (!this.appElement.dataset.openAssetId || !this.openAssetElement) { + if (!this.openAssetId) { console.warn('updateViewer() called without an open asset'); return; } - let openAssetId = this.appElement.dataset.openAssetId; - let asset = this.assets.get(openAssetId); + let asset = this.getAssetData(this.openAssetId); let {canEdit, reason} = this.canEditAsset(asset); @@ -865,7 +865,7 @@ export class ActionApp { this.releaseAsset(); delete this.appElement.dataset.openAssetId; - delete this.openAssetElement; + delete this.openAssetId; this.deleteFromState('asset'); @@ -881,8 +881,7 @@ export class ActionApp { } reserveAsset() { - // TODO: refactor open asset DOM/class references - if (!this.appElement.dataset.openAssetId) { + if (!this.openAssetId) { console.warn('reserveAsset called without an open asset?'); return; } @@ -892,7 +891,7 @@ export class ActionApp { return; } - let asset = this.assets.get(this.appElement.dataset.openAssetId); + let asset = this.getAssetData(this.openAssetId); let {canEdit, reason} = this.canEditAsset(asset); if (!canEdit) { @@ -911,7 +910,7 @@ export class ActionApp { dataType: 'json' }) .done(() => { - if (!this.openAssetElement) { + if (!this.openAssetId) { throw 'Open asset was closed with a reservation request pending'; } @@ -960,7 +959,7 @@ export class ActionApp { } handleAction(action, data) { - if (!this.appElement.dataset.openAssetId) { + if (!this.openAssetId) { console.error( `Unexpected action with no open asset: ${action} with data ${data}` ); @@ -969,8 +968,7 @@ export class ActionApp { console.debug(`User action ${action} with data ${data}`); } - let openAssetId = this.appElement.dataset.openAssetId; - let asset = this.assets.get(openAssetId); + let asset = this.getAssetData(this.openAssetId); let currentTranscriptionId = asset.latest_transcription ? asset.latest_transcription.id : null; @@ -984,7 +982,7 @@ export class ActionApp { case 'save': this.postAction( this.urlTemplates.saveTranscription.expand({ - assetId: openAssetId + assetId: this.openAssetId }), { text: data.text, From 6203cade700c36ed1f6b498b1b9ed35735206b75 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 15:51:49 -0400 Subject: [PATCH 05/12] reserveAsset: don't check reservation URL too early This was harmless but resulted in a console message in the case where an item wasn't editable by policy (e.g. completed, reviewing your own submission, etc.) --- concordia/static/js/action-app/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/concordia/static/js/action-app/index.js b/concordia/static/js/action-app/index.js index 87e238396..b0c064b7e 100644 --- a/concordia/static/js/action-app/index.js +++ b/concordia/static/js/action-app/index.js @@ -886,11 +886,6 @@ export class ActionApp { return; } - if (!this.assetReservationURL) { - console.warn('reserveAsset called without asset reservation URL!'); - return; - } - let asset = this.getAssetData(this.openAssetId); let {canEdit, reason} = this.canEditAsset(asset); @@ -899,6 +894,11 @@ export class ActionApp { return; } + if (!this.assetReservationURL) { + console.warn('reserveAsset called without asset reservation URL!'); + return; + } + let reservationURL = this.assetReservationURL; // TODO: record the last asset renewal time and don't renew early unless the ID has changed From 698127394fc7959d3958dca3a82bd3eeff731370 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 15:56:11 -0400 Subject: [PATCH 06/12] Remove now-correct console warning This condition is now normal in any situation where an asset cannot be edited by policy such as when an item is completed, self-review, etc. --- concordia/static/js/action-app/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/concordia/static/js/action-app/index.js b/concordia/static/js/action-app/index.js index b0c064b7e..9d2532837 100644 --- a/concordia/static/js/action-app/index.js +++ b/concordia/static/js/action-app/index.js @@ -936,7 +936,6 @@ export class ActionApp { releaseAsset() { if (!this.assetReservationURL) { - console.warn('releaseAsset called without asset reservation URL!'); return; } From ca434876a1ae68e4b3c8fb7f8aa9f697ffae7231 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 16:06:14 -0400 Subject: [PATCH 07/12] Asset list: stop setting a class which requires !important to override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since we’re using Bootstrap SCSS now we can just set it to a value which does what we need. --- concordia/static/js/action-app/components.js | 9 ++++----- concordia/static/scss/action-app.scss | 5 ++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/concordia/static/js/action-app/components.js b/concordia/static/js/action-app/components.js index b7437e677..931eb5b21 100644 --- a/concordia/static/js/action-app/components.js +++ b/concordia/static/js/action-app/components.js @@ -259,7 +259,7 @@ class Li { class AssetListItem { constructor([assetListObserver]) { this.el = html('li', { - class: 'asset border', + class: 'asset', tabIndex: 0 }); @@ -279,7 +279,7 @@ class AssetListItem { } this.el.id = assetData.id; - this.el.classList.add('asset', 'border'); + this.el.classList.add('asset'); this.el.dataset.image = thumbnailUrl; this.el.dataset.id = assetData.id; this.el.dataset.status = assetData.status; @@ -397,14 +397,13 @@ export class AssetList extends List { } setActiveAsset(assetElement) { - // TODO: stop using Bootstrap classes directly and toggle semantic classes only $$('.asset.asset-active', this.el).forEach(element => { if (element != assetElement) { - element.classList.remove('asset-active', 'border-primary'); + element.classList.remove('asset-active'); } }); - assetElement.classList.add('asset-active', 'border-primary'); + assetElement.classList.add('asset-active'); this.scrollToActiveAsset(); } diff --git a/concordia/static/scss/action-app.scss b/concordia/static/scss/action-app.scss index c34db2bb9..772f5decd 100644 --- a/concordia/static/scss/action-app.scss +++ b/concordia/static/scss/action-app.scss @@ -110,12 +110,15 @@ main { grid-template-columns: repeat(auto-fill, var(--asset-thumbnail-size)); grid-template-rows: auto; justify-content: center; + .asset { display: flex; width: var(--asset-thumbnail-size); height: var(--asset-thumbnail-size); object-fit: contain; - border-color: $gray-600 !important; + + border: $border-width solid $gray-600; + background-color: #f6f6f6; background-size: contain; background-repeat: no-repeat; From 678ff1af6dd59c8062e4aecbe05277f62d91dc76 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 16:17:14 -0400 Subject: [PATCH 08/12] Make active asset distinguishable again At some point in the restyling we lost the active styles and it was hard to tell which element was active --- concordia/static/scss/action-app.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/concordia/static/scss/action-app.scss b/concordia/static/scss/action-app.scss index 772f5decd..0f4b2d7bf 100644 --- a/concordia/static/scss/action-app.scss +++ b/concordia/static/scss/action-app.scss @@ -129,6 +129,11 @@ main { transition-property: height, width; transition-duration: 0.1s; + &.asset-active { + outline: 2px auto $orange; + outline-offset: -2px; + } + &[data-unavailable] { position: relative; &::before { From 11c246d9802066b00b56c9b196bfa119c218b5c2 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 16:50:48 -0400 Subject: [PATCH 09/12] Preserve the original reason for pages which have not been edited This allows a message such as the page having already been completed to be preserved --- concordia/static/js/action-app/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordia/static/js/action-app/index.js b/concordia/static/js/action-app/index.js index 9d2532837..0947f9244 100644 --- a/concordia/static/js/action-app/index.js +++ b/concordia/static/js/action-app/index.js @@ -844,7 +844,7 @@ export class ActionApp { let {canEdit, reason} = this.canEditAsset(asset); - if (!this.assetReserved) { + if (canEdit && !this.assetReserved) { canEdit = false; reason = 'Asset reservation in progress'; } From 50141dc40bfa326c345db808d4edf2342ef433be Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 17:11:33 -0400 Subject: [PATCH 10/12] Better status wording for completed pages --- concordia/static/js/action-app/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concordia/static/js/action-app/index.js b/concordia/static/js/action-app/index.js index 0947f9244..c05c44654 100644 --- a/concordia/static/js/action-app/index.js +++ b/concordia/static/js/action-app/index.js @@ -580,7 +580,7 @@ export class ActionApp { let reason = ''; if (asset.status == 'completed') { - reason = 'This page has already been completed'; + reason = 'This page has been completed'; canEdit = false; } else if (this.currentMode == 'review') { if (asset.status != 'submitted') { From f4526a960a7c5195327b258f75593002f333a6f0 Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 17:12:10 -0400 Subject: [PATCH 11/12] Include `sent` property in asset-related API endpoints This makes it easier for the front-end code to maintain a coherent view of when things changed without the possibility of confusion due to a client with a skewed clock --- concordia/api_views.py | 4 ++-- concordia/signals/handlers.py | 3 +++ concordia/views.py | 15 +++++++++------ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/concordia/api_views.py b/concordia/api_views.py index 1a0e110dd..82b522867 100644 --- a/concordia/api_views.py +++ b/concordia/api_views.py @@ -13,7 +13,7 @@ The base APIViewMixin implements a base implementation of serialize_object which uses the generic django.forms.models.model_to_dict and can be overridden as needed. """ -import time +from time import time from django.core.serializers.json import DjangoJSONEncoder from django.forms.models import model_to_dict @@ -107,7 +107,7 @@ def get_paginate_by(self, queryset): def serialize_context(self, context): data = { "objects": [self.serialize_object(i) for i in context["object_list"]], - "sent": int(time.time()), + "sent": time(), } page_obj = context["page_obj"] diff --git a/concordia/signals/handlers.py b/concordia/signals/handlers.py index c7afcb403..42a01919e 100644 --- a/concordia/signals/handlers.py +++ b/concordia/signals/handlers.py @@ -1,3 +1,5 @@ +from time import time + from asgiref.sync import AsyncToSync from channels.layers import get_channel_layer from django.conf import settings @@ -105,5 +107,6 @@ def send_asset_reservation_message( "type": message_type, "asset_pk": asset_pk, "reservation_token": reservation_token, + "sent": time(), }, ) diff --git a/concordia/views.py b/concordia/views.py index 5d14876b0..a2cf8a4cf 100644 --- a/concordia/views.py +++ b/concordia/views.py @@ -1,11 +1,11 @@ import json import os import re -import time from datetime import timedelta from functools import wraps from logging import getLogger from smtplib import SMTPException +from time import time from urllib.parse import urlencode import markdown @@ -107,7 +107,7 @@ def inner(*args, **kwargs): @never_cache def healthz(request): status = { - "current_time": time.time(), + "current_time": time(), "load_average": os.getloadavg(), "debug": settings.DEBUG, } @@ -243,7 +243,7 @@ def post(self, request, *args, **kwargs): rate=self.ratelimit_rate, ) recent_captcha = ( - time.time() - request.session.get("captcha_validation_time", 0) + time() - request.session.get("captcha_validation_time", 0) ) < 86400 if blocked and not recent_captcha: @@ -255,7 +255,7 @@ def post(self, request, *args, **kwargs): return self.form_invalid(form) def form_valid(self, form): - self.request.session["captcha_validation_time"] = time.time() + self.request.session["captcha_validation_time"] = time() return super().form_valid(form) @@ -796,7 +796,7 @@ def ajax_captcha(request): ).delete() if deleted > 0: - request.session["captcha_validation_time"] = time.time() + request.session["captcha_validation_time"] = time() return JsonResponse({"valid": True}) key = CaptchaStore.generate_key() @@ -813,7 +813,7 @@ def validate_anonymous_captcha(view): def inner(request, *args, **kwargs): if not request.user.is_authenticated: captcha_last_validated = request.session.get("captcha_validation_time", 0) - age = time.time() - captcha_last_validated + age = time() - captcha_last_validated if age > settings.ANONYMOUS_CAPTCHA_VALIDATION_INTERVAL: return ajax_captcha(request) @@ -873,6 +873,7 @@ def save_transcription(request, *, asset_pk): return JsonResponse( { "id": transcription.pk, + "sent": time(), "submissionUrl": reverse("submit-transcription", args=(transcription.pk,)), "asset": { "id": transcription.asset.id, @@ -907,6 +908,7 @@ def submit_transcription(request, *, pk): return JsonResponse( { "id": transcription.pk, + "sent": time(), "asset": { "id": transcription.asset.id, "status": transcription.asset.transcription_status, @@ -951,6 +953,7 @@ def review_transcription(request, *, pk): return JsonResponse( { "id": transcription.pk, + "sent": time(), "asset": { "id": transcription.asset.id, "status": transcription.asset.transcription_status, From e401be6eb1ff39cd6913d7958570cc460a7d4d0d Mon Sep 17 00:00:00 2001 From: Chris Adams Date: Wed, 15 May 2019 17:20:04 -0400 Subject: [PATCH 12/12] Activity UI: update sent time in action updates This reduces the degree of churn in the update cycle when sorting on most-recently-updated --- concordia/static/js/action-app/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/concordia/static/js/action-app/index.js b/concordia/static/js/action-app/index.js index c05c44654..bd8503976 100644 --- a/concordia/static/js/action-app/index.js +++ b/concordia/static/js/action-app/index.js @@ -994,6 +994,7 @@ export class ActionApp { asset.latest_transcription.id = responseData.id; asset.latest_transcription.text = responseData.text; this.mergeAssetUpdate(responseData.asset.id, { + sent: responseData.sent, status: responseData.asset.status }); updateViews(); @@ -1009,6 +1010,7 @@ export class ActionApp { }) ).done(responseData => { this.mergeAssetUpdate(responseData.asset.id, { + sent: responseData.sent, status: responseData.asset.status }); updateViews(); @@ -1022,6 +1024,7 @@ export class ActionApp { {action: 'accept'} ).done(responseData => { this.mergeAssetUpdate(responseData.asset.id, { + sent: responseData.sent, status: responseData.asset.status }); this.releaseAsset(); @@ -1036,6 +1039,7 @@ export class ActionApp { {action: 'reject'} ).done(responseData => { this.mergeAssetUpdate(responseData.asset.id, { + sent: responseData.sent, status: responseData.asset.status }); });