diff --git a/client/commonFramework.js b/client/commonFramework.js
index 01b81c7e98..4311f7d1ff 100644
--- a/client/commonFramework.js
+++ b/client/commonFramework.js
@@ -1,31 +1,49 @@
-// common namespace
-// all classes should be stored here
-var common = common || {
- // init is an array of functions that are
- // called at the beginning of dom ready
- init: []
-};
+var common = (function() {
+ // common namespace
+ // all classes should be stored here
+ var common = window.common || {
+ // init is an array of functions that are
+ // called at the beginning of dom ready
+ init: []
+ };
-common.challengeName = common.challengeName || window.challenge_Name ?
- window.challenge_Name :
- '';
+ common.challengeName = common.challengeName || window.challenge_Name ?
+ window.challenge_Name :
+ '';
-common.challengeType = common.challengeType || window.challengeType ?
- window.challengeType :
- 0;
+ common.challengeType = common.challengeType || window.challengeType ?
+ window.challengeType :
+ 0;
-common.challengeId = common.challengeId || window.challenge_Id;
+ common.challengeId = common.challengeId || window.challenge_Id;
-common.challengeSeed = common.challengeSeed || window.challengeSeed ?
- window.challengeSeed :
- [];
+ common.challengeSeed = common.challengeSeed || window.challengeSeed ?
+ window.challengeSeed :
+ [];
-common.seed = common.challengeSeed.reduce(function(seed, line) {
- return seed + line + '\n';
-}, '');
+ common.seed = common.challengeSeed.reduce(function(seed, line) {
+ return seed + line + '\n';
+ }, '');
+
+ common.replaceScriptTags = function replaceScriptTags(value) {
+ return value
+ .replace(/');
+ };
+
+ return common;
+})();
// store code in the URL
common.codeUri = (function(common, encode, decode, location, history) {
+ var replaceScriptTags = common.replaceScriptTags;
+ var replaceSafeTags = common.replaceSafeTags;
var codeUri = {
encode: function(code) {
return encode(code);
@@ -67,7 +85,7 @@ common.codeUri = (function(common, encode, decode, location, history) {
null,
location.href.split('?')[0]
);
- location.hash = '#?' + query;
+ location.hash = '#?' + replaceScriptTags(query);
}
} else {
query = location.hash.replace(/^\#\?/, '');
@@ -82,13 +100,15 @@ common.codeUri = (function(common, encode, decode, location, history) {
var key = param.split('=')[0];
var value = param.split('=')[1];
if (key === 'solution') {
- return codeUri.decode(value);
+ return replaceSafeTags(codeUri.decode(value || ''));
}
return solution;
}, null);
},
querify: function(solution) {
- location.hash = '?solution=' + codeUri.encode(solution);
+ location.hash = '?solution=' +
+ codeUri.encode(replaceScriptTags(solution));
+
return solution;
}
};
@@ -306,12 +326,6 @@ var sandBox = (function(jailed, codeOutput) {
return sandBox;
}(window.jailed, common.codeOutput));
-function replaceSafeTags(value) {
- return value
- .replace(/fccss/gi, '');
-}
-
var BDDregex = new RegExp(
'(expect(\\s+)?\\(.*\\;)|' +
'(assert(\\s+)?\\(.*\\;)|' +
@@ -416,7 +430,7 @@ var editor = (function(CodeMirror, emmetCodeMirror, common) {
common.seed;
}
- editor.setValue(replaceSafeTags(editorValue));
+ editor.setValue(common.replaceSafeTags(editorValue));
editor.refresh();
});
@@ -659,7 +673,7 @@ function showCompletion() {
}
var resetEditor = function resetEditor() {
- editor.setValue(replaceSafeTags(common.seed));
+ editor.setValue(common.replaceSafeTags(common.seed));
$('#testSuite').empty();
bonfireExecute(true);
common.codeStorage.updateStorage();
diff --git a/client/less/main.less b/client/less/main.less
index 067290e9be..62fdadff4a 100644
--- a/client/less/main.less
+++ b/client/less/main.less
@@ -490,6 +490,10 @@ thead {
border-radius: 5px;
}
+.story-section {
+ min-height: 500px;
+}
+
.testimonial-copy {
font-size: 20px;
text-align: center;
diff --git a/client/main.js b/client/main.js
index c1578c1d6f..af32484411 100644
--- a/client/main.js
+++ b/client/main.js
@@ -1,12 +1,19 @@
-var mapShareKey = 'map-shares';
+var main = window.main || {};
+
+main.mapShareKey = 'map-shares';
+
var lastCompleted = typeof lastCompleted !== 'undefined' ?
lastCompleted :
'';
function getMapShares() {
- var alreadyShared = JSON.parse(localStorage.getItem(mapShareKey) || '[]');
+ var alreadyShared = JSON.parse(
+ localStorage.getItem(main.mapShareKey) ||
+ '[]'
+ );
+
if (!alreadyShared || !Array.isArray(alreadyShared)) {
- localStorage.setItem(mapShareKey, JSON.stringify([]));
+ localStorage.setItem(main.mapShareKey, JSON.stringify([]));
alreadyShared = [];
}
return alreadyShared;
@@ -23,7 +30,7 @@ function setMapShare(id) {
if (!found) {
alreadyShared.push(id);
}
- localStorage.setItem(mapShareKey, JSON.stringify(alreadyShared));
+ localStorage.setItem(main.mapShareKey, JSON.stringify(alreadyShared));
return alreadyShared;
}
diff --git a/common/models/pledge.json b/common/models/pledge.json
new file mode 100644
index 0000000000..08e46df414
--- /dev/null
+++ b/common/models/pledge.json
@@ -0,0 +1,55 @@
+{
+ "name": "pledge",
+ "base": "PersistedModel",
+ "idInjection": true,
+ "trackChanges": false,
+ "properties": {
+ "nonprofit": {
+ "type": "string",
+ "index": true
+ },
+ "amount": {
+ "type": "number"
+ },
+ "dateStarted": {
+ "type": "date",
+ "defaultFn": "now"
+ },
+ "dateEnded": {
+ "type": "date"
+ },
+ "formerUserId": {
+ "type": "string"
+ },
+ "isOrphaned": {
+ "type": "boolean"
+ },
+ "isCompleted": {
+ "type": "boolean",
+ "default": "false"
+ }
+ },
+ "validations": [],
+ "relations": {
+ "user": {
+ "type": "hasMany",
+ "model": "user",
+ "foreignKey": "userId"
+ }
+ },
+ "acls": [
+ {
+ "accessType": "*",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "DENY"
+ },
+ {
+ "accessType": "READ",
+ "principalType": "ROLE",
+ "principalId": "$everyone",
+ "permission": "ALLOW"
+ }
+ ],
+ "methods": []
+}
diff --git a/common/models/user.json b/common/models/user.json
index 776c2c500e..f85cc1023c 100644
--- a/common/models/user.json
+++ b/common/models/user.json
@@ -166,6 +166,11 @@
"type": "hasMany",
"model": "userIdentity",
"foreignKey": ""
+ },
+ "pledge": {
+ "type": "hasOne",
+ "model": "pledge",
+ "foreignKey": ""
}
},
"acls": [
diff --git a/seed/challenges/basic-bonfires.json b/seed/challenges/basic-bonfires.json
index 931c157ce1..afebb968ea 100644
--- a/seed/challenges/basic-bonfires.json
+++ b/seed/challenges/basic-bonfires.json
@@ -1,6 +1,6 @@
{
"name": "Basic Algorithm Scripting",
- "order": 7,
+ "order": 8,
"challenges": [
{
"id": "ad7123c8c441eddfaeb5bdef",
@@ -378,7 +378,7 @@
"truncate(\"A-tisket a-tasket A green and yellow basket\", 11, \"\");"
],
"tests": [
- "assert(truncate(\"A-tisket a-tasket A green and yellow basket\", 11) === \"A-tisket...\", 'message: truncate(\"A-tisket a-tasket A green and yellow basket\", 1)
should return \"A-tisket...\".');",
+ "assert(truncate(\"A-tisket a-tasket A green and yellow basket\", 11) === \"A-tisket...\", 'message: truncate(\"A-tisket a-tasket A green and yellow basket\", 11)
should return \"A-tisket...\".');",
"assert(truncate(\"Peter Piper picked a peck of pickled peppers\", 14) === \"Peter Piper...\", 'message: truncate(\"Peter Piper picked a peck of pickled peppers\", 14)
should return \"Peter Piper...\".');",
"assert(truncate(\"A-tisket a-tasket A green and yellow basket\", \"A-tisket a-tasket A green and yellow basket\".length) === \"A-tisket a-tasket A green and yellow basket\", 'message: truncate(\"A-tisket a-tasket A green and yellow basket\", \"A-tisket a-tasket A green and yellow basket\".length)
should return \"A-tisket a-tasket A green and yellow basket\".');",
"assert(truncate('A-tisket a-tasket A green and yellow basket', 'A-tisket a-tasket A green and yellow basket'.length + 2) === 'A-tisket a-tasket A green and yellow basket', 'message: truncate(\"A-tisket a-tasket A green and yellow basket\", \"A-tisket a-tasket A green and yellow basket\".length + 2)
should return \"A-tisket a-tasket A green and yellow basket\".');"
diff --git a/seed/challenges/basic-javascript.json b/seed/challenges/basic-javascript.json
index c10af0def1..68eab0176c 100644
--- a/seed/challenges/basic-javascript.json
+++ b/seed/challenges/basic-javascript.json
@@ -1,6 +1,6 @@
{
"name": "Basic JavaScript",
- "order": 5,
+ "order": 6,
"challenges": [
{
"id":"bd7123c9c441eddfaeb4bdef",
@@ -1039,7 +1039,7 @@
],
"tests":[
"assert(test === 2, 'message: Your RegEx should have found two numbers in the testString
.');",
- "assert(editor.getValue().match(/\\/\\\\d\\+\\//gi), 'message: You should be using the following expression /\\\\d+/gi
to find the numbers in the testString
.');"
+ "assert(editor.getValue().match(/\\/\\\\d\\+\\//g), 'message: You should be using the following expression /\\d+/g
to find the numbers in the testString
.');"
],
"challengeSeed":[
"var test = (function() {",
@@ -1047,7 +1047,7 @@
"",
" // Only change code below this line.",
"",
- " var expression = /.+/gi;",
+ " var expression = /.+/g;",
"",
" // Only change code above this line.",
" // We use this function to show you the value of your variable in your output box.",
@@ -1069,7 +1069,7 @@
],
"tests":[
"assert(test === 7, 'message: Your RegEx should have found seven spaces in the testString
.');",
- "assert(editor.getValue().match(/\\/\\\\s\\+\\//gi), 'message: You should be using the following expression /\\\\s+/gi
to find the spaces in the testString
.');"
+ "assert(editor.getValue().match(/\\/\\\\s\\+\\//g), 'message: You should be using the following expression /\\s+/g
to find the spaces in the testString
.');"
],
"challengeSeed":[
"var test = (function(){",
@@ -1077,7 +1077,7 @@
"",
" // Only change code below this line.",
"",
- " var expression = /.+/gi;",
+ " var expression = /.+/g;",
"",
" // Only change code above this line.",
" // We use this function to show you the value of your variable in your output box.",
@@ -1092,12 +1092,12 @@
"title": "Invert Regular Expression Matches with JavaScript",
"difficulty":"9.987",
"description":[
- "Use /\\S/gi
to match everything that isn't a space in the string.",
+ "Use /\\S/g
to match everything that isn't a space in the string.",
"You can invert any match by using the uppercase version of the selector \\s
versus \\S
for example."
],
"tests":[
"assert(test === 49, 'message: Your RegEx should have found forty nine non-space characters in the testString
.');",
- "assert(editor.getValue().match(/\\/\\\\S\\/gi/gi), 'message: You should be using the following expression /\\\\S/gi
to find non-space characters in the testString
.');"
+ "assert(editor.getValue().match(/\\/\\\\S\\/g/g), 'message: You should be using the following expression /\\S/g
to find non-space characters in the testString
.');"
],
"challengeSeed":[
"var test = (function(){",
@@ -1105,7 +1105,7 @@
"",
" // Only change code below this line.",
"",
- " var expression = /./gi;",
+ " var expression = /./g;",
"",
" // Only change code above this line.",
" // We use this function to show you the value of your variable in your output box.",
diff --git a/seed/challenges/basic-ziplines.json b/seed/challenges/basic-ziplines.json
index cf2ac272f6..59f2c75237 100644
--- a/seed/challenges/basic-ziplines.json
+++ b/seed/challenges/basic-ziplines.json
@@ -1,6 +1,6 @@
{
"name": "Basic Front End Development Projects",
- "order": 8,
+ "order": 9,
"challenges": [
{
"id": "bd7158d8c442eddfbeb5bd1f",
diff --git a/seed/challenges/bootstrap.json b/seed/challenges/bootstrap.json
index d405871390..3982fe18d5 100644
--- a/seed/challenges/bootstrap.json
+++ b/seed/challenges/bootstrap.json
@@ -9,7 +9,7 @@
"Now let's go back to our Cat Photo App. This time, we'll style it using the popular Bootstrap responsive CSS framework.",
"Bootstrap will figure out how wide your screen is and respond by resizing your HTML elements - hence the name Responsive Design
.",
"With responsive design, there is no need to design a mobile version of your website. It will look good on devices with screens of any width.",
- "You can add Bootstrap to any app just by including it with <link rel=\"stylesheet\" href=\"//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css\"/>
at the top of your HTML. But we've gone ahead and automatically added it to your Cat Photo App for you.",
+ "You can add Bootstrap to any app just by including it with <link rel=\"stylesheet\" href=\"//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css\"/>
at the top of your HTML. But we've added it for you to this page behind the scenes.",
"To get started, we should nest all of our HTML in a div
element with the class container-fluid
."
],
"tests": [
diff --git a/seed/challenges/gear-up-for-success.json b/seed/challenges/gear-up-for-success.json
new file mode 100644
index 0000000000..a9986a4b45
--- /dev/null
+++ b/seed/challenges/gear-up-for-success.json
@@ -0,0 +1,110 @@
+{
+ "name": "Gear up for Success",
+ "order": 4,
+ "challenges": [
+ {
+ "id": "560add65cb82ac38a17513c2",
+ "title": "Browse Camper News",
+ "challengeSeed": [],
+ "description": [
+ [
+ "http://i.imgur.com/nmSiMy1.gif",
+ "A gif showing how you can access our Camper News page and click the \"upvote\" button to upvote a story.",
+ "Click the \"News\" button in your upper right hand corner. You can browse links on Camper News and upvote ones that you enjoy.",
+ ""
+ ]
+ ],
+ "type": "Waypoint",
+ "challengeType": 7,
+ "tests": [],
+ "nameCn": "",
+ "descriptionCn": [],
+ "nameFr": "",
+ "descriptionFr": [],
+ "nameRu": "",
+ "descriptionRu": [],
+ "nameEs": "",
+ "descriptionEs": [],
+ "namePt": "",
+ "descriptionPt": []
+ },
+ {
+ "id": "560add65cb82ac38a17513c1",
+ "title": "Reference our Wiki",
+ "challengeSeed": [],
+ "description": [
+ [
+ "http://i.imgur.com/DoOqkNW.gif",
+ "A gif showing how you can click the \"Wiki\" button in your upper-right corner to access the wiki.",
+ "Try this: Click the \"Wiki\" button in your upper right hand corner. Our community has contributed lots of useful information to this searchable wiki.",
+ ""
+ ]
+ ],
+ "type": "Waypoint",
+ "challengeType": 7,
+ "tests": [],
+ "nameCn": "",
+ "descriptionCn": [],
+ "nameFr": "",
+ "descriptionFr": [],
+ "nameRu": "",
+ "descriptionRu": [],
+ "nameEs": "",
+ "descriptionEs": [],
+ "namePt": "",
+ "descriptionPt": []
+ },
+ {
+ "id": "570add8ccb82ac38a17513c3",
+ "title": "Join our LinkedIn Alumni Network",
+ "challengeSeed": [],
+ "description": [
+ [
+ "http://i.imgur.com/P7qfJXt.gif",
+ "A gif showing how you can click the link below and fill in the necessary fields to add your Free Code Camp studies to your LinkedIn profile.",
+ "You can add Free Code Camp to your LinkedIn education background. Set your graduation date as next year. For \"Degree\", type \"Full Stack Web Development\". For \"Field of study\", type \"Computer Software Engineering\". Then click \"Save Changes\".",
+ "https://www.linkedin.com/profile/edit-education?school=Free+Code+Camp"
+ ]
+ ],
+ "type": "Waypoint",
+ "challengeType": 7,
+ "tests": [],
+ "nameCn": "",
+ "descriptionCn": [],
+ "nameFr": "",
+ "descriptionFr": [],
+ "nameRu": "",
+ "descriptionRu": [],
+ "nameEs": "",
+ "descriptionEs": [],
+ "namePt": "",
+ "descriptionPt": []
+ },
+ {
+ "id": "560add8ccb81ac38a17513c4",
+ "title": "Commit to a Goal and a Nonprofit",
+ "challengeSeed": [],
+ "description": [
+ [
+ "http://i.imgur.com/P7qfJXt.gif",
+ "A gif showing how you can commit to a goal for your Free Code Camp studies and pledge a monthly donation to a nonprofit to give you external motivation to reach that goal.",
+ "You can set a goal and pledge to donate to a nonprofit each month until you achieve that goal. give you external motivation in your quest to learn to code, as well as the opportunity to help nonprofits right away. Choose your goal, choose a monthly donation. When you click \"commit\", the nonprofit's donate page will open in a new tab. You can change your committment or stop it at any time.",
+ "http://freecodecamp.com/commit"
+ ]
+ ],
+ "type": "Waypoint",
+ "challengeType": 7,
+ "tests": [],
+ "nameCn": "",
+ "descriptionCn": [],
+ "nameFr": "",
+ "descriptionFr": [],
+ "nameRu": "",
+ "descriptionRu": [],
+ "nameEs": "",
+ "descriptionEs": [],
+ "namePt": "",
+ "descriptionPt": []
+ }
+ ]
+}
diff --git a/seed/challenges/getting-started.json b/seed/challenges/getting-started.json
index 1a97e5c10c..1e750177d4 100644
--- a/seed/challenges/getting-started.json
+++ b/seed/challenges/getting-started.json
@@ -134,51 +134,19 @@
},
{
"id": "560add56cb82ac38a17513c0",
- "title": "Configure your Public Profile",
+ "title": "Configure your Code Portfolio",
"challengeSeed": [],
"description": [
[
"http://i.imgur.com/FkEzbto.gif",
- "A gif showing how you can click your profile image in your upper right hand corner to access the account page and connect GitHub.",
- "Check out your portfolio page. Click your picture your upper right hand corner. To activate your portfolio page, you'll need to link your GitHub account with Free Code Camp.",
+ "A gif showing how you can click your profile image in your upper right hand corner to your code portfolio and connect GitHub.",
+ "Check out your code portfolio. Click your picture your upper right hand corner. To activate your code portfolio, you'll need to link your GitHub account with Free Code Camp.",
""
],
[
"http://i.imgur.com/WKzEr1q.gif",
- "A gif showing how you can access your profile page and hover over different days to see how many brownie points you got on those days.",
- "Your portfolio page shows your progress and how many Brownie Points you have. You can get Brownie Points by completing challenges and by helping other campers in our chat rooms. If you get Brownie Points on several days in a row, you'll get a streak.",
- ""
- ]
- ],
- "type": "Waypoint",
- "challengeType": 7,
- "tests": [],
- "nameCn": "",
- "descriptionCn": [],
- "nameFr": "",
- "descriptionFr": [],
- "nameRu": "",
- "descriptionRu": [],
- "nameEs": "",
- "descriptionEs": [],
- "namePt": "",
- "descriptionPt": []
- },
- {
- "id": "560add65cb82ac38a17513c1",
- "title": "Try our Wiki and Camper News",
- "challengeSeed": [],
- "description": [
- [
- "http://i.imgur.com/DoOqkNW.gif",
- "A gif showing how you can click the \"Wiki\" button in your upper-right corner to access the wiki.",
- "Try this: Click the \"Wiki\" button in your upper right hand corner. Our community has contributed lots of useful information to this searchable wiki.",
- ""
- ],
- [
- "http://i.imgur.com/nmSiMy1.gif",
- "A gif showing how you can access our Camper News page and click the \"upvote\" button to upvote a story.",
- "Click the \"News\" button in your upper right hand corner. You can browse links on Camper News and upvote ones that you enjoy.",
+ "A gif showing how you can access your code portfolio and hover over different days to see how many brownie points you got on those days.",
+ "Your code portfolio shows your progress and how many Brownie Points you have. You can get Brownie Points by completing challenges and by helping other campers in our chat rooms. If you get Brownie Points on several days in a row, you'll get a streak.",
""
]
],
@@ -260,38 +228,6 @@
"namePt": "",
"descriptionPt": []
},
- {
- "id": "560add8ccb82ac38a17513c3",
- "title": "Join our Alumni Network and Commit to Your Goal",
- "challengeSeed": [],
- "description": [
- [
- "http://i.imgur.com/P7qfJXt.gif",
- "A gif showing how you can click the link below and fill in the necessary fields to add your Free Code Camp studies to your LinkedIn profile.",
- "You can add Free Code Camp to your LinkedIn education background. Set your graduation date as next year. For \"Degree\", type \"Full Stack Web Development\". For \"Field of study\", type \"Computer Software Engineering\". Then click \"Save Changes\".",
- "https://www.linkedin.com/profile/edit-education?school=Free+Code+Camp"
- ],
- [
- "",
- "",
- "Free Code Camp will always be free. If you want to feel more motivated to earn our certificates faster, we encourage you to instead pledge to donate to a nonprofit each day.",
- ""
- ]
- ],
- "type": "Waypoint",
- "challengeType": 7,
- "tests": [],
- "nameCn": "",
- "descriptionCn": [],
- "nameFr": "",
- "descriptionFr": [],
- "nameRu": "",
- "descriptionRu": [],
- "nameEs": "",
- "descriptionEs": [],
- "namePt": "",
- "descriptionPt": []
- },
{
"id": "560add8ccb82ac38a17513c4",
"title": "Learn What to Do If You Get Stuck",
diff --git a/seed/challenges/jquery.json b/seed/challenges/jquery.json
index 8d3de88c68..6aa103d38c 100644
--- a/seed/challenges/jquery.json
+++ b/seed/challenges/jquery.json
@@ -1,6 +1,6 @@
{
"name": "jQuery",
- "order": 4,
+ "order": 5,
"challenges": [
{
"id": "bad87fee1348bd9acdd08826",
diff --git a/seed/challenges/json-apis-and-ajax.json b/seed/challenges/json-apis-and-ajax.json
deleted file mode 100644
index f8e11696b7..0000000000
--- a/seed/challenges/json-apis-and-ajax.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "JSON APIs and Ajax",
- "order": 10,
- "challenges": [
- {
- "id": "bad88fee1348bd9ae8c08416",
- "title": "Stand in challenge",
- "dashedName": "waypoint-stand-in-challenge",
- "difficulty": 3.24,
- "description": [
- ""
- ],
- "tests": [
-
- ],
- "challengeSeed": [
- ""
- ],
- "challengeType": 0,
- "type": "waypoint"
- }
- ]
-}
diff --git a/seed/challenges/object-oriented-and-functional-programming.json b/seed/challenges/object-oriented-and-functional-programming.json
index 2d8c22f32d..f00086db9c 100644
--- a/seed/challenges/object-oriented-and-functional-programming.json
+++ b/seed/challenges/object-oriented-and-functional-programming.json
@@ -1,6 +1,6 @@
{
"name": "Object Oriented and Functional Programming",
- "order": 6,
+ "order": 7,
"note": [
"Methods",
"Closures",
@@ -185,7 +185,7 @@
],
"tests":[
"assert.deepEqual(array, [4,5,6,7,8], 'message: You should add three to each value in the array.');",
- "assert(editor.getValue().match(/\\.map\\(/gi), 'message: You should be making use of the map method.');",
+ "assert(editor.getValue().match(/\\.map\\s*\\(/gi), 'message: You should be making use of the map method.');",
"assert(editor.getValue().match(/\\[1\\,2\\,3\\,4\\,5\\]/gi), 'message: You should only modify the array with .map
.');"
],
"challengeSeed":[
@@ -212,8 +212,8 @@
"});
"
],
"tests":[
- "assert(singleVal == 30, 'message: singleVal
should have been set to the result of you reduce operation.');",
- "assert(editor.getValue().match(/\\.reduce\\(/gi), 'message: You should have made use of the reduce method.');"
+ "assert(singleVal == 30, 'message: singleVal
should have been set to the result of your reduce operation.');",
+ "assert(editor.getValue().match(/\\.reduce\\s*\\(/gi), 'message: You should have made use of the reduce method.');"
],
"challengeSeed":[
"var array = [4,5,6,7,8];",
@@ -241,7 +241,7 @@
],
"tests":[
"assert.deepEqual(array, [1,2,3,4], 'message: You should have removed all the values from the array that are greater than 4.');",
- "assert(editor.getValue().match(/array\\.filter\\(/gi), 'message: You should be using the filter method to remove the values from the array.');",
+ "assert(editor.getValue().match(/array\\.filter\\s*\\(/gi), 'message: You should be using the filter method to remove the values from the array.');",
"assert(editor.getValue().match(/\\[1\\,2\\,3\\,4\\,5\\,6\\,7\\,8\\,9\\,10\\]/gi), 'message: You should only be using .filter
to modify the contents of the array.');"
],
"challengeSeed":[
@@ -269,7 +269,7 @@
"tests":[
"assert.deepEqual(array, ['alpha', 'beta', 'charlie'], 'message: You should have sorted the array alphabetically.');",
"assert(editor.getValue().match(/\\[\\'beta\\'\\,\\s\\'alpha\\'\\,\\s'charlie\\'\\];/gi), 'message: You should be sorting the array using sort.');",
- "assert(editor.getValue().match(/\\.sort\\(\\)/gi), 'message: You should have made use of the sort method.');"
+ "assert(editor.getValue().match(/\\.sort\\s*\\(\\)/gi), 'message: You should have made use of the sort method.');"
],
"challengeSeed":[
"var array = ['beta', 'alpha', 'charlie'];",
@@ -291,7 +291,7 @@
],
"tests": [
"assert.deepEqual(array, [7,6,5,4,3,2,1], 'message: You should reverse the array.');",
- "assert(editor.getValue().match(/\\.reverse\\(\\)/gi), 'message: You should use the reverse method.');",
+ "assert(editor.getValue().match(/\\.reverse\\s*\\(\\)/gi), 'message: You should use the reverse method.');",
"assert(editor.getValue().match(/\\[1\\,2\\,3\\,4\\,5\\,6\\,7/gi), 'message: You should return [7,6,5,4,3,2,1]
.');"
],
"challengeSeed": [
@@ -315,7 +315,7 @@
],
"tests": [
"assert.deepEqual(array, [1,2,3,4,5,6], 'You should concat the two arrays together.');",
- "assert(editor.getValue().match(/\\.concat\\(/gi), 'message: You should be use the concat method to merge the two arrays.');",
+ "assert(editor.getValue().match(/\\.concat\\s*\\(/gi), 'message: You should be use the concat method to merge the two arrays.');",
"assert(editor.getValue().match(/\\[1\\,2\\,3\\]/gi) && editor.getValue().match(/\\[4\\,5\\,6\\]/gi), 'message: You should only modify the two arrays without changing the origional ones.');"
],
"challengeSeed": [
diff --git a/seed/index.js b/seed/index.js
index 334a11d3dc..bd05f2de96 100644
--- a/seed/index.js
+++ b/seed/index.js
@@ -43,6 +43,7 @@ Challenge.destroyAll(function(err, info) {
var challengeSpec = require('./challenges/' + file);
var order = challengeSpec.order;
var block = challengeSpec.name;
+ var isBeta = !!challengeSpec.isBeta;
// challenge file has no challenges...
if (challengeSpec.challenges.length === 0) {
@@ -66,6 +67,7 @@ Challenge.destroyAll(function(err, info) {
challenge.order = order;
challenge.suborder = index + 1;
challenge.block = block;
+ challenge.isBeta = challenge.isBeta || isBeta;
return challenge;
});
diff --git a/server/boot/certificate.js b/server/boot/certificate.js
index b084d9f4b8..400d756bcc 100644
--- a/server/boot/certificate.js
+++ b/server/boot/certificate.js
@@ -18,6 +18,10 @@ import {
fullStackChallangeId
} from '../utils/constantStrings.json';
+import {
+ completeCommitment$
+} from '../utils/commit';
+
const debug = debugFactory('freecc:certification');
const sendMessageToNonUser = ifNoUserSend(
'must be logged in to complete.'
@@ -114,7 +118,20 @@ export default function certificate(app) {
completedDate: new Date(),
challengeType
});
- return saveUser(user);
+ return saveUser(user)
+ // If user has commited to nonprofit,
+ // this will complete his pledge
+ .flatMap(
+ user => completeCommitment$(user),
+ (user, pledgeOrMessage) => {
+ if (typeof pledgeOrMessage === 'string') {
+ debug(pledgeOrMessage);
+ }
+ // we are only interested in the user object
+ // so we ignore return from completeCommitment$
+ return user;
+ }
+ );
}
return Observable.just(user);
})
diff --git a/server/boot/challenge.js b/server/boot/challenge.js
index 745f8be876..a1a748b402 100644
--- a/server/boot/challenge.js
+++ b/server/boot/challenge.js
@@ -16,9 +16,11 @@ import {
ifNoUserSend
} from '../utils/middleware';
+const isDev = process.env.NODE_ENV !== 'production';
+const isBeta = !!process.env.BETA;
const debug = debugFactory('freecc:challenges');
const challengesRegex = /^(bonfire|waypoint|zipline|basejump)/i;
-const firstChallenge = 'waypoint-say-hello-to-html-elements';
+const firstChallenge = 'waypoint-learn-how-free-code-camp-works';
const challengeView = {
0: 'coursewares/showHTML',
1: 'coursewares/showJS',
@@ -105,6 +107,9 @@ module.exports = function(app) {
null,
Scheduler.default
))
+ // filter out all challenges that have isBeta flag set
+ // except in development or beta site
+ .filter(challenge => isDev || isBeta || !challenge.isBeta)
.shareReplay();
// create a stream of challenge blocks
@@ -543,8 +548,10 @@ module.exports = function(app) {
}
return sum;
}, 0);
+ const isBeta = _.every(blockArray, 'isBeta');
return {
+ isBeta,
name: blockArray[0].block,
dashedName: dasherize(blockArray[0].block),
challenges: blockArray,
diff --git a/server/boot/commit.js b/server/boot/commit.js
index a54dc19506..9a48ea5236 100644
--- a/server/boot/commit.js
+++ b/server/boot/commit.js
@@ -1,15 +1,224 @@
+import _ from 'lodash';
+import { Observable } from 'rx';
+import debugFactory from 'debug';
+import dedent from 'dedent';
+
+import nonprofits from '../utils/commit.json';
+import {
+ commitGoals,
+ completeCommitment$
+} from '../utils/commit';
+
+import {
+ unDasherize
+} from '../utils';
+
+import {
+ observeQuery,
+ saveInstance
+} from '../utils/rx';
+
+import {
+ ifNoUserRedirectTo
+} from '../utils/middleware';
+
+const sendNonUserToFront = ifNoUserRedirectTo('/');
+const sendNonUserToCommit = ifNoUserRedirectTo(
+ '/commit',
+ 'Must be signed in to update commit'
+);
+const debug = debugFactory('freecc:commit');
+
+function findNonprofit(name) {
+ let nonprofit;
+ if (name) {
+ nonprofit = _.find(nonprofits, (nonprofit) => {
+ return name === nonprofit.name;
+ });
+ }
+
+ nonprofit = nonprofit || nonprofits[0];
+ return nonprofit;
+}
+
export default function commit(app) {
const router = app.loopback.Router();
+ const { Pledge } = app.models;
+
router.get(
'/commit',
commitToNonprofit
);
+ router.get(
+ '/commit/pledge',
+ sendNonUserToFront,
+ pledge
+ );
+
+ router.get(
+ '/commit/directory',
+ renderDirectory
+ );
+
+ router.post(
+ '/commit/stop-commitment',
+ sendNonUserToCommit,
+ stopCommit
+ );
+
+ router.post(
+ '/commit/complete-goal',
+ sendNonUserToCommit,
+ completeCommitment
+ );
+
app.use(router);
- function commitToNonprofit(req, res) {
- res.render('commit/', {
- title: 'Commit to a nonprofit. Commit to your goal.'
+ function commitToNonprofit(req, res, next) {
+ const { user } = req;
+ let nonprofitName = unDasherize(req.query.nonprofit);
+
+ debug('looking for nonprofit', nonprofitName);
+ const nonprofit = findNonprofit(nonprofitName);
+
+ Observable.just(user)
+ .flatMap(user => {
+ if (user) {
+ debug('getting user pledge');
+ return observeQuery(user, 'pledge');
+ }
+ return Observable.just();
+ })
+ .subscribe(
+ pledge => {
+ if (pledge) {
+ debug('found previous pledge');
+ req.flash('info', {
+ msg: dedent`
+ Looks like you already have a pledge to ${pledge.displayName}.
+ Hitting commit here will replace your old commitment.
+ `
+ });
+ }
+ res.render(
+ 'commit/',
+ Object.assign(
+ {
+ title: 'Commit to a nonprofit. Commit to your goal.',
+ pledge,
+ frontEndCert: commitGoals.frontEndCert,
+ fullStackCert: commitGoals.fullStackCert
+ },
+ nonprofit
+ )
+ );
+ },
+ next
+ );
+
+ }
+
+ function pledge(req, res, next) {
+ const { user } = req;
+ const {
+ nonprofit: nonprofitName = 'girl develop it',
+ amount = '5',
+ goal = commitGoals.frontEndCert
+ } = req.query;
+
+ const nonprofit = findNonprofit(nonprofitName);
+
+ observeQuery(user, 'pledge')
+ .flatMap(oldPledge => {
+ // create new pledge for user
+ const pledge = Pledge(
+ Object.assign(
+ {
+ amount,
+ goal,
+ userId: user.id
+ },
+ nonprofit
+ )
+ );
+
+ if (oldPledge) {
+ debug('user already has pledge, creating a new one');
+ // we orphan last pledge since a user only has one pledge at a time
+ oldPledge.userId = '';
+ oldPledge.formerUser = user.id;
+ oldPledge.endDate = new Date();
+ oldPledge.isOrphaned = true;
+ return saveInstance(oldPledge)
+ .flatMap(() => {
+ return saveInstance(pledge);
+ });
+ }
+ return saveInstance(pledge);
+ })
+ .subscribe(
+ ({ nonprofit, goal, amount }) => {
+ req.flash('success', {
+ msg: dedent`
+ Congratulations, you have committed to giving
+ ${nonprofit} $${amount} each month until you have completed
+ your ${goal}.
+ `
+ });
+ res.redirect('/' + user.username);
+ },
+ next
+ );
+ }
+
+ function renderDirectory(req, res) {
+ res.render('commit/directory', {
+ title: 'Commit Directory',
+ nonprofits
});
}
+
+ function completeCommitment(req, res, next) {
+ const { user } = req;
+
+ return completeCommitment$(user)
+ .subscribe(
+ msgOrPledge => {
+ if (typeof msgOrPledge === 'string') {
+ return res.send(msgOrPledge);
+ }
+ return res.send(true);
+ },
+ next
+ );
+ }
+
+ function stopCommit(req, res, next) {
+ const { user } = req;
+
+ observeQuery(user, 'pledge')
+ .flatMap(pledge => {
+ if (!pledge) {
+ return Observable.just();
+ }
+
+ pledge.formerUserId = pledge.userId;
+ pledge.userId = null;
+ pledge.isOrphaned = true;
+ pledge.dateEnded = new Date();
+ return saveInstance(pledge);
+ })
+ .subscribe(
+ pledge => {
+ let msg = `You have successfully stopped your pledge.`;
+ if (!pledge) {
+ msg = `No pledge found for user ${user.username}.`;
+ }
+ req.flash('errors', { msg });
+ return res.redirect(`/${user.username}`);
+ },
+ next
+ );
+ }
}
diff --git a/server/boot/labs.js b/server/boot/labs.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/server/boot/randomAPIs.js b/server/boot/randomAPIs.js
index bd35e05e62..c9f3a54f0e 100644
--- a/server/boot/randomAPIs.js
+++ b/server/boot/randomAPIs.js
@@ -5,6 +5,8 @@ var Rx = require('rx'),
request = require('request'),
debug = require('debug')('freecc:cntr:resources'),
constantStrings = require('../utils/constantStrings.json'),
+ labs = require('../resources/labs.json'),
+ testimonials = require('../resources/testimonials.json'),
secrets = require('../../config/secrets');
module.exports = function(app) {
@@ -32,6 +34,8 @@ module.exports = function(app) {
router.get('/unsubscribed', unsubscribed);
router.get('/get-started', getStarted);
router.get('/submit-cat-photo', submitCatPhoto);
+ router.get('/labs', showLabs);
+ router.get('/stories', showTestimonials);
app.use(router);
@@ -96,10 +100,10 @@ module.exports = function(app) {
});
},
- challenges: function (callback) {
+ challenges: function(callback) {
Challenge.find(
{ fields: { name: true } },
- function (err, challenges) {
+ function(err, challenges) {
if (err) {
debug('Challenge err: ', err);
callback(err);
@@ -116,10 +120,10 @@ module.exports = function(app) {
}
});
},
- stories: function (callback) {
+ stories: function(callback) {
Story.find(
{ field: { link: true } },
- function (err, stories) {
+ function(err, stories) {
if (err) {
debug('Story err: ', err);
callback(err);
@@ -137,7 +141,7 @@ module.exports = function(app) {
}
);
},
- nonprofits: function (callback) {
+ nonprofits: function(callback) {
Nonprofit.find(
{ field: { name: true } },
function(err, nonprofits) {
@@ -180,6 +184,20 @@ module.exports = function(app) {
res.redirect('https://gitter.im/FreeCodeCamp/FreeCodeCamp');
}
+ function showLabs(req, res) {
+ res.render('resources/labs', {
+ title: 'Projects Built by Free Code Camp Students',
+ projects: labs
+ });
+ }
+
+ function showTestimonials(req, res) {
+ res.render('resources/stories', {
+ title: 'Stories from Happy Free Code Camp Campers',
+ stories: testimonials
+ });
+ }
+
function submitCatPhoto(req, res) {
res.send('Submitted!');
}
@@ -197,13 +215,6 @@ module.exports = function(app) {
});
}
- function catPhotoSubmit(req, res) {
- res.send(
- 'Success! You have submitted your cat photo. Return to your website ' +
- 'by typing any letter into your code editor.'
- );
- }
-
function sponsors(req, res) {
res.render('sponsors/sponsors', {
title: 'The Sponsors who make Free Code Camp Possible'
@@ -247,7 +258,7 @@ module.exports = function(app) {
return next(err);
}
user.sendMonthlyEmail = false;
- user.save(function () {
+ user.save(function() {
if (err) {
return next(err);
}
@@ -302,7 +313,7 @@ module.exports = function(app) {
secrets.github.clientSecret
].join(''),
githubHeaders,
- function (err, status2, issues) {
+ function(err, status2, issues) {
if (err) { return next(err); }
issues = ((pulls === parseInt(pulls, 10)) && issues) ?
Object.keys(JSON.parse(issues)).length - pulls :
@@ -336,7 +347,7 @@ module.exports = function(app) {
'https://www.googleapis.com/blogger/v3/blogs/2421288658305323950/' +
'posts?key=' +
secrets.blogger.key,
- function (err, status, blog) {
+ function(err, status, blog) {
if (err) { return next(err); }
blog = (status && status.statusCode === 200) ?
diff --git a/server/boot/user.js b/server/boot/user.js
index fa529cdd62..f266fa6f12 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -15,6 +15,12 @@ const debug = debugFactory('freecc:boot:user');
const daysBetween = 1.5;
const sendNonUserToMap = ifNoUserRedirectTo('/map');
+function replaceScriptTags(value) {
+ return value
+ .replace(/