freeCodeCamp/server/boot/challenge.js

601 lines
17 KiB
JavaScript
Raw Normal View History

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';
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,
2015-10-02 18:47:36 +00:00
observeQuery
2015-08-10 05:14:31 +00:00
} from '../utils/rx';
import {
ifNoUserSend
} from '../utils/middleware';
const isDev = process.env.NODE_ENV !== 'production';
const isBeta = !!process.env.BETA;
2015-08-10 05:14:31 +00:00
const debug = debugFactory('freecc:challenges');
const challengesRegex = /^(bonfire|waypoint|zipline|basejump)/i;
const firstChallenge = 'waypoint-learn-how-free-code-camp-works';
const challengeView = {
0: 'coursewares/showHTML',
1: 'coursewares/showJS',
2: 'coursewares/showVideo',
3: 'coursewares/showZiplineOrBasejump',
4: 'coursewares/showZiplineOrBasejump',
2015-09-27 22:58:59 +00:00
5: 'coursewares/showBonfire',
7: 'coursewares/showStep'
};
2015-06-20 20:35:26 +00:00
const dasherize = utils.dasherize;
const unDasherize = utils.unDasherize;
const getMDNLinks = utils.getMDNLinks;
2015-06-20 20:35:26 +00:00
2015-11-01 05:13:18 +00:00
/*
function makeChallengesUnique(challengeArr) {
// clone and reverse challenges
// then filter by unique id's
// then reverse again
return _.uniq(challengeArr.slice().reverse(), 'id').reverse();
}
2015-11-01 05:13:18 +00:00
*/
2015-08-10 05:14:31 +00:00
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function updateUserProgress(user, challengeId, completedChallenge) {
let { completedChallenges } = user;
// migrate user challenges object to remove
2015-10-07 13:05:33 +00:00
/* if (!user.isUniqMigrated) {
user.isUniqMigrated = true;
completedChallenges = user.completedChallenges =
makeChallengesUnique(completedChallenges);
2015-10-07 13:05:33 +00:00
}*/
const indexOfChallenge = _.findIndex(completedChallenges, {
id: challengeId
});
const alreadyCompleted = indexOfChallenge !== -1;
if (!alreadyCompleted) {
user.progressTimestamps.push({
timestamp: Date.now(),
completedChallenge: challengeId
});
user.completedChallenges.push(completedChallenge);
return user;
}
const oldCompletedChallenge = completedChallenges[indexOfChallenge];
user.completedChallenges[indexOfChallenge] =
Object.assign(
{},
completedChallenge,
{
completedDate: oldCompletedChallenge.completedDate,
lastUpdated: completedChallenge.completedDate
}
);
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();
const challengesQuery = {
order: [
'order ASC',
'suborder ASC'
]
};
// challenge model
2015-08-10 05:14:31 +00:00
const Challenge = app.models.Challenge;
// challenge find query stream
2015-08-10 05:14:31 +00:00
const findChallenge$ = observeMethod(Challenge, 'find');
// create a stream of all the challenges
const challenge$ = findChallenge$(challengesQuery)
2015-08-19 18:28:14 +00:00
.doOnNext(() => debug('query challenges'))
.flatMap(challenges => Observable.from(
challenges,
null,
null,
Scheduler.default
))
// filter out all challenges that have isBeta flag set
// except in development or beta site
.filter(challenge => isDev || isBeta || !challenge.isBeta)
.shareReplay();
// create a stream of challenge blocks
const blocks$ = challenge$
2015-08-16 21:19:30 +00:00
.map(challenge => challenge.toJSON())
// 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);
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
router.get('/map', challengeMap);
2015-06-20 18:43:12 +00:00
router.get(
'/challenges/next-challenge',
returnNextChallenge
);
router.get('/challenges/:challengeName', returnIndividualChallenge);
app.use(router);
2015-06-03 02:02:54 +00:00
function returnNextChallenge(req, res, next) {
let nextChallengeName = firstChallenge;
2015-09-09 04:45:53 +00:00
const challengeId = req.query.id;
2015-08-19 18:28:14 +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')
.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 firstChallengeOfNextBlock$ = blocks$
.elementAt(blockIndex + 1, {})
.map(({ challenges = [] }) => challenges[0]);
return blocks$
.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);
});
});
});
})
.map(nextChallenge => {
if (!nextChallenge) {
return null;
}
nextChallengeName = nextChallenge.dashedName;
return nextChallengeName;
})
2015-06-20 18:43:12 +00:00
.subscribe(
function() {},
next,
function() {
debug('next challengeName', nextChallengeName);
if (!nextChallengeName || nextChallengeName === firstChallenge) {
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(' ')
});
return res.redirect('/map');
}
2015-06-20 20:35:26 +00:00
res.redirect('/challenges/' + nextChallengeName);
2015-06-20 18:43:12 +00:00
}
);
}
2015-06-03 02:02:54 +00:00
function returnIndividualChallenge(req, res, next) {
const origChallengeName = req.params.challengeName;
const solutionCode = req.query.solution;
const unDashedName = unDasherize(origChallengeName);
2015-06-20 20:35:26 +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
const testChallengeName = new RegExp(challengeName, 'i');
debug('looking for %s', testChallengeName);
challenge$
.filter((challenge) => {
return testChallengeName.test(challenge.name);
})
.last({ defaultValue: null })
.flatMap(challenge => {
2015-06-03 02:02:54 +00:00
// Handle not found
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.'
});
return Observable.just('/challenges');
2015-06-03 02:02:54 +00:00
}
2015-06-20 20:35:26 +00:00
if (dasherize(challenge.name) !== origChallengeName) {
let redirectUrl = `/challenges/${dasherize(challenge.name)}`;
if (solutionCode) {
redirectUrl += `?solution=${encodeURIComponent(solutionCode)}`;
}
return Observable.just(redirectUrl);
2015-06-20 20:35:26 +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,
2015-09-27 22:58:59 +00:00
description: 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()
});
})
.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) {
debug('compltedBonfire');
var completedWith = req.body.challengeInfo.completedWith || false;
2015-06-03 02:02:54 +00:00
var challengeId = req.body.challengeInfo.challengeId;
var challengeData = {
id: challengeId,
name: req.body.challengeInfo.challengeName || '',
completedDate: Math.round(+new Date()),
solution: req.body.challengeInfo.solution,
challengeType: 5
};
2015-10-02 18:47:36 +00:00
observeQuery(
User,
'findOne',
{ where: { username: ('' + completedWith).toLowerCase() } }
)
.doOnNext(function(pairedWith) {
debug('paired with ', pairedWith);
if (pairedWith) {
updateUserProgress(
pairedWith,
challengeId,
assign({ completedWith: req.user.id }, challengeData)
);
}
})
.withLatestFrom(
2015-08-10 05:14:31 +00:00
Observable.just(req.user),
function(pairedWith, user) {
return {
user: user,
pairedWith: pairedWith
};
}
)
// 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
);
})
// iterate users
.flatMap(function(dats) {
debug('flatmap');
2015-08-10 05:14:31 +00:00
return Observable.from([dats.user, dats.pairedWith]);
})
// 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);
}
},
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
updateUserProgress(
req.user,
2015-07-23 06:10:57 +00:00
id || challengeId,
{
2015-07-23 06:10:57 +00:00
id: id || challengeId,
completedDate: completedDate,
name: name || challengeName || '',
solution: null,
githubLink: null,
verified: true
}
);
saveUser(req.user)
.subscribe(
function(user) {
debug(
'user save points %s',
user && user.progressTimestamps && user.progressTimestamps.length
);
},
next,
function() {
res.sendStatus(200);
}
);
}
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 || '';
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;
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' ?
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-21 07:17:44 +00:00
var challengeData = {
id: challengeId,
name: req.body.challengeInfo.challengeName || '',
completedDate: completedDate,
solution: solutionLink,
githubLink: githubLink,
challengeType: challengeType,
verified: false
};
2015-10-02 18:47:36 +00:00
observeQuery(
User,
'findOne',
{ where: { username: completedWith.toLowerCase() } }
)
.doOnNext(function(pairedWith) {
if (pairedWith) {
updateUserProgress(
pairedWith,
challengeId,
assign({ completedWith: req.user.id }, challengeData)
);
}
})
2015-08-10 05:14:31 +00:00
.withLatestFrom(Observable.just(req.user), function(pairedWith, user) {
return {
user: user,
pairedWith: pairedWith
};
})
2015-08-03 23:35:22 +00:00
.doOnNext(function({ user, pairedWith }) {
updateUserProgress(
2015-08-03 23:35:22 +00:00
user,
challengeId,
2015-08-03 23:35:22 +00:00
pairedWith ?
assign({ completedWith: pairedWith.id }, challengeData) :
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]);
})
// 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);
}
},
next,
function() {
return res.status(200).send(true);
2015-06-03 02:02:54 +00:00
}
);
}
2015-08-10 05:14:31 +00:00
function challengeMap({ user = {} }, res, next) {
2015-09-29 02:20:29 +00:00
let lastCompleted;
2015-08-10 05:14:31 +00:00
const daysRunning = moment().diff(new Date('10/15/2014'), 'days');
2015-08-10 05:14:31 +00:00
// if user
// get the id's of all the users completed challenges
const completedChallenges = !user.completedChallenges ?
[] :
_.uniq(user.completedChallenges).map(({ id, _id }) => id || _id);
2015-08-10 05:14:31 +00:00
const camperCount$ = userCount$()
.map(camperCount => numberWithCommas(camperCount));
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;
}
2015-11-02 01:45:01 +00:00
if (typeof challenge.releasedOn !== 'undefined' &&
moment(challenge.releasedOn, 'MMM MMMM DD, YYYY').diff(moment(),
'days') >= -30) {
challenge.markNew = true;
}
2015-08-10 05:14:31 +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');
2015-08-10 05:14:31 +00:00
return {
isBeta,
2015-08-10 05:14:31 +00:00
name: blockArray[0].block,
dashedName: dasherize(blockArray[0].block),
challenges: blockArray,
completed: completedCount / blockArray.length * 100,
2015-11-01 05:13:18 +00:00
time: blockArray[0] && blockArray[0].time || '???'
2015-08-10 05:14:31 +00:00
};
})
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
2015-09-29 02:20:29 +00:00
.toArray()
.doOnNext((blocks) => {
const lastCompletedBlock = _.findLast(blocks, (block) => {
return block.completed === 100;
});
lastCompleted = lastCompletedBlock && lastCompletedBlock.name || null;
2015-09-29 02:20:29 +00:00
});
2015-08-10 05:14:31 +00:00
Observable.combineLatest(
camperCount$,
blocks$,
(camperCount, blocks) => ({ camperCount, blocks })
)
.subscribe(
({ camperCount, blocks }) => {
res.render('challengeMap/show', {
blocks,
daysRunning,
2015-11-01 05:13:18 +00:00
globalCompletedCount: numberWithCommas(
5612952 + (Math.floor((Date.now() - 1446268581061) / 3000))
),
2015-08-10 05:14:31 +00:00
camperCount,
2015-09-29 02:20:29 +00:00
lastCompleted,
2015-10-31 09:59:09 +00:00
title: "A Map to Learn to Code and Become a Software Engineer"
2015-08-10 05:14:31 +00:00
});
},
next
);
}
2015-06-03 02:02:54 +00:00
};