Merge pull request #3570 from FreeCodeCamp/feature/certificates

Add certification page
This commit is contained in:
Quincy Larson
2015-10-05 17:19:17 -07:00
17 changed files with 453 additions and 86 deletions

View File

@ -865,16 +865,55 @@ common.init.push((function() {
} }
next(); next();
}); });
} }
function handleActionClick() { function handleActionClick(e) {
$(this) 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() .parent()
.find('.disabled') .find('.disabled')
.removeClass('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('<p>' + data + '</p>');
})
.fail(function() {
console.log('failed');
});
}
function handleFinishClick(e) { function handleFinishClick(e) {
e.preventDefault(); e.preventDefault();
$(submitModalId).modal('show'); $(submitModalId).modal('show');

View File

@ -102,14 +102,31 @@
}, },
"isLocked": { "isLocked": {
"type": "boolean", "type": "boolean",
"default": false "default": false,
"description": "Campers profile does not show challenges to the public"
}, },
"currentChallenge": { "currentChallenge": {
"type": {} "type": {}
}, },
"isUniqMigrated": { "isUniqMigrated": {
"type": "boolean", "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": { "completedChallenges": {
"type": [ "type": [

BIN
public/fonts/saxmono.ttf Executable file

Binary file not shown.

View File

@ -5,13 +5,36 @@
{ {
"id": "561add10cb82ac38a17513be", "id": "561add10cb82ac38a17513be",
"title": "Claim Your Front End Development Certificate", "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": [ "description": [
[ [
"http://i.imgur.com/RlEk2IF.jpg", "http://i.imgur.com/luMkKst.jpg",
"a picture of Free Code Camp's 4 benefits: Get connected, Learn JavaScript, Build your Portfolio, Help nonprofits", "An image of our Front End Development Certificate",
"Welcome to Free Code Camp. We're an open source community of busy people who learn to code and help nonprofits.", "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 elses 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.",
"" ""
] ]
], ],

View File

@ -6,12 +6,36 @@
"id": "660add10cb82ac38a17513be", "id": "660add10cb82ac38a17513be",
"title": "Claim Your Full Stack Development Certificate", "title": "Claim Your Full Stack Development Certificate",
"difficulty": 0.00, "difficulty": 0.00,
"challengeSeed": [], "challengeSeed": [
{
"properties": ["isHonest", "isFullStackCert"],
"apis": ["/certificate/honest", "/certificate/verify/full-stack"],
"stepIndex": [1, 2]
}
],
"description": [ "description": [
[ [
"http://i.imgur.com/RlEk2IF.jpg", "http://i.imgur.com/qXublEe.jpg",
"a picture of Free Code Camp's 4 benefits: Get connected, Learn JavaScript, Build your Portfolio, Help nonprofits", "An image of our Full Stack Development Certificate",
"Welcome to Free Code Camp. We're an open source community of busy people who learn to code and help nonprofits.", "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 elses 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.",
"" ""
] ]
], ],

139
server/boot/certificate.js Normal file
View File

@ -0,0 +1,139 @@
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';
const frontEndChallangeId = '561add10cb82ac38a17513be';
const fullStackChallangeId = '660add10cb82ac38a17513be';
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,
{
tests: true
}
)
.map(({ tests = [] }) => tests)
.shareReplay();
const fullStackChallangeIds$ = observeQuery(
Challenge,
'findById',
fullStackChallangeId,
{
tests: true
}
)
.map(({ tests = [] }) => tests)
.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((tests) => {
const { user } = req;
if (
isFront && !user.isFrontEndCert && isCertified(tests, user) ||
!isFront && !user.isFullStackCert && isCertified(tests, user)
) {
debug('certified');
if (isFront) {
user.isFrontEndCert = true;
user.completedChallenges.push({
completedDate: new Date(),
id: frontEndChallangeId
})
} else {
user.isFullStackCert = true;
user.completedChallenges.push({
completedDate: new Date(),
id: fullStackChallangeId
})
}
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
);
}
}

View File

@ -9,11 +9,10 @@ import utils from '../utils';
import { import {
saveUser, saveUser,
observeMethod, observeMethod,
observableQueryFromModel observeQuery
} from '../utils/rx'; } from '../utils/rx';
import { import {
userMigration,
ifNoUserSend ifNoUserSend
} from '../utils/middleware'; } from '../utils/middleware';
@ -147,8 +146,6 @@ module.exports = function(app) {
completedBonfire completedBonfire
); );
// the follow routes are covered by userMigration
router.use(userMigration);
router.get('/map', challengeMap); router.get('/map', challengeMap);
router.get( router.get(
'/challenges/next-challenge', '/challenges/next-challenge',
@ -330,7 +327,7 @@ module.exports = function(app) {
challengeType: 5 challengeType: 5
}; };
observableQueryFromModel( observeQuery(
User, User,
'findOne', 'findOne',
{ where: { username: ('' + completedWith).toLowerCase() } } { where: { username: ('' + completedWith).toLowerCase() } }
@ -458,7 +455,7 @@ module.exports = function(app) {
verified: false verified: false
}; };
observableQueryFromModel( observeQuery(
User, User,
'findOne', 'findOne',
{ where: { username: completedWith.toLowerCase() } } { where: { username: completedWith.toLowerCase() } }

View File

@ -1,8 +1,11 @@
import _ from 'lodash';
import dedent from 'dedent'; import dedent from 'dedent';
import moment from 'moment'; import moment from 'moment';
import { Observable } from 'rx';
import debugFactory from 'debug'; import debugFactory from 'debug';
import { ifNoUser401, ifNoUserRedirectTo } from '../utils/middleware'; import { ifNoUser401, ifNoUserRedirectTo } from '../utils/middleware';
import { observeQuery } from '../utils/rx';
const debug = debugFactory('freecc:boot:user'); const debug = debugFactory('freecc:boot:user');
const daysBetween = 1.5; const daysBetween = 1.5;
@ -52,7 +55,16 @@ function dayDiff([head, tail]) {
module.exports = function(app) { module.exports = function(app) {
var router = app.loopback.Router(); var router = app.loopback.Router();
var User = app.models.User; 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) { router.get('/login', function(req, res) {
res.redirect(301, '/signin'); res.redirect(301, '/signin');
@ -85,7 +97,18 @@ module.exports = function(app) {
); );
router.get('/vote1', vote1); router.get('/vote1', vote1);
router.get('/vote2', vote2); 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); router.get('/:username', returnUser);
app.use(router); app.use(router);
@ -184,14 +207,20 @@ module.exports = function(app) {
return (obj.name || '').match(/^Waypoint/i); return (obj.name || '').match(/^Waypoint/i);
}); });
debug('user is fec', profileUser.isFrontEndCert);
res.render('account/show', { res.render('account/show', {
title: 'Camper ' + profileUser.username + '\'s portfolio', title: 'Camper ' + profileUser.username + '\'s portfolio',
username: profileUser.username, username: profileUser.username,
name: profileUser.name, name: profileUser.name,
isMigrationGrandfathered: profileUser.isMigrationGrandfathered, isMigrationGrandfathered: profileUser.isMigrationGrandfathered,
isGithubCool: profileUser.isGithubCool, isGithubCool: profileUser.isGithubCool,
isLocked: !!profileUser.isLocked, isLocked: !!profileUser.isLocked,
isFrontEndCert: profileUser.isFrontEndCert,
isFullStackCert: profileUser.isFullStackCert,
isHonest: profileUser.isHonest,
location: profileUser.location, location: profileUser.location,
calender: data, calender: data,
@ -216,6 +245,64 @@ 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 (
showFront && user.isFrontEndCert ||
!showFront && user.isFullStackCert
) {
var { completedDate } = _.find(user.completedChallenges, {
id: showFront ?
'561add10cb82ac38a17513be' :
'660add10cb82ac38a17513be'
});
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) { function toggleLockdownMode(req, res, next) {
if (req.user.isLocked === true) { if (req.user.isLocked === true) {
req.user.isLocked = false; req.user.isLocked = false;
@ -297,11 +384,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) { function postForgot(req, res) {
const errors = req.validationErrors(); const errors = req.validationErrors();
const email = req.body.email.toLowerCase(); const email = req.body.email.toLowerCase();

View File

@ -1,60 +1,24 @@
var R = require('ramda'); export function ifNoUserRedirectTo(url) {
/*
* 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) {
return function(req, res, next) { return function(req, res, next) {
if (req.user) { if (req.user) {
return next(); return next();
} }
return res.redirect(url); return res.redirect(url);
}; };
}; }
exports.ifNoUserSend = function ifNoUserSend(sendThis) { export function ifNoUserSend(sendThis) {
return function(req, res, next) { return function(req, res, next) {
if (req.user) { if (req.user) {
return next(); return next();
} }
return res.status(200).send(sendThis); return res.status(200).send(sendThis);
}; };
}; }
exports.ifNoUser401 = function ifNoUser401(req, res, next) { export function ifNoUser401(req, res, next) {
if (req.user) { if (req.user) {
return next(); return next();
} }
return res.status(401).end(); return res.status(401).end();
}; }

View File

@ -1,7 +1,9 @@
var Rx = require('rx'); import Rx from 'rx';
var debug = require('debug')('freecc:rxUtils'); 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) { return new Rx.Observable.create(function(observer) {
if (!instance || typeof instance.save !== 'function') { if (!instance || typeof instance.save !== 'function') {
debug('no instance or save method'); debug('no instance or save method');
@ -17,16 +19,15 @@ exports.saveInstance = function saveInstance(instance) {
observer.onCompleted(); observer.onCompleted();
}); });
}); });
}; }
// alias saveInstance // alias saveInstance
exports.saveUser = exports.saveInstance; export const saveUser = saveInstance;
exports.observeQuery = exports.observableQueryFromModel = export function observeQuery(Model, method, query) {
function observableQueryFromModel(Model, method, query) {
return Rx.Observable.fromNodeCallback(Model[method], Model)(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); return Rx.Observable.fromNodeCallback(context[methodName], context);
}; }

View File

@ -58,8 +58,13 @@ block content
h1.flat-top.wrappable= name h1.flat-top.wrappable= name
h1.flat-top.wrappable= location h1.flat-top.wrappable= location
h1.flat-top.text-primary= "[ " + (progressTimestamps.length) + " ]" 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) 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 i.fa.fa-plus-square
| Add them to my personal leaderboard | Add them to my personal leaderboard

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
extends ../layout
block content
.panel.panel-info
.panel-heading.text-center
h1 Certificate
.panel-body
p foo

View File

@ -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;
})();

View File

@ -9,7 +9,7 @@ block content
.caption .caption
p.large-p= step[2] p.large-p= step[2]
if step[3] 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 if index + 1 === description.length
.btn.btn-block.btn-primary.challenge-step-btn-finish(id='last' class=step[3] ? 'disabled' : '') Finish challenge .btn.btn-block.btn-primary.challenge-step-btn-finish(id='last' class=step[3] ? 'disabled' : '') Finish challenge
else 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 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(src=rev('/js', 'commonFramework.js'))
script. script.
var common = common || { init: [] }; var common = window.common || { init: [] };
common.challengeId = !{JSON.stringify(challengeId)}; common.challengeId = !{JSON.stringify(challengeId)};
common.challengeName = !{JSON.stringify(name)}; common.challengeName = !{JSON.stringify(name)};
common.challengeType = 7; common.challengeType = 7;
common.dashedName = !{JSON.stringify(dashedName || '')}; 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 || [])};