2015-08-10 05:14:31 +00:00
|
|
|
import _ from 'lodash';
|
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-08-05 21:49:23 +00:00
|
|
|
import dedent from 'dedent';
|
2015-11-10 01:27:56 +00:00
|
|
|
|
2016-06-01 22:52:08 +00:00
|
|
|
import { ifNoUserSend } from '../utils/middleware';
|
2016-08-04 17:49:37 +00:00
|
|
|
import { cachedMap } from '../utils/map';
|
|
|
|
import createNameIdMap from '../../common/utils/create-name-id-map';
|
2016-08-05 21:49:23 +00:00
|
|
|
import {
|
|
|
|
checkMapData,
|
|
|
|
getFirstChallenge
|
|
|
|
} from '../../common/utils/get-first-challenge';
|
2015-08-10 05:14:31 +00:00
|
|
|
|
2016-08-04 17:49:37 +00:00
|
|
|
const log = debug('fcc:boot:challenges');
|
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
|
|
|
}
|
|
|
|
|
2016-08-04 17:49:37 +00:00
|
|
|
export default function(app) {
|
2015-08-10 05:14:31 +00:00
|
|
|
const send200toNonUser = ifNoUserSend(true);
|
2016-08-04 17:49:37 +00:00
|
|
|
const api = app.loopback.Router();
|
|
|
|
const router = app.loopback.Router();
|
|
|
|
const Block = app.models.Block;
|
|
|
|
const map$ = cachedMap(Block);
|
2015-06-22 23:43:31 +00:00
|
|
|
|
2016-08-04 17:49:37 +00:00
|
|
|
api.post(
|
2016-06-01 22:52:08 +00:00
|
|
|
'/modern-challenge-completed',
|
|
|
|
send200toNonUser,
|
|
|
|
modernChallengeCompleted
|
|
|
|
);
|
|
|
|
|
2016-06-08 18:26:33 +00:00
|
|
|
// deprecate endpoint
|
|
|
|
// remove once new endpoint is live
|
2016-08-04 17:49:37 +00:00
|
|
|
api.post(
|
2016-06-08 18:11:13 +00:00
|
|
|
'/completed-challenge',
|
2015-06-22 23:43:31 +00:00
|
|
|
send200toNonUser,
|
|
|
|
completedChallenge
|
|
|
|
);
|
2016-06-01 22:52:08 +00:00
|
|
|
|
2016-08-04 17:49:37 +00:00
|
|
|
api.post(
|
2016-06-08 18:26:33 +00:00
|
|
|
'/challenge-completed',
|
|
|
|
send200toNonUser,
|
|
|
|
completedChallenge
|
|
|
|
);
|
|
|
|
|
2016-06-08 18:11:13 +00:00
|
|
|
// deprecate endpoint
|
|
|
|
// remove once new endpoint is live
|
2016-08-04 17:49:37 +00:00
|
|
|
api.post(
|
2015-06-22 23:43:31 +00:00
|
|
|
'/completed-zipline-or-basejump',
|
|
|
|
send200toNonUser,
|
2016-06-08 18:11:13 +00:00
|
|
|
projectCompleted
|
|
|
|
);
|
|
|
|
|
2016-08-04 17:49:37 +00:00
|
|
|
api.post(
|
2016-06-08 18:11:13 +00:00
|
|
|
'/project-completed',
|
|
|
|
send200toNonUser,
|
|
|
|
projectCompleted
|
2015-06-22 23:43:31 +00:00
|
|
|
);
|
2015-06-03 02:02:54 +00:00
|
|
|
|
2016-08-04 17:49:37 +00:00
|
|
|
router.get(
|
|
|
|
'/challenges/current-challenge',
|
|
|
|
redirectToCurrentChallenge
|
|
|
|
);
|
|
|
|
|
|
|
|
app.use(api);
|
|
|
|
app.use('/:lang', router);
|
2015-06-03 23:31:42 +00:00
|
|
|
|
2016-06-01 22:52:08 +00:00
|
|
|
function modernChallengeCompleted(req, res, next) {
|
|
|
|
const type = accepts(req).type('html', 'json', 'text');
|
|
|
|
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
|
|
|
req.checkBody('files', 'files must be an object with polyvinyls for keys')
|
|
|
|
.isFiles();
|
2016-02-15 01:10:26 +00:00
|
|
|
|
2016-06-01 22:52:08 +00:00
|
|
|
const errors = req.validationErrors(true);
|
|
|
|
if (errors) {
|
|
|
|
if (type === 'json') {
|
|
|
|
return res.status(403).send({ errors });
|
|
|
|
}
|
2016-02-15 01:10:26 +00:00
|
|
|
|
2016-06-01 22:52:08 +00:00
|
|
|
log('errors', errors);
|
|
|
|
return res.sendStatus(403);
|
2016-01-15 09:44:18 +00:00
|
|
|
}
|
2016-02-15 01:10:26 +00:00
|
|
|
|
2016-06-01 22:52:08 +00:00
|
|
|
const user = req.user;
|
|
|
|
return user.getChallengeMap$()
|
|
|
|
.flatMap(() => {
|
|
|
|
const completedDate = Date.now();
|
|
|
|
const {
|
|
|
|
id,
|
|
|
|
files
|
|
|
|
} = req.body;
|
2016-02-15 01:10:26 +00:00
|
|
|
|
2016-06-01 22:52:08 +00:00
|
|
|
const { alreadyCompleted, updateData } = buildUserUpdate(
|
|
|
|
user,
|
|
|
|
id,
|
|
|
|
{
|
|
|
|
id,
|
|
|
|
files,
|
|
|
|
completedDate
|
|
|
|
}
|
|
|
|
);
|
2016-02-15 01:10:26 +00:00
|
|
|
|
2016-06-01 22:52:08 +00:00
|
|
|
const points = alreadyCompleted ? user.points : user.points + 1;
|
2016-01-14 23:15:44 +00:00
|
|
|
|
2016-06-01 22:52:08 +00:00
|
|
|
return user.update$(updateData)
|
|
|
|
.doOnNext(({ count }) => log('%s documents updated', count))
|
|
|
|
.map(() => {
|
|
|
|
if (type === 'json') {
|
|
|
|
return res.json({
|
|
|
|
points,
|
|
|
|
alreadyCompleted
|
2016-01-14 23:15:44 +00:00
|
|
|
});
|
2016-06-01 22:52:08 +00:00
|
|
|
}
|
|
|
|
return res.sendStatus(200);
|
|
|
|
});
|
2016-01-14 23:15:44 +00:00
|
|
|
})
|
|
|
|
.subscribe(() => {}, next);
|
2015-05-16 04:39:43 +00:00
|
|
|
}
|
|
|
|
|
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-01-10 04:08:01 +00:00
|
|
|
const type = accepts(req).type('html', 'json', 'text');
|
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();
|
2016-06-08 18:11:13 +00:00
|
|
|
const { id, solution, timezone } = req.body;
|
2016-04-07 04:08:19 +00:00
|
|
|
|
|
|
|
const { alreadyCompleted, updateData } = buildUserUpdate(
|
|
|
|
req.user,
|
|
|
|
id,
|
2016-06-08 18:11:13 +00:00
|
|
|
{ id, solution, completedDate },
|
2016-04-07 04:08:19 +00:00
|
|
|
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
|
|
|
|
2016-06-08 18:11:13 +00:00
|
|
|
function projectCompleted(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();
|
2016-06-08 18:11:13 +00:00
|
|
|
req.checkBody('challengeType', 'must be a number').isNumber();
|
|
|
|
req.checkBody('solution', 'solution must be a URL').isURL();
|
2016-02-11 06:10:06 +00:00
|
|
|
|
|
|
|
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,
|
2016-06-08 18:11:13 +00:00
|
|
|
[ 'id', 'solution', 'githubLink', 'challengeType' ]
|
2016-02-11 06:10:06 +00:00
|
|
|
);
|
|
|
|
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
|
|
|
}
|
2016-08-04 17:49:37 +00:00
|
|
|
|
|
|
|
function redirectToCurrentChallenge(req, res, next) {
|
|
|
|
const { user } = req;
|
|
|
|
return map$
|
|
|
|
.map(({ entities, result }) => ({
|
|
|
|
result,
|
|
|
|
entities: createNameIdMap(entities)
|
|
|
|
}))
|
2016-08-05 21:49:23 +00:00
|
|
|
.map(map => {
|
|
|
|
checkMapData(map);
|
|
|
|
const {
|
|
|
|
entities: { challenge: challengeMap, challengeIdToName }
|
|
|
|
} = map;
|
2016-08-04 17:49:37 +00:00
|
|
|
let finalChallenge;
|
|
|
|
const dashedName = challengeIdToName[user && user.currentChallengeId];
|
|
|
|
finalChallenge = challengeMap[dashedName];
|
|
|
|
// redirect to first challenge
|
|
|
|
if (!finalChallenge) {
|
2016-08-05 21:49:23 +00:00
|
|
|
finalChallenge = getFirstChallenge(map);
|
2016-08-04 17:49:37 +00:00
|
|
|
}
|
|
|
|
const { block, dashedName: finalDashedName } = finalChallenge || {};
|
2016-08-05 21:49:23 +00:00
|
|
|
if (!finalDashedName || !block) {
|
|
|
|
// this should normally not be hit if database is properly seeded
|
|
|
|
console.error(new Error(dedent`
|
|
|
|
Attemped to find '${dashedName}'
|
|
|
|
from '${user && user.currentChallengeId || 'no challenge id found'}'
|
|
|
|
but came up empty.
|
|
|
|
db may not be properly seeded.
|
|
|
|
`));
|
|
|
|
if (dashedName) {
|
|
|
|
// attempt to find according to dashedName
|
|
|
|
return `/challenges/${dashedName}`;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2016-08-04 17:49:37 +00:00
|
|
|
return `/challenges/${block}/${finalDashedName}`;
|
|
|
|
})
|
|
|
|
.subscribe(
|
|
|
|
redirect => res.redirect(redirect || '/map'),
|
|
|
|
next
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|