Skip to content

Commit

Permalink
Merge pull request #30 from developmentseed/badge_db
Browse files Browse the repository at this point in the history
Pull badges from the DB and calculate for each user on the backend
  • Loading branch information
kamicut committed Oct 2, 2018
2 parents 3c716ec + eb5e496 commit 12694e9
Show file tree
Hide file tree
Showing 21 changed files with 273 additions and 302 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
],
"arrow-parens": ["error", "always"],
"prefer-destructuring": "off",
"function-paren-newline": ["error", "consistent"]
"function-paren-newline": ["error", "consistent"],
"arrow-body-style": "off"
}
}
15 changes: 15 additions & 0 deletions api/src/badge_logic/date_check_sequential.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const sequentializeDates = require('../utils/sequential_dates')
const findLongestStreak = require('../utils/longest_streak')
const getBadgeInfo = require('./get_badge_info')

module.exports = (dates, badges) => {
const sequentialDates = sequentializeDates(dates)
const userTotal = findLongestStreak(sequentialDates)
const key = 'daysInRow'

return {
[key]: getBadgeInfo(userTotal, key, badges.find((element) => {
return element.metric === key
}))
}
}
17 changes: 17 additions & 0 deletions api/src/badge_logic/date_check_total.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const getBadgeInfo = require('./get_badge_info')
const { uniq } = require('ramda')

module.exports = (dates, badges) => {
// Truncate hours/minutes/seconds from timestamp
const days = dates.map((date) => {
date = new Date(date)
return date.setHours(0, 0, 0, 0)
})

const key = 'daysTotal'
return {
[key]: getBadgeInfo(uniq(days).length, key, badges.find((element) => {
return element.metric === key
}))
}
}
45 changes: 45 additions & 0 deletions api/src/badge_logic/get_badge_info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Given a metricValue and a metricName to a badge
* calculate the level of that badge and the percentage amount
* needed to obtain the next badge
*/
//eslint-disable-next-line no-unused-vars, consistent-return
module.exports = (metricValue, metricName, badge) => {
const { tiers, name, id } = badge
let badgeLevel = 0

if (metricValue >= tiers[0] && metricValue < tiers[1]) {
badgeLevel = 1
}
else if (metricValue >= tiers[1] && metricValue < tiers[2]) {
badgeLevel = 2
}
else if (metricValue >= tiers[2]) {
badgeLevel = 3
}

const nextBadgeLevel = badgeLevel + 1
const currentPoints = Number(metricValue)
let lastPoints = 0
let nextPoints = 0
let percentage = 100

if (badgeLevel < Object.keys(tiers).length) {
if (badgeLevel > 0) lastPoints = tiers[badgeLevel - 1]
nextPoints = tiers[nextBadgeLevel]
percentage = (currentPoints - lastPoints) / (nextPoints - lastPoints) * 100
return {
name: name,
category: id,
metric: metricName,
description: badge.description,
badgeLevel,
nextBadgeLevel,
points: {
currentPoints,
nextPoints,
percentage
}
}
}
}
66 changes: 66 additions & 0 deletions api/src/badge_logic/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const getSumBadges = require('./sum_based_badges')
const dateSequentialCheck = require('./date_check_sequential')
const dateTotalCheck = require('./date_check_total')
const {
mergeAll, reject, isNil, filter, map, prop, compose, sum
} = require('ramda')

const getJosmEditCount = compose(
sum,
map(prop('count')),
filter((x) => x.editor.toLowerCase().includes('josm'))
)

