diff --git a/packages/app-project/package.json b/packages/app-project/package.json index 01b2d5a6af..be24565ab4 100644 --- a/packages/app-project/package.json +++ b/packages/app-project/package.json @@ -51,6 +51,7 @@ "morgan": "^1.10.0", "newrelic": "~7.1.0", "next": "~9.5.5", + "node-fetch": "~2.6.1", "panoptes-client": "~3.2.1", "path-match": "~1.2.4", "polished": "~3.6.4", diff --git a/packages/app-project/src/helpers/fetchSubjectSets/fetchSubjectSets.js b/packages/app-project/src/helpers/fetchSubjectSets/fetchSubjectSets.js new file mode 100644 index 0000000000..bb40739811 --- /dev/null +++ b/packages/app-project/src/helpers/fetchSubjectSets/fetchSubjectSets.js @@ -0,0 +1,64 @@ +import { panoptes } from '@zooniverse/panoptes-js' +import fetch from 'node-fetch' + +import { logToSentry } from '@helpers/logger' + +async function fetchSubjectSetData(subjectSetIDs, env) { + let subject_sets = [] + try { + const query = { + env, + id: subjectSetIDs.join(',') + } + const response = await panoptes.get('/subject_sets', query) + subject_sets = response.body.subject_sets + await Promise.allSettled(subject_sets.map(subjectSet => fetchPreviewImage(subjectSet, env))) + } catch (error) { + console.error(error) + logToSentry(error) + } + return subject_sets +} + +async function fetchWorkflowCellectStatus(workflow) { + let groups = {} + if (workflow.grouped) { + try { + const workflowURL = `https://cellect.zooniverse.org/workflows/${workflow.id}/status` + const response = await fetch(workflowURL) + const body = await response.json() + groups = body.groups ?? {} + } catch (error) { + console.error(error) + logToSentry(error) + } + } + return groups +} + +async function fetchPreviewImage (subjectSet, env) { + try { + const response = await panoptes + .get('/set_member_subjects', { + env, + subject_set_id: subjectSet.id, + include: 'subject', + page_size: 1 + }) + const { linked } = response.body + subjectSet.subjects = linked.subjects + } catch (error) { + console.error(error) + logToSentry(error) + } +} + +export default async function fetchSubjectSets(workflow, env) { + const subjectSetCounts = await fetchWorkflowCellectStatus(workflow) + const subjectSetIDs = Object.keys(subjectSetCounts) + const subjectSets = await fetchSubjectSetData(subjectSetIDs, env) + subjectSets.forEach(subjectSet => { + subjectSet.availableSubjects = subjectSetCounts[subjectSet.id] + }) + return subjectSets +} diff --git a/packages/app-project/src/helpers/fetchSubjectSets/index.js b/packages/app-project/src/helpers/fetchSubjectSets/index.js new file mode 100644 index 0000000000..61b7ffdfd1 --- /dev/null +++ b/packages/app-project/src/helpers/fetchSubjectSets/index.js @@ -0,0 +1 @@ +export { default } from './fetchSubjectSets' diff --git a/packages/app-project/src/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.js b/packages/app-project/src/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.js index 362f04c1ca..283cc98781 100644 --- a/packages/app-project/src/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.js +++ b/packages/app-project/src/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.js @@ -1,67 +1,72 @@ import { panoptes } from '@zooniverse/panoptes-js' +import fetch from 'node-fetch' + +import { logToSentry } from '@helpers/logger' +import fetchSubjectSets from '@helpers/fetchSubjectSets' async function fetchWorkflowData (activeWorkflows, env) { - const response = await panoptes - .get('/workflows', { + try { + const query = { complete: false, env, - fields: 'completeness,grouped', - id: activeWorkflows.join(','), - include: 'subject_sets' - }) - const { workflows, linked } = response.body - const subjectSets = linked ? linked.subject_sets : [] - await Promise.allSettled(subjectSets.map(subjectSet => fetchPreviewImage(subjectSet, env))) - return { subjectSets, workflows } + fields: 'completeness,display_name,grouped', + id: activeWorkflows.join(',') + } + const response = await panoptes.get('/workflows', query) + return response.body.workflows + } catch (error) { + logToSentry(error) + throw error + } } - -function fetchDisplayNames (language, activeWorkflows, env) { - return panoptes - .get('/translations', { +async function fetchDisplayNames (language, activeWorkflows, env) { + let displayNames = {} + try { + const response = await panoptes.get('/translations', { env, fields: 'strings,translated_id', language, 'translated_id': activeWorkflows.join(','), 'translated_type': 'workflow' }) - .then(response => response.body.translations) - .then(createDisplayNamesMap) + const { translations } = response.body + displayNames = createDisplayNamesMap(translations) + } catch (error) { + logToSentry(error) + throw error + } + return displayNames } -async function fetchPreviewImage (subjectSet, env) { - const response = await panoptes - .get('/set_member_subjects', { - env, - subject_set_id: subjectSet.id, - include: 'subject', - page_size: 1 - }) - const { linked } = response.body - subjectSet.subjects = linked.subjects +async function buildWorkflow(workflow, displayName, isDefault, env) { + const workflowData = { + completeness: workflow.completeness || 0, + default: isDefault, + displayName, + grouped: workflow.grouped, + id: workflow.id, + subjectSets: [] + } + if (workflow.grouped) { + workflowData.subjectSets = await fetchSubjectSets(workflow, env) + } + + return workflowData } async function fetchWorkflowsHelper (language = 'en', activeWorkflows, defaultWorkflow, env) { - const { subjectSets, workflows } = await fetchWorkflowData(activeWorkflows, env) + const workflows = await fetchWorkflowData(activeWorkflows, env) const workflowIds = workflows.map(workflow => workflow.id) const displayNames = await fetchDisplayNames(language, workflowIds, env) - return workflows.map(workflow => { + const awaitWorkflows = workflows.map(workflow => { const isDefault = workflows.length === 1 || workflow.id === defaultWorkflow - const workflowSubjectSets = workflow.links.subject_sets - .map(subjectSetID => { - return subjectSets.find(subjectSet => subjectSet.id === subjectSetID) - }) - .filter(Boolean) - - return { - completeness: workflow.completeness || 0, - default: isDefault, - displayName: displayNames[workflow.id], - grouped: workflow.grouped, - id: workflow.id, - subjectSets: workflowSubjectSets - } + const displayName = displayNames[workflow.id] || workflow.display_name + return buildWorkflow(workflow, displayName, isDefault, env) }) + const workflowStatuses = await Promise.allSettled(awaitWorkflows) + const workflowsWithSubjectSets = workflowStatuses.map(result => result.value || result.reason) + return workflowsWithSubjectSets } function createDisplayNamesMap (translations) { diff --git a/packages/app-project/src/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.spec.js b/packages/app-project/src/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.spec.js index fb645cb8c6..7cb8e71ee0 100644 --- a/packages/app-project/src/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.spec.js +++ b/packages/app-project/src/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.spec.js @@ -2,50 +2,71 @@ import nock from 'nock' import fetchWorkflowsHelper from './fetchWorkflowsHelper' -const WORKFLOWS = [ - { - id: '1', - completeness: 0.4, - grouped: false, - links: { - subject_sets: ['1', '2', '3'] +describe('Helpers > fetchWorkflowsHelper', function () { + const WORKFLOWS = [ + { + id: '1', + completeness: 0.4, + grouped: false, + links: { + subject_sets: ['1', '2', '3'] + } + }, + { + id: '2', + completeness: 0.7, + grouped: true, + links: { + subject_sets: ['1', '2', '3'] + } } - }, - { - id: '2', - completeness: 0.7, - grouped: false, - links: { - subject_sets: ['1', '2', '3'] + ] + + // `translated_id` is a number because of a bug in the translations API :( + const TRANSLATIONS = [ + { + translated_id: 1, + strings: { + display_name: 'Foo' + } + }, + { + translated_id: 2, + strings: { + display_name: 'Bar' + } } + ] + + const availableSubjects = { + 1: 4, + 2: 10, + 3: 10 } -] - -// `translated_id` is a number because of a bug in the translations API :( -const TRANSLATIONS = [ - { - translated_id: 1, - strings: { - display_name: 'Foo' - } - }, - { - translated_id: 2, - strings: { - display_name: 'Bar' + + function subjectSet(id) { + return { + id, + display_name: `test set ${id}`, + set_member_subjects_count: 10 } } -] -function subjectSet(id) { - return { - id, - display_name: `test set ${id}`, - set_member_subjects_count: 10 - } -} + before(function () { + const cellect = nock('https://cellect.zooniverse.org') + .persist() + .get('/workflows/1/status') + .reply(200, {}) + .get('/workflows/2/status') + .reply(200, { + groups: availableSubjects + }) + }) + + after(function () { + nock.cleanAll() + }) -describe('Helpers > fetchWorkflowsHelper', function () { it('should provide the expected result with a single workflow', async function () { const scope = nock('https://panoptes-staging.zooniverse.org/api') .get('/translations') @@ -56,14 +77,16 @@ describe('Helpers > fetchWorkflowsHelper', function () { .get('/workflows') .query(true) .reply(200, { - workflows: WORKFLOWS.slice(0, 1), - linked: { - subject_sets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] - } + workflows: WORKFLOWS.slice(0, 1) + }) + .get('/subject_sets') + .query(query => query.id === '1,2,3') + .reply(200, { + subject_sets: [ + subjectSet('1'), + subjectSet('2'), + subjectSet('3') + ] }) const result = await fetchWorkflowsHelper('en', ['1']) @@ -75,11 +98,7 @@ describe('Helpers > fetchWorkflowsHelper', function () { grouped: false, id: '1', displayName: 'Foo', - subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] + subjectSets: [] } ]) }) @@ -94,14 +113,16 @@ describe('Helpers > fetchWorkflowsHelper', function () { .get('/workflows') .query(true) .reply(200, { - workflows: WORKFLOWS, - linked: { - subject_sets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] - } + workflows: WORKFLOWS + }) + .get('/subject_sets') + .query(true) + .reply(200, { + subject_sets: [ + subjectSet('1'), + subjectSet('2'), + subjectSet('3') + ] }) const result = await fetchWorkflowsHelper('en', ['1', '2']) @@ -112,22 +133,18 @@ describe('Helpers > fetchWorkflowsHelper', function () { grouped: false, id: '1', displayName: 'Foo', - subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] + subjectSets: [] }, { completeness: 0.7, default: false, - grouped: false, + grouped: true, id: '2', displayName: 'Bar', subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') + Object.assign(subjectSet('1'), { availableSubjects: availableSubjects[1]}), + Object.assign(subjectSet('2'), { availableSubjects: availableSubjects[2]}), + Object.assign(subjectSet('3'), { availableSubjects: availableSubjects[3]}) ] } ]) @@ -144,14 +161,16 @@ describe('Helpers > fetchWorkflowsHelper', function () { .get('/workflows') .query(true) .reply(200, { - workflows: WORKFLOWS, - linked: { - subject_sets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] - } + workflows: WORKFLOWS + }) + .get('/subject_sets') + .query(true) + .reply(200, { + subject_sets: [ + subjectSet('1'), + subjectSet('2'), + subjectSet('3') + ] }) const result = await fetchWorkflowsHelper('en', ['1', '2'], '2') @@ -162,22 +181,18 @@ describe('Helpers > fetchWorkflowsHelper', function () { grouped: false, id: '1', displayName: 'Foo', - subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] + subjectSets: [] }, { completeness: 0.7, default: true, - grouped: false, + grouped: true, id: '2', displayName: 'Bar', subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') + Object.assign(subjectSet('1'), { availableSubjects: availableSubjects[1]}), + Object.assign(subjectSet('2'), { availableSubjects: availableSubjects[2]}), + Object.assign(subjectSet('3'), { availableSubjects: availableSubjects[3]}) ] } ]) @@ -204,36 +219,37 @@ describe('Helpers > fetchWorkflowsHelper', function () { }) describe(`when there's an error`, function () { + let workflows + it('should allow the error to be thrown for the consumer to handle', async function () { - const error = { - message: 'oh dear. oh dear god' - } + let thrownError + const mockError = new Error('oh dear. oh dear god') const scope = nock('https://panoptes-staging.zooniverse.org/api') .get('/translations') .query(true) - .replyWithError(error) + .replyWithError(mockError) .get('/workflows') .query(true) .reply(200, { - workflows: WORKFLOWS, - linked: { - subject_sets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] - } + workflows: WORKFLOWS + }) + .get('/subject_sets') + .query(true) + .reply(200, { + subject_sets: [ + subjectSet('1'), + subjectSet('2'), + subjectSet('3') + ] }) try { - await fetchWorkflowsHelper('en', ['1', '2'], '2') - expect.fail() + workflows = await fetchWorkflowsHelper('en', ['1', '2'], '2') } catch (error) { - expect(error).to.deep.equal({ - ...error, - response: undefined - }) + thrownError = error } + expect(thrownError).to.deep.equal(mockError) + expect(workflows).to.be.undefined() }) }) }) diff --git a/packages/app-project/src/helpers/getDefaultPageProps/getDefaultPageProps.spec.js b/packages/app-project/src/helpers/getDefaultPageProps/getDefaultPageProps.spec.js index 10daffc8c1..64ad08c086 100644 --- a/packages/app-project/src/helpers/getDefaultPageProps/getDefaultPageProps.spec.js +++ b/packages/app-project/src/helpers/getDefaultPageProps/getDefaultPageProps.spec.js @@ -13,6 +13,16 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { } } + const GROUPED_PROJECT = { + id: '2', + default_workflow: '2', + primary_language: 'en', + slug: 'test-owner/grouped-project', + links: { + active_workflows: ['2'] + } + } + const TRANSLATION = { translated_id: 1, strings: { @@ -20,6 +30,13 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { } } + const GROUPED_TRANSLATION = { + translated_id: 2, + strings: { + display_name: 'Bar' + } + } + const WORKFLOW = { id: '1', completeness: 0.4, @@ -29,6 +46,21 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { } } + const GROUPED_WORKFLOW = { + id: '2', + completeness: 0.4, + grouped: true, + links: { + subject_sets: ['1', '2', '3'] + } + } + + const availableSubjects = { + 1: 4, + 2: 10, + 3: 10 + } + function subjectSet(id) { return { id, @@ -37,47 +69,77 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { } } + function mockAPI(panoptesHost) { + const cellect = nock('https://cellect.zooniverse.org') + .persist() + .get('/workflows/2/status') + .reply(200, { + groups: availableSubjects + }) + const scope = nock(panoptesHost) + .persist() + .get('/projects') + .query(query => query.slug === 'test-owner/test-project') + .reply(200, { + projects: [PROJECT] + }) + .get('/projects') + .query(query => query.slug === 'test-owner/grouped-project') + .reply(200, { + projects: [GROUPED_PROJECT] + }) + .get('/projects') + .query(query => query.slug === 'test-owner/test-wrong-project') + .reply(200, { + projects: [] + }) + .get('/translations') + .query(query => { + return query.translated_type === 'workflow' + && query.translated_id === '1' + && query.language === 'en' + }) + .reply(200, { + translations: [TRANSLATION] + }) + .get('/translations') + .query(query => { + return query.translated_type === 'workflow' + && query.translated_id === '2' + && query.language === 'en' + }) + .reply(200, { + translations: [GROUPED_TRANSLATION] + }) + .get('/subject_sets') + .query(query => query.id === '1,2,3') + .reply(200, { + subject_sets: [ + subjectSet('1'), + subjectSet('2'), + subjectSet('3') + ] + }) + .get('/workflows') + .query(query => query.id === '1') + .reply(200, { + workflows: [WORKFLOW] + }) + .get('/workflows') + .query(query => query.id === '2') + .reply(200, { + workflows: [GROUPED_WORKFLOW] + }) + .get('/workflows') + .query(query => parseInt(query.id) > 2) + .reply(200, { + workflows: [] + }) + } + describe('with the staging API', function () { before(function () { - const slug = 'test-owner/test-project' - const scope = nock('https://panoptes-staging.zooniverse.org/api') - .persist() - .get('/projects') - .query(query => query.slug === slug) - .reply(200, { - projects: [PROJECT] - }) - .get('/projects') - .query(query => query.slug !== slug) - .reply(200, { - projects: [] - }) - .get('/translations') - .query(query => { - return query.translated_type === 'workflow' - && query.translated_id === '1' - && query.language === 'en' - }) - .reply(200, { - translations: [TRANSLATION] - }) - .get('/workflows') - .query(query => query.id === '1') - .reply(200, { - workflows: [WORKFLOW], - linked: { - subject_sets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] - } - }) - .get('/workflows') - .query(query => query.id !== '1') - .reply(200, { - workflows: [] - }) + mockAPI('https://panoptes-staging.zooniverse.org/api') }) after(function () { @@ -110,11 +172,7 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { grouped: false, id: '1', displayName: 'Foo', - subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] + subjectSets: [] } ]) }) @@ -157,6 +215,43 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { }) }) + describe('with a grouped workflow', function () { + it('should return the project\'s active workflows with subject sets', async function () { + const params = { + owner: 'test-owner', + project: 'grouped-project', + workflowID: '2' + } + const query = { + env: 'staging' + } + const req = { + connection: { + encrypted: true + }, + headers: { + host: 'www.zooniverse.org' + } + } + const res = {} + const { props } = await getDefaultPageProps({ params, query, req, res }) + expect(props.workflows).to.deep.equal([ + { + completeness: 0.4, + default: true, + grouped: true, + id: '2', + displayName: 'Bar', + subjectSets: [ + Object.assign(subjectSet('1'), { availableSubjects: availableSubjects[1]}), + Object.assign(subjectSet('2'), { availableSubjects: availableSubjects[2]}), + Object.assign(subjectSet('3'), { availableSubjects: availableSubjects[3]}) + ] + } + ]) + }) + }) + describe('with an invalid workflow ID', function () { let props let res = {} @@ -165,7 +260,7 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { const params = { owner: 'test-owner', project: 'test-project', - workflowID: '2' + workflowID: '3' } const query = { env: 'staging' @@ -191,52 +286,14 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { }) it('should pass an error message to the error page', function () { - expect(props.title).to.equal('Workflow 2 was not found') + expect(props.title).to.equal('Workflow 3 was not found') }) }) }) describe('with the production API', function () { before(function () { - const slug = 'test-owner/test-project' - const scope = nock('https://www.zooniverse.org/api') - .persist() - .get('/projects') - .query(query => query.slug === slug) - .reply(200, { - projects: [PROJECT] - }) - .get('/projects') - .query(query => query.slug !== slug) - .reply(200, { - projects: [] - }) - .get('/translations') - .query(query => { - return query.translated_type === 'workflow' - && query.translated_id === '1' - && query.language === 'en' - }) - .reply(200, { - translations: [TRANSLATION] - }) - .get('/workflows') - .query(query => query.id === '1') - .reply(200, { - workflows: [WORKFLOW], - linked: { - subject_sets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] - } - }) - .get('/workflows') - .query(query => query.id !== '1') - .reply(200, { - workflows: [] - }) + mockAPI('https://www.zooniverse.org/api') }) after(function () { @@ -269,11 +326,7 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { grouped: false, id: '1', displayName: 'Foo', - subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] + subjectSets: [] } ]) }) @@ -316,6 +369,43 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { }) }) + describe('with a grouped workflow', function () { + it('should return the project\'s active workflows with subject sets', async function () { + const params = { + owner: 'test-owner', + project: 'grouped-project', + workflowID: '2' + } + const query = { + env: 'production' + } + const req = { + connection: { + encrypted: true + }, + headers: { + host: 'www.zooniverse.org' + } + } + const res = {} + const { props } = await getDefaultPageProps({ params, query, req, res }) + expect(props.workflows).to.deep.equal([ + { + completeness: 0.4, + default: true, + grouped: true, + id: '2', + displayName: 'Bar', + subjectSets: [ + Object.assign(subjectSet('1'), { availableSubjects: availableSubjects[1]}), + Object.assign(subjectSet('2'), { availableSubjects: availableSubjects[2]}), + Object.assign(subjectSet('3'), { availableSubjects: availableSubjects[3]}) + ] + } + ]) + }) + }) + describe('with an invalid workflow ID', function () { let props let res = {} @@ -324,7 +414,7 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { const params = { owner: 'test-owner', project: 'test-project', - workflowID: '2' + workflowID: '3' } const query = { env: 'production' @@ -350,7 +440,7 @@ describe('Components > ProjectHomePage > getDefaultPageProps', function () { }) it('should pass an error message to the error page', function () { - expect(props.title).to.equal('Workflow 2 was not found') + expect(props.title).to.equal('Workflow 3 was not found') }) }) }) diff --git a/packages/app-project/src/helpers/getStaticPageProps/getStaticPageProps.spec.js b/packages/app-project/src/helpers/getStaticPageProps/getStaticPageProps.spec.js index cf8dadd496..cf1601ef54 100644 --- a/packages/app-project/src/helpers/getStaticPageProps/getStaticPageProps.spec.js +++ b/packages/app-project/src/helpers/getStaticPageProps/getStaticPageProps.spec.js @@ -14,6 +14,16 @@ describe('Helpers > getStaticPageProps', function () { } } + const GROUPED_PROJECT = { + id: '2', + default_workflow: '2', + primary_language: 'en', + slug: 'test-owner/grouped-project', + links: { + active_workflows: ['2'] + } + } + const TRANSLATION = { translated_id: 1, strings: { @@ -21,6 +31,13 @@ describe('Helpers > getStaticPageProps', function () { } } + const GROUPED_TRANSLATION = { + translated_id: 2, + strings: { + display_name: 'Bar' + } + } + const WORKFLOW = { id: '1', completeness: 0.4, @@ -30,6 +47,21 @@ describe('Helpers > getStaticPageProps', function () { } } + const GROUPED_WORKFLOW = { + id: '2', + completeness: 0.4, + grouped: true, + links: { + subject_sets: ['1', '2', '3'] + } + } + + const availableSubjects = { + 1: 4, + 2: 10, + 3: 10 + } + function subjectSet(id) { return { id, @@ -38,47 +70,77 @@ describe('Helpers > getStaticPageProps', function () { } } + function mockAPI(panoptesHost) { + const cellect = nock('https://cellect.zooniverse.org') + .persist() + .get('/workflows/2/status') + .reply(200, { + groups: availableSubjects + }) + const scope = nock(panoptesHost) + .persist() + .get('/projects') + .query(query => query.slug === 'test-owner/test-project') + .reply(200, { + projects: [PROJECT] + }) + .get('/projects') + .query(query => query.slug === 'test-owner/grouped-project') + .reply(200, { + projects: [GROUPED_PROJECT] + }) + .get('/projects') + .query(query => query.slug === 'test-owner/test-wrong-project') + .reply(200, { + projects: [] + }) + .get('/translations') + .query(query => { + return query.translated_type === 'workflow' + && query.translated_id === '1' + && query.language === 'en' + }) + .reply(200, { + translations: [TRANSLATION] + }) + .get('/translations') + .query(query => { + return query.translated_type === 'workflow' + && query.translated_id === '2' + && query.language === 'en' + }) + .reply(200, { + translations: [GROUPED_TRANSLATION] + }) + .get('/subject_sets') + .query(query => query.id === '1,2,3') + .reply(200, { + subject_sets: [ + subjectSet('1'), + subjectSet('2'), + subjectSet('3') + ] + }) + .get('/workflows') + .query(query => query.id === '1') + .reply(200, { + workflows: [WORKFLOW] + }) + .get('/workflows') + .query(query => query.id === '2') + .reply(200, { + workflows: [GROUPED_WORKFLOW] + }) + .get('/workflows') + .query(query => parseInt(query.id) > 2) + .reply(200, { + workflows: [] + }) + } + describe('with the staging API', function () { before(function () { - const slug = 'test-owner/test-project' - const scope = nock('https://panoptes-staging.zooniverse.org/api') - .persist() - .get('/projects') - .query(query => query.slug === slug) - .reply(200, { - projects: [PROJECT] - }) - .get('/projects') - .query(query => query.slug !== slug) - .reply(200, { - projects: [] - }) - .get('/translations') - .query(query => { - return query.translated_type === 'workflow' - && query.translated_id === '1' - && query.language === 'en' - }) - .reply(200, { - translations: [TRANSLATION] - }) - .get('/workflows') - .query(query => query.id === '1') - .reply(200, { - workflows: [WORKFLOW], - linked: { - subject_sets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] - } - }) - .get('/workflows') - .query(query => query.id !== '1') - .reply(200, { - workflows: [] - }) + mockAPI('https://panoptes-staging.zooniverse.org/api') }) after(function () { @@ -102,11 +164,7 @@ describe('Helpers > getStaticPageProps', function () { grouped: false, id: '1', displayName: 'Foo', - subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] + subjectSets: [] } ]) }) @@ -136,6 +194,43 @@ describe('Helpers > getStaticPageProps', function () { }) }) + describe('with a grouped workflow', function () { + it('should return the project\'s active workflows with subject sets', async function () { + const params = { + owner: 'test-owner', + project: 'grouped-project', + workflowID: '2' + } + const query = { + env: 'staging' + } + const req = { + connection: { + encrypted: true + }, + headers: { + host: 'www.zooniverse.org' + } + } + const res = {} + const { props } = await getStaticPageProps({ params, query }) + expect(props.workflows).to.deep.equal([ + { + completeness: 0.4, + default: true, + grouped: true, + id: '2', + displayName: 'Bar', + subjectSets: [ + Object.assign(subjectSet('1'), { availableSubjects: availableSubjects[1]}), + Object.assign(subjectSet('2'), { availableSubjects: availableSubjects[2]}), + Object.assign(subjectSet('3'), { availableSubjects: availableSubjects[3]}) + ] + } + ]) + }) + }) + describe('with an invalid workflow ID', function () { let props @@ -143,7 +238,7 @@ describe('Helpers > getStaticPageProps', function () { const params = { owner: 'test-owner', project: 'test-project', - workflowID: '2' + workflowID: '3' } const query = { env: 'staging' @@ -157,52 +252,14 @@ describe('Helpers > getStaticPageProps', function () { }) it('should return a workflow error message', function () { - expect(props.title).to.equal('Workflow 2 was not found') + expect(props.title).to.equal('Workflow 3 was not found') }) }) }) describe('with the production API', function () { before(function () { - const slug = 'test-owner/test-project' - const scope = nock('https://www.zooniverse.org/api') - .persist() - .get('/projects') - .query(query => query.slug === slug) - .reply(200, { - projects: [PROJECT] - }) - .get('/projects') - .query(query => query.slug !== slug) - .reply(200, { - projects: [] - }) - .get('/translations') - .query(query => { - return query.translated_type === 'workflow' - && query.translated_id === '1' - && query.language === 'en' - }) - .reply(200, { - translations: [TRANSLATION] - }) - .get('/workflows') - .query(query => query.id === '1') - .reply(200, { - workflows: [WORKFLOW], - linked: { - subject_sets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] - } - }) - .get('/workflows') - .query(query => query.id !== '1') - .reply(200, { - workflows: [] - }) + mockAPI('https://www.zooniverse.org/api') }) after(function () { @@ -226,11 +283,7 @@ describe('Helpers > getStaticPageProps', function () { grouped: false, id: '1', displayName: 'Foo', - subjectSets: [ - subjectSet('1'), - subjectSet('2'), - subjectSet('3') - ] + subjectSets: [] } ]) }) @@ -268,6 +321,43 @@ describe('Helpers > getStaticPageProps', function () { }) }) + describe('with a grouped workflow', function () { + it('should return the project\'s active workflows with subject sets', async function () { + const params = { + owner: 'test-owner', + project: 'grouped-project', + workflowID: '2' + } + const query = { + env: 'production' + } + const req = { + connection: { + encrypted: true + }, + headers: { + host: 'www.zooniverse.org' + } + } + const res = {} + const { props } = await getStaticPageProps({ params, query }) + expect(props.workflows).to.deep.equal([ + { + completeness: 0.4, + default: true, + grouped: true, + id: '2', + displayName: 'Bar', + subjectSets: [ + Object.assign(subjectSet('1'), { availableSubjects: availableSubjects[1]}), + Object.assign(subjectSet('2'), { availableSubjects: availableSubjects[2]}), + Object.assign(subjectSet('3'), { availableSubjects: availableSubjects[3]}) + ] + } + ]) + }) + }) + describe('with an invalid workflow ID', function () { let props diff --git a/packages/app-project/src/shared/components/SubjectSetPicker/components/SubjectSetCard/SubjectSetCard.js b/packages/app-project/src/shared/components/SubjectSetPicker/components/SubjectSetCard/SubjectSetCard.js index e9b58db79d..22a9c59d69 100644 --- a/packages/app-project/src/shared/components/SubjectSetPicker/components/SubjectSetCard/SubjectSetCard.js +++ b/packages/app-project/src/shared/components/SubjectSetPicker/components/SubjectSetCard/SubjectSetCard.js @@ -6,13 +6,15 @@ import { array, number, string } from 'prop-types' import React from 'react' function SubjectSetCard (props) { - const { display_name, id, set_member_subjects_count, subjects } = props + const { availableSubjects, display_name, id, set_member_subjects_count, subjects } = props const [subject] = subjects const { publicRuntimeConfig = {} } = getConfig() || {} const assetPrefix = publicRuntimeConfig.assetPrefix || '' const placeholderUrl = `${assetPrefix}/subject-placeholder.png` const subjectURLs = subject ? subject.locations.map(location => Object.values(location)[0]) : [] const alt = subject ? `Subject ${subject.id}` : 'Loading' + const completeness = 1 - (availableSubjects / set_member_subjects_count) + const percentComplete = parseInt(100 * completeness) return ( {display_name} - + {`${set_member_subjects_count} subjects`} +
+ + {`${percentComplete}% complete`} +
diff --git a/yarn.lock b/yarn.lock index 584b98e9e1..02ab6eef80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13400,7 +13400,7 @@ node-fetch@2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@~2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==