Merge pull request #10041 from BerkeleyTrue/regression/add-current-challenge
Fix(accounts): show challenge info on user profile
This commit is contained in:
@ -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 }) => (
|
||||
|
12
common/utils/create-name-id-map.js
Normal file
12
common/utils/create-name-id-map.js
Normal 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;
|
||||
}, {})
|
||||
};
|
||||
}
|
@ -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,
|
||||
|
@ -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
136
server/utils/map.js
Normal 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 };
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user