module.exports = (userData, badges) => {
/* eslint-disable camelcase */
const {
buildings_add,
waterways_add,
poi_add,
km_roads_add,
km_roads_mod,
country_list,
hashtags,
editors,
edit_times
} = userData
/* eslint-enable camelcase */
const sumBadges = reject(isNil)(getSumBadges({
buildings: Number(buildings_add),
waterways: Number(waterways_add),
pois: Number(poi_add),
roadKms: Number(km_roads_add),
roadKmMods: Number(km_roads_mod),
countries: Object.keys(country_list).length,
josm: getJosmEditCount(editors),
hashtags: Object.keys(hashtags).length
}, badges))
const consistencyBadge = dateSequentialCheck(edit_times, badges)
const historyBadge = dateTotalCheck(edit_times, badges)

const allBadges = mergeAll([sumBadges, consistencyBadge, historyBadge])
const earnedBadges = {}
/* eslint-disable no-restricted-syntax */
for (const key in allBadges) {
const val = allBadges[key]
if (val && val.badgeLevel > 0) {
earnedBadges[key] = val
}
}
/* eslint-enable no-restricted-syntax */

const sortedSumBadges = Object.keys(sumBadges).sort((a, b) => {
return sumBadges[a].points.percentage - sumBadges[b].points.percentage
})

const mostObtainableNames = sortedSumBadges.slice(-3)
const mostObtainable = sumBadges[mostObtainableNames[mostObtainableNames.length - 1]]
const secondMostObtainable = sumBadges[mostObtainableNames[mostObtainableNames.length - 2]]
const thirdMostObtainable = sumBadges[mostObtainableNames[mostObtainableNames.length - 3]]

return {
all: allBadges,
earnedBadges,
mostAttainable: [mostObtainable, secondMostObtainable, thirdMostObtainable]
}
}
17 changes: 17 additions & 0 deletions api/src/badge_logic/sum_based_badges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const getBadgeInfo = require('./get_badge_info')
const { keys } = require('ramda')

/**
* Given the userData object containing total amounts for each
* metric, get the badge info for that metric
*
* @param {*} userData - User stats
* @param {*} badgesDB - All badges in the database
* @returns {*} keys - For every key in userData, get the badge info associated with that key
*/
//eslint-disable-next-line no-unused-vars
module.exports = (userData, badgesDB) => {
return keys(userData).map((key) => getBadgeInfo(userData[key], key, badgesDB.find((element) => {
return element.metric === key
})))
}
36 changes: 36 additions & 0 deletions api/src/db/migrations/20180921114504_badges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

exports.up = async function(knex, Promise) {
try {
await knex.schema.createTable('badges', table => {
table.increments('id');
table.string('name');
table.specificType('tiers', 'int[]');
table.string('metric');
table.string('description');
table.timestamps();
});
await knex('badges').insert([
{'id': 15, 'tiers': [25, 50, 100], 'description': 'Map early, map often. Map as many days as you can to achieve new levels.', 'metric': 'daysTotal', 'name': 'Year-long Mapper'},
{'id': 14, 'tiers': [5, 20, 50], 'description': 'Great mappers map everyday. Edit for a consecutive numbers of days in a month to achieve new levels.', 'metric': 'daysInRow', 'name': 'Consistency'},
{'id': 3, 'tiers': [500, 2500, 5000], 'description': 'Places of interest guide where you can go. Every community needs hospitals, schools, businesses mapped to enable access. Each new level is achieved by creating new places on the map.', 'metric': 'pois', 'name': 'On Point'},
{'id': 4, 'tiers': [100, 500, 1000], 'description': 'Frank Lloyd Wright knew buildings, and so do you. Each new level is achieved by mapping and editing buildings.', 'metric': 'buildings', 'name': 'The Wright Stuff'},
{'id': 6, 'tiers': [50, 100, 500], 'description': 'Transportation matters. Put communities on the map by creating new roads. Each new level achieved by creating new roads.', 'metric': 'roadKms', 'name': 'On The Road Again'},
{'id': 7, 'tiers': [50, 100, 500], 'description': 'Roads need maintainence. Existing roads are replaced by new roads and they need to be updated. Each new level achieved by editing existing roads.', 'metric': 'roadKmMods', 'name': 'Long and Winding Road'},
{'id': 8, 'tiers': [50, 100, 500], 'description': 'Waterways, rivers, streams and more. Adding water features to the map adds regional context and valuable information in the event of flooding. Add these features to reach new levels of this badge.', 'metric': 'waterways', 'name': 'White Water Rafting'},
{'id': 9, 'tiers': [5, 10, 25], 'description': 'You are famous around the globe. The more you edit in new countries, the more you can become world renown. Each new level is achieved by mapping in new countries around the world.', 'metric': 'countries', 'name': 'World Renown'},
{'id': 12, 'tiers': [1, 10, 100], 'description': 'JOSM is a tool used to edit OpenStreetMap. It is particularly useful for mapping larger areas more quickly and contains many additional, advanced tools. Map using JOSM to achieve this badge.', 'metric': 'josm', 'name': 'Awesome JOSM'},
{'id': 13, 'tiers': [5, 20, 50], 'description': 'Mapathons are entry points to mapping. They also provide structure to train and become a better mapper. Each new level is achieved by attending and participating in mapathons.', 'metric': 'hashtags', 'name': 'Mapathoner'}
])
}
catch (e) {
console.error(e);
}
};

