fix(current-challenge): Fix current-challenge API

This commit is contained in:
Bouncey
2018-10-28 06:18:13 +00:00
committed by mrugesh mohapatra
parent e7316e4567
commit dace86663f
4 changed files with 144 additions and 128 deletions

View File

@ -55,23 +55,20 @@
"type": "string" "type": "string"
}, },
"description": { "description": {
"type": "array"
},
"image": {
"type": "string" "type": "string"
}, },
"tests": { "tests": {
"type": "array" "type": "array"
}, },
"head": { "head": {
"type": "array", "type": "string",
"description": "Appended to user code", "description": "Appended to user code",
"default": [] "default": ""
}, },
"tail": { "tail": {
"type": "array", "type": "string",
"description": "Prepended to user code", "description": "Prepended to user code",
"default": [] "default": ""
}, },
"helpRoom": { "helpRoom": {
"type": "string", "type": "string",

View File

@ -10,16 +10,16 @@ import debug from 'debug';
import accepts from 'accepts'; import accepts from 'accepts';
import dedent from 'dedent'; import dedent from 'dedent';
import { ifNoUserSend } from '../utils/middleware'; import { homeLocation } from '../../../config/env.json';
import { getChallengeById, cachedMap } from '../utils/map';
import { dasherize } from '../utils';
import { ifNoUserSend } from '../utils/middleware';
import { dasherize } from '../utils';
import pathMigrations from '../resources/pathMigration.json'; import pathMigrations from '../resources/pathMigration.json';
import { fixCompletedChallengeItem } from '../../common/utils'; import { fixCompletedChallengeItem } from '../../common/utils';
const log = debug('fcc:boot:challenges'); const log = debug('fcc:boot:challenges');
const learnURL = 'https://learn.freecodecamp.org'; const learnURL = `${homeLocation}/learn`;
const jsProjects = [ const jsProjects = [
'aaa48de84e1ecc7c742e1124', 'aaa48de84e1ecc7c742e1124',
@ -29,12 +29,7 @@ const jsProjects = [
'aa2e6f85cab2ab736c9a9b24' 'aa2e6f85cab2ab736c9a9b24'
]; ];
function buildUserUpdate( function buildUserUpdate(user, challengeId, _completedChallenge, timezone) {
user,
challengeId,
_completedChallenge,
timezone
) {
const { files } = _completedChallenge; const { files } = _completedChallenge;
let completedChallenge = {}; let completedChallenge = {};
@ -43,17 +38,9 @@ function buildUserUpdate(
..._completedChallenge, ..._completedChallenge,
files: Object.keys(files) files: Object.keys(files)
.map(key => files[key]) .map(key => files[key])
.map(file => _.pick( .map(file =>
file, _.pick(file, ['contents', 'key', 'index', 'name', 'path', 'ext'])
[ )
'contents',
'key',
'index',
'name',
'path',
'ext'
]
))
}; };
} else { } else {
completedChallenge = _.omit(_completedChallenge, ['files']); completedChallenge = _.omit(_completedChallenge, ['files']);
@ -110,11 +97,54 @@ function buildUserUpdate(
}; };
} }
export default function(app) { function buildChallengeUrl(challenge) {
const { superBlock, block, dashedName } = challenge;
return `/learn/${dasherize(superBlock)}/${dasherize(block)}/${dashedName}`;
}
function getFirstChallenge(Challenge) {
return new Promise(resolve => {
Challenge.find(
{ where: { challengeOrder: 0, superOrder: 1, order: 0 } },
(err, challenge) => {
if (err) {
console.log(err);
return resolve('/learn');
}
return resolve(buildChallengeUrl(challenge));
}
);
});
}
async function createChallengeUrlResolver(app) {
const { Challenge } = app.models;
const cache = new Map();
const firstChallenge = await getFirstChallenge(Challenge);
return function resolveChallengeUrl(id) {
return new Promise(resolve => {
if (cache.has(id)) {
return resolve(cache.get(id));
}
return Challenge.findById(id, (err, challenge) => {
if (err) {
console.log(err);
return firstChallenge;
}
const challengeUrl = buildChallengeUrl(challenge);
cache.set(id, challengeUrl);
return resolve(challengeUrl);
});
});
};
}
export default async function bootChallenge(app, done) {
const send200toNonUser = ifNoUserSend(true); const send200toNonUser = ifNoUserSend(true);
const api = app.loopback.Router(); const api = app.loopback.Router();
const router = app.loopback.Router(); const router = app.loopback.Router();
const map = cachedMap(app.models); const challengeUrlResolver = await createChallengeUrlResolver(app);
api.post( api.post(
'/modern-challenge-completed', '/modern-challenge-completed',
@ -124,17 +154,9 @@ export default function(app) {
// deprecate endpoint // deprecate endpoint
// remove once new endpoint is live // remove once new endpoint is live
api.post( api.post('/completed-challenge', send200toNonUser, completedChallenge);
'/completed-challenge',
send200toNonUser,
completedChallenge
);
api.post( api.post('/challenge-completed', send200toNonUser, completedChallenge);
'/challenge-completed',
send200toNonUser,
completedChallenge
);
// deprecate endpoint // deprecate endpoint
// remove once new endpoint is live // remove once new endpoint is live
@ -144,11 +166,7 @@ export default function(app) {
projectCompleted projectCompleted
); );
api.post( api.post('/project-completed', send200toNonUser, projectCompleted);
'/project-completed',
send200toNonUser,
projectCompleted
);
api.post( api.post(
'/backend-challenge-completed', '/backend-challenge-completed',
@ -156,10 +174,7 @@ export default function(app) {
backendChallengeCompleted backendChallengeCompleted
); );
router.get( router.get('/challenges/current-challenge', redirectToCurrentChallenge);
'/challenges/current-challenge',
redirectToCurrentChallenge
);
router.get('/challenges', redirectToLearn); router.get('/challenges', redirectToLearn);
@ -187,26 +202,22 @@ export default function(app) {
} }
const user = req.user; const user = req.user;
return user.getCompletedChallenges$() return user
.getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const completedDate = Date.now(); const completedDate = Date.now();
const { const { id, files } = req.body;
id,
files
} = req.body;
const { const { alreadyCompleted, updateData } = buildUserUpdate(user, id, {
alreadyCompleted,
updateData
} = buildUserUpdate(
user,
id, id,
{ id, files, completedDate } files,
); completedDate
});
const points = alreadyCompleted ? user.points : user.points + 1; const points = alreadyCompleted ? user.points : user.points + 1;
return user.update$(updateData) return user
.update$(updateData)
.doOnNext(() => user.manualReload()) .doOnNext(() => user.manualReload())
.doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(({ count }) => log('%s documents updated', count))
.map(() => { .map(() => {
@ -237,15 +248,13 @@ export default function(app) {
return res.sendStatus(403); return res.sendStatus(403);
} }
return req.user.getCompletedChallenges$() return req.user
.getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const completedDate = Date.now(); const completedDate = Date.now();
const { id, solution, timezone, files } = req.body; const { id, solution, timezone, files } = req.body;
const { const { alreadyCompleted, updateData } = buildUserUpdate(
alreadyCompleted,
updateData
} = buildUserUpdate(
req.user, req.user,
id, id,
{ id, solution, completedDate, files }, { id, solution, completedDate, files },
@ -255,7 +264,8 @@ export default function(app) {
const user = req.user; const user = req.user;
const points = alreadyCompleted ? user.points : user.points + 1; const points = alreadyCompleted ? user.points : user.points + 1;
return user.update$(updateData) return user
.update$(updateData)
.doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(({ count }) => log('%s documents updated', count))
.map(() => { .map(() => {
if (type === 'json') { if (type === 'json') {
@ -289,36 +299,38 @@ export default function(app) {
const { user, body = {} } = req; const { user, body = {} } = req;
const completedChallenge = _.pick( const completedChallenge = _.pick(body, [
body, 'id',
[ 'id', 'solution', 'githubLink', 'challengeType', 'files' ] 'solution',
); 'githubLink',
'challengeType',
'files'
]);
completedChallenge.completedDate = Date.now(); completedChallenge.completedDate = Date.now();
if ( if (
!completedChallenge.solution || !completedChallenge.solution ||
// only basejumps require github links // only basejumps require github links
( (completedChallenge.challengeType === 4 && !completedChallenge.githubLink)
completedChallenge.challengeType === 4 &&
!completedChallenge.githubLink
)
) { ) {
req.flash( req.flash(
'danger', 'danger',
'You haven\'t supplied the necessary URLs for us to inspect your work.' "You haven't supplied the necessary URLs for us to inspect your work."
); );
return res.sendStatus(403); return res.sendStatus(403);
} }
return user
return user.getCompletedChallenges$() .getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const { const { alreadyCompleted, updateData } = buildUserUpdate(
alreadyCompleted, user,
updateData completedChallenge.id,
} = buildUserUpdate(user, completedChallenge.id, completedChallenge); completedChallenge
);
return user.update$(updateData) return user
.update$(updateData)
.doOnNext(() => user.manualReload()) .doOnNext(() => user.manualReload())
.doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(({ count }) => log('%s documents updated', count))
.doOnNext(() => { .doOnNext(() => {
@ -352,21 +364,20 @@ export default function(app) {
const { user, body = {} } = req; const { user, body = {} } = req;
const completedChallenge = _.pick( const completedChallenge = _.pick(body, ['id', 'solution']);
body,
[ 'id', 'solution' ]
);
completedChallenge.completedDate = Date.now(); completedChallenge.completedDate = Date.now();
return user
return user.getCompletedChallenges$() .getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const { const { alreadyCompleted, updateData } = buildUserUpdate(
alreadyCompleted, user,
updateData completedChallenge.id,
} = buildUserUpdate(user, completedChallenge.id, completedChallenge); completedChallenge
);
return user.update$(updateData) return user
.update$(updateData)
.doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(({ count }) => log('%s documents updated', count))
.doOnNext(() => { .doOnNext(() => {
if (type === 'json') { if (type === 'json') {
@ -382,27 +393,22 @@ export default function(app) {
.subscribe(() => {}, next); .subscribe(() => {}, next);
} }
function redirectToCurrentChallenge(req, res, next) { async function redirectToCurrentChallenge(req, res, next) {
const { user } = req; const { user } = req;
const challengeId = user && user.currentChallengeId; const challengeId = user && user.currentChallengeId;
return getChallengeById(map, challengeId) log(req.user.username);
.map(challenge => { log(challengeId);
const { block, dashedName, superBlock } = challenge; const challengeUrl = await challengeUrlResolver(challengeId).catch(next);
if (!dashedName || !block) { log(challengeUrl);
if (challengeUrl === '/learn') {
// this should normally not be hit if database is properly seeded // this should normally not be hit if database is properly seeded
throw new Error(dedent` throw new Error(dedent`
Attempted to find '${dashedName}' Attempted to find the url for ${challengeId}'
from '${ challengeId || 'no challenge id found'}'
but came up empty. but came up empty.
db may not be properly seeded. db may not be properly seeded.
`); `);
} }
return `${learnURL}/${dasherize(superBlock)}/${block}/${dashedName}`; return res.redirect(`${homeLocation}${challengeUrl}`);
})
.subscribe(
redirect => res.redirect(redirect || learnURL),
next
);
} }
function redirectToLearn(req, res) { function redirectToLearn(req, res) {
@ -413,4 +419,5 @@ export default function(app) {
} }
return res.status(302).redirect(learnURL); return res.status(302).redirect(learnURL);
} }
done();
} }

View File

@ -4,6 +4,8 @@ const readDirP = require('readdirp-walk');
const { parseMarkdown } = require('@freecodecamp/challenge-md-parser'); const { parseMarkdown } = require('@freecodecamp/challenge-md-parser');
const { dasherize } = require('./utils');
const challengesDir = path.resolve(__dirname, './challenges'); const challengesDir = path.resolve(__dirname, './challenges');
exports.getChallengesForLang = function getChallengesForLang(lang) { exports.getChallengesForLang = function getChallengesForLang(lang) {
@ -57,6 +59,7 @@ async function buildCurriculum(file, curriculum) {
); );
const { name: blockName, order, superOrder } = meta; const { name: blockName, order, superOrder } = meta;
challenge.block = blockName; challenge.block = blockName;
challenge.dashedName = dasherize(challenge.title);
challenge.order = order; challenge.order = order;
challenge.superOrder = superOrder; challenge.superOrder = superOrder;
challenge.superBlock = superBlock; challenge.superBlock = superBlock;

View File

@ -1,7 +1,7 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') }); require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
const MongoClient = require('mongodb').MongoClient; const { MongoClient, ObjectID } = require('mongodb');
const { getChallengesForLang } = require('@freecodecamp/curriculum'); const { getChallengesForLang } = require('@freecodecamp/curriculum');
const { flatten } = require('lodash'); const { flatten } = require('lodash');
const debug = require('debug'); const debug = require('debug');
@ -9,7 +9,7 @@ const debug = require('debug');
const { createPathMigrationMap } = require('./createPathMigrationMap'); const { createPathMigrationMap } = require('./createPathMigrationMap');
const log = debug('fcc:tools:seedChallenges'); const log = debug('fcc:tools:seedChallenges');
const { MONGOHQ_URL, LOCALE: lang } = process.env; const { MONGOHQ_URL, LOCALE: lang = 'english' } = process.env;
function handleError(err, client) { function handleError(err, client) {
if (err) { if (err) {
@ -32,14 +32,16 @@ MongoClient.connect(
function(err, client) { function(err, client) {
handleError(err, client); handleError(err, client);
log('Connected successfully to mongo'); log('Connected successfully to mongo at %s', MONGOHQ_URL);
const db = client.db('freecodecamp'); const db = client.db('freecodecamp');
const challenges = db.collection('challenge'); const challengeCollection = db.collection('challenge');
challenges.deleteMany({}, err => { challengeCollection.deleteMany({}, err => {
handleError(err, client); handleError(err, client);
log('deleted all the challenges');
const curriculum = getChallengesForLang(lang); const curriculum = getChallengesForLang(lang);
const allChallenges = Object.keys(curriculum) const allChallenges = Object.keys(curriculum)
@ -51,18 +53,25 @@ MongoClient.connect(
return [...challengeArray, ...flatten(challengesForBlock)]; return [...challengeArray, ...flatten(challengesForBlock)];
}, []) }, [])
.map(challenge => { .map(challenge => {
challenge._id = challenge.id.slice(0); const currentId = challenge.id.slice(0);
challenge._id = ObjectID(currentId);
delete challenge.id; delete challenge.id;
return challenge; return challenge;
}); });
try { try {
challenges.insertMany(allChallenges, { ordered: false }); challengeCollection.insertMany(
allChallenges,
{ ordered: false },
err => {
handleError(err, client);
log('challenge seed complete');
client.close();
}
);
} catch (e) { } catch (e) {
handleError(e, client); handleError(e, client);
} finally { } finally {
log('challenge seed complete');
client.close();
log('generating path migration map'); log('generating path migration map');
const pathMap = createPathMigrationMap(curriculum); const pathMap = createPathMigrationMap(curriculum);
const outputDir = path.resolve( const outputDir = path.resolve(