diff --git a/api/src/routes/index.js b/api/src/routes/index.js index 941b676a8..4acd3377e 100644 --- a/api/src/routes/index.js +++ b/api/src/routes/index.js @@ -20,6 +20,7 @@ const settings = require('./settings') */ router.get('/users', users.list) router.get('/users/stats', users.stats) +router.post('/users/names', users.getNames) router.get('/users/:id', user.get) router.put('/users/:id', user.put) router.get('/campaigns/:tasker_id-:tm_id', campaign) diff --git a/api/src/routes/users.js b/api/src/routes/users.js index f50047986..3a4d3cef0 100644 --- a/api/src/routes/users.js +++ b/api/src/routes/users.js @@ -165,7 +165,35 @@ async function list (req, res) { } } +/** + * Get names route + * /users/names + * + * List the osm names for a set of osm ids. This is a helper route that is used by + * getAllTeams action. + * + * @param req + * @param res + * @returns {Promise<*>} + * @async + */ +async function getNames (req, res) { + const { body } = req + try { + const { ids } = body + const data = await db + .select('osm_id', 'full_name') + .from('users') + .whereIn('osm_id', ids) + res.send(data) + } catch (err) { + console.error(err) + return res.boom.badRequest('Could not get user names') + } +} + module.exports = { stats, - list + list, + getNames } diff --git a/api/tests/api/users.test.js b/api/tests/api/users.test.js index 4c0127f90..8f4c3b0b5 100644 --- a/api/tests/api/users.test.js +++ b/api/tests/api/users.test.js @@ -38,6 +38,22 @@ test('Pull all users with stats', async (t) => { t.false(Number.isNaN(response.body.records[0].edit_count)) }) +test.serial('Get user names from osm ids', async (t) => { + const allUsersResponse = await request(app) + .get('/scoreboard/api/users/stats') + const ids = allUsersResponse.body.records.map(prop('osm_id')) + const params = { ids } + const response = await request(app) + .post('/scoreboard/api/users/names').send(params) + .expect(200) + const records = response.body + t.is(records.length, ids.length) + records.forEach(({ osm_id, full_name }) => { + t.truthy(osm_id) + t.truthy(full_name) + }) +}) + test('Sort users by most recently active', async (t) => { const response = await request(app) .get('/scoreboard/api/users/stats/?q=&page=1&sortType=Most%20recent&active=false') diff --git a/components/common/Table.js b/components/common/Table.js index 0069635f7..47872c8f5 100644 --- a/components/common/Table.js +++ b/components/common/Table.js @@ -80,7 +80,11 @@ function selectCellFormatter (datatype, idMap, countryMap, campaignMap) { } function prepareAllHeaders (table) { - const headers = glossary.filter(term => Object.keys(table.headers).includes(term.id)) + const tableHeaders = Object.keys(table.headers) + const headers = glossary.filter(term => tableHeaders.includes(term.id)) + if (headers.length !== tableHeaders.length) { + throw new Error('Header(s) are missing from the i18n glossary') + } // appends a boolean property to headers to indicate whether the tooltip is showing headers.map(header => ( table.displaysTooltip.includes(header.id) ? ( diff --git a/lib/i18n/glossary_en.json b/lib/i18n/glossary_en.json index b01126c6f..9b9d10265 100644 --- a/lib/i18n/glossary_en.json +++ b/lib/i18n/glossary_en.json @@ -118,5 +118,25 @@ "id": "campaign-priority", "name": "Priority", "description": "Priority of campaign for mappers on team (High/Medium/Low)" + }, + { + "id": "team-name", + "name": "Team Name", + "description": "Team Name" + }, + { + "id": "#-members", + "name": "# of Members", + "description": "Number of Members" + }, + { + "id": "moderator-names", + "name": "Moderator Names", + "description": "Moderator Names" + }, + { + "id":"team-hashtag", + "name":"Hashtag", + "description":"Hashtag for this team" } ] diff --git a/lib/store/actions/teams.js b/lib/store/actions/teams.js index e2956bc43..b6d8bb776 100644 --- a/lib/store/actions/teams.js +++ b/lib/store/actions/teams.js @@ -1,10 +1,31 @@ +import { flatten, fromPairs } from 'ramda' import fetch from '../../utils/api' export default store => ({ getAllTeams (state) { return fetch('/api/teams') - .then(async res => { - const records = await res.json() + .then(res => res.json()) + .then(async teams => { + // enhance the teams listing with moderator names + const moderatorIds = new Set(flatten(teams.map(team => team.moderators))) + const fetchParams = { ids: Array.from(moderatorIds) } + const fetchInit = { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify(fetchParams) + } + const moderatorNames = await fetch('/api/users/names', fetchInit) + .then(res => res.json()) + .then(records => fromPairs(records.map(({ osm_id, full_name }) => [osm_id, full_name]))) + return teams.map(team => ({ + ...team, + moderators: fromPairs(team.moderators.map(osmId => [osmId, moderatorNames[osmId]])) + })) + }) + .then(records => { store.setState({ teams: { records } }) }) .catch(err => { diff --git a/lib/utils/format.js b/lib/utils/format.js index 7ebabae8c..2a38dfb24 100644 --- a/lib/utils/format.js +++ b/lib/utils/format.js @@ -49,16 +49,3 @@ export function formatEditTimeDescription (timeVar) { export function formatUpdateDescription (timeVar) { return `${distanceInWords(timeVar, new Date())} ago` } - -/** - * Prefix with '#' for consistent display of hashtag. - * - * @param {string} hashtag - * @returns {string} - */ -export function normalizeHashtag (hashtag) { - if (/^#/.test(hashtag)) { - return hashtag - } - return `#${hashtag}` -} diff --git a/pages/team.js b/pages/team.js index 7ae12de08..235bb2768 100644 --- a/pages/team.js +++ b/pages/team.js @@ -9,7 +9,7 @@ import TeamStatsTable from '../components/campaign/CampaignTable' import Table from '../components/common/Table' import Blurb from '../components/teams/TeamBlurb' import Link from '../components/Link' -import { formatDecimal, normalizeHashtag } from '../lib/utils/format' +import { formatDecimal } from '../lib/utils/format' import { actions } from '../lib/store' import { calculateBlurbStats, @@ -113,7 +113,7 @@ export class Team extends Component {