diff --git a/api-server/common/models/challenge.json b/api-server/common/models/challenge.json index 42e11c24f0..a04e661c99 100644 --- a/api-server/common/models/challenge.json +++ b/api-server/common/models/challenge.json @@ -55,23 +55,20 @@ "type": "string" }, "description": { - "type": "array" - }, - "image": { "type": "string" }, "tests": { "type": "array" }, "head": { - "type": "array", + "type": "string", "description": "Appended to user code", - "default": [] + "default": "" }, "tail": { - "type": "array", + "type": "string", "description": "Prepended to user code", - "default": [] + "default": "" }, "helpRoom": { "type": "string", diff --git a/api-server/server/boot/challenge.js b/api-server/server/boot/challenge.js index 83d754984f..6e3ab0071a 100644 --- a/api-server/server/boot/challenge.js +++ b/api-server/server/boot/challenge.js @@ -10,31 +10,26 @@ import debug from 'debug'; import accepts from 'accepts'; import dedent from 'dedent'; -import { ifNoUserSend } from '../utils/middleware'; -import { getChallengeById, cachedMap } from '../utils/map'; -import { dasherize } from '../utils'; +import { homeLocation } from '../../../config/env.json'; +import { ifNoUserSend } from '../utils/middleware'; +import { dasherize } from '../utils'; import pathMigrations from '../resources/pathMigration.json'; import { fixCompletedChallengeItem } from '../../common/utils'; const log = debug('fcc:boot:challenges'); -const learnURL = 'https://learn.freecodecamp.org'; +const learnURL = `${homeLocation}/learn`; const jsProjects = [ -'aaa48de84e1ecc7c742e1124', -'a7f4d8f2483413a6ce226cac', -'56533eb9ac21ba0edf2244e2', -'aff0395860f5d3034dc0bfc9', -'aa2e6f85cab2ab736c9a9b24' + 'aaa48de84e1ecc7c742e1124', + 'a7f4d8f2483413a6ce226cac', + '56533eb9ac21ba0edf2244e2', + 'aff0395860f5d3034dc0bfc9', + 'aa2e6f85cab2ab736c9a9b24' ]; -function buildUserUpdate( - user, - challengeId, - _completedChallenge, - timezone -) { +function buildUserUpdate(user, challengeId, _completedChallenge, timezone) { const { files } = _completedChallenge; let completedChallenge = {}; @@ -43,17 +38,9 @@ function buildUserUpdate( ..._completedChallenge, files: Object.keys(files) .map(key => files[key]) - .map(file => _.pick( - file, - [ - 'contents', - 'key', - 'index', - 'name', - 'path', - 'ext' - ] - )) + .map(file => + _.pick(file, ['contents', 'key', 'index', 'name', 'path', 'ext']) + ) }; } else { 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 api = app.loopback.Router(); const router = app.loopback.Router(); - const map = cachedMap(app.models); + const challengeUrlResolver = await createChallengeUrlResolver(app); api.post( '/modern-challenge-completed', @@ -124,17 +154,9 @@ export default function(app) { // deprecate endpoint // remove once new endpoint is live - api.post( - '/completed-challenge', - send200toNonUser, - completedChallenge - ); + api.post('/completed-challenge', send200toNonUser, completedChallenge); - api.post( - '/challenge-completed', - send200toNonUser, - completedChallenge - ); + api.post('/challenge-completed', send200toNonUser, completedChallenge); // deprecate endpoint // remove once new endpoint is live @@ -144,11 +166,7 @@ export default function(app) { projectCompleted ); - api.post( - '/project-completed', - send200toNonUser, - projectCompleted - ); + api.post('/project-completed', send200toNonUser, projectCompleted); api.post( '/backend-challenge-completed', @@ -156,10 +174,7 @@ export default function(app) { backendChallengeCompleted ); - router.get( - '/challenges/current-challenge', - redirectToCurrentChallenge - ); + router.get('/challenges/current-challenge', redirectToCurrentChallenge); router.get('/challenges', redirectToLearn); @@ -187,26 +202,22 @@ export default function(app) { } const user = req.user; - return user.getCompletedChallenges$() + return user + .getCompletedChallenges$() .flatMap(() => { const completedDate = Date.now(); - const { - id, - files - } = req.body; + const { id, files } = req.body; - const { - alreadyCompleted, - updateData - } = buildUserUpdate( - user, + const { alreadyCompleted, updateData } = buildUserUpdate(user, id, { id, - { id, files, completedDate } - ); + files, + completedDate + }); const points = alreadyCompleted ? user.points : user.points + 1; - return user.update$(updateData) + return user + .update$(updateData) .doOnNext(() => user.manualReload()) .doOnNext(({ count }) => log('%s documents updated', count)) .map(() => { @@ -237,15 +248,13 @@ export default function(app) { return res.sendStatus(403); } - return req.user.getCompletedChallenges$() + return req.user + .getCompletedChallenges$() .flatMap(() => { const completedDate = Date.now(); const { id, solution, timezone, files } = req.body; - const { - alreadyCompleted, - updateData - } = buildUserUpdate( + const { alreadyCompleted, updateData } = buildUserUpdate( req.user, id, { id, solution, completedDate, files }, @@ -255,7 +264,8 @@ export default function(app) { const user = req.user; const points = alreadyCompleted ? user.points : user.points + 1; - return user.update$(updateData) + return user + .update$(updateData) .doOnNext(({ count }) => log('%s documents updated', count)) .map(() => { if (type === 'json') { @@ -289,36 +299,38 @@ export default function(app) { const { user, body = {} } = req; - const completedChallenge = _.pick( - body, - [ 'id', 'solution', 'githubLink', 'challengeType', 'files' ] - ); + const completedChallenge = _.pick(body, [ + 'id', + 'solution', + 'githubLink', + 'challengeType', + 'files' + ]); completedChallenge.completedDate = Date.now(); if ( !completedChallenge.solution || // only basejumps require github links - ( - completedChallenge.challengeType === 4 && - !completedChallenge.githubLink - ) + (completedChallenge.challengeType === 4 && !completedChallenge.githubLink) ) { req.flash( '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 user.getCompletedChallenges$() + return user + .getCompletedChallenges$() .flatMap(() => { - const { - alreadyCompleted, - updateData - } = buildUserUpdate(user, completedChallenge.id, completedChallenge); + const { alreadyCompleted, updateData } = buildUserUpdate( + user, + completedChallenge.id, + completedChallenge + ); - return user.update$(updateData) + return user + .update$(updateData) .doOnNext(() => user.manualReload()) .doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(() => { @@ -352,21 +364,20 @@ export default function(app) { const { user, body = {} } = req; - const completedChallenge = _.pick( - body, - [ 'id', 'solution' ] - ); + const completedChallenge = _.pick(body, ['id', 'solution']); completedChallenge.completedDate = Date.now(); - - return user.getCompletedChallenges$() + return user + .getCompletedChallenges$() .flatMap(() => { - const { - alreadyCompleted, - updateData - } = buildUserUpdate(user, completedChallenge.id, completedChallenge); + const { alreadyCompleted, updateData } = buildUserUpdate( + user, + completedChallenge.id, + completedChallenge + ); - return user.update$(updateData) + return user + .update$(updateData) .doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(() => { if (type === 'json') { @@ -382,27 +393,22 @@ export default function(app) { .subscribe(() => {}, next); } - function redirectToCurrentChallenge(req, res, next) { + async function redirectToCurrentChallenge(req, res, next) { const { user } = req; const challengeId = user && user.currentChallengeId; - return getChallengeById(map, challengeId) - .map(challenge => { - const { block, dashedName, superBlock } = challenge; - if (!dashedName || !block) { - // this should normally not be hit if database is properly seeded - throw new Error(dedent` - Attempted to find '${dashedName}' - from '${ challengeId || 'no challenge id found'}' - but came up empty. - db may not be properly seeded. - `); - } - return `${learnURL}/${dasherize(superBlock)}/${block}/${dashedName}`; - }) - .subscribe( - redirect => res.redirect(redirect || learnURL), - next - ); + log(req.user.username); + log(challengeId); + const challengeUrl = await challengeUrlResolver(challengeId).catch(next); + log(challengeUrl); + if (challengeUrl === '/learn') { + // this should normally not be hit if database is properly seeded + throw new Error(dedent` + Attempted to find the url for ${challengeId}' + but came up empty. + db may not be properly seeded. + `); + } + return res.redirect(`${homeLocation}${challengeUrl}`); } function redirectToLearn(req, res) { @@ -413,4 +419,5 @@ export default function(app) { } return res.status(302).redirect(learnURL); } + done(); } diff --git a/curriculum/getChallenges.js b/curriculum/getChallenges.js index f68826e0a0..0173784775 100644 --- a/curriculum/getChallenges.js +++ b/curriculum/getChallenges.js @@ -4,6 +4,8 @@ const readDirP = require('readdirp-walk'); const { parseMarkdown } = require('@freecodecamp/challenge-md-parser'); +const { dasherize } = require('./utils'); + const challengesDir = path.resolve(__dirname, './challenges'); exports.getChallengesForLang = function getChallengesForLang(lang) { @@ -57,6 +59,7 @@ async function buildCurriculum(file, curriculum) { ); const { name: blockName, order, superOrder } = meta; challenge.block = blockName; + challenge.dashedName = dasherize(challenge.title); challenge.order = order; challenge.superOrder = superOrder; challenge.superBlock = superBlock; diff --git a/tools/scripts/seed/seedChallenges.js b/tools/scripts/seed/seedChallenges.js index 02b635c295..e34beccf57 100644 --- a/tools/scripts/seed/seedChallenges.js +++ b/tools/scripts/seed/seedChallenges.js @@ -1,7 +1,7 @@ const path = require('path'); const fs = require('fs'); require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') }); -const MongoClient = require('mongodb').MongoClient; +const { MongoClient, ObjectID } = require('mongodb'); const { getChallengesForLang } = require('@freecodecamp/curriculum'); const { flatten } = require('lodash'); const debug = require('debug'); @@ -9,7 +9,7 @@ const debug = require('debug'); const { createPathMigrationMap } = require('./createPathMigrationMap'); 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) { if (err) { @@ -32,14 +32,16 @@ MongoClient.connect( function(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 challenges = db.collection('challenge'); + const challengeCollection = db.collection('challenge'); - challenges.deleteMany({}, err => { + challengeCollection.deleteMany({}, err => { handleError(err, client); + log('deleted all the challenges'); + const curriculum = getChallengesForLang(lang); const allChallenges = Object.keys(curriculum) @@ -51,18 +53,25 @@ MongoClient.connect( return [...challengeArray, ...flatten(challengesForBlock)]; }, []) .map(challenge => { - challenge._id = challenge.id.slice(0); + const currentId = challenge.id.slice(0); + challenge._id = ObjectID(currentId); delete challenge.id; return challenge; }); try { - challenges.insertMany(allChallenges, { ordered: false }); + challengeCollection.insertMany( + allChallenges, + { ordered: false }, + err => { + handleError(err, client); + log('challenge seed complete'); + client.close(); + } + ); } catch (e) { handleError(e, client); } finally { - log('challenge seed complete'); - client.close(); log('generating path migration map'); const pathMap = createPathMigrationMap(curriculum); const outputDir = path.resolve(