Merge pull request #10041 from BerkeleyTrue/regression/add-current-challenge

Fix(accounts): show challenge info on user profile
This commit is contained in:
Mrugesh Mohapatra
2016-08-02 22:35:55 +05:30
committed by GitHub
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