feat(legacy-certs): Claim legacy certificates from the settings page

This commit is contained in:
Stuart Taylor
2018-02-27 14:03:06 +00:00
parent b3aed512d6
commit 2d0f8f7b9b
21 changed files with 664 additions and 312 deletions

View File

@@ -1,5 +1,6 @@
import _ from 'lodash';
import loopback from 'loopback';
import moment from 'moment-timezone';
import path from 'path';
import dedent from 'dedent';
import { Observable } from 'rx';
@@ -13,26 +14,23 @@ import {
import { observeQuery } from '../utils/rx';
import {
// legacy
frontEndChallengeId,
backEndChallengeId,
dataVisId,
legacyFrontEndChallengeId,
legacyBackEndChallengeId,
legacyDataVisId,
// modern
respWebDesignId,
frontEndLibsId,
dataVis2018Id,
jsAlgoDataStructId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
completeCommitment$
} from '../utils/commit';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
const log = debug('fcc:certification');
const renderCertifedEmail = loopback.template(path.join(
__dirname,
@@ -46,6 +44,48 @@ function isCertified(ids, challengeMap = {}) {
return _.every(ids, ({ id }) => _.has(challengeMap, id));
}
const certIds = {
[certTypes.frontEnd]: legacyFrontEndChallengeId,
[certTypes.backEnd]: legacyBackEndChallengeId,
[certTypes.dataVis]: legacyDataVisId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
};
const certViews = {
[certTypes.frontEnd]: 'certificate/legacy/front-end.jade',
[certTypes.backEnd]: 'certificate/legacy/back-end.jade',
[certTypes.dataVis]: 'certificate/legacy/data-visualization.jade',
[certTypes.fullStack]: 'certificate/legacy/full-stack.jade',
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis2018]: 'certificate/data-visualization.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
};
const certText = {
[certTypes.frontEnd]: 'Legacy Front End certified',
[certTypes.backEnd]: 'Legacy Back End Certified',
[certTypes.dataVis]: 'Legacy Data Visualization Certified',
[certTypes.fullStack]: 'Legacy Full Stack Certified',
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis2018]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
};
function getIdsForCert$(id, Challenge) {
return observeQuery(
Challenge,
@@ -117,13 +157,24 @@ function sendCertifiedEmail(
export default function certificate(app) {
const router = app.loopback.Router();
const { Email, Challenge } = app.models;
const { Email, Challenge, User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(
User,
'findOne',
{
where: { username },
fields
}
);
}
const certTypeIds = {
// legacy
[certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(backEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(dataVisId, Challenge),
[certTypes.frontEnd]: getIdsForCert$(legacyFrontEndChallengeId, Challenge),
[certTypes.backEnd]: getIdsForCert$(legacyBackEndChallengeId, Challenge),
[certTypes.dataVis]: getIdsForCert$(legacyDataVisId, Challenge),
// modern
[certTypes.respWebDesign]: getIdsForCert$(respWebDesignId, Challenge),
@@ -145,6 +196,10 @@ export default function certificate(app) {
ifNoSuperBlock404,
verifyCert
);
router.get(
'/c/:username/:cert',
showCert
);
app.use(router);
@@ -172,16 +227,12 @@ export default function certificate(app) {
function verifyCert(req, res, next) {
const { body: { superBlock }, user } = req;
log(superBlock);
let certType = superBlockCertTypeMap[superBlock];
log(certType);
if (certType === 'isDataVisCert') {
certType = 'is2018DataVisCert';
log(certType);
}
return user.getChallengeMap$()
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
.flatMap(() => certTypeIds[certType])
.flatMap(challenge => {
const {
id,
tests,
@@ -251,4 +302,101 @@ export default function certificate(app) {
}
return res.status(404).end();
}
function showCert(req, res, next) {
let { username, cert } = req.params;
username = username.toLowerCase();
const certType = superBlockCertTypeMap[cert];
const certId = certIds[certType];
return findUserByUsername$(
username,
{
isCheater: true,
isLocked: true,
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
username: true,
name: true,
challengeMap: true
}
)
.subscribe(
user => {
const profile = `/${user.username}`;
if (!user) {
req.flash(
'danger',
`We couldn't find a user with the username ${username}`
);
return res.redirect('/');
}
if (!user.name) {
req.flash(
'danger',
dedent`
This user needs to add their name to their account
in order for others to be able to view their certificate.
`
);
return res.redirect(profile);
}
if (user.isCheater) {
return res.redirect(`/${user.username}`);
}
if (user.isLocked) {
req.flash(
'danger',
dedent`
${username} has chosen to make their profile
private. They will need to make their profile public
in order for others to be able to view their certificate.
`
);
return res.redirect('/');
}
if (!user.isHonest) {
req.flash(
'danger',
dedent`
${username} has not yet agreed to our Academic Honesty Pledge.
`
);
return res.redirect(profile);
}
if (user[certType]) {
const { challengeMap = {} } = user;
const { completedDate = new Date() } = challengeMap[certId] || {};
return res.render(
certViews[certType],
{
username: user.username,
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
name: user.name
}
);
}
req.flash(
'danger',
`Looks like user ${username} is not ${certText[certType]}`
);
return res.redirect(profile);
},
next
);
}
}

View File

@@ -1,88 +1,22 @@
import dedent from 'dedent';
import moment from 'moment-timezone';
import debugFactory from 'debug';
import { curry } from 'lodash';
import {
frontEndChallengeId,
backEndChallengeId,
respWebDesignId,
frontEndLibsId,
jsAlgoDataStructId,
dataVisId,
dataVis2018Id,
apisMicroservicesId,
infosecQaId
} from '../utils/constantStrings.json';
import certTypes from '../utils/certTypes.json';
import superBlockCertTypeMap from '../utils/superBlockCertTypeMap';
import {
ifNoUser401,
ifNoUserRedirectTo,
ifNotVerifiedRedirectToSettings
} from '../utils/middleware';
import { observeQuery } from '../utils/rx';
const debug = debugFactory('fcc:boot:user');
const sendNonUserToMap = ifNoUserRedirectTo('/map');
const sendNonUserToMapWithMessage = curry(ifNoUserRedirectTo, 2)('/map');
const certIds = {
[certTypes.frontEnd]: frontEndChallengeId,
[certTypes.backEnd]: backEndChallengeId,
[certTypes.respWebDesign]: respWebDesignId,
[certTypes.frontEndLibs]: frontEndLibsId,
[certTypes.jsAlgoDataStruct]: jsAlgoDataStructId,
[certTypes.dataVis]: dataVisId,
[certTypes.dataVis2018]: dataVis2018Id,
[certTypes.apisMicroservices]: apisMicroservicesId,
[certTypes.infosecQa]: infosecQaId
};
const certViews = {
[certTypes.frontEnd]: 'certificate/front-end.jade',
[certTypes.backEnd]: 'certificate/back-end.jade',
[certTypes.fullStack]: 'certificate/full-stack.jade',
[certTypes.respWebDesign]: 'certificate/responsive-web-design.jade',
[certTypes.frontEndLibs]: 'certificate/front-end-libraries.jade',
[certTypes.jsAlgoDataStruct]:
'certificate/javascript-algorithms-and-data-structures.jade',
[certTypes.dataVis]: 'certificate/data-visualization.jade',
[certTypes.dataVis2018]: 'certificate/data-visualization-2018.jade',
[certTypes.apisMicroservices]: 'certificate/apis-and-microservices.jade',
[certTypes.infosecQa]:
'certificate/information-security-and-quality-assurance.jade'
};
const certText = {
[certTypes.frontEnd]: 'Front End certified',
[certTypes.backEnd]: 'Back End Certified',
[certTypes.fullStack]: 'Full Stack Certified',
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
[certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified',
[certTypes.dataVis]: 'Data Visualization Certified',
[certTypes.dataVis2018]: 'Data Visualization Certified',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
};
module.exports = function(app) {
const router = app.loopback.Router();
const api = app.loopback.Router();
const { Email, User } = app.models;
function findUserByUsername$(username, fields) {
return observeQuery(
User,
'findOne',
{
where: { username },
fields
}
);
}
api.post(
'/account/delete',
ifNoUser401,
@@ -105,11 +39,6 @@ module.exports = function(app) {
);
// Ensure these are the last routes!
api.get(
'/c/:username/:cert',
showCert
);
router.get(
'/user/:username/report-user/',
sendNonUserToMapWithMessage('You must be signed in to report a user'),
@@ -185,100 +114,6 @@ module.exports = function(app) {
});
}
function showCert(req, res, next) {
let { username, cert } = req.params;
username = username.toLowerCase();
const certType = superBlockCertTypeMap[cert];
const certId = certIds[certType];
return findUserByUsername$(username, {
isCheater: true,
isLocked: true,
isFrontEndCert: true,
isBackEndCert: true,
isFullStackCert: true,
isRespWebDesignCert: true,
isFrontEndLibsCert: true,
isJsAlgoDataStructCert: true,
isDataVisCert: true,
is2018DataVisCert: true,
isApisMicroservicesCert: true,
isInfosecQaCert: true,
isHonest: true,
username: true,
name: true,
challengeMap: true
})
.subscribe(
user => {
const profile = `/${user.username}`;
if (!user) {
req.flash(
'danger',
`We couldn't find a user with the username ${username}`
);
return res.redirect('/');
}
if (!user.name) {
req.flash(
'danger',
dedent`
This user needs to add their name to their account
in order for others to be able to view their certificate.
`
);
return res.redirect(profile);
}
if (user.isCheater) {
return res.redirect(`/${user.username}`);
}
if (user.isLocked) {
req.flash(
'danger',
dedent`
${username} has chosen to make their profile
private. They will need to make their profile public
in order for others to be able to view their certificate.
`
);
return res.redirect('/');
}
if (!user.isHonest) {
req.flash(
'danger',
dedent`
${username} has not yet agreed to our Academic Honesty Pledge.
`
);
return res.redirect(profile);
}
if (user[certType]) {
const { challengeMap = {} } = user;
const { completedDate = new Date() } = challengeMap[certId] || {};
return res.render(
certViews[certType],
{
username: user.username,
date: moment(new Date(completedDate)).format('MMMM D, YYYY'),
name: user.name
}
);
}
req.flash(
'danger',
`Looks like user ${username} is not ${certText[certType]}`
);
return res.redirect(profile);
},
next
);
}
function postDeleteAccount(req, res, next) {
User.destroyById(req.user.id, function(err) {
if (err) { return next(err); }