-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
lib-user: Add Contributors CSV data export (#6113)
* Add initial ProjectStats component * Add initial MemberStats component * Add initial Contributors component * Add Contributors boxShadows * Refactor GroupContainer for GroupStats and Contributors * Create ContributorsContainer, Contributors, and refactor ContributorsList * Add conditional for ProjectStats displayName with private project * Add ContributorsList tests, related refactors * Remove data export from user stats page * Refactor ContentLink, usePanoptesUser, and stats.mock per data export * Add CSV data export to Contributors * Add Contributors data export tests * Fix order of hooks issue * Refactor tests per rebase * Refactor ContributorsList with privateProjectIndex * Sanitize group name for filename * Refactor tests per mock data changes * Add convertStatsSecondsToHours util * Refactor components with convertStatsSecondsToHours * Remove unnecessary TitledStat NaN conditional and story * Fix missing groupId in TopContributors
- Loading branch information
Showing
21 changed files
with
313 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
packages/lib-user/src/components/Contributors/helpers/generateExport.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { arrayOf, func, number, shape, string } from 'prop-types' | ||
|
||
import { getExportData } from './getExportData' | ||
|
||
const DEFAULT_HANDLER = () => true | ||
|
||
export function generateExport({ | ||
group, | ||
handleFileName = DEFAULT_HANDLER, | ||
handleDataExportUrl = DEFAULT_HANDLER, | ||
projects, | ||
stats, | ||
users | ||
}) { | ||
const data = getExportData({ projects, stats, users }) | ||
|
||
let str = '' | ||
|
||
data.forEach((row) => { | ||
str += row.map((col) => JSON.stringify(col)).join(',').concat('\n') | ||
}) | ||
|
||
// The following regexp sanitizes the group name by removing all non-alphanumeric characters (i.e. emojis, spaces, punctuation, etc.) | ||
let sanitizedGroupName = group.display_name.replace(/[^a-zA-Z0-9]/g, '') | ||
|
||
let newFilename = `${sanitizedGroupName}.data_export.${Date.now()}.csv` | ||
handleFileName(newFilename) | ||
|
||
let file = new File([str], newFilename, { type: 'text/csv' }) | ||
let newDataExportUrl = URL.createObjectURL(file) | ||
handleDataExportUrl(newDataExportUrl) | ||
} | ||
|
||
generateExport.propTypes = { | ||
group: shape({ | ||
display_name: string | ||
}), | ||
handleFileName: func, | ||
handleDataExportUrl: func, | ||
projects: arrayOf(shape({ | ||
display_name: string, | ||
id: string | ||
})), | ||
stats: shape({ | ||
group_member_stats_breakdown: arrayOf(shape({ | ||
user_id: number, | ||
count: number, | ||
session_time: number, | ||
project_contributions: arrayOf(shape({ | ||
project_id: number, | ||
count: number, | ||
session_time: number | ||
})) | ||
})) | ||
}), | ||
users: arrayOf(shape({ | ||
id: string, | ||
display_name: string, | ||
login: string | ||
})) | ||
} |
38 changes: 38 additions & 0 deletions
38
packages/lib-user/src/components/Contributors/helpers/generateExport.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import sinon from 'sinon' | ||
|
||
import { PROJECTS, USER_GROUPS, USER, GROUP_MEMBER_USER, GROUP_ADMIN_USER } from '../../../../test/mocks/panoptes' | ||
import { group_member_stats_breakdown } from '../../../../test/mocks/stats.mock' | ||
|
||
import { generateExport } from './generateExport' | ||
|
||
describe('Contributors > generateExport', function () { | ||
let setFilename | ||
let projects | ||
let stats | ||
let users | ||
|
||
before(function () { | ||
setFilename = sinon.stub() | ||
projects = PROJECTS | ||
stats = { | ||
group_member_stats_breakdown | ||
} | ||
users = [USER, GROUP_MEMBER_USER, GROUP_ADMIN_USER] | ||
}) | ||
|
||
after(function () { | ||
setFilename.resetHistory() | ||
}) | ||
|
||
it('should call setFilename with the correct filename', function () { | ||
generateExport({ | ||
group: USER_GROUPS[0], | ||
handleFileName: setFilename, | ||
projects, | ||
stats, | ||
users | ||
}) | ||
|
||
expect(setFilename).to.be.calledWithMatch(/TestGroup.data_export.\d+.csv/) | ||
}) | ||
}) |
80 changes: 80 additions & 0 deletions
80
packages/lib-user/src/components/Contributors/helpers/getExportData.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { arrayOf, number, shape, string } from 'prop-types' | ||
|
||
import { convertStatsSecondsToHours } from '@utils' | ||
|
||
export function getExportData ({ | ||
stats, | ||
projects, | ||
users | ||
}) { | ||
// create a list of all project names | ||
let privateProjectIndex = 1 | ||
const projectNames = [...new Set( | ||
stats.group_member_stats_breakdown.flatMap(member => { | ||
return member.project_contributions.map(statsProject => { | ||
const project = projects.find(project => project.id === statsProject.project_id.toString()) | ||
return project?.display_name || `Private Project ${privateProjectIndex++}` | ||
}) | ||
}))] | ||
|
||
// create header row | ||
const data = [ | ||
[ | ||
'Display Name', 'Username', 'Total Classifications', 'Total Hours', | ||
...projectNames.flatMap(projectName => [`${projectName} Classifications`, `${projectName} Hours`]) | ||
] | ||
] | ||
|
||
// iterate over each member and create a row for each member's stats | ||
for (const member of stats.group_member_stats_breakdown) { | ||
const user = users.find(user => user.id === member.user_id.toString()) | ||
if (!user) continue | ||
|
||
const memberHours = convertStatsSecondsToHours(member.session_time) | ||
|
||
const row = [ | ||
user.display_name, | ||
user.login, | ||
member.count, | ||
memberHours | ||
] | ||
|
||
// iterate over each project and add the member stats for that project or 0 if the member has no stats for that project | ||
for (const projectName of projectNames) { | ||
const projectStats = member.project_contributions | ||
.find(statsProject => { | ||
const project = projects.find(project => project.id === statsProject.project_id.toString()) | ||
return project?.display_name === projectName | ||
}) | ||
row.push(projectStats?.count || 0) | ||
row.push(convertStatsSecondsToHours(projectStats?.session_time)) | ||
} | ||
data.push(row) | ||
} | ||
|
||
return data | ||
} | ||
|
||
getExportData.propTypes = { | ||
projects: arrayOf(shape({ | ||
display_name: string, | ||
id: string | ||
})), | ||
stats: shape({ | ||
group_member_stats_breakdown: arrayOf(shape({ | ||
user_id: number, | ||
count: number, | ||
session_time: number, | ||
project_contributions: arrayOf(shape({ | ||
project_id: number, | ||
count: number, | ||
session_time: number | ||
})) | ||
})) | ||
}), | ||
users: arrayOf(shape({ | ||
id: string, | ||
display_name: string, | ||
login: string | ||
})) | ||
} |
45 changes: 45 additions & 0 deletions
45
packages/lib-user/src/components/Contributors/helpers/getExportData.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { PROJECTS, USER, GROUP_MEMBER_USER, GROUP_ADMIN_USER } from '../../../../test/mocks/panoptes' | ||
import { group_member_stats_breakdown } from '../../../../test/mocks/stats.mock' | ||
|
||
import { getExportData } from './getExportData' | ||
|
||
describe('Contributors > getExportData', function () { | ||
let projects | ||
let stats | ||
let users | ||
|
||
before(function () { | ||
projects = PROJECTS | ||
stats = { | ||
group_member_stats_breakdown | ||
} | ||
users = [USER, GROUP_MEMBER_USER, GROUP_ADMIN_USER] | ||
}) | ||
|
||
it('should return an array of arrays with the correct data', function () { | ||
const projectNames = [ | ||
`Notes from Nature - Capturing California's Flowers`, | ||
'NEST QUEST GO: EASTERN BLUEBIRDS', | ||
'Corresponding with Quakers', | ||
'Wildwatch Kenya is a short project name compared to the longest live project at 80', | ||
'Planet Hunters TESS' | ||
] | ||
const data = getExportData({ projects, stats, users }) | ||
|
||
expect(data).to.eql([ | ||
[ | ||
'Display Name', 'Username', 'Total Classifications', 'Total Hours', | ||
...projectNames.flatMap(projectName => [`${projectName} Classifications`, `${projectName} Hours`]) | ||
], | ||
[ | ||
'Test User 1', 'TestUser', 13425, 65, 121, 6, 93, 10, 45, 16, 36, 19, 0, 0 | ||
], | ||
[ | ||
'Student User 1', 'StudentUser', 9574, 96, 0, 0, 56, 10, 45, 16, 23, 19, 0, 0 | ||
], | ||
[ | ||
'Teacher User 1', 'TeacherUser', 648, 127, 0, 0, 0, 0, 45, 16, 3, 19, 56, 13 | ||
] | ||
]) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.