From 967f92e6f42aee5fa0bf752e9ad41c0a77fba8f7 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 9 Aug 2015 22:14:31 -0700 Subject: [PATCH] fix challenge naming scheme --- common/models/challenge.json | 3 + seed/index.js | 5 + server/boot/challenge.js | 160 +++++++++++++++++----------- server/utils/index.js | 11 -- server/views/challengeMap/show.jade | 17 ++- 5 files changed, 116 insertions(+), 80 deletions(-) diff --git a/common/models/challenge.json b/common/models/challenge.json index a6b71d5a36..743332107d 100644 --- a/common/models/challenge.json +++ b/common/models/challenge.json @@ -25,6 +25,9 @@ "dashedName": { "type": "string" }, + "block": { + "type": "string" + }, "difficulty": { "type": "string" }, diff --git a/seed/index.js b/seed/index.js index b2a4e3e84c..3dbbf9d869 100644 --- a/seed/index.js +++ b/seed/index.js @@ -39,6 +39,8 @@ Challenge.destroyAll(function(err, info) { challenges.forEach(function(file) { var challengeSpec = require('./challenges/' + file); var order = challengeSpec.order; + var block = challengeSpec.name; + var challenges = challengeSpec.challenges .map(function(challenge, index) { // NOTE(berks): add title for displaying in views @@ -46,11 +48,14 @@ Challenge.destroyAll(function(err, info) { _.capitalize(challenge.type) + ': ' + challenge.title.replace(/[^a-zA-Z0-9\s]/g, ''); + challenge.dashedName = challenge.name .toLowerCase() .replace(/\:/g, '') .replace(/\s/g, '-'); challenge.order = +('' + order + (index + 1)); + challenge.block = block; + return challenge; }); diff --git a/server/boot/challenge.js b/server/boot/challenge.js index d88f2acec0..1fc4342def 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -1,17 +1,24 @@ -var R = require('ramda'), - Rx = require('rx'), - assign = require('object.assign'), - debug = require('debug')('freecc:challenges'), - utils = require('../utils'), +import _ from 'lodash'; +import moment from 'moment'; +import R from 'ramda'; +import { Observable } from 'rx'; +import assign from 'object.assign'; +import debugFactory from 'debug'; +import utils from '../utils'; - // this would be so much cleaner with destructering... - saveUser = require('../utils/rx').saveUser, - observableQueryFromModel = require('../utils/rx').observableQueryFromModel, +import { + saveUser, + observeMethod, + observableQueryFromModel +} from '../utils/rx'; - userMigration = require('../utils/middleware').userMigration, - ifNoUserRedirectTo = require('../utils/middleware').ifNoUserRedirectTo, - ifNoUserSend = require('../utils/middleware').ifNoUserSend; +import { + userMigration, + ifNoUserRedirectTo, + ifNoUserSend +} from '../utils/middleware'; +const debug = debugFactory('freecc:challenges'); var challengeMapWithNames = utils.getChallengeMapWithNames(); var challengeMapWithIds = utils.getChallengeMapWithIds(); var challengeMapWithDashedNames = utils.getChallengeMapWithDashedNames(); @@ -22,6 +29,10 @@ var unDasherize = utils.unDasherize; var getMDNLinks = utils.getMDNLinks; +function numberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + function updateUserProgress(user, challengeId, completedChallenge) { var alreadyCompleted = user.completedChallenges.some(({ id }) => { return id === challengeId; @@ -37,12 +48,18 @@ function updateUserProgress(user, challengeId, completedChallenge) { } module.exports = function(app) { - var router = app.loopback.Router(); - var Challenge = app.models.Challenge; - var User = app.models.User; - var redirectNonUser = - ifNoUserRedirectTo('/challenges/learn-how-free-code-camp-works'); - var send200toNonUser = ifNoUserSend(true); + const router = app.loopback.Router(); + + const Challenge = app.models.Challenge; + const findChallenge$ = observeMethod(Challenge, 'find'); + + const User = app.models.User; + const userCount$ = observeMethod(User, 'count'); + + const send200toNonUser = ifNoUserSend(true); + const redirectNonUser = ifNoUserRedirectTo( + '/challenges/learn-how-free-code-camp-works' + ); router.post( '/completed-challenge/', @@ -182,7 +199,7 @@ module.exports = function(app) { .map(function(key) { return challengeMapWithIds[key] .filter(function(elem) { - return elem === ('' + challenge.id); + return elem === '' + challenge.id; }) .map(function() { return key; @@ -266,7 +283,7 @@ module.exports = function(app) { } }) .withLatestFrom( - Rx.Observable.just(req.user), + Observable.just(req.user), function(pairedWith, user) { return { user: user, @@ -289,7 +306,7 @@ module.exports = function(app) { // not iterate users .flatMap(function(dats) { debug('flatmap'); - return Rx.Observable.from([dats.user, dats.pairedWith]); + return Observable.from([dats.user, dats.pairedWith]); }) // save user .flatMap(function(user) { @@ -392,7 +409,7 @@ module.exports = function(app) { ); } }) - .withLatestFrom(Rx.Observable.just(req.user), function(pairedWith, user) { + .withLatestFrom(Observable.just(req.user), function(pairedWith, user) { return { user: user, pairedWith: pairedWith @@ -408,7 +425,7 @@ module.exports = function(app) { ); }) .flatMap(function({ user, pairedWith }) { - return Rx.Observable.from([user, pairedWith]); + return Observable.from([user, pairedWith]); }) // save users .flatMap(function(user) { @@ -428,50 +445,73 @@ module.exports = function(app) { ); } - function challengeMap(req, res, next) { - var completedList = []; + function challengeMap({ user = {} }, res, next) { + const daysRunning = moment().diff(new Date('10/15/2014'), 'days'); - if (req.user) { - completedList = req.user.completedChallenges; - } + // if user + // get the id's of all the users completed challenges + const completedChallenges = !user.completedChallenges ? + [] : + _.uniq(user.completedChallenges).map(({ id }) => id); - var noDuplicatedChallenges = R.uniq(completedList); + const camperCount$ = userCount$() + .map(camperCount => numberWithCommas(camperCount)); - var completedChallengeList = noDuplicatedChallenges - .map(function(challenge) { - // backwards compatibility - return (challenge.id || challenge._id); - }); - var challengeList = utils. - getChallengeMapForDisplay(completedChallengeList); + const query = { + order: 'order ASC' + }; - Object.keys(challengeList).forEach(function(key) { - challengeList[key].completed = challengeList[key] - .challenges.filter(function(elem) { - // backwards compatibility hack - return completedChallengeList.indexOf(elem.id || elem._id) > -1; - }); - }); + // create a stream of all the challenges + const challenge$ = findChallenge$(query) + .flatMap(challenges => Observable.from(challenges)) + .shareReplay(); - function numberWithCommas(x) { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - } + // create a stream of an array of all the challenge blocks + const blocks$ = challenge$ + // mark challenge completed + .map(challenge => { + 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; + }); - var date1 = new Date('10/15/2014'); - var date2 = new Date(); - var timeDiff = Math.abs(date2.getTime() - date1.getTime()); - var daysRunning = Math.ceil(timeDiff / (1000 * 3600 * 24)); + return { + name: blockArray[0].block, + dashedName: dasherize(blockArray[0].block), + challenges: blockArray, + completed: completedCount / blockArray.length * 100 + }; + }) + // turn stream of blocks into a stream of an array + .toArray(); - User.count(function(err, camperCount) { - if (err) { return next(err); } - - res.render('challengeMap/show', { - daysRunning: daysRunning, - camperCount: numberWithCommas(camperCount), - title: "A map of all Free Code Camp's Challenges", - challengeList: challengeList, - completedChallengeList: completedChallengeList - }); - }); + 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 + ); } }; diff --git a/server/utils/index.js b/server/utils/index.js index 2fd19d05c1..72b37da98b 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -19,17 +19,6 @@ var allNonprofitNames, challengeMapWithNames, allChallengeIds, challengeMapWithDashedNames; -Array.zip = function(left, right, combinerFunction) { - var counter, - results = []; - - for (counter = 0; counter < Math.min(left.length, right.length); counter++) { - results.push(combinerFunction(left[counter], right[counter])); - } - - return results; -}; - (function() { if (!challengeMap) { var localChallengeMap = {}; diff --git a/server/views/challengeMap/show.jade b/server/views/challengeMap/show.jade index 77b42ca672..22927bdedb 100644 --- a/server/views/challengeMap/show.jade +++ b/server/views/challengeMap/show.jade @@ -1,8 +1,7 @@ extends ../layout block content script. - var completedChallenges = !{JSON.stringify(completedChallengeList)}; - var challengeList = !{JSON.stringify(challengeList)}; + var challenges = !{JSON.stringify(challenges)}; .bg-danger.default-border-radius p      a(href='https://github.com/FreeCodeCamp/freecodecamp/wiki/beta' target='_blank') You're using our experimental beta site. None of your progress here will be saved. Please click this to learn more. @@ -28,10 +27,10 @@ block content .col-xs-12.col-sm-8.col-sm-offset-2 h3 800 Hours of Practice: ol - for challengeBlock in challengeList + for challengeBlock in blocks .row if (user) - if (challengeBlock.completed.length === challengeBlock.challenges.length) + if (challengeBlock.completed === 100) .hidden-xs.col-sm-3.col-md-2.text-primary.ion-checkmark-circled.padded-ionic-icon.text-center.large-p.negative-10 .col-xs-12.col-sm-9.col-md-10 li.large-p.faded.negative-10 @@ -39,7 +38,7 @@ block content else .hidden-xs.col-sm-3.col-md-2 .progress.progress-bar-padding.text-center.thin-progress-bar - .progress-bar(role='progressbar', aria-valuenow=((challengeBlock.completed.length / challengeBlock.challenges.length) * 100), aria-valuemin='0', aria-valuemax='100', style='width: ' + ((challengeBlock.completed.length / challengeBlock.challenges.length) * 100) + '%;') + .progress-bar(role='progressbar', aria-valuenow=(challengeBlock.completed), aria-valuemin='0', aria-valuemax='100', style='width: ' + challengeBlock.completed + '%;') .col-xs-12.col-sm-9.col-md-10 li.large-p.negative-10 a(href='#' + challengeBlock.dashedName)= challengeBlock.name @@ -74,7 +73,7 @@ block content li.large-p.negative-10 300-hour Nonprofit Project hr - for challengeBlock in challengeList + for challengeBlock in blocks .row a(href='#' name=challengeBlock.dashedName) .spacer.negative-55 @@ -86,12 +85,12 @@ block content .col-xs-12 ol for challenge in challengeBlock.challenges - if completedChallengeList.indexOf(challenge.id || challenge._id) > -1 + if challenge.completed .row .hidden-xs.col-sm-3.col-md-2.text-primary.ion-checkmark-circled.padded-ionic-icon.text-center.large-p.negative-10 .col-xs-12.col-sm-9.col-md-10 li.faded.large-p.negative-10 - a(href="/challenges/#{challenge.dashedName}")= challenge.name + a(href="/challenges/#{challenge.dashedName}")= challenge.title else .row @@ -99,7 +98,7 @@ block content span.negative-10 .col-xs-12.col-sm-9.col-md-10 li.large-p.negative-10 - a(href="/challenges/#{challenge.dashedName}")= challenge.name + a(href="/challenges/#{challenge.dashedName}")= challenge.title //#announcementModal.modal(tabindex='-1') // .modal-dialog.animated.fadeInUp.fast-animation