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';
|
2015-08-10 05:14:31 +00:00
|
|
|
import assign from 'object.assign';
|
|
|
|
import debugFactory from 'debug';
|
|
|
|
import utils from '../utils';
|
|
|
|
|
|
|
|
import {
|
|
|
|
saveUser,
|
|
|
|
observeMethod,
|
|
|
|
observableQueryFromModel
|
|
|
|
} from '../utils/rx';
|
|
|
|
|
|
|
|
import {
|
|
|
|
userMigration,
|
|
|
|
ifNoUserSend
|
|
|
|
} from '../utils/middleware';
|
|
|
|
|
|
|
|
const debug = debugFactory('freecc:challenges');
|
2015-08-18 22:21:26 +00:00
|
|
|
const challengesRegex = /^(bonfire|waypoint|zipline|basejump)/i;
|
2015-08-12 18:44:29 +00:00
|
|
|
const firstChallenge = 'waypoint-say-hello-to-html-elements';
|
2015-08-20 06:11:21 +00:00
|
|
|
const challengeView = {
|
|
|
|
0: 'coursewares/showHTML',
|
|
|
|
1: 'coursewares/showJS',
|
|
|
|
2: 'coursewares/showVideo',
|
|
|
|
3: 'coursewares/showZiplineOrBasejump',
|
|
|
|
4: 'coursewares/showZiplineOrBasejump',
|
|
|
|
5: 'coursewares/showBonfire'
|
|
|
|
};
|
2015-06-20 20:35:26 +00:00
|
|
|
|
2015-08-12 18:44:29 +00:00
|
|
|
const dasherize = utils.dasherize;
|
|
|
|
const unDasherize = utils.unDasherize;
|
|
|
|
const getMDNLinks = utils.getMDNLinks;
|
2015-06-20 20:35:26 +00:00
|
|
|
|
2015-08-10 05:14:31 +00:00
|
|
|
function numberWithCommas(x) {
|
|
|
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
|
|
}
|
|
|
|
|
2015-06-21 02:52:37 +00:00
|
|
|
function updateUserProgress(user, challengeId, completedChallenge) {
|
2015-08-12 18:44:29 +00:00
|
|
|
const alreadyCompleted = user.completedChallenges.some(({ id }) => {
|
2015-07-23 21:16:22 +00:00
|
|
|
return id === challengeId;
|
|
|
|
});
|
|
|
|
|
2015-08-11 21:17:08 +00:00
|
|
|
if (!alreadyCompleted) {
|
2015-08-07 20:31:48 +00:00
|
|
|
user.progressTimestamps.push({
|
2015-08-11 21:17:08 +00:00
|
|
|
timestamp: Date.now(),
|
|
|
|
completedChallenge
|
2015-08-07 20:31:48 +00:00
|
|
|
});
|
2015-06-21 02:52:37 +00:00
|
|
|
}
|
|
|
|
user.completedChallenges.push(completedChallenge);
|
|
|
|
return user;
|
|
|
|
}
|
|
|
|
|
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: [
|
|
|
|
'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-19 18:28:14 +00:00
|
|
|
.doOnNext(() => debug('query challenges'))
|
2015-08-20 06:11:21 +00:00
|
|
|
.flatMap(challenges => Observable.from(
|
|
|
|
challenges,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
Scheduler.default
|
|
|
|
))
|
2015-08-12 18:44:29 +00:00
|
|
|
.shareReplay();
|
|
|
|
|
|
|
|
// create a stream of challenge blocks
|
|
|
|
const blocks$ = challenge$
|
2015-08-16 21:19:30 +00:00
|
|
|
.map(challenge => challenge.toJSON())
|
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
|
|
|
|
.flatMap(block$ => block$.toArray())
|
|
|
|
// turn array into stream of object
|
|
|
|
.map(blockArray => ({
|
|
|
|
name: blockArray[0].block,
|
|
|
|
dashedName: dasherize(blockArray[0].block),
|
|
|
|
challenges: blockArray
|
2015-08-13 03:39:40 +00:00
|
|
|
}))
|
|
|
|
.filter(({ name })=> {
|
|
|
|
return name !== 'Hikes';
|
|
|
|
})
|
|
|
|
.shareReplay();
|
2015-08-10 05:14:31 +00:00
|
|
|
|
|
|
|
const User = app.models.User;
|
|
|
|
const userCount$ = observeMethod(User, 'count');
|
|
|
|
|
|
|
|
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
|
|
|
|
);
|
|
|
|
router.post(
|
|
|
|
'/completed-bonfire',
|
|
|
|
send200toNonUser,
|
|
|
|
completedBonfire
|
|
|
|
);
|
2015-06-03 02:02:54 +00:00
|
|
|
|
2015-06-19 21:49:10 +00:00
|
|
|
// the follow routes are covered by userMigration
|
|
|
|
router.use(userMigration);
|
|
|
|
router.get('/map', challengeMap);
|
2015-06-20 18:43:12 +00:00
|
|
|
router.get(
|
|
|
|
'/challenges/next-challenge',
|
|
|
|
returnNextChallenge
|
|
|
|
);
|
|
|
|
|
|
|
|
router.get('/challenges/:challengeName', returnIndividualChallenge);
|
|
|
|
|
2015-06-03 23:31:42 +00:00
|
|
|
app.use(router);
|
|
|
|
|
2015-06-03 02:02:54 +00:00
|
|
|
function returnNextChallenge(req, res, next) {
|
2015-08-12 18:44:29 +00:00
|
|
|
let nextChallengeName = firstChallenge;
|
|
|
|
|
2015-09-09 04:45:53 +00:00
|
|
|
const challengeId = req.query.id;
|
2015-08-19 18:28:14 +00:00
|
|
|
|
2015-08-12 18:44:29 +00:00
|
|
|
// find challenge
|
|
|
|
return challenge$
|
2015-08-16 21:19:30 +00:00
|
|
|
.map(challenge => challenge.toJSON())
|
2015-08-13 03:39:40 +00:00
|
|
|
.filter(({ block }) => block !== 'Hikes')
|
2015-08-12 18:44:29 +00:00
|
|
|
.filter(({ id }) => id === 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 nextBlock$ = blocks$.elementAt(blockIndex + 1);
|
|
|
|
const firstChallengeOfNextBlock$ = nextBlock$
|
|
|
|
.map(block => block.challenges[0]);
|
|
|
|
|
|
|
|
return blocks$
|
|
|
|
.elementAt(blockIndex)
|
|
|
|
.flatMap(block => {
|
|
|
|
// find where our challenge lies in the block
|
2015-08-20 06:11:21 +00:00
|
|
|
const challengeIndex$ = Observable.from(
|
|
|
|
block.challenges,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
Scheduler.default
|
|
|
|
)
|
2015-08-12 18:44:29 +00:00
|
|
|
.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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.map(nextChallenge => {
|
|
|
|
nextChallengeName = nextChallenge.dashedName;
|
|
|
|
return nextChallengeName;
|
|
|
|
})
|
2015-06-20 18:43:12 +00:00
|
|
|
.subscribe(
|
|
|
|
function() {},
|
|
|
|
next,
|
|
|
|
function() {
|
2015-08-12 18:44:29 +00:00
|
|
|
debug('next challengeName', nextChallengeName);
|
|
|
|
if (!nextChallengeName || nextChallengeName === firstChallenge) {
|
2015-09-09 06:47:00 +00:00
|
|
|
req.flash('info', {
|
2015-08-29 08:48:01 +00:00
|
|
|
msg: dedent`
|
|
|
|
Once you have completed all of our challenges, you should
|
|
|
|
join our <a href=\"//gitter.im/freecodecamp/HalfWayClub\"
|
|
|
|
target=\"_blank\">Half Way Club</a> and start getting
|
|
|
|
ready for our nonprofit projects.
|
|
|
|
`.split('\n').join(' ')
|
2015-08-12 18:44:29 +00:00
|
|
|
});
|
2015-08-25 22:51:47 +00:00
|
|
|
return res.redirect('/map');
|
2015-08-12 18:44:29 +00:00
|
|
|
}
|
2015-06-20 20:35:26 +00:00
|
|
|
res.redirect('/challenges/' + nextChallengeName);
|
2015-06-20 18:43:12 +00:00
|
|
|
}
|
|
|
|
);
|
2015-05-16 04:39:43 +00:00
|
|
|
}
|
|
|
|
|
2015-06-03 02:02:54 +00:00
|
|
|
function returnIndividualChallenge(req, res, next) {
|
2015-08-20 06:11:21 +00:00
|
|
|
const origChallengeName = req.params.challengeName;
|
|
|
|
const unDashedName = unDasherize(origChallengeName);
|
2015-06-20 20:35:26 +00:00
|
|
|
|
2015-08-20 06:11:21 +00:00
|
|
|
const challengeName = challengesRegex.test(unDashedName) ?
|
2015-06-20 20:35:26 +00:00
|
|
|
// remove first word if matches
|
|
|
|
unDashedName.split(' ').slice(1).join(' ') :
|
|
|
|
unDashedName;
|
2015-06-03 02:02:54 +00:00
|
|
|
|
2015-08-20 06:11:21 +00:00
|
|
|
const testChallengeName = new RegExp(challengeName, 'i');
|
|
|
|
debug('looking for %s', testChallengeName);
|
|
|
|
challenge$
|
|
|
|
.filter((challenge) => {
|
|
|
|
return testChallengeName.test(challenge.name);
|
|
|
|
})
|
|
|
|
.lastOrDefault(null)
|
|
|
|
.flatMap(challenge => {
|
2015-06-03 02:02:54 +00:00
|
|
|
|
|
|
|
// Handle not found
|
2015-06-15 22:50:18 +00:00
|
|
|
if (!challenge) {
|
2015-06-20 20:35:26 +00:00
|
|
|
debug('did not find challenge for ' + origChallengeName);
|
2015-06-03 02:02:54 +00:00
|
|
|
req.flash('errors', {
|
2015-06-20 18:43:12 +00:00
|
|
|
msg:
|
|
|
|
'404: We couldn\'t find a challenge with the name `' +
|
2015-06-20 20:35:26 +00:00
|
|
|
origChallengeName +
|
2015-06-20 18:43:12 +00:00
|
|
|
'` Please double check the name.'
|
2015-05-16 04:39:43 +00:00
|
|
|
});
|
2015-08-20 06:11:21 +00:00
|
|
|
return Observable.just('/challenges');
|
2015-06-03 02:02:54 +00:00
|
|
|
}
|
2015-08-20 06:11:21 +00:00
|
|
|
|
2015-06-20 20:35:26 +00:00
|
|
|
if (dasherize(challenge.name) !== origChallengeName) {
|
2015-08-20 06:11:21 +00:00
|
|
|
return Observable.just('/challenges/' + dasherize(challenge.name));
|
2015-06-20 20:35:26 +00:00
|
|
|
}
|
|
|
|
|
2015-09-09 05:41:03 +00:00
|
|
|
// save user does nothing if user does not exist
|
|
|
|
return Observable.just({
|
|
|
|
title: challenge.name,
|
|
|
|
dashedName: origChallengeName,
|
|
|
|
name: challenge.name,
|
|
|
|
details: challenge.description,
|
|
|
|
tests: challenge.tests,
|
|
|
|
challengeSeed: challenge.challengeSeed,
|
|
|
|
verb: utils.randomVerb(),
|
|
|
|
phrase: utils.randomPhrase(),
|
|
|
|
compliment: utils.randomCompliment(),
|
|
|
|
challengeId: challenge.id,
|
|
|
|
challengeType: challenge.challengeType,
|
|
|
|
// video challenges
|
|
|
|
video: challenge.challengeSeed[0],
|
|
|
|
// bonfires specific
|
|
|
|
difficulty: Math.floor(+challenge.difficulty),
|
|
|
|
bonfires: challenge,
|
|
|
|
MDNkeys: challenge.MDNlinks,
|
|
|
|
MDNlinks: getMDNLinks(challenge.MDNlinks),
|
|
|
|
// htmls specific
|
|
|
|
environment: utils.whichEnvironment()
|
|
|
|
});
|
2015-08-20 06:11:21 +00:00
|
|
|
})
|
|
|
|
.subscribe(
|
|
|
|
function(data) {
|
|
|
|
if (typeof data === 'string') {
|
|
|
|
debug('redirecting to %s', data);
|
|
|
|
return res.redirect(data);
|
|
|
|
}
|
|
|
|
var view = challengeView[data.challengeType];
|
|
|
|
res.render(view, data);
|
|
|
|
},
|
|
|
|
next,
|
|
|
|
function() {}
|
|
|
|
);
|
2015-06-03 02:02:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function completedBonfire(req, res, next) {
|
2015-06-21 02:52:37 +00:00
|
|
|
debug('compltedBonfire');
|
|
|
|
var completedWith = req.body.challengeInfo.completedWith || false;
|
2015-06-03 02:02:54 +00:00
|
|
|
var challengeId = req.body.challengeInfo.challengeId;
|
2015-05-16 04:39:43 +00:00
|
|
|
|
2015-06-21 02:52:37 +00:00
|
|
|
var challengeData = {
|
|
|
|
id: challengeId,
|
2015-08-19 02:23:43 +00:00
|
|
|
name: req.body.challengeInfo.challengeName || '',
|
2015-06-21 02:52:37 +00:00
|
|
|
completedDate: Math.round(+new Date()),
|
|
|
|
solution: req.body.challengeInfo.solution,
|
|
|
|
challengeType: 5
|
|
|
|
};
|
|
|
|
|
|
|
|
observableQueryFromModel(
|
|
|
|
User,
|
|
|
|
'findOne',
|
|
|
|
{ where: { username: ('' + completedWith).toLowerCase() } }
|
|
|
|
)
|
|
|
|
.doOnNext(function(pairedWith) {
|
|
|
|
debug('paired with ', pairedWith);
|
2015-05-25 21:27:27 +00:00
|
|
|
if (pairedWith) {
|
2015-06-21 02:52:37 +00:00
|
|
|
updateUserProgress(
|
|
|
|
pairedWith,
|
|
|
|
challengeId,
|
|
|
|
assign({ completedWith: req.user.id }, challengeData)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.withLatestFrom(
|
2015-08-10 05:14:31 +00:00
|
|
|
Observable.just(req.user),
|
2015-06-21 02:52:37 +00:00
|
|
|
function(pairedWith, user) {
|
|
|
|
return {
|
|
|
|
user: user,
|
|
|
|
pairedWith: pairedWith
|
|
|
|
};
|
2015-05-25 21:27:27 +00:00
|
|
|
}
|
2015-06-21 02:52:37 +00:00
|
|
|
)
|
|
|
|
// side effects should always be done in do's and taps
|
|
|
|
.doOnNext(function(dats) {
|
|
|
|
updateUserProgress(
|
|
|
|
dats.user,
|
|
|
|
challengeId,
|
|
|
|
dats.pairedWith ?
|
|
|
|
// paired programmer found and adding to data
|
|
|
|
assign({ completedWith: dats.pairedWith.id }, challengeData) :
|
|
|
|
// user said they paired, but pair wasn't found
|
|
|
|
challengeData
|
|
|
|
);
|
|
|
|
})
|
2015-08-20 06:11:21 +00:00
|
|
|
// iterate users
|
2015-06-21 02:52:37 +00:00
|
|
|
.flatMap(function(dats) {
|
|
|
|
debug('flatmap');
|
2015-08-10 05:14:31 +00:00
|
|
|
return Observable.from([dats.user, dats.pairedWith]);
|
2015-06-21 02:52:37 +00:00
|
|
|
})
|
|
|
|
// save user
|
|
|
|
.flatMap(function(user) {
|
|
|
|
// save user will do nothing if user is falsey
|
|
|
|
return saveUser(user);
|
|
|
|
})
|
|
|
|
.subscribe(
|
|
|
|
function(user) {
|
|
|
|
debug('onNext');
|
|
|
|
if (user) {
|
|
|
|
debug('user %s saved', user.username);
|
2015-05-25 21:27:27 +00:00
|
|
|
}
|
2015-06-21 02:52:37 +00:00
|
|
|
},
|
|
|
|
next,
|
|
|
|
function() {
|
|
|
|
debug('completed');
|
|
|
|
return res.status(200).send(true);
|
|
|
|
}
|
|
|
|
);
|
2015-06-03 02:02:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function completedChallenge(req, res, next) {
|
|
|
|
|
2015-07-23 06:10:57 +00:00
|
|
|
const completedDate = Math.round(+new Date());
|
|
|
|
const { id, name } = req.body;
|
|
|
|
const { challengeId, challengeName } = req.body.challengeInfo || {};
|
2015-06-03 02:02:54 +00:00
|
|
|
|
2015-06-21 02:52:37 +00:00
|
|
|
updateUserProgress(
|
|
|
|
req.user,
|
2015-07-23 06:10:57 +00:00
|
|
|
id || challengeId,
|
2015-06-21 02:52:37 +00:00
|
|
|
{
|
2015-07-23 06:10:57 +00:00
|
|
|
id: id || challengeId,
|
2015-06-21 02:52:37 +00:00
|
|
|
completedDate: completedDate,
|
2015-08-19 02:23:43 +00:00
|
|
|
name: name || challengeName || '',
|
2015-06-21 02:52:37 +00:00
|
|
|
solution: null,
|
|
|
|
githubLink: null,
|
|
|
|
verified: true
|
2015-05-21 01:50:31 +00:00
|
|
|
}
|
2015-06-21 02:52:37 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
saveUser(req.user)
|
|
|
|
.subscribe(
|
2015-08-07 20:31:48 +00:00
|
|
|
function(user) {
|
|
|
|
debug(
|
|
|
|
'user save points %s',
|
|
|
|
user && user.progressTimestamps && user.progressTimestamps.length
|
|
|
|
);
|
|
|
|
},
|
2015-06-21 02:52:37 +00:00
|
|
|
next,
|
|
|
|
function() {
|
|
|
|
res.sendStatus(200);
|
|
|
|
}
|
|
|
|
);
|
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) {
|
|
|
|
|
2015-08-03 23:35:22 +00:00
|
|
|
var completedWith = req.body.challengeInfo.completedWith || '';
|
2015-06-21 02:52:37 +00:00
|
|
|
var completedDate = Math.round(+new Date());
|
2015-06-03 02:02:54 +00:00
|
|
|
var challengeId = req.body.challengeInfo.challengeId;
|
|
|
|
var solutionLink = req.body.challengeInfo.publicURL;
|
2015-06-21 02:52:37 +00:00
|
|
|
|
|
|
|
var githubLink = req.body.challengeInfo.challengeType === '4' ?
|
|
|
|
req.body.challengeInfo.githubURL :
|
|
|
|
true;
|
|
|
|
|
2015-06-03 02:02:54 +00:00
|
|
|
var challengeType = req.body.challengeInfo.challengeType === '4' ?
|
2015-06-21 02:52:37 +00:00
|
|
|
4 :
|
|
|
|
3;
|
|
|
|
|
2015-06-03 02:02:54 +00:00
|
|
|
if (!solutionLink || !githubLink) {
|
|
|
|
req.flash('errors', {
|
|
|
|
msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
|
|
|
|
'your work.'
|
|
|
|
});
|
|
|
|
return res.sendStatus(403);
|
2015-05-20 02:31:01 +00:00
|
|
|
}
|
2015-05-21 07:17:44 +00:00
|
|
|
|
2015-06-21 02:52:37 +00:00
|
|
|
var challengeData = {
|
|
|
|
id: challengeId,
|
2015-08-19 02:23:43 +00:00
|
|
|
name: req.body.challengeInfo.challengeName || '',
|
2015-06-21 02:52:37 +00:00
|
|
|
completedDate: completedDate,
|
|
|
|
solution: solutionLink,
|
|
|
|
githubLink: githubLink,
|
|
|
|
challengeType: challengeType,
|
|
|
|
verified: false
|
|
|
|
};
|
|
|
|
|
|
|
|
observableQueryFromModel(
|
|
|
|
User,
|
|
|
|
'findOne',
|
|
|
|
{ where: { username: completedWith.toLowerCase() } }
|
|
|
|
)
|
|
|
|
.doOnNext(function(pairedWith) {
|
|
|
|
if (pairedWith) {
|
|
|
|
updateUserProgress(
|
|
|
|
pairedWith,
|
|
|
|
challengeId,
|
|
|
|
assign({ completedWith: req.user.id }, challengeData)
|
|
|
|
);
|
2015-05-20 02:31:01 +00:00
|
|
|
}
|
2015-06-21 02:52:37 +00:00
|
|
|
})
|
2015-08-10 05:14:31 +00:00
|
|
|
.withLatestFrom(Observable.just(req.user), function(pairedWith, user) {
|
2015-06-21 02:52:37 +00:00
|
|
|
return {
|
|
|
|
user: user,
|
|
|
|
pairedWith: pairedWith
|
|
|
|
};
|
|
|
|
})
|
2015-08-03 23:35:22 +00:00
|
|
|
.doOnNext(function({ user, pairedWith }) {
|
2015-06-21 02:52:37 +00:00
|
|
|
updateUserProgress(
|
2015-08-03 23:35:22 +00:00
|
|
|
user,
|
2015-06-21 02:52:37 +00:00
|
|
|
challengeId,
|
2015-08-03 23:35:22 +00:00
|
|
|
pairedWith ?
|
|
|
|
assign({ completedWith: pairedWith.id }, challengeData) :
|
2015-06-21 02:52:37 +00:00
|
|
|
challengeData
|
|
|
|
);
|
|
|
|
})
|
2015-08-03 23:35:22 +00:00
|
|
|
.flatMap(function({ user, pairedWith }) {
|
2015-08-10 05:14:31 +00:00
|
|
|
return Observable.from([user, pairedWith]);
|
2015-06-21 02:52:37 +00:00
|
|
|
})
|
|
|
|
// save users
|
|
|
|
.flatMap(function(user) {
|
|
|
|
// save user will do nothing if user is falsey
|
|
|
|
return saveUser(user);
|
|
|
|
})
|
|
|
|
.subscribe(
|
|
|
|
function(user) {
|
|
|
|
if (user) {
|
|
|
|
debug('user %s saved', user.username);
|
2015-05-20 02:31:01 +00:00
|
|
|
}
|
2015-06-21 02:52:37 +00:00
|
|
|
},
|
|
|
|
next,
|
|
|
|
function() {
|
|
|
|
return res.status(200).send(true);
|
2015-06-03 02:02:54 +00:00
|
|
|
}
|
2015-06-21 02:52:37 +00:00
|
|
|
);
|
2015-05-20 02:31:01 +00:00
|
|
|
}
|
2015-06-19 21:49:10 +00:00
|
|
|
|
2015-08-10 05:14:31 +00:00
|
|
|
function challengeMap({ user = {} }, res, next) {
|
|
|
|
const daysRunning = moment().diff(new Date('10/15/2014'), 'days');
|
2015-06-19 21:49:10 +00:00
|
|
|
|
2015-08-10 05:14:31 +00:00
|
|
|
// if user
|
|
|
|
// get the id's of all the users completed challenges
|
|
|
|
const completedChallenges = !user.completedChallenges ?
|
|
|
|
[] :
|
2015-08-16 12:51:55 +00:00
|
|
|
_.uniq(user.completedChallenges).map(({ id, _id }) => id || _id);
|
2015-06-19 21:49:10 +00:00
|
|
|
|
2015-08-10 05:14:31 +00:00
|
|
|
const camperCount$ = userCount$()
|
|
|
|
.map(camperCount => numberWithCommas(camperCount));
|
2015-06-19 21:49:10 +00:00
|
|
|
|
2015-08-10 05:14:31 +00:00
|
|
|
// create a stream of an array of all the challenge blocks
|
|
|
|
const blocks$ = challenge$
|
|
|
|
// mark challenge completed
|
2015-08-16 21:19:30 +00:00
|
|
|
.map(challengeModel => {
|
|
|
|
const challenge = challengeModel.toJSON();
|
2015-08-10 05:14:31 +00:00
|
|
|
if (completedChallenges.indexOf(challenge.id) !== -1) {
|
|
|
|
challenge.completed = true;
|
|
|
|
}
|
|
|
|
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;
|
2015-08-15 01:05:04 +00:00
|
|
|
}, 0);
|
2015-06-19 21:49:10 +00:00
|
|
|
|
2015-08-10 05:14:31 +00:00
|
|
|
return {
|
|
|
|
name: blockArray[0].block,
|
|
|
|
dashedName: dasherize(blockArray[0].block),
|
|
|
|
challenges: blockArray,
|
|
|
|
completed: completedCount / blockArray.length * 100
|
|
|
|
};
|
|
|
|
})
|
2015-08-13 03:39:40 +00:00
|
|
|
.filter(({ name }) => name !== 'Hikes')
|
2015-08-10 05:14:31 +00:00
|
|
|
// turn stream of blocks into a stream of an array
|
|
|
|
.toArray();
|
|
|
|
|
|
|
|
Observable.combineLatest(
|
|
|
|
camperCount$,
|
|
|
|
blocks$,
|
|
|
|
(camperCount, blocks) => ({ camperCount, blocks })
|
|
|
|
)
|
|
|
|
.subscribe(
|
|
|
|
({ camperCount, blocks }) => {
|
|
|
|
res.render('challengeMap/show', {
|
|
|
|
blocks,
|
|
|
|
daysRunning,
|
|
|
|
camperCount,
|
|
|
|
title: "A map of all Free Code Camp's Challenges"
|
|
|
|
});
|
|
|
|
},
|
|
|
|
next
|
|
|
|
);
|
2015-06-19 21:49:10 +00:00
|
|
|
}
|
2015-06-03 02:02:54 +00:00
|
|
|
};
|