2015-08-10 05:14:31 +00:00
|
|
|
import _ from 'lodash';
|
2015-08-29 08:48:01 +00:00
|
|
|
import dedent from 'dedent';
|
2015-08-10 05:14:31 +00:00
|
|
|
import moment from 'moment';
|
2015-08-20 06:11:21 +00:00
|
|
|
import { Observable, Scheduler } from 'rx';
|
2016-02-09 22:33:25 +00:00
|
|
|
import debug from 'debug';
|
2016-01-10 04:08:01 +00:00
|
|
|
import accepts from 'accepts';
|
2016-02-15 01:10:26 +00:00
|
|
|
import { isMongoId } from 'validator';
|
2015-11-10 01:27:56 +00:00
|
|
|
|
|
|
|
import {
|
|
|
|
dasherize,
|
|
|
|
unDasherize,
|
|
|
|
getMDNLinks,
|
|
|
|
randomVerb,
|
|
|
|
randomPhrase,
|
|
|
|
randomCompliment
|
|
|
|
} from '../utils';
|
2015-08-10 05:14:31 +00:00
|
|
|
|
2016-02-09 22:33:25 +00:00
|
|
|
import { observeMethod } from '../utils/rx';
|
2015-08-10 05:14:31 +00:00
|
|
|
|
|
|
|
import {
|
2016-05-07 12:16:39 +00:00
|
|
|
ifNoUserSend,
|
|
|
|
flashIfNotVerified
|
2015-08-10 05:14:31 +00:00
|
|
|
} from '../utils/middleware';
|
|
|
|
|
2015-11-10 01:27:56 +00:00
|
|
|
import getFromDisk$ from '../utils/getFromDisk$';
|
2016-02-15 01:10:26 +00:00
|
|
|
import badIdMap from '../utils/bad-id-map';
|
2015-11-10 01:27:56 +00:00
|
|
|
|
2015-10-07 05:37:08 +00:00
|
|
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
|
|
const isBeta = !!process.env.BETA;
|
2016-01-27 19:34:44 +00:00
|
|
|
const log = debug('fcc:challenges');
|
2016-01-02 05:10:08 +00:00
|
|
|
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
|
2015-08-20 06:11:21 +00:00
|
|
|
const challengeView = {
|
2016-01-12 06:47:49 +00:00
|
|
|
0: 'challenges/showHTML',
|
|
|
|
1: 'challenges/showJS',
|
|
|
|
2: 'challenges/showVideo',
|
|
|
|
3: 'challenges/showZiplineOrBasejump',
|
|
|
|
4: 'challenges/showZiplineOrBasejump',
|
|
|
|
5: 'challenges/showBonfire',
|
|
|
|
7: 'challenges/showStep'
|
2015-08-20 06:11:21 +00:00
|
|
|
};
|
2015-06-20 20:35:26 +00:00
|
|
|
|
2015-12-10 00:16:47 +00:00
|
|
|
function isChallengeCompleted(user, challengeId) {
|
|
|
|
if (!user) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-02-10 18:35:40 +00:00
|
|
|
return !!user.challengeMap[challengeId];
|
2015-12-10 00:16:47 +00:00
|
|
|
}
|
|
|
|
|
2016-01-12 06:47:49 +00:00
|
|
|
/*
|
2015-08-10 05:14:31 +00:00
|
|
|
function numberWithCommas(x) {
|
|
|
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
|
|
}
|
2016-01-12 06:47:49 +00:00
|
|
|
*/
|
2015-08-10 05:14:31 +00:00
|
|
|
|
2016-02-09 22:33:25 +00:00
|
|
|
function buildUserUpdate(
|
|
|
|
user,
|
|
|
|
challengeId,
|
|
|
|
completedChallenge,
|
|
|
|
timezone
|
|
|
|
) {
|
|
|
|
const updateData = { $set: {} };
|
|
|
|
let finalChallenge;
|
|
|
|
const { timezone: userTimezone, challengeMap = {} } = user;
|
|
|
|
|
|
|
|
const oldChallenge = challengeMap[challengeId];
|
|
|
|
const alreadyCompleted = !!oldChallenge;
|
|
|
|
|
|
|
|
|
|
|
|
if (alreadyCompleted) {
|
|
|
|
// add data from old challenge
|
|
|
|
finalChallenge = {
|
|
|
|
...completedChallenge,
|
|
|
|
completedDate: oldChallenge.completedDate,
|
|
|
|
lastUpdated: completedChallenge.completedDate
|
|
|
|
};
|
2016-02-10 20:01:00 +00:00
|
|
|
} else {
|
2016-02-09 22:33:25 +00:00
|
|
|
updateData.$push = {
|
2016-02-10 20:01:00 +00:00
|
|
|
progressTimestamps: {
|
|
|
|
timestamp: Date.now(),
|
|
|
|
completedChallenge: challengeId
|
|
|
|
}
|
2016-02-09 22:33:25 +00:00
|
|
|
};
|
|
|
|
finalChallenge = completedChallenge;
|
2015-06-21 02:52:37 +00:00
|
|
|
}
|
2015-10-02 04:44:24 +00:00
|
|
|
|
2016-02-09 22:33:25 +00:00
|
|
|
updateData.$set = {
|
|
|
|
[`challengeMap.${challengeId}`]: finalChallenge
|
|
|
|
};
|
|
|
|
|
|
|
|
if (
|
2016-02-10 18:05:51 +00:00
|
|
|
timezone &&
|
2016-02-09 22:33:25 +00:00
|
|
|
timezone !== 'UTC' &&
|
|
|
|
(!userTimezone || userTimezone === 'UTC')
|
|
|
|
) {
|
|
|
|
updateData.$set = {
|
|
|
|
...updateData.$set,
|
|
|
|
timezone: userTimezone
|
|
|
|
};
|
|
|
|
}
|
2016-01-10 04:08:01 +00:00
|
|
|
|
2016-02-09 22:33:25 +00:00
|
|
|
log('user update data', updateData);
|
|
|
|
|
|
|
|
return { alreadyCompleted, updateData };
|
2015-06-21 02:52:37 +00:00
|
|
|
}
|
|
|
|
|
2015-12-05 06:13:14 +00:00
|
|
|
|
|
|
|
// small helper function to determine whether to mark something as new
|
|
|
|
const dateFormat = 'MMM MMMM DD, YYYY';
|
|
|
|
function shouldShowNew(element, block) {
|
|
|
|
if (element) {
|
|
|
|
return typeof element.releasedOn !== 'undefined' &&
|
2016-01-01 06:34:05 +00:00
|
|
|
moment(element.releasedOn, dateFormat).diff(moment(), 'days') >= -60;
|
2015-12-05 06:13:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (block) {
|
|
|
|
const newCount = block.reduce((sum, { markNew }) => {
|
|
|
|
if (markNew) {
|
|
|
|
return sum + 1;
|
|
|
|
}
|
|
|
|
return sum;
|
|
|
|
}, 0);
|
|
|
|
return newCount / block.length * 100 === 100;
|
|
|
|
}
|
2016-03-03 04:54:14 +00:00
|
|
|
return null;
|
2015-12-05 06:13:14 +00:00
|
|
|
}
|
|
|
|
|
2015-12-14 21:18:54 +00:00
|
|
|
// meant to be used with a filter method
|
|
|
|
// on an array or observable stream
|
|
|
|
// true if challenge should be passed through
|
|
|
|
// false if should filter challenge out of array or stream
|
|
|
|
function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) {
|
|
|
|
return isDev ||
|
|
|
|
!isComingSoon ||
|
|
|
|
(isBeta && challengeIsBeta);
|
|
|
|
}
|
|
|
|
|
2016-01-12 06:47:49 +00:00
|
|
|
function getRenderData$(user, challenge$, origChallengeName, solution) {
|
|
|
|
const challengeName = unDasherize(origChallengeName)
|
|
|
|
.replace(challengesRegex, '');
|
|
|
|
|
|
|
|
const testChallengeName = new RegExp(challengeName, 'i');
|
2016-02-09 22:33:25 +00:00
|
|
|
log('looking for %s', testChallengeName);
|
2016-01-12 06:47:49 +00:00
|
|
|
|
|
|
|
return challenge$
|
2016-01-15 09:44:18 +00:00
|
|
|
.map(challenge => challenge.toJSON())
|
2016-04-07 04:08:19 +00:00
|
|
|
.filter(challenge => {
|
2016-02-15 01:10:26 +00:00
|
|
|
return shouldNotFilterComingSoon(challenge) &&
|
|
|
|
challenge.type !== 'hike' &&
|
|
|
|
testChallengeName.test(challenge.name);
|
2016-01-12 06:47:49 +00:00
|
|
|
})
|
|
|
|
.last({ defaultValue: null })
|
|
|
|
.flatMap(challenge => {
|
|
|
|
if (challenge && isDev) {
|
|
|
|
return getFromDisk$(challenge);
|
|
|
|
}
|
|
|
|
return Observable.just(challenge);
|
|
|
|
})
|
|
|
|
.flatMap(challenge => {
|
|
|
|
|
|
|
|
// Handle not found
|
|
|
|
if (!challenge) {
|
2016-02-09 22:33:25 +00:00
|
|
|
log('did not find challenge for ' + origChallengeName);
|
2016-01-12 06:47:49 +00:00
|
|
|
return Observable.just({
|
|
|
|
type: 'redirect',
|
|
|
|
redirectUrl: '/map',
|
|
|
|
message: dedent`
|
2016-01-15 14:08:54 +00:00
|
|
|
We couldn't find a challenge with the name ${origChallengeName}.
|
2016-01-12 06:47:49 +00:00
|
|
|
Please double check the name.
|
|
|
|
`
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dasherize(challenge.name) !== origChallengeName) {
|
|
|
|
let redirectUrl = `/challenges/${dasherize(challenge.name)}`;
|
|
|
|
|
|
|
|
if (solution) {
|
|
|
|
redirectUrl += `?solution=${encodeURIComponent(solution)}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Observable.just({
|
|
|
|
type: 'redirect',
|
|
|
|
redirectUrl
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// save user does nothing if user does not exist
|
|
|
|
return Observable.just({
|
|
|
|
data: {
|
|
|
|
...challenge,
|
|
|
|
// identifies if a challenge is completed
|
|
|
|
isCompleted: isChallengeCompleted(user, challenge.id),
|
|
|
|
|
|
|
|
// video challenges
|
|
|
|
video: challenge.challengeSeed[0],
|
|
|
|
|
|
|
|
// bonfires specific
|
|
|
|
bonfires: challenge,
|
|
|
|
MDNkeys: challenge.MDNlinks,
|
|
|
|
MDNlinks: getMDNLinks(challenge.MDNlinks),
|
|
|
|
|
|
|
|
// htmls specific
|
|
|
|
verb: randomVerb(),
|
|
|
|
phrase: randomPhrase(),
|
2016-01-30 08:13:41 +00:00
|
|
|
compliment: randomCompliment(),
|
|
|
|
|
|
|
|
// Google Analytics
|
|
|
|
gaName: challenge.title + '~' + challenge.checksum
|
2016-01-12 06:47:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// create a stream of an array of all the challenge blocks
|
2016-02-09 22:33:25 +00:00
|
|
|
function getSuperBlocks$(challenge$, challengeMap) {
|
2016-01-12 06:47:49 +00:00
|
|
|
return challenge$
|
|
|
|
// mark challenge completed
|
|
|
|
.map(challengeModel => {
|
|
|
|
const challenge = challengeModel.toJSON();
|
2016-02-09 22:33:25 +00:00
|
|
|
challenge.completed = !!challengeMap[challenge.id];
|
2016-01-12 06:47:49 +00:00
|
|
|
challenge.markNew = shouldShowNew(challenge);
|
2016-02-09 22:33:25 +00:00
|
|
|
|
|
|
|
if (challenge.type === 'hike') {
|
|
|
|
challenge.url = '/videos/' + challenge.dashedName;
|
|
|
|
} else {
|
|
|
|
challenge.url = '/challenges/' + challenge.dashedName;
|
|
|
|
}
|
|
|
|
|
2016-01-12 06:47:49 +00:00
|
|
|
return challenge;
|
|
|
|
})
|
|
|
|
// group challenges by block | returns a stream of observables
|
|
|
|
.groupBy(challenge => challenge.block)
|
|
|
|
// turn block group stream into an array
|
|
|
|
.flatMap(block$ => block$.toArray())
|
|
|
|
.map(blockArray => {
|
|
|
|
const completedCount = blockArray.reduce((sum, { completed }) => {
|
|
|
|
if (completed) {
|
|
|
|
return sum + 1;
|
|
|
|
}
|
|
|
|
return sum;
|
|
|
|
}, 0);
|
|
|
|
const isBeta = _.every(blockArray, 'isBeta');
|
|
|
|
const isComingSoon = _.every(blockArray, 'isComingSoon');
|
2016-01-13 08:27:08 +00:00
|
|
|
const isRequired = _.every(blockArray, 'isRequired');
|
2016-01-12 06:47:49 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
isBeta,
|
|
|
|
isComingSoon,
|
2016-01-13 22:59:51 +00:00
|
|
|
isRequired,
|
2016-01-12 06:47:49 +00:00
|
|
|
name: blockArray[0].block,
|
|
|
|
superBlock: blockArray[0].superBlock,
|
|
|
|
dashedName: dasherize(blockArray[0].block),
|
|
|
|
markNew: shouldShowNew(null, blockArray),
|
|
|
|
challenges: blockArray,
|
|
|
|
completed: completedCount / blockArray.length * 100,
|
|
|
|
time: blockArray[0] && blockArray[0].time || '???'
|
|
|
|
};
|
|
|
|
})
|
|
|
|
.toArray()
|
|
|
|
.flatMap(blocks => Observable.from(blocks, null, null, Scheduler.default))
|
|
|
|
.groupBy(block => block.superBlock)
|
|
|
|
.flatMap(blocks$ => blocks$.toArray())
|
|
|
|
.map(superBlockArray => ({
|
|
|
|
name: superBlockArray[0].superBlock,
|
|
|
|
blocks: superBlockArray
|
|
|
|
}))
|
|
|
|
.toArray();
|
|
|
|
}
|
|
|
|
|
2016-01-14 23:15:44 +00:00
|
|
|
function getChallengeById$(challenge$, challengeId) {
|
|
|
|
// return first challenge if no id is given
|
|
|
|
if (!challengeId) {
|
|
|
|
return challenge$
|
|
|
|
.map(challenge => challenge.toJSON())
|
|
|
|
.filter(shouldNotFilterComingSoon)
|
|
|
|
// filter out hikes
|
2016-02-15 01:10:26 +00:00
|
|
|
.filter(({ superBlock }) => !(/^videos/gi).test(superBlock))
|
2016-01-14 23:15:44 +00:00
|
|
|
.first();
|
|
|
|
}
|
|
|
|
return challenge$
|
|
|
|
.map(challenge => challenge.toJSON())
|
|
|
|
// filter out challenges coming soon
|
|
|
|
.filter(shouldNotFilterComingSoon)
|
|
|
|
// filter out hikes
|
2016-02-15 01:10:26 +00:00
|
|
|
.filter(({ superBlock }) => !(/^videos/gi).test(superBlock))
|
2016-01-14 23:15:44 +00:00
|
|
|
.filter(({ id }) => id === challengeId);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getNextChallenge$(challenge$, blocks$, challengeId) {
|
|
|
|
return getChallengeById$(challenge$, challengeId)
|
|
|
|
// now lets find the block it belongs to
|
|
|
|
.flatMap(challenge => {
|
|
|
|
// find the index of the block this challenge resides in
|
|
|
|
const blockIndex$ = blocks$
|
|
|
|
.findIndex(({ name }) => name === challenge.block);
|
|
|
|
|
|
|
|
|
|
|
|
return blockIndex$
|
|
|
|
.flatMap(blockIndex => {
|
|
|
|
// could not find block?
|
|
|
|
if (blockIndex === -1) {
|
|
|
|
return Observable.throw(
|
|
|
|
'could not find challenge block for ' + challenge.block
|
|
|
|
);
|
|
|
|
}
|
|
|
|
const firstChallengeOfNextBlock$ = blocks$
|
|
|
|
.elementAt(blockIndex + 1, {})
|
|
|
|
.map(({ challenges = [] }) => challenges[0]);
|
|
|
|
|
|
|
|
return blocks$
|
|
|
|
.filter(shouldNotFilterComingSoon)
|
|
|
|
.elementAt(blockIndex)
|
|
|
|
.flatMap(block => {
|
|
|
|
// find where our challenge lies in the block
|
|
|
|
const challengeIndex$ = Observable.from(
|
|
|
|
block.challenges,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
Scheduler.default
|
|
|
|
)
|
|
|
|
.findIndex(({ id }) => id === challengeId);
|
|
|
|
|
|
|
|
// grab next challenge in this block
|
|
|
|
return challengeIndex$
|
|
|
|
.map(index => {
|
|
|
|
return block.challenges[index + 1];
|
|
|
|
})
|
|
|
|
.flatMap(nextChallenge => {
|
|
|
|
if (!nextChallenge) {
|
|
|
|
return firstChallengeOfNextBlock$;
|
|
|
|
}
|
|
|
|
return Observable.just(nextChallenge);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.first();
|
|
|
|
}
|
|
|
|
|
2015-06-03 02:02:54 +00:00
|
|
|
module.exports = function(app) {
|
2015-08-10 05:14:31 +00:00
|
|
|
const router = app.loopback.Router();
|
|
|
|
|
2015-08-12 18:44:29 +00:00
|
|
|
const challengesQuery = {
|
|
|
|
order: [
|
2015-12-07 05:44:34 +00:00
|
|
|
'superOrder ASC',
|
2015-08-12 18:44:29 +00:00
|
|
|
'order ASC',
|
|
|
|
'suborder ASC'
|
|
|
|
]
|
|
|
|
};
|
|
|
|
|
|
|
|
// challenge model
|
2015-08-10 05:14:31 +00:00
|
|
|
const Challenge = app.models.Challenge;
|
2015-08-12 18:44:29 +00:00
|
|
|
// challenge find query stream
|
2015-08-10 05:14:31 +00:00
|
|
|
const findChallenge$ = observeMethod(Challenge, 'find');
|
2015-08-12 18:44:29 +00:00
|
|
|
// create a stream of all the challenges
|
|
|
|
const challenge$ = findChallenge$(challengesQuery)
|
2015-08-20 06:11:21 +00:00
|
|
|
.flatMap(challenges => Observable.from(
|
|
|
|
challenges,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
Scheduler.default
|
|
|
|
))
|
2015-10-07 05:37:08 +00:00
|
|
|
// filter out all challenges that have isBeta flag set
|
|
|
|
// except in development or beta site
|
|
|
|
.filter(challenge => isDev || isBeta || !challenge.isBeta)
|
2015-08-12 18:44:29 +00:00
|
|
|
.shareReplay();
|
|
|
|
|
|
|
|
// create a stream of challenge blocks
|
2015-12-11 23:45:48 +00:00
|
|
|
const blocks$ = challenge$
|
2015-08-16 21:19:30 +00:00
|
|
|
.map(challenge => challenge.toJSON())
|
2015-12-14 21:18:54 +00:00
|
|
|
.filter(shouldNotFilterComingSoon)
|
2015-08-12 18:44:29 +00:00
|
|
|
// group challenges by block | returns a stream of observables
|
|
|
|
.groupBy(challenge => challenge.block)
|
|
|
|
// turn block group stream into an array
|
2015-12-05 06:13:14 +00:00
|
|
|
.flatMap(blocks$ => blocks$.toArray())
|
2015-08-12 18:44:29 +00:00
|
|
|
// turn array into stream of object
|
2015-12-11 23:45:48 +00:00
|
|
|
.map(blocksArray => ({
|
|
|
|
name: blocksArray[0].block,
|
|
|
|
dashedName: dasherize(blocksArray[0].block),
|
|
|
|
challenges: blocksArray,
|
|
|
|
superBlock: blocksArray[0].superBlock,
|
|
|
|
order: blocksArray[0].order
|
2015-08-13 03:39:40 +00:00
|
|
|
}))
|
2015-12-11 22:44:27 +00:00
|
|
|
// filter out hikes
|
2015-12-05 06:37:15 +00:00
|
|
|
.filter(({ superBlock }) => {
|
2016-02-15 01:10:26 +00:00
|
|
|
return !(/^videos/gi).test(superBlock);
|
2015-08-13 03:39:40 +00:00
|
|
|
})
|
|
|
|
.shareReplay();
|
2015-08-10 05:14:31 +00:00
|
|
|
|
2016-01-14 23:15:44 +00:00
|
|
|
const firstChallenge$ = challenge$
|
|
|
|
.first()
|
|
|
|
.map(challenge => challenge.toJSON())
|
|
|
|
.shareReplay();
|
|
|
|
|
|
|
|
const lastChallenge$ = challenge$
|
|
|
|
.last()
|
|
|
|
.map(challenge => challenge.toJSON())
|
|
|
|
.shareReplay();
|
|
|
|
|
2015-08-10 05:14:31 +00:00
|
|
|
const send200toNonUser = ifNoUserSend(true);
|
2015-06-22 23:43:31 +00:00
|
|
|
|
|
|
|
router.post(
|
|
|
|
'/completed-challenge/',
|
|
|
|
send200toNonUser,
|
|
|
|
completedChallenge
|
|
|
|
);
|
|
|
|
router.post(
|
|
|
|
'/completed-zipline-or-basejump',
|
|
|
|
send200toNonUser,
|
|
|
|
completedZiplineOrBasejump
|
|
|
|
);
|
2015-06-03 02:02:54 +00:00
|
|
|
|
2016-01-13 22:59:51 +00:00
|
|
|
router.get('/map', showMap.bind(null, false));
|
2016-01-15 00:03:55 +00:00
|
|
|
router.get('/map-aside', showMap.bind(null, true));
|
2016-01-14 23:15:44 +00:00
|
|
|
router.get(
|
|
|
|
'/challenges/current-challenge',
|
|
|
|
redirectToCurrentChallenge
|
|
|
|
);
|
2015-06-20 18:43:12 +00:00
|
|
|
router.get(
|
|
|
|
'/challenges/next-challenge',
|
2016-01-14 23:15:44 +00:00
|
|
|
redirectToNextChallenge
|
2015-06-20 18:43:12 +00:00
|
|
|
);
|
|
|
|
|
2016-05-07 12:16:39 +00:00
|
|
|
router.get('/challenges/:challengeName',
|
|
|
|
flashIfNotVerified,
|
|
|
|
showChallenge
|
|
|
|
);
|
2015-06-20 18:43:12 +00:00
|
|
|
|
2015-06-03 23:31:42 +00:00
|
|
|
app.use(router);
|
|
|
|
|
2016-01-14 23:15:44 +00:00
|
|
|
function redirectToCurrentChallenge(req, res, next) {
|
2016-01-15 09:44:18 +00:00
|
|
|
let challengeId = req.query.id || req.cookies.currentChallengeId;
|
|
|
|
// prevent serialized null/undefined from breaking things
|
2016-02-15 01:10:26 +00:00
|
|
|
|
|
|
|
if (badIdMap[challengeId]) {
|
|
|
|
challengeId = badIdMap[challengeId];
|
|
|
|
}
|
|
|
|
|
2016-03-14 01:04:24 +00:00
|
|
|
if (!isMongoId('' + challengeId)) {
|
2016-01-15 09:44:18 +00:00
|
|
|
challengeId = null;
|
|
|
|
}
|
2016-02-15 01:10:26 +00:00
|
|
|
|
2016-01-14 23:15:44 +00:00
|
|
|
getChallengeById$(challenge$, challengeId)
|
|
|
|
.doOnNext(({ dashedName })=> {
|
|
|
|
if (!dashedName) {
|
2016-02-09 22:33:25 +00:00
|
|
|
log('no challenge found for %s', challengeId);
|
2016-01-14 23:15:44 +00:00
|
|
|
req.flash('info', {
|
|
|
|
msg: `We coudn't find a challenge with the id ${challengeId}`
|
2015-08-12 18:44:29 +00:00
|
|
|
});
|
2016-01-14 23:15:44 +00:00
|
|
|
res.redirect('/map');
|
2015-10-05 23:38:58 +00:00
|
|
|
}
|
2016-01-14 23:15:44 +00:00
|
|
|
res.redirect('/challenges/' + dashedName);
|
2015-08-12 18:44:29 +00:00
|
|
|
})
|
2016-01-14 23:15:44 +00:00
|
|
|
.subscribe(() => {}, next);
|
|
|
|
}
|
|
|
|
|
|
|
|
function redirectToNextChallenge(req, res, next) {
|
2016-01-15 09:44:18 +00:00
|
|
|
let challengeId = req.query.id || req.cookies.currentChallengeId;
|
2016-02-15 01:10:26 +00:00
|
|
|
|
|
|
|
if (badIdMap[challengeId]) {
|
|
|
|
challengeId = badIdMap[challengeId];
|
|
|
|
}
|
|
|
|
|
2016-03-14 01:04:24 +00:00
|
|
|
if (!isMongoId('' + challengeId)) {
|
2016-01-15 09:44:18 +00:00
|
|
|
challengeId = null;
|
|
|
|
}
|
2016-01-14 23:15:44 +00:00
|
|
|
|
|
|
|
Observable.combineLatest(
|
|
|
|
firstChallenge$,
|
2016-01-15 07:06:06 +00:00
|
|
|
lastChallenge$
|
2016-01-14 23:15:44 +00:00
|
|
|
)
|
|
|
|
.flatMap(([firstChallenge, { id: lastChallengeId } ]) => {
|
|
|
|
// no id supplied, load first challenge
|
|
|
|
if (!challengeId) {
|
|
|
|
return Observable.just(firstChallenge);
|
|
|
|
}
|
|
|
|
// camper just completed last challenge
|
|
|
|
if (challengeId === lastChallengeId) {
|
|
|
|
return Observable.just()
|
|
|
|
.doOnCompleted(() => {
|
|
|
|
req.flash('info', {
|
2016-05-03 07:45:34 +00:00
|
|
|
msg: 'You\'ve completed the last challenge!'
|
2016-01-14 23:15:44 +00:00
|
|
|
});
|
|
|
|
return res.redirect('/map');
|
2015-08-12 18:44:29 +00:00
|
|
|
});
|
2015-06-20 18:43:12 +00:00
|
|
|
}
|
2016-01-14 23:15:44 +00:00
|
|
|
|
2016-02-15 01:10:26 +00:00
|
|
|
return getNextChallenge$(challenge$, blocks$, challengeId);
|
|
|
|
})
|
|
|
|
.doOnNext(({ dashedName } = {}) => {
|
|
|
|
if (!dashedName) {
|
|
|
|
log('no challenge found for %s', challengeId);
|
|
|
|
res.redirect('/map');
|
|
|
|
}
|
|
|
|
res.redirect('/challenges/' + dashedName);
|
2016-01-14 23:15:44 +00:00
|
|
|
})
|
|
|
|
.subscribe(() => {}, next);
|
2015-05-16 04:39:43 +00:00
|
|
|
}
|
|
|
|
|
2016-01-12 06:47:49 +00:00
|
|
|
function showChallenge(req, res, next) {
|
|
|
|
const solution = req.query.solution;
|
2016-01-15 14:08:54 +00:00
|
|
|
const challengeName = req.params.challengeName.replace(challengesRegex, '');
|
2016-04-07 04:08:19 +00:00
|
|
|
const { user } = req;
|
2016-01-13 06:26:19 +00:00
|
|
|
|
2016-04-07 04:08:19 +00:00
|
|
|
Observable.defer(() => {
|
|
|
|
if (user && user.getChallengeMap$) {
|
|
|
|
return user.getChallengeMap$().map(user);
|
|
|
|
}
|
|
|
|
return Observable.just(null);
|
|
|
|
})
|
|
|
|
.flatMap(user => {
|
|
|
|
return getRenderData$(user, challenge$, challengeName, solution);
|
|
|
|
})
|
2015-08-20 06:11:21 +00:00
|
|
|
.subscribe(
|
2016-01-12 06:47:49 +00:00
|
|
|
({ type, redirectUrl, message, data }) => {
|
|
|
|
if (message) {
|
|
|
|
req.flash('info', {
|
|
|
|
msg: message
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (type === 'redirect') {
|
2016-02-09 22:33:25 +00:00
|
|
|
log('redirecting to %s', redirectUrl);
|
2016-01-12 06:47:49 +00:00
|
|
|
return res.redirect(redirectUrl);
|
2015-08-20 06:11:21 +00:00
|
|
|
}
|
|
|
|
var view = challengeView[data.challengeType];
|
2016-01-15 09:44:18 +00:00
|
|
|
if (data.id) {
|
2016-03-15 19:53:54 +00:00
|
|
|
res.cookie('currentChallengeId', data.id, {
|
|
|
|
expires: new Date(2147483647000)});
|
2016-01-15 09:44:18 +00:00
|
|
|
}
|
2016-03-03 04:54:14 +00:00
|
|
|
return res.render(view, data);
|
2015-08-20 06:11:21 +00:00
|
|
|
},
|
|
|
|
next,
|
|
|
|
function() {}
|
|
|
|
);
|
2015-06-03 02:02:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function completedChallenge(req, res, next) {
|
2016-03-31 03:58:38 +00:00
|
|
|
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
2016-02-11 06:10:06 +00:00
|
|
|
req.checkBody('name', 'name must be at least 3 characters')
|
|
|
|
.isString()
|
|
|
|
.isLength({ min: 3 });
|
|
|
|
req.checkBody('challengeType', 'challengeType must be an integer')
|
2016-03-03 05:53:42 +00:00
|
|
|
.isNumber();
|
|
|
|
|
2016-01-10 04:08:01 +00:00
|
|
|
const type = accepts(req).type('html', 'json', 'text');
|
2015-06-03 02:02:54 +00:00
|
|
|
|
2016-02-11 06:10:06 +00:00
|
|
|
const errors = req.validationErrors(true);
|
|
|
|
|
|
|
|
if (errors) {
|
|
|
|
if (type === 'json') {
|
|
|
|
return res.status(403).send({ errors });
|
|
|
|
}
|
|
|
|
|
|
|
|
log('errors', errors);
|
|
|
|
return res.sendStatus(403);
|
|
|
|
}
|
|
|
|
|
2016-04-07 04:08:19 +00:00
|
|
|
return req.user.getChallengeMap$()
|
|
|
|
.flatMap(() => {
|
|
|
|
const completedDate = Date.now();
|
|
|
|
const {
|
|
|
|
id,
|
|
|
|
name,
|
|
|
|
challengeType,
|
|
|
|
solution,
|
|
|
|
timezone
|
|
|
|
} = req.body;
|
|
|
|
|
|
|
|
const { alreadyCompleted, updateData } = buildUserUpdate(
|
|
|
|
req.user,
|
|
|
|
id,
|
|
|
|
{
|
|
|
|
id,
|
|
|
|
challengeType,
|
|
|
|
solution,
|
|
|
|
name,
|
|
|
|
completedDate
|
|
|
|
},
|
|
|
|
timezone
|
|
|
|
);
|
|
|
|
|
|
|
|
const user = req.user;
|
|
|
|
const points = alreadyCompleted ? user.points : user.points + 1;
|
|
|
|
|
|
|
|
return user.update$(updateData)
|
|
|
|
.doOnNext(({ count }) => log('%s documents updated', count))
|
|
|
|
.map(() => {
|
|
|
|
if (type === 'json') {
|
|
|
|
return res.json({
|
|
|
|
points,
|
|
|
|
alreadyCompleted
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return res.sendStatus(200);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.subscribe(() => {}, next);
|
2015-05-21 01:50:31 +00:00
|
|
|
}
|
2015-05-20 02:31:01 +00:00
|
|
|
|
2015-06-03 02:02:54 +00:00
|
|
|
function completedZiplineOrBasejump(req, res, next) {
|
2016-02-11 06:10:06 +00:00
|
|
|
const type = accepts(req).type('html', 'json', 'text');
|
|
|
|
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
|
|
|
req.checkBody('name', 'Name must be at least 3 characters')
|
|
|
|
.isString()
|
|
|
|
.isLength({ min: 3 });
|
|
|
|
req.checkBody('challengeType', 'must be a number')
|
2016-03-03 05:53:42 +00:00
|
|
|
.isNumber();
|
2016-02-11 06:10:06 +00:00
|
|
|
req.checkBody('solution', 'solution must be a url').isURL();
|
|
|
|
|
|
|
|
const errors = req.validationErrors(true);
|
|
|
|
|
|
|
|
if (errors) {
|
|
|
|
if (type === 'json') {
|
|
|
|
return res.status(403).send({ errors });
|
2016-01-20 06:30:01 +00:00
|
|
|
}
|
2016-02-11 06:10:06 +00:00
|
|
|
log('errors', errors);
|
|
|
|
return res.sendStatus(403);
|
|
|
|
}
|
2015-06-03 02:02:54 +00:00
|
|
|
|
2016-02-11 06:10:06 +00:00
|
|
|
const { user, body = {} } = req;
|
2015-06-21 02:52:37 +00:00
|
|
|
|
2016-02-11 06:10:06 +00:00
|
|
|
const completedChallenge = _.pick(
|
|
|
|
body,
|
|
|
|
[ 'id', 'name', 'solution', 'githubLink', 'challengeType' ]
|
|
|
|
);
|
|
|
|
completedChallenge.challengeType = +completedChallenge.challengeType;
|
|
|
|
completedChallenge.completedDate = Date.now();
|
2015-06-21 02:52:37 +00:00
|
|
|
|
2016-01-20 06:30:01 +00:00
|
|
|
if (
|
|
|
|
!completedChallenge.solution ||
|
|
|
|
// only basejumps require github links
|
|
|
|
(
|
|
|
|
completedChallenge.challengeType === 4 &&
|
|
|
|
!completedChallenge.githubLink
|
|
|
|
)
|
|
|
|
) {
|
2015-06-03 02:02:54 +00:00
|
|
|
req.flash('errors', {
|
|
|
|
msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
|
2016-02-11 06:10:06 +00:00
|
|
|
'your work.'
|
2015-06-03 02:02:54 +00:00
|
|
|
});
|
|
|
|
return res.sendStatus(403);
|
2015-05-20 02:31:01 +00:00
|
|
|
}
|
2015-05-21 07:17:44 +00:00
|
|
|
|
2016-01-20 06:30:01 +00:00
|
|
|
|
2016-04-07 04:08:19 +00:00
|
|
|
return user.getChallengeMap$()
|
|
|
|
.flatMap(() => {
|
|
|
|
const {
|
|
|
|
alreadyCompleted,
|
|
|
|
updateData
|
|
|
|
} = buildUserUpdate(user, completedChallenge.id, completedChallenge);
|
|
|
|
|
|
|
|
return user.update$(updateData)
|
|
|
|
.doOnNext(({ count }) => log('%s documents updated', count))
|
|
|
|
.doOnNext(() => {
|
|
|
|
if (type === 'json') {
|
|
|
|
return res.send({
|
|
|
|
alreadyCompleted,
|
|
|
|
points: alreadyCompleted ? user.points : user.points + 1
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return res.status(200).send(true);
|
2016-02-11 06:10:06 +00:00
|
|
|
});
|
|
|
|
})
|
2016-01-20 06:30:01 +00:00
|
|
|
.subscribe(() => {}, next);
|
2015-05-20 02:31:01 +00:00
|
|
|
}
|
2015-06-19 21:49:10 +00:00
|
|
|
|
2016-04-07 04:08:19 +00:00
|
|
|
function showMap(showAside, { user }, res, next) {
|
|
|
|
return Observable.defer(() => {
|
|
|
|
if (user && typeof user.getChallengeMap$ === 'function') {
|
|
|
|
return user.getChallengeMap$();
|
|
|
|
}
|
|
|
|
return Observable.just({});
|
|
|
|
})
|
|
|
|
.flatMap(challengeMap => getSuperBlocks$(challenge$, challengeMap))
|
2015-08-10 05:14:31 +00:00
|
|
|
.subscribe(
|
2016-01-12 06:47:49 +00:00
|
|
|
superBlocks => {
|
2016-01-15 07:06:06 +00:00
|
|
|
res.render('map/show', {
|
2015-12-05 06:13:14 +00:00
|
|
|
superBlocks,
|
2016-01-15 07:06:06 +00:00
|
|
|
title: 'A Map to Learn to Code and Become a Software Engineer',
|
|
|
|
showAside
|
2015-08-10 05:14:31 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
next
|
|
|
|
);
|
2015-06-19 21:49:10 +00:00
|
|
|
}
|
2015-06-03 02:02:54 +00:00
|
|
|
};
|