Add certification page

pull/3570/head
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();
});
}
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,14 +5,25 @@
{
"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": [0, 1]
}
],
"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/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",

View File

@ -6,13 +6,25 @@
"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": [0, 1]
}
],
"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/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",

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 {
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,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) {
if (req.user.isLocked === true) {
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) {
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 || [])};