Skip to content

Commit

Permalink
Engaging Crowds: enable indexed subject selection (#2148)
Browse files Browse the repository at this point in the history
* Add subjectSet.isIndexed

* Add workflow.hasIndexedSubjects

* Add getIndexedSubjects helper
getIndexedSubjects gets all subjects, in order, from a prioritised subject set, starting from an optional priority.

* Add getIndexedSubjects to SubjectStore
Add getIndexedSubjects to subjects.populateQueue(). Add subjects.last as a helper to get the last subject from the queue.

* Add subjectSelectionStrategy helper
Add a helper which generates the URL and query params for each subject selection strategy: specific subjects; indexed, ordered subjects; subject groups; grouped random selection; random selection.

* Test getIndexedSubjects

* Loop back to the beginning of an indexed set
  • Loading branch information
eatyourgreens authored May 14, 2021
1 parent dbee4f5 commit 9bfa439
Show file tree
Hide file tree
Showing 13 changed files with 499 additions and 18 deletions.
6 changes: 6 additions & 0 deletions packages/lib-classifier/src/store/SubjectSet/SubjectSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ const SubjectSet = types
.model('SubjectSet', {
display_name: types.string,
links: types.frozen({}),
metadata: types.frozen({}),
set_member_subjects_count: types.number
})
.views(self => ({
get isIndexed () {
return self.metadata.indexFields?.length > 0
}
}))

export default types.compose('SubjectSetResource', Resource, SubjectSet)
57 changes: 57 additions & 0 deletions packages/lib-classifier/src/store/SubjectSet/SubjectSet.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,61 @@ describe('Model > SubjectSet', function () {
it('should have a subject count', function () {
expect(model.set_member_subjects_count).to.equal(19)
})

describe('isIndexed', function () {
let isIndexed

describe('without metadata', function () {
before(function () {
const metadataModel = SubjectSet.create({
id: '1234',
display_name: 'Hello there!',
set_member_subjects_count: 19
})
isIndexed = metadataModel.isIndexed
})

it('should be false', function () {
expect(isIndexed).to.be.false()
})
})

describe('with metadata', function () {
describe('but no indexed fields', function () {
before(function () {
const metadataModel = SubjectSet.create({
id: '1234',
display_name: 'Hello there!',
metadata: {
indexFields: ''
},
set_member_subjects_count: 19
})
isIndexed = metadataModel.isIndexed
})

it('should be false', function () {
expect(isIndexed).to.be.false()
})
})

describe('and indexed fields', function () {
before(function () {
const metadataModel = SubjectSet.create({
id: '1234',
display_name: 'Hello there!',
metadata: {
indexFields: 'Date,Creator'
},
set_member_subjects_count: 19
})
isIndexed = metadataModel.isIndexed
})

it('should be true', function () {
expect(isIndexed).to.be.true()
})
})
})
})
})
30 changes: 13 additions & 17 deletions packages/lib-classifier/src/store/SubjectStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import asyncStates from '@zooniverse/async-states'
import { autorun } from 'mobx'
import { addDisposer, addMiddleware, flow, getRoot, isValidReference, onPatch, tryReference, types } from 'mobx-state-tree'
import { getBearerToken } from './utils'
import { subjectSelectionStrategy } from './helpers'
import { filterByLabel, filters } from '../components/Classifier/components/MetaTools/components/Metadata/components/MetadataModal'
import ResourceStore from './ResourceStore'
import Subject from './Subject'
Expand Down Expand Up @@ -56,6 +57,17 @@ const SubjectStore = types
}

return false
},

/** a helper to get the last subject in the queue */
get last () {
let lastSubject

if ( self.resources.size > 0 ) {
const activeSubjects = Array.from(self.resources.values())
lastSubject = activeSubjects[self.resources.size - 1]
}
return lastSubject
}
}))

Expand Down Expand Up @@ -152,26 +164,10 @@ const SubjectStore = types
const root = getRoot(self)
const client = root.client.panoptes
const workflow = tryReference(() => root.workflows.active)
let apiUrl = '/subjects/queued'

