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,
createErrorObservable
} from '../../../redux/actions';
import createNameIdMap from '../../../../utils/create-name-id-map';
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 }) {
return action$
.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 debugFactory from 'debug';
import supportedLanguages from '../../common/utils/supported-languages';
import {
frontEndChallengeId,
dataVisChallengeId,
backEndChallengeId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import {
ifNoUser401,
ifNoUserRedirectTo
@ -22,6 +19,9 @@ import {
calcCurrentStreak,
calcLongestStreak
} 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 sendNonUserToMap = ifNoUserRedirectTo('/map');
@ -85,25 +85,35 @@ function getChallengeGroup(challenge) {
return 'challenges';
}
// buildDisplayChallenges(challengeMap: Object, tz: String) => Observable[{
// buildDisplayChallenges(
// entities: { challenge: Object, challengeIdToName: Object },
// challengeMap: Object,
// tz: String
// ) => Observable[{
// algorithms: Array,
// projects: Array,
// challenges: Array
// }]
function buildDisplayChallenges(challengeMap = {}, timezone) {
return Observable.from(Object.keys(challengeMap))
.map(challengeId => challengeMap[challengeId])
.map(challenge => {
let finalChallenge = { ...challenge };
if (challenge.completedDate) {
function buildDisplayChallenges(
{ challenge: challengeMap = {}, challengeIdToName },
userChallengeMap = {},
timezone
) {
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
.tz(challenge.completedDate, timezone)
.tz(userChallenge.completedDate, timezone)
.format(dateFormat);
}
if (challenge.lastUpdated) {
if (userChallenge.lastUpdated) {
finalChallenge.lastUpdated = moment
.tz(challenge.lastUpdated, timezone)
.tz(userChallenge.lastUpdated, timezone)
.format(dateFormat);
}
@ -128,6 +138,8 @@ module.exports = function(app) {
const router = app.loopback.Router();
const api = app.loopback.Router();
const User = app.models.User;
const Block = app.models.Block;
const map$ = cachedMap(Block);
function findUserByUsername$(username, fields) {
return observeQuery(
User,
@ -187,7 +199,7 @@ module.exports = function(app) {
(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(api);
@ -248,7 +260,7 @@ module.exports = function(app) {
return res.redirect('/' + username);
}
function returnUser(req, res, next) {
function showUserProfile(req, res, next) {
const username = req.params.username.toLowerCase();
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 => ({
...userPortfolio,
...displayChallenges,

View File

@ -1,134 +1,12 @@
import _ from 'lodash';
import { Observable } from 'rx';
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
import debug from 'debug';
import { nameify, unDasherize } from '../utils';
import supportedLanguages from '../../common/utils/supported-languages';
import { unDasherize } from '../utils';
import { mapChallengeToLang, cachedMap, getMapForLang } from '../utils/map';
const isDev = process.env.NODE_ENV !== 'production';
const isBeta = !!process.env.BETA;
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const log = debug('fcc:services:challenges');
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 };
};
}
const log = debug('fcc:services:map');
function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) {
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 () {
var cal = new CalHeatMap();
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({
itemSelector: "#cal-heatmap",
domain: "month",
@ -118,13 +104,13 @@ block content
for challenge in projects
tr
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.lastUpdated ? challenge.lastUpdated : ''
td.col-xs-2.hidden-xs
a(href=challenge.solution, target='_blank') View project
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)
.col-sm-12
table.table.table-striped
@ -136,19 +122,19 @@ block content
th.col-xs-2.hidden-xs Solution
for challenge in algorithms
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.lastUpdated ? challenge.lastUpdated : ''
td.col-xs-2.hidden-xs
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
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
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
a(href='/challenges/' + removeOldTerms(challenge.name))= removeOldTerms(challenge.name)
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}')= challenge.name
if (challenges.length > 0)
.col-sm-12
table.table.table-striped
@ -160,20 +146,20 @@ block content
th.col-xs-2.hidden-xs Solution
for challenge in challenges
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.lastUpdated ? challenge.lastUpdated : ''
td.col-xs-2.hidden-xs
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)
a(href='/challenges/' + removeOldTerms(challenge.name)) View this challenge
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}') View this challenge
else
span N/A
td.col-xs-12.visible-xs
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)
a(href='/challenges/' + removeOldTerms(challenge.name))= removeOldTerms(challenge.name)
a(href='/challenges/#{challenge.block}/#{challenge.dashedName}')= challenge.name
else
span N/A