From 753fba73f55edfb8a7f71c3b3a83ba2e5b889460 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 3 May 2016 00:45:34 -0700 Subject: [PATCH] feature: notify camper by email on completion --- server/boot/a-extendEmail.js | 6 ++ server/boot/certificate.js | 161 +++++++++++++++++++++++------ server/boot/challenge.js | 7 +- server/views/emails/a-new-user.ejs | 7 ++ server/views/emails/certified.ejs | 13 +++ 5 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 server/boot/a-extendEmail.js create mode 100644 server/views/emails/a-new-user.ejs create mode 100644 server/views/emails/certified.ejs diff --git a/server/boot/a-extendEmail.js b/server/boot/a-extendEmail.js new file mode 100644 index 0000000000..fca44a011e --- /dev/null +++ b/server/boot/a-extendEmail.js @@ -0,0 +1,6 @@ +import { Observable } from 'rx'; + +export default function extendEmail(app) { + const { Email } = app.models; + Email.send$ = Observable.fromNodeCallback(Email.send, Email); +} diff --git a/server/boot/certificate.js b/server/boot/certificate.js index b45ec34ee6..c41ac148ad 100644 --- a/server/boot/certificate.js +++ b/server/boot/certificate.js @@ -1,4 +1,7 @@ import _ from 'lodash'; +import moment from 'moment'; +import loopback from 'loopback'; +import path from 'path'; import dedent from 'dedent'; import { Observable } from 'rx'; import debug from 'debug'; @@ -23,6 +26,20 @@ import { import certTypes from '../utils/certTypes.json'; const log = debug('fcc:certification'); +const renderCertifedEmail = loopback.template(path.join( + __dirname, + '..', + 'views', + 'emails', + 'certified.ejs' +)); +const renderNotifyEmail = loopback.template(path.join( + __dirname, + '..', + 'views', + 'emails', + 'a-new-user.ejs' +)); const sendMessageToNonUser = ifNoUserSend( 'must be logged in to complete.' ); @@ -46,9 +63,85 @@ function getIdsForCert$(id, Challenge) { .shareReplay(); } +// getFormatedDate(challengeMap: Object, challengeId: String) => String, throws +function getFormatedDate(challengeMap, challengeId) { + return moment(challengeMap[challengeId].completedDate) + .format('MMM Do, YYYY'); +} + +// sendCertifiedEmail( +// { +// email: String, +// username: String, +// isFrontEndCert: Boolean, +// isBackEndCert: Boolean, +// isDataVisCert: Boolean +// }, +// send$: Observable +// ) => Observable +function sendCertifiedEmail( + { + email, + name, + username, + isFrontEndCert, + isBackEndCert, + isDataVisCert, + challengeMap + }, + send$ +) { + if ( + !isFrontEndCert || + !isBackEndCert || + !isDataVisCert + ) { + return Observable.just(false); + } + let frontEndDate; + let backEndDate; + let dataVisDate; + try { + frontEndDate = getFormatedDate(challengeMap, frontEndChallengeId); + backEndDate = getFormatedDate(challengeMap, backEndChallengeId); + dataVisDate = getFormatedDate(challengeMap, dataVisChallengeId); + } catch (err) { + return Observable.throw(err); + } + + const notifyTeam = { + type: 'email', + to: 'Michael@FreeCodeCamp.com', + from: 'Team@FreeCodeCamp.com', + subject: 'A new user has arrived!', + text: renderNotifyEmail({ + username, + name, + frontEndDate, + dataVisDate, + backEndDate + }) + }; + const notifyUser = { + type: 'email', + to: email, + from: 'Michael@FreeCodeCamp.com', + subject: 'Congratulation on gaining your third certification!', + text: renderCertifedEmail({ + username, + name + }) + }; + return Observable.combineLatest( + send$(notifyTeam), + send$(notifyUser), + () => true + ); +} + export default function certificate(app) { const router = app.loopback.Router(); - const { Challenge } = app.models; + const { Email, Challenge } = app.models; const certTypeIds = { [certTypes.frontEnd]: getIdsForCert$(frontEndChallengeId, Challenge), @@ -94,36 +187,46 @@ export default function certificate(app) { challengeType } = challenge; if ( - !user[certType] && - isCertified(tests, user.challengeMap) + user[certType] || + !isCertified(tests, user.challengeMap) ) { - const updateData = { - $set: { - [`challengeMap.${id}`]: { - id, - name, - completedDate: new Date(), - challengeType - }, - [certType]: true - } - }; - - return req.user.update$(updateData) - // If user has commited to nonprofit, - // this will complete his pledge - .flatMap( - () => completeCommitment$(user), - ({ count }, pledgeOrMessage) => { - if (typeof pledgeOrMessage === 'string') { - log(pledgeOrMessage); - } - log(`${count} documents updated`); - return true; - } - ); + return Observable.just(false); } - return Observable.just(false); + const updateData = { + $set: { + [`challengeMap.${id}`]: { + id, + name, + completedDate: new Date(), + challengeType + }, + [certType]: true + } + }; + // set here so sendCertifiedEmail works properly + // not used otherwise + user[certType] = true; + user.challengeMap[id] = { completedDate: new Date() }; + return Observable.combineLatest( + // update user data + user.update$(updateData), + // If user has committed to nonprofit, + // this will complete their pledge + completeCommitment$(user), + // sends notification email is user has all three certs + // if not it noop + sendCertifiedEmail(user, Email.send$), + ({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage }) + ) + .map( + ({ count, pledgeOrMessage }) => { + if (typeof pledgeOrMessage === 'string') { + log(pledgeOrMessage); + } + log(`${count} documents updated`); + return true; + } + ); }) .subscribe( (didCertify) => { diff --git a/server/boot/challenge.js b/server/boot/challenge.js index c194311404..0325c5248a 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -474,12 +474,7 @@ module.exports = function(app) { return Observable.just() .doOnCompleted(() => { req.flash('info', { - msg: dedent` - Once you have completed all of our challenges, you should - join our Half Way Club and start getting - ready for our nonprofit projects. - `.split('\n').join(' ') + msg: 'You\'ve completed the last challenge!' }); return res.redirect('/map'); }); diff --git a/server/views/emails/a-new-user.ejs b/server/views/emails/a-new-user.ejs new file mode 100644 index 0000000000..b4be2e4c27 --- /dev/null +++ b/server/views/emails/a-new-user.ejs @@ -0,0 +1,7 @@ +Camper <%= username %> has completed all three certifications! + +Completed front end cert on <%= frontEndDate %>. +Completed data vis cert on <%= dataVisDate %>. +Completed back end cert on <%= backEndDate %>. + +https://www.freecodecamp.com/<%= username %> diff --git a/server/views/emails/certified.ejs b/server/views/emails/certified.ejs new file mode 100644 index 0000000000..0987058f4e --- /dev/null +++ b/server/views/emails/certified.ejs @@ -0,0 +1,13 @@ +Hi <%= name || username %>, + +Congratulations on recently completing all three Free Code Camp certifications. But you have not yet finished Free Code Camp. The most important part still remains: the nonprofit projects. + +These will help you contextualize and apply the raw skills you've picked up. You'll practice by building full stack JavaScript apps that real people will use. And in the end, you'll have code in production that you can showcase in your portfolio. This will be critical to your job search. + +Please fill this out immediately: http://bit.ly/20VwLg0 + +Once you fill this out we will get back to you as quickly as possible to get you started. + +Best, + +Michael - nonprofit guy @ Free Code Camp