if (workflow) {
self.loadingState = asyncStates.loading
const params = { workflow_id: workflow.id }

if (workflow.grouped) {
params.subject_set_id = workflow.subjectSetId
}

if (workflow.configuration.subject_viewer === 'subjectGroup') {
apiUrl = '/subjects/grouped'
params.num_rows = workflow.configuration.subject_viewer_config?.grid_rows || 1
params.num_columns = workflow.configuration.subject_viewer_config?.grid_columns || 1
}

if (subjectIDs) {
apiUrl = '/subjects/selection'
params.ids = subjectIDs
}
const { apiUrl, params } = yield subjectSelectionStrategy(workflow, subjectIDs, self.last?.priority)

try {
const { authClient } = getRoot(self)
Expand Down
5 changes: 5 additions & 0 deletions packages/lib-classifier/src/store/Workflow/Workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ const Workflow = types
selectedSubjects: undefined
}))
.views(self => ({
get hasIndexedSubjects () {
const activeSet = tryReference(() => self.subjectSet)
return self.grouped && !!activeSet?.isIndexed
},

get subjectSetId () {
const activeSet = tryReference(() => self.subjectSet)
return activeSet?.id
Expand Down
61 changes: 60 additions & 1 deletion packages/lib-classifier/src/store/Workflow/Workflow.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sinon from 'sinon'
import RootStore from '../RootStore'
import Workflow from './Workflow'

import { MultipleChoiceTaskFactory } from '@test/factories'
import { MultipleChoiceTaskFactory, SubjectSetFactory } from '@test/factories'

describe('Model > Workflow', function () {
it('should exist', function () {
Expand All @@ -30,6 +30,10 @@ describe('Model > Workflow', function () {
it('should not use transcription task', function () {
expect(workflow.usesTranscriptionTask).to.be.false()
})

it('should not use indexed subject selection', function () {
expect(workflow.hasIndexedSubjects).to.be.false()
})
})

describe('workflow steps', function () {
Expand Down Expand Up @@ -159,6 +163,61 @@ describe('Model > Workflow', function () {
})
})

describe('Views > hasIndexedSubjects', function () {
let indexedSet
let workflow

beforeEach(function () {
const rootStore = RootStore.create();
const subjectSets = Factory.buildList('subject_set', 5)
indexedSet = SubjectSetFactory.build({
metadata: {
indexFields: 'Date,Creator'
}
})
subjectSets.push(indexedSet)
rootStore.subjectSets.setResources(subjectSets)
const workflowSnapshot = WorkflowFactory.build({
id: 'workflow1',
display_name: 'A test workflow',
grouped: true,
links: {
subject_sets: subjectSets.map(subjectSet => subjectSet.id)
},
version: '0.0'
})
workflow = Workflow.create(workflowSnapshot)
rootStore.workflows.setResources([workflow])
})

describe('with no selected subject set', function () {

it('should be false', async function () {
expect(workflow.hasIndexedSubjects).to.be.false()
})
})

describe('with a selected subject set', function () {

describe('without indexed subjects', function () {
it('should be false', async function () {
expect(workflow.subjectSetId).to.be.undefined()
const subjectSetID = workflow.links.subject_sets[1]
await workflow.selectSubjectSet(subjectSetID)
expect(workflow.hasIndexedSubjects).to.be.false()
})
})

describe('with indexed subjects', function () {
it('should be true', async function () {
expect(workflow.subjectSetId).to.be.undefined()
await workflow.selectSubjectSet(indexedSet.id)
expect(workflow.hasIndexedSubjects).to.be.true()
})
})
})
})

describe('Views > subjectSetId', function () {
let rootStore
let workflow
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default async function getIndexedSubjects(subjectSetId, priority = -1) {
const subjectSetURL = `https://subject-set-search-api.zooniverse.org/subjects/${subjectSetId}.json`
const query = `priority__gt=${priority}&_sort=priority`
const mode = 'cors'
const response = await fetch(`${subjectSetURL}?${query}`, { mode })
const { columns, rows } = await response.json()
const subjectIDColumn = columns.indexOf('subject_id')
const subjectIds = rows.map(row => row[subjectIDColumn]).slice(0,10)
return subjectIds.join(',')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import nock from 'nock'

import getIndexedSubjects from './getIndexedSubjects'

describe('Store > Helpers > getIndexedSubjects', function () {
let subjectIDs

before(async function () {
nock('https://subject-set-search-api.zooniverse.org')
.get('/subjects/2.json')
.query(query => query.priority__gt === '-1')
.reply(200, {
columns: ['subject_id', 'priority'],
rows: [
['12345', '1'],
['34567', '2'],
['56789', '3']
]
})
.get('/subjects/2.json')
.query(query => query.priority__gt === '1')
.reply(200, {
columns: ['subject_id', 'priority'],
rows: [
['34567', '2'],
['56789', '3']
]
})
})

describe('with a subject priority', function () {
before(async function () {
subjectIDs = await getIndexedSubjects('2', 1)
})

it('should return the next subjects from the set', function () {
expect(subjectIDs).to.equal('34567,56789')
})
})

describe('without a subject priority', function () {
before(async function () {
subjectIDs = await getIndexedSubjects('2')
})

it('should return the first subjects from the set', function () {
expect(subjectIDs).to.equal('12345,34567,56789')
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './getIndexedSubjects'
2 changes: 2 additions & 0 deletions packages/lib-classifier/src/store/helpers/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { default as convertWorkflowToUseSteps } from './convertWorkflowToUseSteps'
export { default as createStore } from './createStore'
export { default as getIndexedSubjects } from './getIndexedSubjects'
export { default as subjectSelectionStrategy } from './subjectSelectionStrategy'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './subjectSelectionStrategy'
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { getIndexedSubjects } from '@store/helpers'

export default async function subjectSelectionStrategy(workflow, subjectIDs, priority = -1) {
const workflow_id = workflow.id

/** Fetch specific subjects for any workflow */
if (subjectIDs) {
const apiUrl = '/subjects/selection'
const ids = subjectIDs.join(',')
const params = {
ids,
workflow_id
}
return {
apiUrl,
params
}
}

/** Fetch subject groups for workflows that use the subject group viewer */
if (workflow.configuration.subject_viewer === 'subjectGroup') {
const apiUrl = '/subjects/grouped'
const num_rows = workflow.configuration.subject_viewer_config?.grid_rows || 1
const num_columns = workflow.configuration.subject_viewer_config?.grid_columns || 1
const params = {
num_columns,
num_rows,
workflow_id
}
return {
apiUrl,
params
}
}

/** fetch ordered subjects for indexed subject sets */
if (workflow.hasIndexedSubjects) {
const apiUrl = '/subjects/selection'
let ids = await getIndexedSubjects(workflow.subjectSetId, priority)
if (ids === '') {
ids = await getIndexedSubjects(workflow.subjectSetId)
}
const params = {
ids,
workflow_id
}
return {
apiUrl,
params
}
}

/** Random grouped selection for grouped workflows */
if (workflow.grouped) {
const apiUrl = '/subjects/queued'
const subject_set_id = workflow.subjectSetId
const params = {
subject_set_id,
workflow_id
}
return {
apiUrl,
params
}
}

/** default random queued selection */
const apiUrl = '/subjects/queued'
const params = {
workflow_id
}
return {
apiUrl,
params
}
}
Loading

0 comments on commit 9bfa439

Please sign in to comment.