Add certification page

This commit is contained in:
Berkeley Martinez
2015-10-02 11:47:36 -07:00
parent d9332e7d03
commit 8c48626f03
17 changed files with 415 additions and 82 deletions

View File

@ -865,14 +865,53 @@ common.init.push((function() {
} }
next(); next();
}); });
} }
function handleActionClick() { function handleActionClick(e) {
$(this) var props = common.challengeSeed[0] ||
.parent() { stepIndex: [] };
.find('.disabled')
.removeClass('disabled'); 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) { function handleFinishClick(e) {

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,14 +5,25 @@
{ {
"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": [0, 1]
}
],
"description": [ "description": [
[ [
"http://i.imgur.com/RlEk2IF.jpg", "http://i.imgur.com/RlEk2IF.jpg",
"a picture of Free Code Camp's 4 benefits: Get connected, Learn JavaScript, Build your Portfolio, Help nonprofits", "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.", "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/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.",
"#"
] ]
], ],
"type": "Waypoint", "type": "Waypoint",

View File

@ -6,13 +6,25 @@
"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": [0, 1]
}
],
"description": [ "description": [
[ [
"http://i.imgur.com/RlEk2IF.jpg", "http://i.imgur.com/RlEk2IF.jpg",
"a picture of Free Code Camp's 4 benefits: Get connected, Learn JavaScript, Build your Portfolio, Help nonprofits", "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.", "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/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.",
"#"
] ]
], ],
"type": "Waypoint", "type": "Waypoint",

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

@ -0,0 +1,131 @@
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;
} else {
user.isFullStackCert = true;
}
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,62 @@ 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: '561add10cb82ac38a17513be'
});
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 +382,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 || [])};