diff --git a/package.json b/package.json index 4863f4d47a..7fe7c38bd8 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "node-slack": "0.0.7", "node-uuid": "^1.4.3", "nodemailer": "~1.3.0", + "object.assign": "^3.0.0", "passport-facebook": "^2.0.0", "passport-google-oauth2": "^0.1.6", "passport-linkedin-oauth2": "^1.2.1", diff --git a/public/js/calculator.js b/public/js/calculator.js index 8786b78bea..9cd991a9e7 100644 --- a/public/js/calculator.js +++ b/public/js/calculator.js @@ -154,6 +154,10 @@ $(document).ready(function () { }, 1000); }); + d3.selectAll("#chart").on("click", function () { + change(); + }); + function change() { if ($("body").data("state") === "stacked") { transitionGrouped(); diff --git a/public/js/lib/coursewares/coursewaresJSFramework_0.0.6.js b/public/js/lib/coursewares/coursewaresJSFramework_0.0.6.js index 16073e2554..ae7237ef5d 100644 --- a/public/js/lib/coursewares/coursewaresJSFramework_0.0.6.js +++ b/public/js/lib/coursewares/coursewaresJSFramework_0.0.6.js @@ -1,3 +1,7 @@ +$(document).ready(function() { + $('#reset-button').on('click', resetEditor); +}); + var widgets = []; var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("codeEditor"), { lineNumbers: true, @@ -41,73 +45,90 @@ editor.setOption("extraKeys", { /* - Local Storage Update System By Andrew Cay(Resto) - localBonfire: singleton object that contains properties and methods related to - dealing with the localStorage system. - The keys work off of the variable challenge_name to make unique identifiers per bonfire + Local Storage Update System By Andrew Cay(Resto) + codeStorage: singleton object that contains properties and methods related to + dealing with the localStorage system. + The keys work off of the variable challenge_name to make unique identifiers per bonfire - Two extra functionalities: - Added anonymous version checking system incase of future updates to the system - Added keyup listener to editor(myCodeMirror) so the last update has been saved to storage + Two extra functionalities: + Added anonymous version checking system incase of future updates to the system + Added keyup listener to editor(myCodeMirror) so the last update has been saved to storage */ -var localBonfire = { - version: 0.01, - keyVersion:"saveVersion", - keyStamp: challenge_Name + 'Stamp', - keyValue: challenge_Name + 'Val', - stampExpireTime: (1000 *60) *60 *24, - updateWait: 1500,// 1.5 seconds - updateTimeoutId: null +var codeStorage = { + version: 0.01, + keyVersion:"saveVersion", + keyValue: null,//where the value of the editor is saved + updateWait: 2000,// 2 seconds + updateTimeoutId: null, + eventArray: []//for firing saves }; -localBonfire.getEditorValue = function(){ - return localStorage.getItem(localBonfire.keyValue); +// Returns true if the editor code was saved since last key press (use this if you want to make a "saved" notification somewhere") +codeStorage.hasSaved = function(){ + return ( updateTimeoutId === null ); }; -localBonfire.getStampTime = function(){ - //localstorage always saves as strings. - return Number.parseInt( localStorage.getItem(localBonfire.keyStamp) ); +codeStorage.onSave = function(func){ + codeStorage.eventArray.push(func); }; -localBonfire.isAlive = function(){// returns true if IDE was edited within expire time - return ( Date.now() - localBonfire.getStampTime() < localBonfire.stampExpireTime ); +codeStorage.setSaveKey = function(key){ + codeStorage.keyValue = key + 'Val'; }; -localBonfire.updateStorage = function(){ - if(typeof(Storage) !== undefined) { - var stamp = Date.now(), - value = editor.getValue(); - localStorage.setItem(localBonfire.keyValue, value); - localStorage.setItem(localBonfire.keyStamp, stamp); - } else { - if( debugging ){ - console.log('no web storage'); - } - } - localBonfire.updateTimeoutId = null; +codeStorage.getEditorValue = function(){ + return ('' + localStorage.getItem(codeStorage.keyValue)); }; -// ANONYMOUS 1 TIME UPDATE VERSION + +codeStorage.isAlive = function() { + var val = this.getEditorValue() + return val !== 'null' && + val !== 'undefined' && + (val && val.length > 0); +} +codeStorage.updateStorage = function(){ + if(typeof(Storage) !== undefined) { + var value = editor.getValue(); + localStorage.setItem(codeStorage.keyValue, value); + } else { + var debugging = false; + if( debugging ){ + console.log('no web storage'); + } + } + codeStorage.updateTimeoutId = null; + codeStorage.eventArray.forEach(function(func){ + func(); + }); +}; +//Update Version (function(){ - var savedVersion = localStorage.getItem('saveVersion'); - if( savedVersion === null ){ - localStorage.setItem(localBonfire.keyVersion, localBonfire.version);//just write current version - }else{ - //do checking if not current version - if( savedVersion !== localBonfire.version ){ - //update version - } - } + var savedVersion = localStorage.getItem('saveVersion'); + if( savedVersion === null ){ + localStorage.setItem(codeStorage.keyVersion, codeStorage.version);//just write current version + }else{ + if( savedVersion !== codeStorage.version ){ + //Update version + } + } })(); -editor.on('keyup', function(codMir, event){ - window.clearTimeout(localBonfire.updateTimeoutId); - localBonfire.updateTimeoutId = window.setTimeout(localBonfire.updateStorage, localBonfire.updateWait); + + +///Set everything up one page +/// Update local save when editor has changed +codeStorage.setSaveKey(challenge_Name); +editor.on('keyup', function(){ + window.clearTimeout(codeStorage.updateTimeoutId); + codeStorage.updateTimeoutId = window.setTimeout(codeStorage.updateStorage, codeStorage.updateWait); }); + var attempts = 0; if (attempts) { attempts = 0; } -var resetEditor = function() { +var resetEditor = function resetEditor() { editor.setValue(allSeeds); - localBonfire.updateStorage(); + codeStorage.updateStorage(); + }; var codeOutput = CodeMirror.fromTextArea(document.getElementById("codeOutput"), { @@ -141,11 +162,11 @@ var tests = tests || []; var allSeeds = ''; (function() { challengeSeed.forEach(function(elem) { - allSeeds += elem + '\n'; + allSeeds += elem + '\n'; }); })(); -editorValue = (localBonfire.isAlive())? localBonfire.getEditorValue() : allSeeds; +editorValue = (codeStorage.isAlive())? codeStorage.getEditorValue() : allSeeds; myCodeMirror.setValue(editorValue); diff --git a/public/js/main_0.0.2.js b/public/js/main_0.0.2.js index 7fce4bdaba..1b41b9066b 100644 --- a/public/js/main_0.0.2.js +++ b/public/js/main_0.0.2.js @@ -331,7 +331,6 @@ $(document).ready(function() { $('#story-submit').on('click', storySubmitButtonHandler); - $('#reset-button').on('click', resetEditor); var commentSubmitButtonHandler = function commentSubmitButtonHandler() { $('#comment-button').unbind('click'); diff --git a/seed/bonfireMDNlinks.js b/seed/bonfireMDNlinks.js index 9a9ea86741..bedd51b2db 100644 --- a/seed/bonfireMDNlinks.js +++ b/seed/bonfireMDNlinks.js @@ -12,6 +12,7 @@ var links = "Currying": "https://leanpub.com/javascript-allonge/read#pabc", "Smallest Common Multiple": "https://www.mathsisfun.com/least-common-multiple.html", "Permutations": "https://www.mathsisfun.com/combinatorics/combinations-permutations.html", + "HTML Entities": "http://dev.w3.org/html5/html-author/charref", // ========= GLOBAL OBJECTS "Global Array Object" : "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array", diff --git a/seed/challenges/basic-bonfires.json b/seed/challenges/basic-bonfires.json index 6c8952a980..ae9b0b571a 100644 --- a/seed/challenges/basic-bonfires.json +++ b/seed/challenges/basic-bonfires.json @@ -1097,7 +1097,7 @@ "dashedName": "bonfire-convert-html-entities", "difficulty": "2.07", "description": [ - "Convert the characters \"&\", \"<\", \">\", '\"', and \"'\", in a string to their corresponding HTML entities.", + "Convert the characters \"&\", \"<\", \">\", '\"' (double quote), and \"'\" (apostrophe), in a string to their corresponding HTML entities.", "Remember to use RSAP if you get stuck. Try to pair program. Write your own code." ], "challengeSeed": [ @@ -1114,10 +1114,12 @@ "assert.strictEqual(convert('Sixty > twelve'), 'Sixty > twelve', 'should escape characters');", "assert.strictEqual(convert('Stuff in \"quotation marks\"'), 'Stuff in "quotation marks"', 'should escape characters');", "assert.strictEqual(convert(\"Shindler's List\"), 'Shindler's List', 'should escape characters');", + "assert.strictEqual(convert('<>'), '<>', 'should escape characters');", "assert.strictEqual(convert('abc'), 'abc', 'should handle strings with nothing to escape');" ], "MDNlinks": [ - "RegExp" + "RegExp", + "HTML Entities" ], "challengeType": 5, "nameCn": "", diff --git a/seed/challenges/basic-html5-and-css.json b/seed/challenges/basic-html5-and-css.json index 52bd6f49b2..fb8c4aa952 100644 --- a/seed/challenges/basic-html5-and-css.json +++ b/seed/challenges/basic-html5-and-css.json @@ -1098,7 +1098,8 @@ "In addition to pixels, you can also specify a border-radius using a percentage." ], "tests": [ - "assert(parseInt($('img').css('border-top-left-radius')) > 48, 'Your image should have a border radius of 50 percent, making it perfectly circular.')" + "assert(parseInt($('img').css('border-top-left-radius')) > 48, 'Your image should have a border radius of 50 percent, making it perfectly circular.')", + "assert(editor.match(/50%/g), 'Be sure to use a percentage instead of a pixel value.')" ], "challengeSeed": [ "", @@ -1756,7 +1757,7 @@ ], "tests": [ "assert($('input[placeholder]').length > 0, 'Add a placeholder attribute text input element.')", - "assert($('input').attr('placeholder').match(/cat\\s+photo\\s+URL/gi), 'Set the value of your placeholder attribute to \"cat photo URL\".')" + "assert($('input') && $('input').attr('placeholder') && $('input').attr('placeholder').match(/cat\\s+photo\\s+URL/gi), 'Set the value of your placeholder attribute to \"cat photo URL\".')" ], "challengeSeed": [ "", @@ -1835,7 +1836,7 @@ "For example: <form action=\"/url-where-you-want-to-submit-form-data\"></form>." ], "tests": [ - "assert($('form').length > 0, 'Wrap your text input element within a form element.')", + "assert($('form') && $('form').children('input') && $('form').children('input').length > 0, 'Wrap your text input element within a form element.')", "assert($('form').attr('action'), 'Your form element should have an action attribute.')", "assert(editor.match(/<\\/form>/g) && editor.match(/
/g).length === editor.match(/form element has a closing tag.')", "assert(editor.match(/\\/submit-cat-photo/ig), 'Make sure your form action is set to /submit-cat-photo.')" diff --git a/seed/challenges/bootstrap.json b/seed/challenges/bootstrap.json index 9f2b704153..41a7229731 100644 --- a/seed/challenges/bootstrap.json +++ b/seed/challenges/bootstrap.json @@ -1264,7 +1264,7 @@ "tests": [ "assert($('button[type=\\'submit\\']').hasClass('btn btn-primary'), 'Give the submit button in your form the classes \"btn btn-primary\".')", "assert($('button[type=\\'submit\\']:has(i.fa.fa-paper-plane)').length > 0, 'Add a <i class=\"fa fa-paper-plane\"></i> within your submit button element.')", - "assert($('input[type=\\'text\\']').hasClass('form-control'), 'Give the text input in your form the class \"form-control\".')", + "assert($('input[type=\\'text\\']').hasClass('form-control'), 'Give the text input in your form the class \"form-control\".')", "assert(editor.match(/<\\/i>/g) && editor.match(/<\\/i/g).length > 3, 'Make sure each of your i elements has a closing tag.')" ], "challengeSeed": [ diff --git a/seed/challenges/get-set-for-free-code-camp.json b/seed/challenges/get-set-for-free-code-camp.json index 83116e1c7f..b576bad9c9 100644 --- a/seed/challenges/get-set-for-free-code-camp.json +++ b/seed/challenges/get-set-for-free-code-camp.json @@ -56,18 +56,20 @@ "name": "Waypoint: Join Our Chat Room", "dashedName": "waypoint-join-our-chat-room", "difficulty": 0.002, - "challengeSeed": ["124555254"], + "challengeSeed": ["131321596"], "description": [ "Now we're going to join the Free Code Camp chat room. You can come here any time of day to hang out, ask questions, or find another camper to pair program with.", - "Make sure your Free Code Camp account includes your email address. Please note that the email address you use will be invisible to the public, but Slack will make it visible to other campers in our slack chat rooms. You can do this here: http://freecodecamp.com/account.", - "Click this link, which will email you can invite to Free Code Camp's Slack chat rooms: http://freecodecamp.com/api/slack.", - "Now check your email and click the link in the email from Slack.", - "Complete the sign up process, then update your biographical information and upload an image. A picture of your face works best. This is how people will see you in our chat rooms, so put your best foot forward.", - "Now enter the General chat room and introduce yourself to our chat room by typing: \"Hello world!\".", + "Create an account with GitHub here: https://github.com/join.", + "Click the pixel art in the upper right hand corner of GitHub, then choose settings. Upload a picture of yourself. A picture of your face works best. This is how people will see you in our chat rooms, so put your best foot forward. You can add your city and your personal website if you have one.", + "Now follow this link to enter our Welcome chat room: https://gitter.im/FreeCodeCamp/welcome.", + "Once you're in our Welcome chat room, introduce yourself by saying : \"Hello world!\".", "Tell your fellow campers how you found Free Code Camp. Also tell us why you want to learn to code.", + "This is the best room for new campers, but feel free to join other chat rooms as well. Our main chat room: https://gitter.im/FreeCodeCamp/FreeCodeCamp.", "Keep the chat room open while you work through the other challenges. That way you ask for help if you get stuck on a challenge. You can also socialize when you feel like taking a break.", + "You can also download a desktop or mobile chat application here: https://gitter.im/apps", "You can also access this chat room by clicking the \"Chat\" button in the upper right hand corner.", - "In order to keep our community a friendly and positive place to learn to code, please read and follow our Code of Conduct: http://freecodecamp.com/field-guide/what-is-the-free-code-camp-code-of-conduct?" + "In order to keep our community a friendly and positive place to learn to code, please read and follow our Code of Conduct: http://freecodecamp.com/field-guide/what-is-the-free-code-camp-code-of-conduct?", + "Now you're ready to move on. Click the \"I've completed this challenge\" button to move on to your next challenge." ], "challengeType": 2, "tests": [], @@ -246,7 +248,7 @@ "Click \"News\" in the upper right hand corner.", "You'll see a variety of links that have been submitted. Click on the \"Discuss\" button under one of them.", "You can upvote links. This will push the link up the rankings of hot links.", - "You an also comment on a link. If someone responds to your comment, you'll get an email notification so you can come back and respond to them.", + "You can also comment on a link. If someone responds to your comment, you'll get an email notification so you can come back and respond to them.", "You can also submit links. You can modify the link's headline and also leave an initial comment about the link.", "You can view the portfolio pages of any camper who has posted links or comments on Camper News. Just click on their photo.", "When you submit a link, you'll get a point. You will also get a point each time someone upvotes your link.", diff --git a/server/boot/challenge.js b/server/boot/challenge.js index bddf9118c4..02cecc639e 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -31,8 +31,17 @@ */ var R = require('ramda'), + Rx = require('rx'), + assign = require('object.assign'), + debug = require('debug')('freecc:challenges'), utils = require('../utils'), - userMigration = require('../utils/middleware').userMigration; + + // this would be so much cleaner with destructering... + saveUser = require('../utils/rx').saveUser, + observableQueryFromModel = require('../utils/rx').observableQueryFromModel, + + userMigration = require('../utils/middleware').userMigration, + ifNoUserRedirectTo = require('../utils/middleware').ifNoUserRedirectTo; var challengeMapWithNames = utils.getChallengeMapWithNames(); var challengeMapWithIds = utils.getChallengeMapWithIds(); @@ -40,6 +49,28 @@ var challengeMapWithDashedNames = utils.getChallengeMapWithDashedNames(); var getMDNLinks = utils.getMDNLinks; +var challangesRegex = /^(bonfire|waypoint|zipline|basejump)/i; +function dasherize(name) { + return ('' + name) + .toLowerCase() + .replace(/\s/g, '-') + .replace(/[^a-z0-9\-\.]/gi, ''); +} + +function unDasherize(name) { + return ('' + name).replace(/\-/g, ' '); +} + +function updateUserProgress(user, challengeId, completedChallenge) { + var index = user.uncompletedChallenges.indexOf(challengeId); + if (index > -1) { + user.progressTimestamps.push(Date.now()); + user.uncompletedChallenges.splice(index, 1); + } + user.completedChallenges.push(completedChallenge); + return user; +} + module.exports = function(app) { var router = app.loopback.Router(); var Challenge = app.models.Challenge; @@ -51,23 +82,30 @@ module.exports = function(app) { // the follow routes are covered by userMigration router.use(userMigration); - router.get('/challenges/next-challenge', returnNextChallenge); - router.get('/challenges/:challengeName', returnIndividualChallenge); - router.get('/challenges/', returnCurrentChallenge); router.get('/map', challengeMap); + router.get( + '/challenges/next-challenge', + ifNoUserRedirectTo('/challenges/learn-how-free-code-camp-works'), + returnNextChallenge + ); + + router.get('/challenges/:challengeName', returnIndividualChallenge); + + router.get( + '/challenges/', + ifNoUserRedirectTo('/challenges/learn-how-free-code-camp-works'), + returnCurrentChallenge + ); app.use(router); function returnNextChallenge(req, res, next) { - if (!req.user) { - return res.redirect('../challenges/learn-how-free-code-camp-works'); - } var completed = req.user.completedChallenges.map(function (elem) { return elem.id; }); req.user.uncompletedChallenges = utils.allChallengeIds() - .filter(function (elem) { + .filter(function(elem) { if (completed.indexOf(elem) === -1) { return elem; } @@ -100,18 +138,17 @@ module.exports = function(app) { nextChallengeName = R.head(challengeMapWithDashedNames[0].challenges); } - req.user.save(function(err) { - if (err) { - return next(err); - } - return res.redirect('../challenges/' + nextChallengeName); - }); + saveUser(req.user) + .subscribe( + function() {}, + next, + function() { + res.redirect('/challenges/' + nextChallengeName); + } + ); } function returnCurrentChallenge(req, res, next) { - if (!req.user) { - return res.redirect('../challenges/learn-how-free-code-camp-works'); - } var completed = req.user.completedChallenges.map(function (elem) { return elem.id; }); @@ -122,6 +159,7 @@ module.exports = function(app) { return elem; } }); + if (!req.user.currentChallenge) { req.user.currentChallenge = {}; req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0]; @@ -133,42 +171,60 @@ module.exports = function(app) { var nameString = req.user.currentChallenge.dashedName; - req.user.save(function(err) { - if (err) { - return next(err); - } - return res.redirect('../challenges/' + nameString); - }); + saveUser(req.user) + .subscribe( + function() {}, + next, + function() { + res.redirect('/challenges/' + nameString); + } + ); } function returnIndividualChallenge(req, res, next) { - var dashedName = req.params.challengeName; + var origChallengeName = req.params.challengeName; + var unDashedName = unDasherize(origChallengeName); + var challengeName = challangesRegex.test(unDashedName) ? + // remove first word if matches + unDashedName.split(' ').slice(1).join(' ') : + unDashedName; + + debug('looking for ', challengeName); Challenge.findOne( - { where: { dashedName: dashedName }}, + { where: { name: { like: challengeName, options: 'i' } } }, function(err, challenge) { if (err) { return next(err); } // Handle not found if (!challenge) { + debug('did not find challenge for ' + origChallengeName); req.flash('errors', { - msg: '404: We couldn\'t find a challenge with that name. ' + - 'Please double check the name.' + msg: + '404: We couldn\'t find a challenge with the name `' + + origChallengeName + + '` Please double check the name.' }); return res.redirect('/challenges'); } // Redirect to full name if the user only entered a partial + if (dasherize(challenge.name) !== origChallengeName) { + debug('redirecting to fullname'); + return res.redirect('/challenges/' + dasherize(challenge.name)); + } + if (req.user) { req.user.currentChallenge = { challengeId: challenge.id, challengeName: challenge.name, dashedName: challenge.dashedName, - challengeBlock: R.head(R.flatten(Object.keys(challengeMapWithIds). - map(function (key) { + challengeBlock: R.head(R.flatten(Object.keys(challengeMapWithIds) + .map(function (key) { return challengeMapWithIds[key] .filter(function (elem) { - return String(elem) === String(challenge.id); - }).map(function () { + return elem === ('' + challenge.id); + }) + .map(function () { return key; }); }) @@ -176,261 +232,169 @@ module.exports = function(app) { }; } - var challengeType = { - 0: function() { - res.render('coursewares/showHTML', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - brief: challenge.description[0], - details: challenge.description.slice(1), - tests: challenge.tests, - challengeSeed: challenge.challengeSeed, - verb: utils.randomVerb(), - phrase: utils.randomPhrase(), - compliment: utils.randomCompliment(), - challengeId: challenge.id, - environment: utils.whichEnvironment(), - challengeType: challenge.challengeType - }); - }, - - 1: function() { - res.render('coursewares/showJS', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - brief: challenge.description[0], - details: challenge.description.slice(1), - tests: challenge.tests, - challengeSeed: challenge.challengeSeed, - verb: utils.randomVerb(), - phrase: utils.randomPhrase(), - compliment: utils.randomCompliment(), - challengeId: challenge.id, - challengeType: challenge.challengeType - }); - }, - - 2: function() { - res.render('coursewares/showVideo', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - details: challenge.description, - tests: challenge.tests, - video: challenge.challengeSeed[0], - verb: utils.randomVerb(), - phrase: utils.randomPhrase(), - compliment: utils.randomCompliment(), - challengeId: challenge.id, - challengeType: challenge.challengeType - }); - }, - - 3: function() { - res.render('coursewares/showZiplineOrBasejump', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - details: challenge.description, - video: challenge.challengeSeed[0], - verb: utils.randomVerb(), - phrase: utils.randomPhrase(), - compliment: utils.randomCompliment(), - challengeId: challenge.id, - challengeType: challenge.challengeType - }); - }, - - 4: function() { - res.render('coursewares/showZiplineOrBasejump', { - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - details: challenge.description, - video: challenge.challengeSeed[0], - verb: utils.randomVerb(), - phrase: utils.randomPhrase(), - compliment: utils.randomCompliment(), - challengeId: challenge.id, - challengeType: challenge.challengeType - }); - }, - - 5: function() { - res.render('coursewares/showBonfire', { - completedWith: null, - title: challenge.name, - dashedName: dashedName, - name: challenge.name, - difficulty: Math.floor(+challenge.difficulty), - brief: challenge.description.shift(), - details: challenge.description, - tests: challenge.tests, - challengeSeed: challenge.challengeSeed, - verb: utils.randomVerb(), - phrase: utils.randomPhrase(), - compliment: utils.randomCompliment(), - bonfires: challenge, - challengeId: challenge.id, - MDNkeys: challenge.MDNlinks, - MDNlinks: getMDNLinks(challenge.MDNlinks), - challengeType: challenge.challengeType - }); - } + var commonLocals = { + title: challenge.name, + dashedName: origChallengeName, + name: challenge.name, + details: challenge.description.slice(1), + 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), + brief: challenge.description.shift(), + bonfires: challenge, + MDNkeys: challenge.MDNlinks, + MDNlinks: getMDNLinks(challenge.MDNlinks), + // htmls specific + environment: utils.whichEnvironment() }; - if (req.user) { - req.user.save(function (err) { - if (err) { - return next(err); + + var challengeView = { + 0: 'coursewares/showHTML', + 1: 'coursewares/showJS', + 2: 'coursewares/showVideo', + 3: 'coursewares/showZiplineOrBasejump', + 4: 'coursewares/showZiplineOrBasejump', + 5: 'coursewares/showBonfire' + }; + + saveUser(req.user) + .subscribe( + function() {}, + next, + function() { + var view = challengeView[challenge.challengeType]; + res.render(view, commonLocals); } - return challengeType[challenge.challengeType](); - }); - } else { - return challengeType[challenge.challengeType](); - } + ); }); } function completedBonfire(req, res, next) { - var isCompletedWith = req.body.challengeInfo.completedWith || ''; - var isCompletedDate = Math.round(+new Date()); + debug('compltedBonfire'); + var completedWith = req.body.challengeInfo.completedWith || false; var challengeId = req.body.challengeInfo.challengeId; - var isSolution = req.body.challengeInfo.solution; - var challengeName = req.body.challengeInfo.challengeName; - if (isCompletedWith) { - User.find({ - where: { 'profile.username': isCompletedWith.toLowerCase() }, - limit: 1 - }, function (err, pairedWith) { - if (err) { return next(err); } + var challengeData = { + id: challengeId, + name: req.body.challengeInfo.challengeName, + completedDate: Math.round(+new Date()), + solution: req.body.challengeInfo.solution, + challengeType: 5 + }; - var index = req.user.uncompletedChallenges.indexOf(challengeId); - if (index > -1) { - req.user.progressTimestamps.push(Date.now() || 0); - req.user.uncompletedChallenges.splice(index, 1); - } - pairedWith = pairedWith.pop(); + observableQueryFromModel( + User, + 'findOne', + { where: { username: ('' + completedWith).toLowerCase() } } + ) + .doOnNext(function(pairedWith) { + debug('paired with ', pairedWith); if (pairedWith) { - - index = pairedWith.uncompletedChallenges.indexOf(challengeId); - if (index > -1) { - pairedWith.progressTimestamps.push(Date.now() || 0); - pairedWith.uncompletedChallenges.splice(index, 1); - - } - - pairedWith.completedChallenges.push({ - id: challengeId, - name: challengeName, - completedWith: req.user.id, - completedDate: isCompletedDate, - solution: isSolution, - challengeType: 5 - }); - - req.user.completedChallenges.push({ - id: challengeId, - name: challengeName, - completedWith: pairedWith.id, - completedDate: isCompletedDate, - solution: isSolution, - challengeType: 5 - }); + updateUserProgress( + pairedWith, + challengeId, + assign({ completedWith: req.user.id }, challengeData) + ); } - // User said they paired, but pair wasn't found - req.user.completedChallenges.push({ - id: challengeId, - name: challengeName, - completedWith: null, - completedDate: isCompletedDate, - solution: isSolution, - challengeType: 5 - }); - - req.user.save(function (err, user) { - if (err) { return next(err); } - - if (pairedWith) { - pairedWith.save(function (err, paired) { - if (err) { - return next(err); - } - if (user && paired) { - return res.send(true); - } - }); - } else if (user) { - res.send(true); + }) + .withLatestFrom( + Rx.Observable.just(req.user), + function(pairedWith, user) { + debug('yo'); + 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 + ); + }) + // not iterate users + .flatMap(function(dats) { + debug('flatmap'); + return Rx.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); } - }); - }); - } else { - req.user.completedChallenges.push({ - id: challengeId, - name: challengeName, - completedWith: null, - completedDate: isCompletedDate, - solution: isSolution, - challengeType: 5 - }); - - var index = req.user.uncompletedChallenges.indexOf(challengeId); - if (index > -1) { - - req.user.progressTimestamps.push(Date.now() || 0); - req.user.uncompletedChallenges.splice(index, 1); - } - - req.user.save(function (err) { - if (err) { return next(err); } - res.send(true); - }); - } + }, + next, + function() { + debug('completed'); + return res.status(200).send(true); + } + ); } function completedChallenge(req, res, next) { - var isCompletedDate = Math.round(+new Date()); + var completedDate = Math.round(+new Date()); var challengeId = req.body.challengeInfo.challengeId; - req.user.completedChallenges.push({ - id: challengeId, - completedDate: isCompletedDate, - name: req.body.challengeInfo.challengeName, - solution: null, - githubLink: null, - verified: true - }); - var index = req.user.uncompletedChallenges.indexOf(challengeId); - - if (index > -1) { - req.user.progressTimestamps.push(Date.now() || 0); - req.user.uncompletedChallenges.splice(index, 1); - } - - req.user.save(function (err, user) { - if (err) { - return next(err); + updateUserProgress( + req.user, + challengeId, + { + id: challengeId, + completedDate: completedDate, + name: req.body.challengeInfo.challengeName, + solution: null, + githubLink: null, + verified: true } - if (user) { - res.sendStatus(200); - } - }); + ); + + saveUser(req.user) + .subscribe( + function() { }, + next, + function() { + res.sendStatus(200); + } + ); } function completedZiplineOrBasejump(req, res, next) { - var isCompletedWith = req.body.challengeInfo.completedWith || false; - var isCompletedDate = Math.round(+new Date()); + var completedWith = req.body.challengeInfo.completedWith || false; + var completedDate = Math.round(+new Date()); var challengeId = req.body.challengeInfo.challengeId; var solutionLink = req.body.challengeInfo.publicURL; - var githubLink = req.body.challengeInfo.challengeType === '4' - ? req.body.challengeInfo.githubURL : true; + + var githubLink = req.body.challengeInfo.challengeType === '4' ? + req.body.challengeInfo.githubURL : + true; + var challengeType = req.body.challengeInfo.challengeType === '4' ? - 4 : 3; + 4 : + 3; + if (!solutionLink || !githubLink) { req.flash('errors', { msg: 'You haven\'t supplied the necessary URLs for us to inspect ' + @@ -439,93 +403,64 @@ module.exports = function(app) { return res.sendStatus(403); } - if (isCompletedWith) { - User.find({ - where: { 'profile.username': isCompletedWith.toLowerCase() }, - limit: 1 - }, function (err, pairedWithFromMongo) { - if (err) { return next(err); } - var index = req.user.uncompletedChallenges.indexOf(challengeId); - if (index > -1) { - req.user.progressTimestamps.push(Date.now() || 0); - req.user.uncompletedChallenges.splice(index, 1); + var challengeData = { + id: challengeId, + name: req.body.challengeInfo.challengeName, + completedDate: completedDate, + solution: solutionLink, + githubLink: githubLink, + challengeType: challengeType, + verified: false + }; + + observableQueryFromModel( + User, + 'findOne', + { where: { username: completedWith.toLowerCase() } } + ) + .doOnNext(function(pairedWith) { + if (pairedWith) { + updateUserProgress( + pairedWith, + challengeId, + assign({ completedWith: req.user.id }, challengeData) + ); } - var pairedWith = pairedWithFromMongo.pop(); - - req.user.completedChallenges.push({ - id: challengeId, - name: req.body.challengeInfo.challengeName, - completedWith: pairedWith.id, - completedDate: isCompletedDate, - solution: solutionLink, - githubLink: githubLink, - challengeType: challengeType, - verified: false - }); - - req.user.save(function (err, user) { - if (err) { return next(err); } - - if (req.user.id.toString() === pairedWith.id.toString()) { - return res.sendStatus(200); + }) + .withLatestFrom(Rx.Observable.just(req.user), function(user, pairedWith) { + return { + user: user, + pairedWith: pairedWith + }; + }) + .doOnNext(function(dats) { + updateUserProgress( + dats.user, + challengeId, + dats.pairedWith ? + assign({ completedWith: dats.pairedWith.id }, challengeData) : + challengeData + ); + }) + .flatMap(function(dats) { + return Rx.Observable.from([dats.user, dats.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); } - index = pairedWith.uncompletedChallenges.indexOf(challengeId); - if (index > -1) { - pairedWith.progressTimestamps.push(Date.now() || 0); - pairedWith.uncompletedChallenges.splice(index, 1); - - } - - pairedWith.completedChallenges.push({ - id: challengeId, - name: req.body.challengeInfo.coursewareName, - completedWith: req.user.id, - completedDate: isCompletedDate, - solution: solutionLink, - githubLink: githubLink, - challengeType: challengeType, - verified: false - }); - pairedWith.save(function (err, paired) { - if (err) { - return next(err); - } - if (user && paired) { - return res.sendStatus(200); - } - }); - }); - }); - } else { - - req.user.completedChallenges.push({ - id: challengeId, - name: req.body.challengeInfo.challengeName, - completedWith: null, - completedDate: isCompletedDate, - solution: solutionLink, - githubLink: githubLink, - challengeType: challengeType, - verified: false - }); - - var index = req.user.uncompletedChallenges.indexOf(challengeId); - if (index > -1) { - req.user.progressTimestamps.push(Date.now() || 0); - req.user.uncompletedChallenges.splice(index, 1); - } - - req.user.save(function (err, user) { - if (err) { - return next(err); + }, + next, + function() { + return res.status(200).send(true); } - // NOTE(berks): under certain conditions this will not close - // the response. - if (user) { - return res.sendStatus(200); - } - }); - } + ); } function challengeMap(req, res, next) { diff --git a/server/boot/randomAPIs.js b/server/boot/randomAPIs.js index 7dd89b1b4f..7b658a36df 100644 --- a/server/boot/randomAPIs.js +++ b/server/boot/randomAPIs.js @@ -15,7 +15,7 @@ module.exports = function(app) { var router = app.loopback.Router(); var User = app.models.User; var Challenge = app.models.Challenge; - var Story = app.models.Store; + var Story = app.models.Story; var FieldGuide = app.models.FieldGuide; var Nonprofit = app.models.Nonprofit; @@ -193,15 +193,15 @@ module.exports = function(app) { users: function(callback) { User.find( { - where: { 'profile.username': { nlike: '' } }, - fields: { 'profile.username': true } + where: { username: { nlike: '' } }, + fields: { username: true } }, function(err, users) { if (err) { debug('User err: ', err); callback(err); } else { - Rx.Observable.from(users) + Rx.Observable.from(users, null, null, Rx.Scheduler.default) .map(function(user) { return user.username; }) @@ -224,7 +224,7 @@ module.exports = function(app) { debug('Challenge err: ', err); callback(err); } else { - Rx.Observable.from(challenges) + Rx.Observable.from(challenges, null, null, Rx.Scheduler.default) .map(function(challenge) { return challenge.name; }) @@ -244,7 +244,7 @@ module.exports = function(app) { debug('Story err: ', err); callback(err); } else { - Rx.Observable.from(stories) + Rx.Observable.from(stories, null, null, Rx.Scheduler.default) .map(function(story) { return story.link; }) @@ -265,7 +265,7 @@ module.exports = function(app) { debug('User err: ', err); callback(err); } else { - Rx.Observable.from(nonprofits) + Rx.Observable.from(nonprofits, null, null, Rx.Scheduler.default) .map(function(nonprofit) { return nonprofit.name; }) @@ -285,7 +285,12 @@ module.exports = function(app) { debug('User err: ', err); callback(err); } else { - Rx.Observable.from(fieldGuides) + Rx.Observable.from( + fieldGuides, + null, + null, + Rx.Scheduler.default + ) .map(function(fieldGuide) { return fieldGuide.name; }) @@ -301,7 +306,7 @@ module.exports = function(app) { if (err) { return next(err); } - setTimeout(function() { + process.nextTick(function() { res.header('Content-Type', 'application/xml'); res.render('resources/sitemap', { appUrl: appUrl, @@ -312,19 +317,13 @@ module.exports = function(app) { nonprofits: results.nonprofits, fieldGuides: results.fieldGuides }); - }, 0); + }); } ); } function chat(req, res) { - if (req.user && req.user.progressTimestamps.length > 5) { - res.redirect('http://freecodecamp.slack.com'); - } else { - res.render('resources/chat', { - title: 'Watch us code live on Twitch.tv' - }); - } + res.redirect('//gitter.im/FreeCodeCamp/FreeCodeCamp'); } function bootcampCalculator(req, res) { @@ -383,7 +382,7 @@ module.exports = function(app) { } function unsubscribe(req, res, next) { - User.findOne({ email: req.params.email }, function(err, user) { + User.findOne({ where: { email: req.params.email } }, function(err, user) { if (user) { if (err) { return next(err); diff --git a/server/boot/user.js b/server/boot/user.js index f36fbe56a2..4575a6decf 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -4,7 +4,7 @@ var _ = require('lodash'), crypto = require('crypto'), nodemailer = require('nodemailer'), moment = require('moment'), - //debug = require('debug')('freecc:cntr:userController'), + // debug = require('debug')('freecc:cntr:userController'), secrets = require('../../config/secrets'); diff --git a/server/config.local.js b/server/config.local.js index c781159696..9b9fc132d6 100644 --- a/server/config.local.js +++ b/server/config.local.js @@ -17,4 +17,4 @@ module.exports = { clientID: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET } -}; \ No newline at end of file +}; diff --git a/server/datasources.local.js b/server/datasources.local.js index 276a4e8dee..8ad9d08abc 100644 --- a/server/datasources.local.js +++ b/server/datasources.local.js @@ -3,8 +3,8 @@ var secrets = require('../config/secrets'); module.exports = { db: { connector: 'mongodb', - connectionTimeout: 15000, - url: process.env.MONGOHQ_URL + connectionTimeout: 5000, + url: secrets.db }, mail: { connector: 'mail', diff --git a/server/utils/middleware.js b/server/utils/middleware.js index 5d2d12741c..5af207e513 100644 --- a/server/utils/middleware.js +++ b/server/utils/middleware.js @@ -33,3 +33,13 @@ exports.userMigration = function userMigration(req, res, next) { ); return next(); }; + +exports.ifNoUserRedirectTo = function ifNoUserRedirectTo(url) { + return function(req, res, next) { + if (req.user) { + return next(); + } + return res.redirect(url); + }; +}; + diff --git a/server/utils/rx.js b/server/utils/rx.js new file mode 100644 index 0000000000..8a4003c00c --- /dev/null +++ b/server/utils/rx.js @@ -0,0 +1,25 @@ +var Rx = require('rx'); +var debug = require('debug')('freecc:rxUtils'); + +exports.saveUser = function saveUser(user) { + return new Rx.Observable.create(function(observer) { + if (!user || typeof user.save !== 'function') { + debug('no user or save method'); + observer.onNext(); + return observer.onCompleted(); + } + user.save(function(err, savedUser) { + if (err) { + return observer.onError(err); + } + debug('user saved'); + observer.onNext(savedUser); + observer.onCompleted(); + }); + }); +}; + +exports.observableQueryFromModel = + function observableQueryFromModel(Model, method, query) { + return Rx.Observable.fromNodeCallback(Model[method], Model)(query); + }; diff --git a/server/views/account/show.jade b/server/views/account/show.jade index 0f7add7096..f1edc4d3c7 100644 --- a/server/views/account/show.jade +++ b/server/views/account/show.jade @@ -47,88 +47,32 @@ block content .background-svg.img-center .points-on-top = "[ " + (progressTimestamps.length) + " ]" - - - .row - .col-xs-12 - if (website1Title && website1Link && website1Image) - .row - .col-xs-12.col-md-5 - img.img-center.img-responsive.portfolio-image(src=website1Image, alt="@#{username}'s #{website1Title}") - .col-xs-12.col-md-7 - h3.text-center.wrappable.flat-top= website1Title - a.btn.btn-lg.btn-block.btn-info(href=website1Link, target='_blank') - i.fa.icon-beaker - | Try it out - br - if (website1Title && website1Link && !website1Image) - .col-xs-12.col-md-12 - h3.text-center.wrappable.flat-top= website1Title - a.btn.btn-lg.btn-block.btn-info(href=website1Link, target='_blank') - i.fa.icon-beaker - | Try it out - br - if (website2Title && website2Link && website2Image) - .row - .col-xs-12.col-md-5 - img.img-responsive.portfolio-image.img-center(src=website2Image, alt="@#{username}'s #{website2Title}") - .col-xs-12.col-md-7 - h3.text-center.wrappable.flat-top= website2Title - a.btn.btn-lg.btn-block.btn-info(href=website2Link, target='_blank') - i.fa.icon-beaker - | Try it out - br - if (website2Title && website2Link && !website2Image) - .col-xs-12.col-md-12 - h3.text-center.wrappable.flat-top= website2Title - a.btn.btn-lg.btn-block.btn-info(href=website2Link, target='_blank') - i.fa.icon-beaker - | Try it out - br - if (website3Title && website3Link && website3Image) - .row - .col-xs-12.col-md-5 - img.img-responsive.portfolio-image.img-center(src=website3Image, alt="@#{username}'s #{website1Title}") - .col-xs-12.col-md-7 - h3.text-center.wrappable.flat-top= website3Title - a.btn.btn-lg.btn-block.btn-info(href=website3Link, target='_blank') - i.fa.icon-beaker - | Try it out - if (website3Title && website3Link && !website3Image) - .col-xs-12.col-md-12 - h3.text-center.wrappable.flat-top= website3Title - a.btn.btn-lg.btn-block.btn-info(href=website3Link, target='_blank') - i.fa.icon-beaker - | Try it out - .spacer .hidden-xs.hidden-sm.col-md-12 #cal-heatmap.d3-centered script. $(document).ready(function () { - setTimeout(function () { - var cal = new CalHeatMap(); - var calendar = !{JSON.stringify(calender)}; - cal.init({ - itemSelector: "#cal-heatmap", - domain: "month", - subDomain: "x_day", - domainGutter: 10, - data: calendar, - cellSize: 15, - align: 'center', - cellRadius: 3, - cellPadding: 2, - tooltip: true, - range: 6, - start: new Date().setDate(new Date().getDate() - 150), - legendColors: ["#cccccc", "#215f1e"], - legend: [1, 2, 3], - label: { - position: "top" - } - }); - }, 300); + var cal = new CalHeatMap(); + var calendar = !{JSON.stringify(calender)}; + cal.init({ + itemSelector: "#cal-heatmap", + domain: "month", + subDomain: "x_day", + domainGutter: 10, + data: calendar, + cellSize: 15, + align: 'center', + cellRadius: 3, + cellPadding: 2, + tooltip: true, + range: 6, + start: new Date().setDate(new Date().getDate() - 150), + legendColors: ["#cccccc", "#215f1e"], + legend: [1, 2, 3], + label: { + position: "top" + } + }); }); .row .hidden-xs.col-sm-12.text-center diff --git a/server/views/coursewares/showBonfire.jade b/server/views/coursewares/showBonfire.jade index c28f20e322..a52578dec0 100644 --- a/server/views/coursewares/showBonfire.jade +++ b/server/views/coursewares/showBonfire.jade @@ -84,7 +84,7 @@ block content label.negative-10.btn.btn-primary.btn-block#submitButton i.fa.fa-play |   Run code (ctrl + enter) - #resetButton.btn.btn-danger.btn-big.btn-block(data-toggle='modal', data-target='#reset-modal', data-backdrop='true') Reset Code + #trigger-reset-modal.btn.btn-danger.btn-big.btn-block(data-toggle='modal', data-target='#reset-modal', data-backdrop='true') Reset Code if (user && user.sentSlackInvite) .button-spacer .btn-group.input-group.btn-group-justified diff --git a/server/views/partials/navbar.jade b/server/views/partials/navbar.jade index 317ce661d1..e35b9c5e8e 100644 --- a/server/views/partials/navbar.jade +++ b/server/views/partials/navbar.jade @@ -13,12 +13,8 @@ nav.navbar.navbar-default.navbar-fixed-top.nav-height a(href='/challenges') Learn li a(href='/map') Map - if (user && user.sentSlackInvite) - li - a(href='/chat', target='_blank') Chat - else - li - a(href='/challenges/waypoint-join-our-chat-room') Chat + li + a(href='//gitter.im/FreeCodeCamp/FreeCodeCamp', target='_blank') Chat li a(href='/stories') News li diff --git a/server/views/resources/calculator.jade b/server/views/resources/calculator.jade index 5e25352139..4373653140 100644 --- a/server/views/resources/calculator.jade +++ b/server/views/resources/calculator.jade @@ -3,6 +3,7 @@ block content script(src="../../../js/calculator.js") .row .col-xs-12.col-sm-10.col-md-8.col-lg-6.col-sm-offset-1.col-md-offset-2.col-lg-offset-3 + h1.text-center Coding Bootcamp Cost Calculator h3.text-center.text-primary#chosen Coming from _______, and making $_______, your true costs will be: #city-buttons .spacer @@ -102,3 +103,13 @@ block content a(href='https://en.wikipedia.org/wiki/Economic_cost' target='_blank') here | . li.large-li Free Code Camp. We don't charge tuition or garnish wages. We're fully online so you don't have to move. We're self-paced so you don't have to quit your job. Thus, your true cost of attending Free Code Camp will be $0. + .spacer + .row + .col-xs-12.col-sm-4.col-md-3 + img.img-responsive.testimonial-image(src='https://www.evernote.com/l/AHRIBndcq-5GwZVnSy1_D7lskpH4OcJcUKUB/image.png') + .col-xs-12.col-sm-8.col-md-9 + h3 Built by Suzanne Atkinson + p.large-p Suzanne is an emergency medicine physician, triathlon coach and web developer from Pittsburgh. You should   + a(href='https://twitter.com/intent/user?screen_name=SteelCityCoach' target='_blank') follow her on Twitter + | . + .spacer