exports.down = async function(knex, Promise) {
try {
await knex.schema.dropTable('badges');
} catch (e) {
console.error(e);
}
};
12 changes: 11 additions & 1 deletion api/src/routes/user.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
const getBadgeProgress = require('../badge_logic')
const {
API_URL
} = require('../config')
const Users = require('../models/users')
const osmesa = require('../services/osmesa')
const { canEditUser } = require('../passport')
const connection = require('../db/connection')

const users = new Users()


/**
* User Stats Route
* /user/:id
Expand All @@ -24,11 +27,18 @@ async function get(req, res) {
return res.boom.badRequest('Invalid id')
}
try {
const db = connection()
const osmesaResponse = await osmesa.getUser(id)
const [{ country }] = await users.findByOsmId(id).select('country')
const json = JSON.parse(osmesaResponse)

const badgesFromDB = await db('badges').select() // array of all badges
const badges = getBadgeProgress(json, badgesFromDB)

json.extent_uri = `${API_URL}/scoreboard/api/extents/${json.extent_uri}`
return res.send({ id, records: json, country })
return res.send({
id, records: json, country, badges
})
}
catch (err) {
console.error(err)
Expand Down
12 changes: 12 additions & 0 deletions api/src/utils/longest_streak.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

// returns the length of the longest array in an array
module.exports = (array) => {
const elements = array.length
let count = 0
for (let i = 0; i < elements; i += 1) {
if (array[i].length > count) {
count = array[i].length
}
}
return count
}
36 changes: 36 additions & 0 deletions api/src/utils/sequential_dates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { uniq } = require('ramda')

// function takes array of dates and returns an array of arrays
// containing each sequential date
// http://stackoverflow.com/questions/16690905/javascript-get-sequential-dates-in-array
module.exports = (dates) => {
// Filter out non-unique dates
const days = uniq(
dates.map((date) => {
date = new Date(date)
return date.setHours(0, 0, 0, 0)
})
)

let k = 0
const sorted = []
sorted[k] = []
days.sort((a, b) => { //eslint-disable-line arrow-body-style
return +a > +b ? 1 : +a === +b ? 0 : -1 //eslint-disable-line no-nested-ternary
})
.forEach((v, i) => {
const a = v
const b = dates[i + 1] || 0
sorted[k].push(+a)
if ((+b - +a) > 86400000) {
sorted[++k] = [] //eslint-disable-line no-plusplus
}
return 1
})

sorted.sort((a, b) => { //eslint-disable-line arrow-body-style
return a.length > b.length ? -1 : 1
})

return sorted
}
51 changes: 0 additions & 51 deletions frontend/src/badge_logic/badges.js

This file was deleted.

11 changes: 0 additions & 11 deletions frontend/src/badge_logic/date_check_sequential.js

This file was deleted.

13 changes: 0 additions & 13 deletions frontend/src/badge_logic/date_check_total.js

This file was deleted.

0 comments on commit 12694e9

Please sign in to comment.