Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions db/migrations/20160804122354-player-stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const config = require('../config')

config()

exports.up = async (r, conn) => {
const players = await r.table('players').run(conn)

const updates = players.map(player => {
const ecc = player.ecc
const projects = {}

const cycles = player.cycleProjectECC || {}
Object.keys(cycles).forEach(cycleId => {
const cycleProjects = cycles[cycleId] || {}

Object.keys(cycleProjects).forEach(projectId => {
const cycleProjectStats = cycleProjects[projectId] || {}

if (!projects[projectId]) {
projects[projectId] = {cycles: {}}
}
if (!projects[projectId].cycles[cycleId]) {
projects[projectId].cycles[cycleId] = {}
}

projects[projectId].cycles[cycleId].abc = cycleProjectStats.abc || 0
projects[projectId].cycles[cycleId].rc = cycleProjectStats.rc || 0
projects[projectId].cycles[cycleId].ecc = cycleProjectStats.ecc || 0
})
})

return r.table('players')
.get(player.id)
.replace(
r.row
.merge({stats: {ecc, projects}})
.without('ecc', 'cycleProjectECC')
)
.run(conn)
})

return Promise.all(updates)
}

exports.down = async (r, conn) => {
const players = await r.table('players').run(conn)

const updates = players.map(player => {
const stats = player.stats || {}
const ecc = stats.ecc || 0
const cycleProjectECC = {}

const projects = stats.projects || {}
Object.keys(projects).forEach(projectId => {
const projectCycles = (projects[projectId] || {}).cycles || {}

Object.keys(projectCycles).forEach(cycleId => {
const projectCycleStats = projectCycles[cycleId] || {}

if (!cycleProjectECC[cycleId]) {
cycleProjectECC[cycleId] = {}
}
if (!cycleProjectECC[cycleId][projectId]) {
cycleProjectECC[cycleId][projectId] = {}
}

cycleProjectECC[cycleId][projectId].abc = projectCycleStats.abc
cycleProjectECC[cycleId][projectId].rc = projectCycleStats.rc
cycleProjectECC[cycleId][projectId].ecc = projectCycleStats.ecc
})
})

return r.table('players')
.get(player.id)
.replace(
r.row
.merge({ecc, cycleProjectECC})
.without('stats')
)
.run(conn)
})

return Promise.all(updates)
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
"data:github-goals": "./node_modules/.bin/babel-node test/generateTestGoals",
"db:create": "./node_modules/.bin/babel-node ./db/create.js",
"db:drop": "./node_modules/.bin/babel-node ./db/drop.js",
"db:migrate:configure": "node ./db/config.js > ./db/database.json",
"db:migrate": "npm run db:migrate:configure && node ./node_modules/.bin/rethink-migrate -r db",
"db:migrate:up": "npm run db:migrate:configure && node ./node_modules/.bin/rethink-migrate -r db up",
"db:migrate:down": "npm run db:migrate:configure && node ./node_modules/.bin/rethink-migrate -r db down",
"db:migrate:configure": "./node_modules/.bin/babel-node ./db/config.js > ./db/database.json",
"db:migrate": "npm run db:migrate:configure && ./node_modules/.bin/babel-node ./node_modules/.bin/rethink-migrate -r db",
"db:migrate:up": "npm run db:migrate:configure && ./node_modules/.bin/babel-node ./node_modules/.bin/rethink-migrate -r db up",
"db:migrate:down": "npm run db:migrate:configure && ./node_modules/.bin/babel-node ./node_modules/.bin/rethink-migrate -r db down",
"start": "npm run icons:fetch && node server",
"workers": "./node_modules/.bin/babel-node ./server/workers",
"postinstall": "npm run build",
Expand Down
2 changes: 1 addition & 1 deletion server/actions/__tests__/formProjects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ async function _generatePlayers(chapterId, options = {}) {
const numAdvanced = options.advanced || 0
return {
regular: await factory.createMany('player', {chapterId}, numTotal - numAdvanced),
advanced: await factory.createMany('player', {chapterId, ecc: TEST_ADVANCED_PLAYER_ECC}, numAdvanced)
advanced: await factory.createMany('player', {chapterId, stats: {ecc: TEST_ADVANCED_PLAYER_ECC}}, numAdvanced)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,35 @@ import {withDBCleanup, useFixture} from '../../../test/helpers'
import {getPlayerById} from '../../../server/db/player'

import {
calculateProjectECCStatsForPlayer,
updateTeamECCStats,
} from '../updateTeamECCStats'
calculatePlayerProjectStats,
updateProjectStats,
} from '../updateProjectStats'

describe(testContext(__filename), function () {
describe('calculateProjectECCStatsForPlayer()', function () {
describe('calculatePlayerProjectStats()', function () {
specify('when there are scores from all team members', function () {
expect(calculateProjectECCStatsForPlayer({teamSize: 4, relativeContributionScores: [10, 20, 20, 30]}))
expect(calculatePlayerProjectStats({teamSize: 4, relativeContributionScores: [10, 20, 20, 30]}))
.to.deep.eq({ecc: 80, abc: 4, rc: 20})
})
specify('when there are not scores from all team members', function () {
expect(calculateProjectECCStatsForPlayer({teamSize: 4, relativeContributionScores: [20, 25, 30]}))
expect(calculatePlayerProjectStats({teamSize: 4, relativeContributionScores: [20, 25, 30]}))
.to.deep.eq({ecc: 100, abc: 4, rc: 25})
})
specify('when the result is over 100', function () {
expect(calculateProjectECCStatsForPlayer({teamSize: 4, relativeContributionScores: [50, 50, 50, 50]}))
expect(calculatePlayerProjectStats({teamSize: 4, relativeContributionScores: [50, 50, 50, 50]}))
.to.deep.eq({ecc: 200, abc: 4, rc: 50})
})
specify('when project length is > 1', function () {
expect(calculateProjectECCStatsForPlayer({teamSize: 4, relativeContributionScores: [50, 50, 50, 50], projectLength: 3}))
expect(calculatePlayerProjectStats({teamSize: 4, relativeContributionScores: [50, 50, 50, 50], buildCycles: 3}))
.to.deep.eq({ecc: 600, abc: 12, rc: 50})
})
specify('when RC is a decimal, round', function () {
expect(calculateProjectECCStatsForPlayer({teamSize: 5, relativeContributionScores: [10, 10, 21, 21]}))
expect(calculatePlayerProjectStats({teamSize: 5, relativeContributionScores: [10, 10, 21, 21]}))
.to.deep.eq({ecc: 80, abc: 5, rc: 16})
})
})

describe('updateTeamECCStats', function () {
describe('updateProjectStats', function () {
withDBCleanup()
useFixture.buildSurvey()

Expand Down Expand Up @@ -64,10 +64,10 @@ describe(testContext(__filename), function () {

it('updates the players ECC based on the survey responses', async function() {
const eccChange = 20 * this.teamPlayerIds.length
await updateTeamECCStats(this.project, this.cycleId)
await updateProjectStats(this.project, this.cycleId)

const updatedPlayer = await getPlayerById(this.teamPlayerIds[0])
expect(updatedPlayer.ecc).to.eq(eccChange)
expect(updatedPlayer.stats.ecc).to.eq(eccChange)
})
})
})
4 changes: 3 additions & 1 deletion server/actions/formProjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ function _formGoalGroups(players, playerVotes) {
const regularPlayers = new Map()

players.forEach(player => {
if (parseInt(player.ecc, 10) >= MIN_ADVANCED_PLAYER_ECC) {
const playerECC = parseInt((player.stats || {}).ecc, 10) || 0

if (playerECC >= MIN_ADVANCED_PLAYER_ECC) {
advancedPlayers.set(player.id, player)
} else {
regularPlayers.set(player.id, player)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {getSurveyById} from '../../server/db/survey'
import {getRelativeContributionQuestionForSurvey} from '../../server/db/question'
import {getSurveyResponses} from '../../server/db/response'
import {updatePlayerECCStats} from '../../server/db/player'
import {savePlayerProjectStats} from '../../server/db/player'
import {
getProjectHistoryForCycle,
} from '../../server/db/project'

export async function updateTeamECCStats(project, cycleId) {
export async function updateProjectStats(project, cycleId) {
const projectCycle = getProjectHistoryForCycle(project, cycleId)
const teamSize = projectCycle.playerIds.length
const surveyId = projectCycle.retrospectiveSurveyId
Expand All @@ -17,16 +17,16 @@ export async function updateTeamECCStats(project, cycleId) {
const promises = []
responsesBySubjectId.forEach((responses, subjectPlayerId) => {
const relativeContributionScores = responses.map(({value}) => value)
const projectECC = calculateProjectECCStatsForPlayer({teamSize, relativeContributionScores})
promises.push(updatePlayerECCStats(subjectPlayerId, projectECC, cycleId, project.id))
const subjectPlayerStats = calculatePlayerProjectStats({teamSize, relativeContributionScores})
promises.push(savePlayerProjectStats(subjectPlayerId, project.id, cycleId, subjectPlayerStats))
})

await Promise.all(promises)
}

export function calculateProjectECCStatsForPlayer({teamSize, relativeContributionScores, projectLength}) {
export function calculatePlayerProjectStats({buildCycles, teamSize, relativeContributionScores}) {
// Calculate ABC
const aggregateBuildCycles = (projectLength || 1) * teamSize
const aggregateBuildCycles = (buildCycles || 1) * teamSize

// Calculate RC
const sum = relativeContributionScores.reduce((sum, next) => sum + next, 0)
Expand Down
100 changes: 60 additions & 40 deletions server/db/__tests__/player.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import factory from '../../../test/factories'
import {withDBCleanup} from '../../../test/helpers'

import {
reassignPlayersToChapter,
getPlayerById,
updatePlayerECCStats,
reassignPlayersToChapter,
savePlayerProjectStats,
} from '../player'

describe(testContext(__filename), function () {
Expand Down Expand Up @@ -91,92 +91,112 @@ describe(testContext(__filename), function () {
})
})

describe('updatePlayerECCStats', function () {
describe('savePlayerProjectStats', function () {
beforeEach(async function () {
this.projectIds = [await r.uuid(), await r.uuid()]
this.cycleIds = [await r.uuid(), await r.uuid()]
this.player = await factory.create('player', {ecc: 0})
this.player = await factory.create('player', {stats: {ecc: 0}})
this.fetchPlayer = () => getPlayerById(this.player.id)
})

it('creates the ecc attribute if missing', async function() {
await getPlayerById(this.player.id).replace(p => p.without('ecc'))
await getPlayerById(this.player.id).replace(p => p.without('stats'))
await savePlayerProjectStats(this.player.id, this.projectIds[0], this.cycleIds[0], {ecc: 40, abc: 4, rc: 10})

await updatePlayerECCStats(this.player.id, {ecc: 40, abc: 4, rc: 10}, this.cycleIds[0], this.projectIds[0])
const player = await this.fetchPlayer()

expect(await this.fetchPlayer()).to.have.property('ecc', 40)
expect(player.stats.ecc).to.eq(40)
})

it('adds to the existing ECC', async function() {
expect(this.player).to.have.property('ecc')
await getPlayerById(this.player.id).update({ecc: 10})
it('adds to the existing cumulative ECC', async function() {
expect(this.player).to.have.deep.property('stats.ecc')

await getPlayerById(this.player.id).update({stats: {ecc: 10}})
await savePlayerProjectStats(this.player.id, this.projectIds[1], this.cycleIds[1], {ecc: 20, abc: 4, rc: 5})

await updatePlayerECCStats(this.player.id, {ecc: 20, abc: 4, rc: 5}, this.cycleIds[1], this.projectIds[1])
const player = await this.fetchPlayer()

expect(await this.fetchPlayer()).to.have.property('ecc', 30)
expect(player.stats.ecc).to.eq(30)
})

it('creates the cycleProjectECC attr if neccessary', async function () {
expect(this.player).to.not.have.property('cycleProjectECC')
it('creates the stats.projects attribute if neccessary', async function () {
expect(this.player).to.not.have.deep.property('stats.projects')

const stats = {ecc: 20, abc: 4, rc: 5}
await updatePlayerECCStats(this.player.id, stats, this.cycleIds[0], this.projectIds[0])
await savePlayerProjectStats(this.player.id, this.projectIds[0], this.cycleIds[0], stats)

expect(await this.fetchPlayer()).to.have.property('cycleProjectECC').and.deep.eq({
[this.cycleIds[0]]: {[this.projectIds[0]]: stats}
const player = await this.fetchPlayer()

expect(player.stats.ecc).to.eq(20)
expect(player.stats.projects).to.deep.eq({
[this.projectIds[0]]: {
cycles: {[this.cycleIds[0]]: stats}
},
})
})

it('adds an item to the existing cycleProjectECC if needed', async function () {
expect(this.player).to.not.have.property('cycleProjectECC')
it('adds a project entry to the stats if neccessary', async function () {
expect(this.player).to.not.have.deep.property('stats.projects')

const stats = [
{ecc: 20, abc: 4, rc: 5},
{ecc: 18, abc: 3, rc: 6},
]
await updatePlayerECCStats(this.player.id, stats[0], this.cycleIds[0], this.projectIds[0])
await updatePlayerECCStats(this.player.id, stats[1], this.cycleIds[1], this.projectIds[1])
await savePlayerProjectStats(this.player.id, this.projectIds[0], this.cycleIds[0], stats[0])
await savePlayerProjectStats(this.player.id, this.projectIds[1], this.cycleIds[1], stats[1])

expect(await this.fetchPlayer()).to.have.property('cycleProjectECC').and.deep.eq({
[this.cycleIds[0]]: {[this.projectIds[0]]: stats[0]},
[this.cycleIds[1]]: {[this.projectIds[1]]: stats[1]},
const player = await this.fetchPlayer()

expect(player.stats.ecc).to.eq(38)
expect(player.stats.projects).to.deep.eq({
[this.projectIds[0]]: {
cycles: {[this.cycleIds[0]]: stats[0]}
},
[this.projectIds[1]]: {
cycles: {[this.cycleIds[1]]: stats[1]}
},
})
})

it('adds project ecc to the existing cycleProjectECC item if present', async function () {
expect(this.player).to.not.have.property('cycleProjectECC')
it('adds a cycle entry to the project stats if needed', async function () {
expect(this.player).to.not.have.deep.property('stats.projects')

const stats = [
{ecc: 20, abc: 4, rc: 5},
{ecc: 18, abc: 3, rc: 6},
]
await updatePlayerECCStats(this.player.id, stats[0], this.cycleIds[0], this.projectIds[0])
await updatePlayerECCStats(this.player.id, stats[1], this.cycleIds[0], this.projectIds[1])

expect(await this.fetchPlayer()).to.have.property('cycleProjectECC').and.deep.eq({
[this.cycleIds[0]]: {
[this.projectIds[0]]: stats[0],
[this.projectIds[1]]: stats[1],
await savePlayerProjectStats(this.player.id, this.projectIds[0], this.cycleIds[0], stats[0])
await savePlayerProjectStats(this.player.id, this.projectIds[0], this.cycleIds[1], stats[1])

const player = await this.fetchPlayer()

expect(player.stats.ecc).to.eq(38)
expect(player.stats.projects).to.deep.eq({
[this.projectIds[0]]: {
cycles: {
[this.cycleIds[0]]: stats[0],
[this.cycleIds[1]]: stats[1],
},
},
})
})

it('when called for the same project/cycle more than once, the result is the same as if only the last call were made', async function () {
// Initialize the player with an ECC of 10
await updatePlayerECCStats(this.player.id, {ecc: 10, abc: 2, rc: 5}, this.cycleIds[0], this.projectIds[0])
await savePlayerProjectStats(this.player.id, this.projectIds[0], this.cycleIds[0], {ecc: 10, abc: 2, rc: 5})

// Add 20 for a project
await updatePlayerECCStats(this.player.id, {ecc: 20, abc: 4, rc: 5}, this.cycleIds[1], this.projectIds[1])
expect(await this.fetchPlayer()).to.have.property('ecc', 30)
await savePlayerProjectStats(this.player.id, this.projectIds[1], this.cycleIds[1], {ecc: 20, abc: 4, rc: 5})
expect(await this.fetchPlayer()).to.have.deep.property('stats.ecc', 30)
expect(await this.fetchPlayer()).to.have.deep
.property(`cycleProjectECC.${this.cycleIds[1]}.${this.projectIds[1]}.ecc`, 20)
.property(`stats.projects.${this.projectIds[1]}.cycles.${this.cycleIds[1]}.ecc`, 20)

// Change the ECC for that project to 10
const stats = {ecc: 10, abc: 2, rc: 5}
await updatePlayerECCStats(this.player.id, stats, this.cycleIds[1], this.projectIds[1])
expect(await this.fetchPlayer()).to.have.property('ecc', 20)
await savePlayerProjectStats(this.player.id, this.projectIds[1], this.cycleIds[1], stats)
expect(await this.fetchPlayer()).to.have.deep.property('stats.ecc', 20)
expect(await this.fetchPlayer()).to.have.deep
.property(`cycleProjectECC.${this.cycleIds[1]}.${this.projectIds[1]}`).deep.eq(stats)
.property(`stats.projects.${this.projectIds[1]}.cycles.${this.cycleIds[1]}`).deep.eq(stats)
})
})
})
Loading