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,14 +865,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('<p>' + data + '</p>');
})
.fail(function() {
console.log('failed');
});
}
function handleFinishClick(e) {

View File

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

BIN
public/fonts/saxmono.ttf Executable file

Binary file not shown.

View File

@ -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 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",
"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 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 {
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',
@ -330,7 +327,7 @@ module.exports = function(app) {
challengeType: 5
};
observableQueryFromModel(
observeQuery(
User,
'findOne',
{ where: { username: ('' + completedWith).toLowerCase() } }
@ -458,7 +455,7 @@ module.exports = function(app) {
verified: false
};
observableQueryFromModel(
observeQuery(
User,
'findOne',
{ where: { username: completedWith.toLowerCase() } }

View File

@ -1,8 +1,11 @@
import _ from 'lodash';
import dedent from 'dedent';
import moment from 'moment';
import { Observable } from 'rx';
import debugFactory from 'debug';
import { ifNoUser401, ifNoUserRedirectTo } from '../utils/middleware';
import { observeQuery } from '../utils/rx';
const debug = debugFactory('freecc:boot:user');
const daysBetween = 1.5;
@ -52,7 +55,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 +97,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 +207,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 +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) {
if (req.user.isLocked === true) {
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) {
const errors = req.validationErrors();
const email = req.body.email.toLowerCase();

View File

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

View File

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

View File

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

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
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 || [])};