Fix(accounts): show challenge info on user profile

This changes the behavior of the user profile page to pull the current
challenge info from our challenge map and overwrite the user challenge.
This should also make name changes point to the correct challenge
regardless of the info saved to the user profile
This commit is contained in:
Berkeley Martinez
2016-08-01 16:54:33 -07:00
parent 7f22c59239
commit 606bfd7c88
6 changed files with 197 additions and 179 deletions

View File

@ -12,21 +12,10 @@ import {
delayedRedirect, delayedRedirect,
createErrorObservable createErrorObservable
} from '../../../redux/actions'; } from '../../../redux/actions';
import createNameIdMap from '../../../../utils/create-name-id-map';
const { fetchChallenge, fetchChallenges, replaceChallenge } = types; const { fetchChallenge, fetchChallenges, replaceChallenge } = types;
function createNameIdMap(entities) {
const { challenge } = entities;
return {
...entities,
challengeIdToName: Object.keys(challenge)
.reduce((map, challengeName) => {
map[challenge[challengeName].id] = challenge[challengeName].dashedName;
return map;
}, {})
};
}
export default function fetchChallengesSaga(action$, getState, { services }) { export default function fetchChallengesSaga(action$, getState, { services }) {
return action$ return action$
.filter(({ type }) => ( .filter(({ type }) => (

View File

@ -0,0 +1,12 @@
// createNameIdMap(entities: Object) => Object
export default function createNameIdMap(entities) {
const { challenge } = entities;
return {
...entities,
challengeIdToName: Object.keys(challenge)
.reduce((map, challengeName) => {
map[challenge[challengeName].id] = challenge[challengeName].dashedName;
return map;
}, {})
};
}

View File

@ -3,15 +3,12 @@ import moment from 'moment-timezone';
import { Observable } from 'rx'; import { Observable } from 'rx';
import debugFactory from 'debug'; import debugFactory from 'debug';
import supportedLanguages from '../../common/utils/supported-languages';
import { import {
frontEndChallengeId, frontEndChallengeId,
dataVisChallengeId, dataVisChallengeId,
backEndChallengeId backEndChallengeId
} from '../utils/constantStrings.json'; } from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json'; import certTypes from '../utils/certTypes.json';
import { import {
ifNoUser401, ifNoUser401,
ifNoUserRedirectTo ifNoUserRedirectTo
@ -22,6 +19,9 @@ import {
calcCurrentStreak, calcCurrentStreak,
calcLongestStreak calcLongestStreak
} from '../utils/user-stats'; } from '../utils/user-stats';
import supportedLanguages from '../../common/utils/supported-languages';
import createNameIdMap from '../../common/utils/create-name-id-map';
import { cachedMap } from '../utils/map';
const debug = debugFactory('fcc:boot:user'); const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map'); const sendNonUserToMap = ifNoUserRedirectTo('/map');
@ -85,25 +85,35 @@ function getChallengeGroup(challenge) {
return 'challenges'; return 'challenges';
} }
// buildDisplayChallenges(challengeMap: Object, tz: String) => Observable[{ // buildDisplayChallenges(
// entities: { challenge: Object, challengeIdToName: Object },
// challengeMap: Object,
// tz: String
// ) => Observable[{
// algorithms: Array, // algorithms: Array,
// projects: Array, // projects: Array,
// challenges: Array // challenges: Array
// }] // }]
function buildDisplayChallenges(challengeMap = {}, timezone) { function buildDisplayChallenges(
return Observable.from(Object.keys(challengeMap)) { challenge: challengeMap = {}, challengeIdToName },
.map(challengeId => challengeMap[challengeId]) userChallengeMap = {},
.map(challenge => { timezone
let finalChallenge = { ...challenge }; ) {
if (challenge.completedDate) { return Observable.from(Object.keys(userChallengeMap))
.map(challengeId => userChallengeMap[challengeId])
.map(userChallenge => {
const challengeId = userChallenge.id;
const challenge = challengeMap[ challengeIdToName[challengeId] ];
let finalChallenge = { ...userChallenge, ...challenge };
if (userChallenge.completedDate) {
finalChallenge.completedDate = moment finalChallenge.completedDate = moment
.tz(challenge.completedDate, timezone) .tz(userChallenge.completedDate, timezone)
.format(dateFormat); .format(dateFormat);
} }
if (challenge.lastUpdated) { if (userChallenge.lastUpdated) {
finalChallenge.lastUpdated = moment finalChallenge.lastUpdated = moment
.tz(challenge.lastUpdated, timezone) .tz(userChallenge.lastUpdated, timezone)
.format(dateFormat); .format(dateFormat);
} }
@ -128,6 +138,8 @@ module.exports = function(app) {
const router = app.loopback.Router(); const router = app.loopback.Router();
const api = app.loopback.Router(); const api = app.loopback.Router();
const User = app.models.User; const User = app.models.User;
const Block = app.models.Block;
const map$ = cachedMap(Block);
function findUserByUsername$(username, fields) { function findUserByUsername$(username, fields) {
return observeQuery( return observeQuery(
User, User,
@ -187,7 +199,7 @@ module.exports = function(app) {
(req, res) => res.redirect(req.url.replace('full-stack', 'back-end')) (req, res) => res.redirect(req.url.replace('full-stack', 'back-end'))
); );
router.get('/:username', returnUser); router.get('/:username', showUserProfile);
app.use('/:lang', router); app.use('/:lang', router);
app.use(api); app.use(api);
@ -248,7 +260,7 @@ module.exports = function(app) {
return res.redirect('/' + username); return res.redirect('/' + username);
} }
function returnUser(req, res, next) { function showUserProfile(req, res, next) {
const username = req.params.username.toLowerCase(); const username = req.params.username.toLowerCase();
const { user } = req; const { user } = req;
@ -313,7 +325,12 @@ module.exports = function(app) {
}); });
} }
return buildDisplayChallenges(userPortfolio.challengeMap, timezone) return map$.map(({ entities }) => createNameIdMap(entities))
.flatMap(entities => buildDisplayChallenges(
entities,
userPortfolio.challengeMap,
timezone
))
.map(displayChallenges => ({ .map(displayChallenges => ({
...userPortfolio, ...userPortfolio,
...displayChallenges, ...displayChallenges,

View File

@ -1,134 +1,12 @@
import _ from 'lodash';
import { Observable } from 'rx'; import { Observable } from 'rx';
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
import debug from 'debug'; import debug from 'debug';
import { nameify, unDasherize } from '../utils'; import { unDasherize } from '../utils';
import supportedLanguages from '../../common/utils/supported-languages'; import { mapChallengeToLang, cachedMap, getMapForLang } from '../utils/map';
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
const isBeta = !!process.env.BETA; const isBeta = !!process.env.BETA;
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i; const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const log = debug('fcc:services:challenges'); const log = debug('fcc:services:map');
const challenge = new Schema('challenge', { idAttribute: 'dashedName' });
const block = new Schema('block', { idAttribute: 'dashedName' });
const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' });
block.define({
challenges: arrayOf(challenge)
});
superBlock.define({
blocks: arrayOf(block)
});
const mapSchema = valuesOf(superBlock);
/*
* interface ChallengeMap {
* result: [superBlockDashedName: String]
* entities: {
* superBlock: {
* [superBlockDashedName: String]: {
* blocks: [blockDashedName: String]
* }
* },
* block: {
* [blockDashedName: String]: {
* challenges: [challengeDashedName: String]
* }
* },
* challenge: {
* [challengeDashedName: String]: Challenge
* }
* }
* }
*/
function cachedMap(Block) {
const query = {
include: 'challenges',
order: ['superOrder ASC', 'order ASC']
};
return Block.find$(query)
.flatMap(blocks => Observable.from(blocks.map(block => block.toJSON())))
.reduce((map, block) => {
if (map[block.superBlock]) {
map[block.superBlock].blocks.push(block);
} else {
map[block.superBlock] = {
title: _.startCase(block.superBlock),
order: block.superOrder,
name: nameify(_.startCase(block.superBlock)),
dashedName: block.superBlock,
blocks: [block],
message: block.superBlockMessage
};
}
return map;
}, {})
.map(map => normalize(map, mapSchema))
.map(map => {
// make sure challenges are in the right order
map.entities.block = Object.keys(map.entities.block)
// turn map into array
.map(key => map.entities.block[key])
// perform re-order
.map(block => {
block.challenges = block.challenges.reduce((accu, dashedName) => {
const index = map.entities.challenge[dashedName].suborder;
accu[index - 1] = dashedName;
return accu;
}, []);
return block;
})
// turn back into map
.reduce((blockMap, block) => {
blockMap[block.dashedName] = block;
return blockMap;
}, {});
return map;
})
.map(map => {
// re-order superBlocks result
const result = Object.keys(map.result).reduce((result, supName) => {
const index = map.entities.superBlock[supName].order;
result[index] = supName;
return result;
}, []);
return {
...map,
result
};
})
.shareReplay();
}
function mapChallengeToLang({ translations = {}, ...challenge }, lang) {
if (!supportedLanguages[lang]) {
lang = 'en';
}
const translation = translations[lang] || {};
if (lang !== 'en') {
challenge = {
...challenge,
...translation
};
}
return challenge;
}
function getMapForLang(lang) {
return ({ entities: { challenge: challengeMap, ...entities }, result }) => {
entities.challenge = Object.keys(challengeMap)
.reduce((translatedChallengeMap, key) => {
translatedChallengeMap[key] = mapChallengeToLang(
challengeMap[key],
lang
);
return translatedChallengeMap;
}, {});
return { result, entities };
};
}
function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) { function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) {
return isDev || return isDev ||

136
server/utils/map.js Normal file
View File

@ -0,0 +1,136 @@
import _ from 'lodash';
import { Observable } from 'rx';
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
import { nameify } from '../utils';
import supportedLanguages from '../../common/utils/supported-languages';
const challenge = new Schema('challenge', { idAttribute: 'dashedName' });
const block = new Schema('block', { idAttribute: 'dashedName' });
const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' });
block.define({
challenges: arrayOf(challenge)
});
superBlock.define({
blocks: arrayOf(block)
});
const mapSchema = valuesOf(superBlock);
let mapObservableCache;
/*
* interface ChallengeMap {
* result: [superBlockDashedName: String]
* entities: {
* superBlock: {
* [superBlockDashedName: String]: {
* blocks: [blockDashedName: String]
* }
* },
* block: {
* [blockDashedName: String]: {
* challenges: [challengeDashedName: String]
* }
* },
* challenge: {
* [challengeDashedName: String]: Challenge
* }
* }
* }
*/
export function cachedMap(Block) {
if (mapObservableCache) {
return mapObservableCache;
}
const query = {
include: 'challenges',
order: ['superOrder ASC', 'order ASC']
};
const map$ = Block.find$(query)
.flatMap(blocks => Observable.from(blocks.map(block => block.toJSON())))
.reduce((map, block) => {
if (map[block.superBlock]) {
map[block.superBlock].blocks.push(block);
} else {
map[block.superBlock] = {
title: _.startCase(block.superBlock),
order: block.superOrder,
name: nameify(_.startCase(block.superBlock)),
dashedName: block.superBlock,
blocks: [block],
message: block.superBlockMessage
};
}
return map;
}, {})
.map(map => normalize(map, mapSchema))
.map(map => {
// make sure challenges are in the right order
map.entities.block = Object.keys(map.entities.block)
// turn map into array
.map(key => map.entities.block[key])
// perform re-order
.map(block => {
block.challenges = block.challenges.reduce((accu, dashedName) => {
const index = map.entities.challenge[dashedName].suborder;
accu[index - 1] = dashedName;
return accu;
}, []);
return block;
})
// turn back into map
.reduce((blockMap, block) => {
blockMap[block.dashedName] = block;
return blockMap;
}, {});
return map;
})
.map(map => {
// re-order superBlocks result
const result = Object.keys(map.result).reduce((result, supName) => {
const index = map.entities.superBlock[supName].order;
result[index] = supName;
return result;
}, []);
return {
...map,
result
};
})
.shareReplay();
mapObservableCache = map$;
return map$;
}
export function mapChallengeToLang(
{ translations = {}, ...challenge },
lang
) {
if (!supportedLanguages[lang]) {
lang = 'en';
}
const translation = translations[lang] || {};
if (lang !== 'en') {
challenge = {
...challenge,
...translation
};
}
return challenge;
}
export function getMapForLang(lang) {
return ({ entities: { challenge: challengeMap, ...entities }, result }) => {
entities.challenge = Object.keys(challengeMap)
.reduce((translatedChallengeMap, key) => {
translatedChallengeMap[key] = mapChallengeToLang(
challengeMap[key],
lang
);
return translatedChallengeMap;
}, {});
return { result, entities };
};
}

View File

@ -64,20 +64,6 @@ block content
$(document).ready(function () { $(document).ready(function () {
var cal = new CalHeatMap(); var cal = new CalHeatMap();
var calendar = !{JSON.stringify(calender)}; var calendar = !{JSON.stringify(calender)};
/*
var estUTCOffset = -5;
// moment returns the utc offset in minutes
var userUTCOffset = moment().utcOffset() / 60;
var secondsToOffset =
(estUTCOffset - userUTCOffset) * 3600;
var offsetCalendar = {};
for (var prop in calendar) {
if (calendar.hasOwnProperty(prop)) {
var offsetProp = prop + secondsToOffset;
offsetCalendar[offsetProp] = calendar[prop];
}
}
*/
cal.init({ cal.init({
itemSelector: "#cal-heatmap", itemSelector: "#cal-heatmap",
domain: "month", domain: "month",
@ -118,13 +104,13 @@ block content
for challenge in projects for challenge in projects
tr tr
td.col-xs-5.hidden-xs td.col-xs-5.hidden-xs
a(href='/challenges/' + removeOldTerms(challenge.name), target='_blank')= removeOldTerms(challenge.name) a(href='/challenges/#{challenge.block}/#{challenge.dashedName}', target='_blank')= challenge.name
td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available' td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available'
td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : '' td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : ''
td.col-xs-2.hidden-xs td.col-xs-2.hidden-xs
a(href=challenge.solution, target='_blank') View project a(href=challenge.solution, target='_blank') View project
td.col-xs-12.visible-xs td.col-xs-12.visible-xs
a(href=challenge.solution, target='_blank')= removeOldTerms(challenge.name) a(href=challenge.solution, target='_blank')= challenge.name
if (algorithms.length > 0) if (algorithms.length > 0)
.col-sm-12 .col-sm-12
table.table.table-striped table.table.table-striped
@ -136,19 +122,19 @@ block content
th.col-xs-2.hidden-xs Solution th.col-xs-2.hidden-xs Solution
for challenge in algorithms for challenge in algorithms
tr tr
td.col-xs-5.hidden-xs= removeOldTerms(challenge.name) td.col-xs-5.hidden-xs= challenge.name
td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available' td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available'
td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : '' td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : ''
td.col-xs-2.hidden-xs td.col-xs-2.hidden-xs
if (challenge.solution) if (challenge.solution)
a(href='/challenges/' + removeOldTerms(challenge.name) + '?solution=' + encodeURIComponent(encodeFcc(challenge.solution)), target='_blank') View solution a(href='/challenges/#{challenge.block}/#{challenge.dashedName}?solution=#{encodeURIComponent(encodeFcc(challenge.solution))}', target='_blank') View solution
else else
a(href='/challenges/' + removeOldTerms(challenge.name)) View this challenge a(href='/challenges/#{challenge.block}/#{challenge.dashedName}') View this challenge
td.col-xs-12.visible-xs td.col-xs-12.visible-xs
if (challenge.solution) if (challenge.solution)
a(href='/challenges/' + removeOldTerms(challenge.name) + '?solution=' + encodeURIComponent(encodeFcc(challenge.solution)), target='_blank')= removeOldTerms(challenge.name) a(href='/challenges/#{challenge.block}/#{challenge.dashedName}?solution=#{encodeURIComponent(encodeFcc(challenge.solution))}', target='_blank')= challenge.name
else else
a(href='/challenges/' + removeOldTerms(challenge.name))= removeOldTerms(challenge.name) a(href='/challenges/#{challenge.block}/#{challenge.dashedName}')= challenge.name
if (challenges.length > 0) if (challenges.length > 0)
.col-sm-12 .col-sm-12
table.table.table-striped table.table.table-striped
@ -160,20 +146,20 @@ block content
th.col-xs-2.hidden-xs Solution th.col-xs-2.hidden-xs Solution
for challenge in challenges for challenge in challenges
tr tr
td.col-xs-5.hidden-xs= removeOldTerms(challenge.name) td.col-xs-5.hidden-xs= challenge.name
td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available' td.col-xs-2.hidden-xs= challenge.completedDate ? challenge.completedDate : 'Not Available'
td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : '' td.col-xs-2.hidden-xs= challenge.lastUpdated ? challenge.lastUpdated : ''
td.col-xs-2.hidden-xs td.col-xs-2.hidden-xs
if (challenge.solution && challenge.name) if (challenge.solution && challenge.name)
a(href='/challenges/' + removeOldTerms(challenge.name) + '?solution=' + encodeURIComponent(encodeFcc(challenge.solution)), target='_blank') View solution a(href='/challenges/#{challenge.block}/#{challenge.dashedName}?solution=#{encodeURIComponent(encodeFcc(challenge.solution))}', target='_blank') View solution
else if (challenge.name) else if (challenge.name)
a(href='/challenges/' + removeOldTerms(challenge.name)) View this challenge a(href='/challenges/#{challenge.block}/#{challenge.dashedName}') View this challenge
else else
span N/A span N/A
td.col-xs-12.visible-xs td.col-xs-12.visible-xs
if (challenge.solution && challenge.name) if (challenge.solution && challenge.name)
a(href='/challenges/' + removeOldTerms(challenge.name) + '?solution=' + encodeURIComponent(encodeFcc(challenge.solution)), target='_blank')= removeOldTerms(challenge.name) a(href='/challenges/#{challenge.block}/#{challenge.dashedName}?solution=#{encodeURIComponent(encodeFcc(challenge.solution))}', target='_blank')= challenge.name
else if (challenge.name) else if (challenge.name)
a(href='/challenges/' + removeOldTerms(challenge.name))= removeOldTerms(challenge.name) a(href='/challenges/#{challenge.block}/#{challenge.dashedName}')= challenge.name
else else
span N/A span N/A