From f6e7aa31d9bb1ec5e44086d1c74af00f3bb9d217 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 7 Nov 2024 09:25:20 +0000 Subject: [PATCH 1/5] test(lib-classifier): test classification timestamps - [x] Add tests for classification `started_at` and `finished_at` timestamps. - [ ] fix the failing test for classification start times. --- .../Classification/ClassificationMetadata.js | 7 +-- .../src/store/ClassificationStore.spec.js | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/lib-classifier/src/store/Classification/ClassificationMetadata.js b/packages/lib-classifier/src/store/Classification/ClassificationMetadata.js index 4cbaeb6596..84e3a48535 100644 --- a/packages/lib-classifier/src/store/Classification/ClassificationMetadata.js +++ b/packages/lib-classifier/src/store/Classification/ClassificationMetadata.js @@ -36,9 +36,10 @@ const ClassificationMetadata = types.model('ClassificationMetadata', { .actions(self => ({ afterAttach() { function _onLocaleChange() { - self.update({ - userLanguage: getRoot(self)?.locale - }) + const userLanguage = getRoot(self)?.locale + if (userLanguage) { + self.update({ userLanguage }) + } } addDisposer(self, autorun(_onLocaleChange)) }, diff --git a/packages/lib-classifier/src/store/ClassificationStore.spec.js b/packages/lib-classifier/src/store/ClassificationStore.spec.js index 346714d9a8..fdd77dea43 100644 --- a/packages/lib-classifier/src/store/ClassificationStore.spec.js +++ b/packages/lib-classifier/src/store/ClassificationStore.spec.js @@ -65,6 +65,7 @@ describe('Model > ClassificationStore', function () { describe('when a subject advances', function () { let classifications let rootStore + beforeEach(function () { rootStore = setupStores({ dataVisAnnotating: {}, @@ -106,6 +107,53 @@ describe('Model > ClassificationStore', function () { }) describe('on complete classification', function () { + describe('submitted classifications', function () { + let classifications + let rootStore + let clock + let submittedClassification + + before(function () { + clock = sinon.useFakeTimers(new Date('2024-11-06T13:00:00Z')) + rootStore = setupStores({ + dataVisAnnotating: {}, + drawing: {}, + feedback: {}, + fieldGuide: {}, + subjectViewer: {}, + tutorials: {}, + workflowSteps: {}, + userProjectPreferences: {} + }) + + classifications = rootStore.classifications + classifications.setOnComplete(snapshot => { + console.log(snapshot) + submittedClassification = snapshot + }) + const taskSnapshot = Object.assign({}, singleChoiceTaskSnapshot, { taskKey: singleChoiceAnnotationSnapshot.task }) + taskSnapshot.createAnnotation = () => SingleChoiceAnnotation.create(singleChoiceAnnotationSnapshot) + clock.tick(1 * 60 * 60 * 1000) // wait for one hour before starting the classification. + classifications.addAnnotation(taskSnapshot, singleChoiceAnnotationSnapshot.value) + clock.tick(30 * 1000) // wait for 30 seconds before finishing the classification. + classifications.completeClassification() + }) + + after(function () { + clock.restore() + }) + + it('should record the started at time', function () { + const startedAt = submittedClassification.metadata.started_at + expect(startedAt).to.equal('2024-11-06T14:00:00.000Z') + }) + + it('should record the finished at time', function () { + const finishedAt = submittedClassification.metadata.finished_at + expect(finishedAt).to.equal('2024-11-06T14:00:30.000Z') + }) + }) + describe('with invalid feedback', function () { let classifications let rootStore From 8e1a3d3de96c46f772188880e7c3ad908ccb0157 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 7 Nov 2024 09:43:38 +0000 Subject: [PATCH 2/5] Update startedAt when the first annotation is made --- packages/lib-classifier/src/store/ClassificationStore.js | 4 ++++ packages/lib-classifier/src/store/ClassificationStore.spec.js | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/lib-classifier/src/store/ClassificationStore.js b/packages/lib-classifier/src/store/ClassificationStore.js index bcf0ceeee8..33f53fee3f 100644 --- a/packages/lib-classifier/src/store/ClassificationStore.js +++ b/packages/lib-classifier/src/store/ClassificationStore.js @@ -88,6 +88,10 @@ const ClassificationStore = types if (validClassificationReference) { const classification = self.active + if (classification?.annotations.size === 0) { + // update startedAt if we're starting a new classification + classification.metadata.startedAt = (new Date()).toISOString() + } if (classification) { return classification.addAnnotation(task, annotationValue) } diff --git a/packages/lib-classifier/src/store/ClassificationStore.spec.js b/packages/lib-classifier/src/store/ClassificationStore.spec.js index fdd77dea43..f9a8bd2c0c 100644 --- a/packages/lib-classifier/src/store/ClassificationStore.spec.js +++ b/packages/lib-classifier/src/store/ClassificationStore.spec.js @@ -128,7 +128,6 @@ describe('Model > ClassificationStore', function () { classifications = rootStore.classifications classifications.setOnComplete(snapshot => { - console.log(snapshot) submittedClassification = snapshot }) const taskSnapshot = Object.assign({}, singleChoiceTaskSnapshot, { taskKey: singleChoiceAnnotationSnapshot.task }) From 9783e543bf4a38609cd2fe114102ae19b31661c9 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 7 Nov 2024 10:00:37 +0000 Subject: [PATCH 3/5] test the length of the mock classification --- .../lib-classifier/src/store/ClassificationStore.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/lib-classifier/src/store/ClassificationStore.spec.js b/packages/lib-classifier/src/store/ClassificationStore.spec.js index f9a8bd2c0c..3955d6dcfc 100644 --- a/packages/lib-classifier/src/store/ClassificationStore.spec.js +++ b/packages/lib-classifier/src/store/ClassificationStore.spec.js @@ -135,6 +135,7 @@ describe('Model > ClassificationStore', function () { clock.tick(1 * 60 * 60 * 1000) // wait for one hour before starting the classification. classifications.addAnnotation(taskSnapshot, singleChoiceAnnotationSnapshot.value) clock.tick(30 * 1000) // wait for 30 seconds before finishing the classification. + classifications.addAnnotation(taskSnapshot, 1) classifications.completeClassification() }) @@ -151,6 +152,13 @@ describe('Model > ClassificationStore', function () { const finishedAt = submittedClassification.metadata.finished_at expect(finishedAt).to.equal('2024-11-06T14:00:30.000Z') }) + + it('should only record time spent classifying', function () { + const startedAt = submittedClassification.metadata.started_at + const finishedAt = submittedClassification.metadata.finished_at + const timeSpent = (new Date(finishedAt) - new Date(startedAt)) / 1000 + expect(timeSpent).to.equal(30) + }) }) describe('with invalid feedback', function () { From f0177f7d7161497fca51b74e2f0a9b13eb9401e6 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 7 Nov 2024 13:10:40 +0000 Subject: [PATCH 4/5] Set started_at when annotation._inProgress changes A new, empty annotation is created when the subject first loads, so wait until `annotation._inProgress` is true before setting the classification start time. --- .../store/Classification/Classification.js | 32 ++++++++++++++++++- .../src/store/ClassificationStore.js | 4 --- .../src/store/ClassificationStore.spec.js | 6 ++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/lib-classifier/src/store/Classification/Classification.js b/packages/lib-classifier/src/store/Classification/Classification.js index b510646774..6731e14e35 100644 --- a/packages/lib-classifier/src/store/Classification/Classification.js +++ b/packages/lib-classifier/src/store/Classification/Classification.js @@ -1,9 +1,10 @@ import cuid from 'cuid' -import { types, getSnapshot, getType } from 'mobx-state-tree' +import { types, getSnapshot, getType, addDisposer } from 'mobx-state-tree' import * as tasks from '@plugins/tasks' import AnnotationsStore from '@store/AnnotationsStore' import Resource from '@store/Resource' import ClassificationMetadata from './ClassificationMetadata' +import { autorun } from 'mobx' const annotationModels = Object.values(tasks).map(task => task.AnnotationModel) @@ -19,6 +20,17 @@ const Classification = types metadata: types.maybe(ClassificationMetadata) }) .views(self => ({ + /** + * Returns false until we start updating task annotations. + */ + get inProgress() { + let inProgress = false + self.annotations.forEach(annotation => { + inProgress ||= annotation._inProgress + }) + return inProgress + }, + toSnapshot () { let snapshot = getSnapshot(self) let annotations = [] @@ -58,5 +70,23 @@ const Classification = types newSnapshot.annotations = Object.values(snapshot.annotations) return newSnapshot }) + .actions(self => { + function _onAnnotationsChange () { + // set started at when inProgress changes from false to true + if (self.inProgress) { + self.setStartedAt() + } + } + + return ({ + afterAttach () { + addDisposer(self, autorun(_onAnnotationsChange)) + }, + + setStartedAt () { + self.metadata.startedAt = new Date().toISOString() + } + }) + }) export default types.compose('ClassificationResource', Resource, AnnotationsStore, Classification) diff --git a/packages/lib-classifier/src/store/ClassificationStore.js b/packages/lib-classifier/src/store/ClassificationStore.js index 33f53fee3f..bcf0ceeee8 100644 --- a/packages/lib-classifier/src/store/ClassificationStore.js +++ b/packages/lib-classifier/src/store/ClassificationStore.js @@ -88,10 +88,6 @@ const ClassificationStore = types if (validClassificationReference) { const classification = self.active - if (classification?.annotations.size === 0) { - // update startedAt if we're starting a new classification - classification.metadata.startedAt = (new Date()).toISOString() - } if (classification) { return classification.addAnnotation(task, annotationValue) } diff --git a/packages/lib-classifier/src/store/ClassificationStore.spec.js b/packages/lib-classifier/src/store/ClassificationStore.spec.js index 3955d6dcfc..0cdbc17d3d 100644 --- a/packages/lib-classifier/src/store/ClassificationStore.spec.js +++ b/packages/lib-classifier/src/store/ClassificationStore.spec.js @@ -132,10 +132,12 @@ describe('Model > ClassificationStore', function () { }) const taskSnapshot = Object.assign({}, singleChoiceTaskSnapshot, { taskKey: singleChoiceAnnotationSnapshot.task }) taskSnapshot.createAnnotation = () => SingleChoiceAnnotation.create(singleChoiceAnnotationSnapshot) + const classification = classifications.active + const annotation = classification.createAnnotation(taskSnapshot) clock.tick(1 * 60 * 60 * 1000) // wait for one hour before starting the classification. - classifications.addAnnotation(taskSnapshot, singleChoiceAnnotationSnapshot.value) + annotation.update(0) clock.tick(30 * 1000) // wait for 30 seconds before finishing the classification. - classifications.addAnnotation(taskSnapshot, 1) + annotation.update(1) classifications.completeClassification() }) From 5637ca1673c819c9059c1de479f34740f97558ef Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 7 Nov 2024 14:38:35 +0000 Subject: [PATCH 5/5] support the survey task's _choiceInProgress flag --- .../lib-classifier/src/store/Classification/Classification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib-classifier/src/store/Classification/Classification.js b/packages/lib-classifier/src/store/Classification/Classification.js index 6731e14e35..ed38161940 100644 --- a/packages/lib-classifier/src/store/Classification/Classification.js +++ b/packages/lib-classifier/src/store/Classification/Classification.js @@ -26,7 +26,7 @@ const Classification = types get inProgress() { let inProgress = false self.annotations.forEach(annotation => { - inProgress ||= annotation._inProgress + inProgress ||= annotation._inProgress || annotation._choiceInProgress }) return inProgress },