diff --git a/client/commonFramework.js b/client/commonFramework.js index ced143db58..bff815cb1d 100644 --- a/client/commonFramework.js +++ b/client/commonFramework.js @@ -879,14 +879,53 @@ common.init.push((function() { } next(); }); - } - function handleActionClick() { - $(this) - .parent() - .find('.disabled') - .removeClass('disabled'); + function handleActionClick(e) { + var props = common.challengeSeed[0] || + { stepIndex: [] }; + + var $el = $(this); + var index = +$el.attr('id'); + var propIndex = props.stepIndex.indexOf(index); + + if (propIndex === -1) { + return $el + .parent() + .find('.disabled') + .removeClass('disabled'); + } + + // an API action + // prevent link from opening + e.preventDefault(); + var prop = props.properties[propIndex]; + var api = props.apis[propIndex]; + if (common[prop]) { + return $el + .parent() + .find('.disabled') + .removeClass('disabled'); + } + $ + .post(api) + .done(function(data) { + // assume a boolean indicates passing + if (typeof data === 'boolean') { + return $el + .parent() + .find('.disabled') + .removeClass('disabled'); + } + // assume api returns string when fails + $el + .parent() + .find('.disabled') + .replaceWith('
' + data + '
'); + }) + .fail(function() { + console.log('failed'); + }); } function handleFinishClick(e) { diff --git a/common/models/user.json b/common/models/user.json index c46d1c2f13..776c2c500e 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -102,14 +102,31 @@ }, "isLocked": { "type": "boolean", - "default": false + "default": false, + "description": "Campers profile does not show challenges/certificates to the public" }, "currentChallenge": { "type": {} }, "isUniqMigrated": { "type": "boolean", - "default": false + "default": false, + "description": "Campers completedChallenges array is free of duplicates" + }, + "isHonest": { + "type": "boolean", + "default": false, + "description": "Camper has signed academic honesty policy" + }, + "isFrontEndCert": { + "type": "boolean", + "defaut": false, + "description": "Camper is front end certified" + }, + "isFullStackCert": { + "type": "boolean", + "default": false, + "description": "Campers is full stack certified" }, "completedChallenges": { "type": [ diff --git a/public/fonts/saxmono.ttf b/public/fonts/saxmono.ttf new file mode 100755 index 0000000000..76c77d6411 Binary files /dev/null and b/public/fonts/saxmono.ttf differ diff --git a/seed/challenges/basic-bonfires.json b/seed/challenges/basic-bonfires.json index 931c157ce1..12da55a771 100644 --- a/seed/challenges/basic-bonfires.json +++ b/seed/challenges/basic-bonfires.json @@ -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..82a0430852 100644
--- a/seed/challenges/basic-javascript.json
+++ b/seed/challenges/basic-javascript.json
@@ -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/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/front-end-development-certificate.json b/seed/challenges/front-end-development-certificate.json
index fa0bb1ce40..351809e702 100644
--- a/seed/challenges/front-end-development-certificate.json
+++ b/seed/challenges/front-end-development-certificate.json
@@ -5,13 +5,36 @@
{
"id": "561add10cb82ac38a17513be",
"title": "Claim Your Front End Development Certificate",
- "difficulty": 0.00,
- "challengeSeed": [],
+ "challengeSeed": [
+ {
+ "properties": ["isHonest", "isFrontEndCert"],
+ "apis": ["/certificate/honest", "/certificate/verify/front-end"],
+ "stepIndex": [1, 2]
+ }
+ ],
"description": [
[
- "http://i.imgur.com/RlEk2IF.jpg",
- "a picture of Free Code Camp's 4 benefits: Get connected, Learn JavaScript, Build your Portfolio, Help nonprofits",
- "Welcome to Free Code Camp. We're an open source community of busy people who learn to code and help nonprofits.",
+ "http://i.imgur.com/luMkKst.jpg",
+ "An image of our Front End Development Certificate",
+ "This challenge will give you your verified Front End Development Certificate. Before we issue your certificate, we must verify that you have completed all of our basic and intermediate Bonfires, and all our basic and intermediate Ziplines. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
+ ""
+ ],
+ [
+ "http://i.imgur.com/HArFfMN.jpg",
+ "The definition of plagiarism: Plagiarism (noun) - copying someone else’s work and presenting it as your own without crediting them",
+ "By clicking below, you pledge that all of your submitted code A) is code you or your pair personally wrote, or B) comes from open source libraries like jQuery, or C) has been clearly attributed to its original authors. You also give us permission to audit your challenge solutions and revoke your certificate if we discover evidence of plagiarism.",
+ "#"
+ ],
+ [
+ "http://i.imgur.com/14F2Van.jpg",
+ "An image of the text \"Front End Development Certificate requirements\"",
+ "Let's confirm that you have completed all of our basic and intermediate Bonfires, and all our basic and intermediate Ziplines. Click the button below to verify this.",
+ "#"
+ ],
+ [
+ "http://i.imgur.com/16SIhHO.jpg",
+ "An image of the word \"Congratulations\"",
+ "Congratulations! We've added your Front End Development Certificate to your certificate to your portfolio page. Unless you choose to hide your solutions, this certificate will remain publicly visible and verifiable.",
""
]
],
diff --git a/seed/challenges/full-stack-development-certificate.json b/seed/challenges/full-stack-development-certificate.json
index 901883aebc..7ae95c94e7 100644
--- a/seed/challenges/full-stack-development-certificate.json
+++ b/seed/challenges/full-stack-development-certificate.json
@@ -6,12 +6,36 @@
"id": "660add10cb82ac38a17513be",
"title": "Claim Your Full Stack Development Certificate",
"difficulty": 0.00,
- "challengeSeed": [],
+ "challengeSeed": [
+ {
+ "properties": ["isHonest", "isFullStackCert"],
+ "apis": ["/certificate/honest", "/certificate/verify/full-stack"],
+ "stepIndex": [1, 2]
+ }
+ ],
"description": [
[
- "http://i.imgur.com/RlEk2IF.jpg",
- "a picture of Free Code Camp's 4 benefits: Get connected, Learn JavaScript, Build your Portfolio, Help nonprofits",
- "Welcome to Free Code Camp. We're an open source community of busy people who learn to code and help nonprofits.",
+ "http://i.imgur.com/qXublEe.jpg",
+ "An image of our Full Stack Development Certificate",
+ "This challenge will give you your verified Full Stack Development Certificate. Before we issue your certificate, we must verify that you have completed all of Bonfires, Ziplines and Basejumps. You must also accept our Academic Honesty Pledge. Click the button below to start this process.",
+ ""
+ ],
+ [
+ "http://i.imgur.com/HArFfMN.jpg",
+ "The definition of plagiarism: Plagiarism (noun) - copying someone else’s work and presenting it as your own without crediting them",
+ "By clicking below, you pledge that all of your submitted code A) is code you or your pair personally wrote, or B) comes from open source libraries like jQuery, or C) has been clearly attributed to its original authors. You also give us permission to audit your challenge solutions and revoke your certificate if we discover evidence of plagiarism.",
+ "#"
+ ],
+ [
+ "http://i.imgur.com/2qn7tHp.jpg",
+ "An image of the text \"Full Stack Development Certificate requirements\"",
+ "Let's confirm that you have completed all of our Bonfires, Ziplines and Basejumps. Click the button below to verify this.",
+ "#"
+ ],
+ [
+ "http://i.imgur.com/16SIhHO.jpg",
+ "An image of the word \"Congratulations\"",
+ "Congratulations! We've added your Full Stack Development Certificate to your certificate to your portfolio page. Unless you choose to hide your solutions, this certificate will remain publicly visible and verifiable.",
""
]
],
diff --git a/seed/challenges/getting-started.json b/seed/challenges/getting-started.json
index c35c3541f8..23b43e6b59 100644
--- a/seed/challenges/getting-started.json
+++ b/seed/challenges/getting-started.json
@@ -242,7 +242,7 @@
[
"",
"",
- "Free Code Camp will always be free. If you want to feel more motivated to earn our certificates faster, we encourage you to instead donate each month to a nonprofit.",
+ "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.",
""
]
],
diff --git a/server/boot/certificate.js b/server/boot/certificate.js
new file mode 100644
index 0000000000..b084d9f4b8
--- /dev/null
+++ b/server/boot/certificate.js
@@ -0,0 +1,151 @@
+import _ from 'lodash';
+import dedent from 'dedent';
+import { Observable } from 'rx';
+import debugFactory from 'debug';
+
+import {
+ ifNoUser401,
+ ifNoUserSend
+} from '../utils/middleware';
+
+import {
+ saveUser,
+ observeQuery
+} from '../utils/rx';
+
+import {
+ frontEndChallangeId,
+ fullStackChallangeId
+} from '../utils/constantStrings.json';
+
+const debug = debugFactory('freecc:certification');
+const sendMessageToNonUser = ifNoUserSend(
+ 'must be logged in to complete.'
+);
+
+function isCertified(frontEndIds, { completedChallenges, isFrontEndCert }) {
+ if (isFrontEndCert) {
+ return true;
+ }
+ return _.every(frontEndIds, ({ id }) => _.some(completedChallenges, { id }));
+}
+
+export default function certificate(app) {
+ const router = app.loopback.Router();
+ const { Challenge } = app.models;
+
+ const frontEndChallangeIds$ = observeQuery(
+ Challenge,
+ 'findById',
+ frontEndChallangeId,
+ {
+ id: true,
+ tests: true,
+ name: true,
+ challengeType: true
+ }
+ )
+ .shareReplay();
+
+ const fullStackChallangeIds$ = observeQuery(
+ Challenge,
+ 'findById',
+ fullStackChallangeId,
+ {
+ id: true,
+ tests: true,
+ name: true,
+ challengeType: true
+ }
+ )
+ .shareReplay();
+
+ router.post(
+ '/certificate/verify/front-end',
+ ifNoUser401,
+ verifyCert
+ );
+
+ router.post(
+ '/certificate/verify/full-stack',
+ ifNoUser401,
+ verifyCert
+ );
+
+ router.post(
+ '/certificate/honest',
+ sendMessageToNonUser,
+ postHonest
+ );
+
+ app.use(router);
+
+ function verifyCert(req, res, next) {
+ const isFront = req.path.split('/').pop() === 'front-end';
+ Observable.just({})
+ .flatMap(() => {
+ if (isFront) {
+ return frontEndChallangeIds$;
+ }
+ return fullStackChallangeIds$;
+ })
+ .flatMap(challenge => {
+ const { user } = req;
+ const {
+ id,
+ tests,
+ name,
+ challengeType
+ } = challenge;
+ if (
+ isFront && !user.isFrontEndCert && isCertified(tests, user) ||
+ !isFront && !user.isFullStackCert && isCertified(tests, user)
+ ) {
+ debug('certified');
+ if (isFront) {
+ user.isFrontEndCert = true;
+ } else {
+ user.isFullStackCert = true;
+ }
+
+ user.completedChallenges.push({
+ id,
+ name,
+ completedDate: new Date(),
+ challengeType
+ });
+ return saveUser(user);
+ }
+ return Observable.just(user);
+ })
+ .subscribe(
+ user => {
+ if (
+ isFront && user.isFrontEndCert ||
+ !isFront && user.isFullStackCert
+ ) {
+ return res.status(200).send(true);
+ }
+ return res.status(200).send(
+ dedent`
+ Looks like you have not completed the neccessary steps,
+ Please return the map
+ `
+ );
+ },
+ next
+ );
+ }
+
+ function postHonest(req, res, next) {
+ const { user } = req;
+ user.isHonest = true;
+ saveUser(user)
+ .subscribe(
+ (user) => {
+ res.status(200).send(!!user.isHonest);
+ },
+ next
+ );
+ }
+}
diff --git a/server/boot/challenge.js b/server/boot/challenge.js
index 15cb507855..745f8be876 100644
--- a/server/boot/challenge.js
+++ b/server/boot/challenge.js
@@ -9,11 +9,10 @@ import utils from '../utils';
import {
saveUser,
observeMethod,
- observableQueryFromModel
+ observeQuery
} from '../utils/rx';
import {
- userMigration,
ifNoUserSend
} from '../utils/middleware';
@@ -147,8 +146,6 @@ module.exports = function(app) {
completedBonfire
);
- // the follow routes are covered by userMigration
- router.use(userMigration);
router.get('/map', challengeMap);
router.get(
'/challenges/next-challenge',
@@ -183,9 +180,9 @@ module.exports = function(app) {
'could not find challenge block for ' + challenge.block
);
}
- const nextBlock$ = blocks$.elementAt(blockIndex + 1);
- const firstChallengeOfNextBlock$ = nextBlock$
- .map(block => block.challenges[0]);
+ const firstChallengeOfNextBlock$ = blocks$
+ .elementAtOrDefault(blockIndex + 1, {})
+ .map(({ challenges = [] }) => challenges[0]);
return blocks$
.elementAt(blockIndex)
@@ -214,6 +211,9 @@ module.exports = function(app) {
});
})
.map(nextChallenge => {
+ if (!nextChallenge) {
+ return null;
+ }
nextChallengeName = nextChallenge.dashedName;
return nextChallengeName;
})
@@ -270,12 +270,13 @@ module.exports = function(app) {
}
if (dasherize(challenge.name) !== origChallengeName) {
- return Observable.just(
- '/challenges/' +
- dasherize(challenge.name) +
- '?solution=' +
- encodeURIComponent(solutionCode)
- );
+ let redirectUrl = `/challenges/${dasherize(challenge.name)}`;
+
+ if (solutionCode) {
+ redirectUrl += `?solution=${encodeURIComponent(solutionCode)}`;
+ }
+
+ return Observable.just(redirectUrl);
}
// save user does nothing if user does not exist
@@ -330,7 +331,7 @@ module.exports = function(app) {
challengeType: 5
};
- observableQueryFromModel(
+ observeQuery(
User,
'findOne',
{ where: { username: ('' + completedWith).toLowerCase() } }
@@ -458,7 +459,7 @@ module.exports = function(app) {
verified: false
};
- observableQueryFromModel(
+ observeQuery(
User,
'findOne',
{ where: { username: completedWith.toLowerCase() } }
diff --git a/server/boot/commit.js b/server/boot/commit.js
new file mode 100644
index 0000000000..a54dc19506
--- /dev/null
+++ b/server/boot/commit.js
@@ -0,0 +1,15 @@
+export default function commit(app) {
+ const router = app.loopback.Router();
+ router.get(
+ '/commit',
+ commitToNonprofit
+ );
+
+ app.use(router);
+
+ function commitToNonprofit(req, res) {
+ res.render('commit/', {
+ title: 'Commit to a nonprofit. Commit to your goal.'
+ });
+ }
+}
diff --git a/server/boot/user.js b/server/boot/user.js
index 214c0b9264..fa529cdd62 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -1,8 +1,15 @@
+import _ from 'lodash';
import dedent from 'dedent';
import moment from 'moment';
+import { Observable } from 'rx';
import debugFactory from 'debug';
+import {
+ frontEndChallangeId,
+ fullStackChallangeId
+} from '../utils/constantStrings.json';
import { ifNoUser401, ifNoUserRedirectTo } from '../utils/middleware';
+import { observeQuery } from '../utils/rx';
const debug = debugFactory('freecc:boot:user');
const daysBetween = 1.5;
@@ -52,7 +59,16 @@ function dayDiff([head, tail]) {
module.exports = function(app) {
var router = app.loopback.Router();
var User = app.models.User;
- // var Story = app.models.Story;
+ function findUserByUsername$(username, fields) {
+ return observeQuery(
+ User,
+ 'findOne',
+ {
+ where: { username },
+ fields
+ }
+ );
+ }
router.get('/login', function(req, res) {
res.redirect(301, '/signin');
@@ -85,7 +101,18 @@ module.exports = function(app) {
);
router.get('/vote1', vote1);
router.get('/vote2', vote2);
- // Ensure this is the last route!
+
+ // Ensure these are the last routes!
+ router.get(
+ '/:username/front-end-certification',
+ showCert
+ );
+
+ router.get(
+ '/:username/full-stack-certification',
+ showCert
+ );
+
router.get('/:username', returnUser);
app.use(router);
@@ -184,14 +211,20 @@ module.exports = function(app) {
return (obj.name || '').match(/^Waypoint/i);
});
+ debug('user is fec', profileUser.isFrontEndCert);
res.render('account/show', {
title: 'Camper ' + profileUser.username + '\'s portfolio',
username: profileUser.username,
name: profileUser.name,
+
isMigrationGrandfathered: profileUser.isMigrationGrandfathered,
isGithubCool: profileUser.isGithubCool,
isLocked: !!profileUser.isLocked,
+ isFrontEndCert: profileUser.isFrontEndCert,
+ isFullStackCert: profileUser.isFullStackCert,
+ isHonest: profileUser.isHonest,
+
location: profileUser.location,
calender: data,
@@ -216,6 +249,92 @@ module.exports = function(app) {
);
}
+ function showCert(req, res, next) {
+ const username = req.params.username.toLowerCase();
+ const { user } = req;
+ const showFront = req.path.split('/').pop() === 'front-end-certification';
+ Observable.just(user)
+ .flatMap(user => {
+ if (user && user.username === username) {
+ return Observable.just(user);
+ }
+ return findUserByUsername$(username, {
+ isFrontEndCert: true,
+ isFullStackCert: true,
+ completedChallenges: true,
+ username: true,
+ name: true
+ });
+ })
+ .subscribe(
+ (user) => {
+ if (!user) {
+ req.flash('errors', {
+ msg: `404: We couldn't find the user ${username}`
+ });
+ return res.redirect('/');
+ }
+ if (!user.isGithubCool) {
+ req.flash('errors', {
+ msg: dedent`
+ This user needs to link GitHub with their account
+ in order to display this certificate to the public.
+ `
+ });
+ return res.redirect('back');
+ }
+ if (user.isLocked) {
+ req.flash('errors', {
+ msg: dedent`
+ ${username} has chosen to hide their work from the public.
+ They need to unhide their work in order for this certificate to
+ be verifiable.
+ `
+ });
+ return res.redirect('back');
+ }
+ if (!user.isHonest) {
+ req.flash('errors', {
+ msg: dedent`
+ ${username} has not agreed to our Academic Honesty Pledge yet.
+ `
+ });
+ return res.redirect('back');
+ }
+
+ if (
+ showFront && user.isFrontEndCert ||
+ !showFront && user.isFullStackCert
+ ) {
+ var { completedDate } = _.find(user.completedChallenges, {
+ id: showFront ?
+ frontEndChallangeId :
+ fullStackChallangeId
+ });
+
+ return res.render(
+ showFront ?
+ 'certificate/front-end.jade' :
+ 'certificate/full-stack.jade',
+ {
+ username: user.username,
+ date: moment(new Date(completedDate))
+ .format('MMMM, Do YYYY'),
+ name: user.name
+ }
+ );
+ }
+ req.flash('errors', {
+ msg: showFront ?
+ `Looks like user ${username} is not Front End certified` :
+ `Looks like user ${username} is not Full Stack certified`
+ });
+ res.redirect('/map');
+ },
+ next
+ );
+ }
+
function toggleLockdownMode(req, res, next) {
if (req.user.isLocked === true) {
req.user.isLocked = false;
@@ -297,11 +416,6 @@ module.exports = function(app) {
});
}
- /**
- * POST /forgot
- * Create a random token, then the send user an email with a reset link.
- */
-
function postForgot(req, res) {
const errors = req.validationErrors();
const email = req.body.email.toLowerCase();
diff --git a/server/utils/constantStrings.json b/server/utils/constantStrings.json
index 70f5d9766c..a2f5b29df8 100644
--- a/server/utils/constantStrings.json
+++ b/server/utils/constantStrings.json
@@ -1,3 +1,5 @@
{
- "gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36"
+ "gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
+ "frontEndChallangeId": "561add10cb82ac38a17513be",
+ "fullStackChallangeId": "660add10cb82ac38a17513be"
}
diff --git a/server/utils/middleware.js b/server/utils/middleware.js
index 1edec7a59b..3c541cbb11 100644
--- a/server/utils/middleware.js
+++ b/server/utils/middleware.js
@@ -1,60 +1,24 @@
-var R = require('ramda');
-
-/*
- * Middleware to migrate users from fragmented challenge structure to unified
- * challenge structure
- *
- * @param req
- * @param res
- * @returns null
- */
-exports.userMigration = function userMigration(req, res, next) {
- if (!req.user || req.user.completedChallenges.length !== 0) {
- return next();
- }
- req.user.completedChallenges = R.filter(function(elem) {
- // getting rid of undefined
- return elem;
- }, R.concat(
- req.user.completedCoursewares,
- req.user.completedBonfires.map(function(bonfire) {
- return ({
- completedDate: bonfire.completedDate,
- id: bonfire.id,
- name: bonfire.name,
- completedWith: bonfire.completedWith,
- solution: bonfire.solution,
- githubLink: '',
- verified: false,
- challengeType: 5
- });
- })
- )
- );
- return next();
-};
-
-exports.ifNoUserRedirectTo = function ifNoUserRedirectTo(url) {
+export function ifNoUserRedirectTo(url) {
return function(req, res, next) {
if (req.user) {
return next();
}
return res.redirect(url);
};
-};
+}
-exports.ifNoUserSend = function ifNoUserSend(sendThis) {
+export function ifNoUserSend(sendThis) {
return function(req, res, next) {
if (req.user) {
return next();
}
return res.status(200).send(sendThis);
};
-};
+}
-exports.ifNoUser401 = function ifNoUser401(req, res, next) {
+export function ifNoUser401(req, res, next) {
if (req.user) {
return next();
}
return res.status(401).end();
-};
+}
diff --git a/server/utils/rx.js b/server/utils/rx.js
index d3b1ac41b0..68086d2563 100644
--- a/server/utils/rx.js
+++ b/server/utils/rx.js
@@ -1,7 +1,9 @@
-var Rx = require('rx');
-var debug = require('debug')('freecc:rxUtils');
+import Rx from 'rx';
+import debugFactory from 'debug';
-exports.saveInstance = function saveInstance(instance) {
+const debug = debugFactory('freecc:rxUtils');
+
+export function saveInstance(instance) {
return new Rx.Observable.create(function(observer) {
if (!instance || typeof instance.save !== 'function') {
debug('no instance or save method');
@@ -17,16 +19,15 @@ exports.saveInstance = function saveInstance(instance) {
observer.onCompleted();
});
});
-};
+}
// alias saveInstance
-exports.saveUser = exports.saveInstance;
+export const saveUser = saveInstance;
-exports.observeQuery = exports.observableQueryFromModel =
- function observableQueryFromModel(Model, method, query) {
- return Rx.Observable.fromNodeCallback(Model[method], Model)(query);
- };
+export function observeQuery(Model, method, query) {
+ return Rx.Observable.fromNodeCallback(Model[method], Model)(query);
+}
-exports.observeMethod = function observeMethod(context, methodName) {
+export function observeMethod(context, methodName) {
return Rx.Observable.fromNodeCallback(context[methodName], context);
-};
+}
diff --git a/server/views/account/show.jade b/server/views/account/show.jade
index 5f07918815..530f74b746 100644
--- a/server/views/account/show.jade
+++ b/server/views/account/show.jade
@@ -58,8 +58,13 @@ block content
h1.flat-top.wrappable= name
h1.flat-top.wrappable= location
h1.flat-top.text-primary= "[ " + (progressTimestamps.length) + " ]"
+ if isFrontEndCert
+ a.btn.btn-primary(href='/' + username + '/front-end-certification') View My Front End Development Certification
+ if isFullStackCert
+ .button-spacer
+ a.btn.btn-success(href='/' + username + '/full-stack-certification') View My Full Stack Development Certification
if (user && user.username !== username)
- a.btn.btn-lg.btn-block.btn-twitter.btn-link-social(href='/link/twitter')
+ a.btn.btn-lg.btn-block.btn-twitter.btn-link-social(href='/leaderboard/add?username=#{username}')
i.fa.fa-plus-square
| Add them to my personal leaderboard
diff --git a/server/views/certificate/font.jade b/server/views/certificate/font.jade
new file mode 100644
index 0000000000..54b76b935f
--- /dev/null
+++ b/server/views/certificate/font.jade
@@ -0,0 +1,45 @@
+style.
+ @font-face {
+ font-family: "Sax Mono";
+ src: url("/fonts/saxmono.ttf") format("truetype");
+ }
+
+ body {
+ display: inline-block;
+ font-family: "Sax Mono", monospace;
+ margin: 0;
+ position: absolute;
+ text-align: center;
+ }
+
+ .img-abs {
+ left 0;
+ position: relative;
+ top: 0;
+ width: 2000px
+ }
+
+ .cert-name {
+ font-size: 64px;
+ left: 1000px;
+ position: absolute;
+ top: 704px;
+ z-index: 1000;
+ }
+
+ .cert-date {
+ font-size: 60px;
+ left: 760px;
+ position: absolute;
+ top: 1004.8px;
+ z-index: 1000;
+ }
+
+ .cert-link {
+ font-size: 22px;
+ left: 120px;
+ position: absolute;
+ top: 1488px;
+ z-index: 1000;
+ }
+
diff --git a/server/views/certificate/front-end.jade b/server/views/certificate/front-end.jade
new file mode 100644
index 0000000000..379dfb7dd1
--- /dev/null
+++ b/server/views/certificate/front-end.jade
@@ -0,0 +1,6 @@
+include font
+#name.cert-name= name
+img#cert.img-abs(src='http://i.imgur.com/ToFZKBd.jpg')
+.cert-date= date
+.cert-link verify this certification at: http://freecodecamp.com/#{username}/front-end-certification
+include script
diff --git a/server/views/certificate/full-stack.jade b/server/views/certificate/full-stack.jade
new file mode 100644
index 0000000000..95c94a6eb0
--- /dev/null
+++ b/server/views/certificate/full-stack.jade
@@ -0,0 +1,6 @@
+include font
+#name.cert-name= name
+img#cert.img-abs(src='http://i.imgur.com/Z4PgjBQ.jpg')
+.cert-date= date
+.cert-link verify this certification at: http://freecodecamp.com/#{username}/full-stack-certification
+include script
diff --git a/server/views/certificate/index.jade b/server/views/certificate/index.jade
new file mode 100644
index 0000000000..51e0294db2
--- /dev/null
+++ b/server/views/certificate/index.jade
@@ -0,0 +1,7 @@
+extends ../layout
+block content
+ .panel.panel-info
+ .panel-heading.text-center
+ h1 Certificate
+ .panel-body
+ p foo
diff --git a/server/views/certificate/script.jade b/server/views/certificate/script.jade
new file mode 100644
index 0000000000..ccb83323c5
--- /dev/null
+++ b/server/views/certificate/script.jade
@@ -0,0 +1,8 @@
+script.
+ (function() {
+ var containerWidth = document.getElementById('cert').offsetWidth;
+ var nameDiv = document.getElementById('name');
+ var nameWidth = nameDiv.offsetWidth;
+ console.log(containerWidth, nameWidth);
+ nameDiv.style.left = ((containerWidth - nameWidth) / 2) + 15;
+ })();
diff --git a/server/views/commit/index.jade b/server/views/commit/index.jade
new file mode 100644
index 0000000000..ba15d37fc1
--- /dev/null
+++ b/server/views/commit/index.jade
@@ -0,0 +1,54 @@
+extends ../layout
+block content
+ .panel.panel-info
+ .panel-body
+ h3.text-center Commit to yourself. Commit to a nonprofit.
+ .col-xs-12.col-sm-6.col-sm-offset-3
+ p Are you looking for a burst of motivation? Do you want to help nonprofits before you’re ready to code for them? You can do both by pledging a monthly donation to a nonprofit until you've earned either your Front End or Full Stack Development certificate. Join Commit below or click "maybe later".
+ .col-xs-12.col-sm-6.col-sm-offset-3
+ h4 Step 1: Choose your goal
+ .radio
+ label
+ input(type='radio' id='front-end-development-certificate' name='goal')
+ | Front End Development Certificate (takes about 400 hours)
+ .radio
+ label
+ input(type='radio' id='full-stack-development-certificate' name='goal')
+ | Full Stack Development Certificate (takes about 800 hours)
+ .spacer
+ h4 Step 2: Choose one of our nonprofits
+ .row
+ .col-xs-12.col-sm-6
+ a(href="http://i.imgur.com/U1CyEuA.jpg" data-lightbox="img-enlarge")
+ img.img-responsive(src='http://i.imgur.com/U1CyEuA.jpg' alt="Girl Develop It participants coding at tables.")
+ .radio
+ label
+ input(type='radio' id='girl-develop-it' name='nonprofit')
+ | Girl Develop It is a nonprofit that provides in-person classes for women to learn to code.
+ .col-xs-12.col-sm-6
+ a(href="http://i.imgur.com/NERytFF.jpg" data-lightbox="img-enlarge")
+ img.img-responsive(src='http://i.imgur.com/NERytFF.jpg' alt="Vets in Tech participants standing together at a conference.")
+ .radio
+ label
+ input(type='radio' id='vets-in-tech' name='nonprofit')
+ | Vets in Tech is a nonprofit that helps veterans prepare for tech jobs.
+ .spacer
+ h4 Step 3: Choose your monthly pledge
+ .radio
+ label
+ input(type='radio' id='5-dollar-pledge' name='pledge-amount')
+ | $5 per month
+ .radio
+ label
+ input(type='radio' id='10-dollar-pledge' name='pledge-amount')
+ | $10 per month
+ .radio
+ label
+ input(type='radio' id='50-dollar-pledge' name='pledge-amount')
+ | $50 per month
+
+ .spacer
+ a.button.btn.btn-block.btn-primary(href='https://www.paypal.com/us/cgi-bin/webscr?cmd=_flow&SESSION=T3x0DY-bLMFXuhmjYZXs-BhmDoiXfuNh5BWad5VBcMomkkDSZY0b_-_W3HS&dispatch=5885d80a13c0db1f8e263663d3faee8d0b9dcb01a9b6dc564e45f62871326a5e') Commit
+ .button-spacer
+ a.button.btn.btn-block.btn-warning(href='/') Maybe later
+ .spacer
diff --git a/server/views/coursewares/showStep.jade b/server/views/coursewares/showStep.jade
index f89d65eda2..fa8f11431a 100644
--- a/server/views/coursewares/showStep.jade
+++ b/server/views/coursewares/showStep.jade
@@ -9,7 +9,7 @@ block content
.caption
p.large-p= step[2]
if step[3]
- a.btn.btn-block.btn-primary.challenge-step-btn-action(href='#{step[3]}' target='_blank') Go To Link
+ a.btn.btn-block.btn-primary.challenge-step-btn-action(id='#{index}' href='#{step[3]}' target='_blank') Go To Link
if index + 1 === description.length
.btn.btn-block.btn-primary.challenge-step-btn-finish(id='last' class=step[3] ? 'disabled' : '') Finish challenge
else
@@ -32,8 +32,12 @@ block content
a.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) Go to my next challenge
script(src=rev('/js', 'commonFramework.js'))
script.
- var common = common || { init: [] };
+ var common = window.common || { init: [] };
common.challengeId = !{JSON.stringify(challengeId)};
common.challengeName = !{JSON.stringify(name)};
common.challengeType = 7;
common.dashedName = !{JSON.stringify(dashedName || '')};
+ common.isHonest = !{JSON.stringify(isHonest || false)};
+ common.isFrontEndCert = !{JSON.stringify(isFrontEndCert || false)};
+ common.isFullStackCert = !{JSON.stringify(isFullStackCert || false)};
+ common.challengeSeed = !{JSON.stringify(challengeSeed || [])};