| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  | import _ from 'lodash'; | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  | import loopback from 'loopback'; | 
					
						
							|  |  |  | import path from 'path'; | 
					
						
							| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  | import dedent from 'dedent'; | 
					
						
							|  |  |  | import { Observable } from 'rx'; | 
					
						
							| 
									
										
										
										
											2016-02-09 20:54:49 -08:00
										 |  |  | import debug from 'debug'; | 
					
						
							| 
									
										
										
										
											2017-08-26 00:27:05 +02:00
										 |  |  | import { isEmail } from 'validator'; | 
					
						
							| 
									
										
										
										
											2020-06-03 22:11:26 +09:00
										 |  |  | import { reportError } from '../middlewares/sentry-error-handler.js'; | 
					
						
							| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | import { ifNoUser401 } from '../utils/middleware'; | 
					
						
							| 
									
										
										
										
											2016-02-09 20:54:49 -08:00
										 |  |  | import { observeQuery } from '../utils/rx'; | 
					
						
							| 
									
										
										
										
											2015-10-05 19:58:44 -07:00
										 |  |  | import { | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |   legacyFrontEndChallengeId, | 
					
						
							|  |  |  |   legacyBackEndChallengeId, | 
					
						
							|  |  |  |   legacyDataVisId, | 
					
						
							| 
									
										
										
										
											2020-04-23 09:08:50 -04:00
										 |  |  |   legacyInfosecQaId, | 
					
						
							| 
									
										
										
										
											2020-06-16 01:21:30 +09:00
										 |  |  |   legacyFullStackId, | 
					
						
							| 
									
										
										
										
											2017-12-21 01:15:23 +00:00
										 |  |  |   respWebDesignId, | 
					
						
							|  |  |  |   frontEndLibsId, | 
					
						
							|  |  |  |   jsAlgoDataStructId, | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |   dataVis2018Id, | 
					
						
							| 
									
										
										
										
											2017-12-21 01:15:23 +00:00
										 |  |  |   apisMicroservicesId, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |   qaV7Id, | 
					
						
							|  |  |  |   infosecV7Id, | 
					
						
							|  |  |  |   sciCompPyV7Id, | 
					
						
							|  |  |  |   dataAnalysisPyV7Id, | 
					
						
							|  |  |  |   machineLearningPyV7Id | 
					
						
							| 
									
										
										
										
											2015-10-05 19:58:44 -07:00
										 |  |  | } from '../utils/constantStrings.json'; | 
					
						
							| 
									
										
										
										
											2020-03-26 11:57:15 +03:00
										 |  |  | import { oldDataVizId } from '../../../config/misc'; | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | import certTypes from '../utils/certTypes.json'; | 
					
						
							|  |  |  | import superBlockCertTypeMap from '../utils/superBlockCertTypeMap'; | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | import { completeCommitment$ } from '../utils/commit'; | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  | import { getChallenges } from '../utils/get-curriculum'; | 
					
						
							| 
									
										
										
										
											2015-10-06 21:08:24 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-27 11:34:44 -08:00
										 |  |  | const log = debug('fcc:certification'); | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  | export default function bootCertificate(app, done) { | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   const api = app.loopback.Router(); | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  |   // TODO: rather than getting all the challenges, then grabbing the certs,
 | 
					
						
							|  |  |  |   // consider just getting the certs.
 | 
					
						
							|  |  |  |   getChallenges().then(allChallenges => { | 
					
						
							|  |  |  |     const certTypeIds = createCertTypeIds(allChallenges); | 
					
						
							|  |  |  |     const showCert = createShowCert(app); | 
					
						
							|  |  |  |     const verifyCert = createVerifyCert(certTypeIds, app); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     api.put('/certificate/verify', ifNoUser401, ifNoSuperBlock404, verifyCert); | 
					
						
							|  |  |  |     api.get('/certificate/showCert/:username/:cert', showCert); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     app.use(api); | 
					
						
							|  |  |  |     done(); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-06 15:24:12 +02:00
										 |  |  | export function getFallbackFrontEndDate(completedChallenges, completedDate) { | 
					
						
							|  |  |  |   var chalIds = [...Object.values(certIds), oldDataVizId]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const latestCertDate = completedChallenges | 
					
						
							|  |  |  |     .filter(chal => chalIds.includes(chal.id)) | 
					
						
							|  |  |  |     .sort((a, b) => b.completedDate - a.completedDate)[0].completedDate; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return latestCertDate ? latestCertDate : completedDate; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | const noNameMessage = dedent`
 | 
					
						
							|  |  |  |   We need your name so we can put it on your certification. | 
					
						
							|  |  |  |   Add your name to your account settings and click the save button. | 
					
						
							|  |  |  |   Then we can issue your certification. | 
					
						
							|  |  |  |   `;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const notCertifiedMessage = name => dedent`
 | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  |   It looks like you have not completed the necessary steps. | 
					
						
							|  |  |  |   Please complete the required projects to claim the | 
					
						
							|  |  |  |   ${name} Certification | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   `;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const alreadyClaimedMessage = name => dedent`
 | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  |     It looks like you already have claimed the ${name} Certification | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |     `;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const successMessage = (username, name) => dedent`
 | 
					
						
							|  |  |  |     @${username}, you have successfully claimed | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  |     the ${name} Certification! | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |     Congratulations on behalf of the freeCodeCamp.org team! | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-03 22:11:26 +09:00
										 |  |  | const failureMessage = name => dedent`
 | 
					
						
							|  |  |  |     Something went wrong with the verification of ${name}, please try again. | 
					
						
							|  |  |  |     If you continue to receive this error, you can send a message to | 
					
						
							|  |  |  |     support@freeCodeCamp.org to get help. | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | function ifNoSuperBlock404(req, res, next) { | 
					
						
							|  |  |  |   const { superBlock } = req.body; | 
					
						
							|  |  |  |   if (superBlock && superBlocks.includes(superBlock)) { | 
					
						
							|  |  |  |     return next(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return res.status(404).end(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-02-08 13:29:10 -05:00
										 |  |  | const renderCertifiedEmail = loopback.template( | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   path.join(__dirname, '..', 'views', 'emails', 'certified.ejs') | 
					
						
							|  |  |  | ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  | function createCertTypeIds(allChallenges) { | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   return { | 
					
						
							|  |  |  |     // legacy
 | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  |     [certTypes.frontEnd]: getCertById(legacyFrontEndChallengeId, allChallenges), | 
					
						
							|  |  |  |     [certTypes.backEnd]: getCertById(legacyBackEndChallengeId, allChallenges), | 
					
						
							|  |  |  |     [certTypes.dataVis]: getCertById(legacyDataVisId, allChallenges), | 
					
						
							|  |  |  |     [certTypes.infosecQa]: getCertById(legacyInfosecQaId, allChallenges), | 
					
						
							|  |  |  |     [certTypes.fullStack]: getCertById(legacyFullStackId, allChallenges), | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // modern
 | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  |     [certTypes.respWebDesign]: getCertById(respWebDesignId, allChallenges), | 
					
						
							|  |  |  |     [certTypes.frontEndLibs]: getCertById(frontEndLibsId, allChallenges), | 
					
						
							|  |  |  |     [certTypes.dataVis2018]: getCertById(dataVis2018Id, allChallenges), | 
					
						
							|  |  |  |     [certTypes.jsAlgoDataStruct]: getCertById( | 
					
						
							|  |  |  |       jsAlgoDataStructId, | 
					
						
							|  |  |  |       allChallenges | 
					
						
							|  |  |  |     ), | 
					
						
							|  |  |  |     [certTypes.apisMicroservices]: getCertById( | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |       apisMicroservicesId, | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  |       allChallenges | 
					
						
							|  |  |  |     ), | 
					
						
							|  |  |  |     [certTypes.qaV7]: getCertById(qaV7Id, allChallenges), | 
					
						
							|  |  |  |     [certTypes.infosecV7]: getCertById(infosecV7Id, allChallenges), | 
					
						
							|  |  |  |     [certTypes.sciCompPyV7]: getCertById(sciCompPyV7Id, allChallenges), | 
					
						
							|  |  |  |     [certTypes.dataAnalysisPyV7]: getCertById( | 
					
						
							|  |  |  |       dataAnalysisPyV7Id, | 
					
						
							|  |  |  |       allChallenges | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |     ), | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  |     [certTypes.machineLearningPyV7]: getCertById( | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |       machineLearningPyV7Id, | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  |       allChallenges | 
					
						
							| 
									
										
										
										
											2020-02-25 00:10:32 +05:30
										 |  |  |     ) | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-15 17:58:26 +02:00
										 |  |  | function canClaim(ids, completedChallenges = []) { | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   return _.every(ids, ({ id }) => | 
					
						
							|  |  |  |     _.find(completedChallenges, ({ id: completedId }) => completedId === id) | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |   ); | 
					
						
							| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | const certIds = { | 
					
						
							|  |  |  |   [certTypes.frontEnd]: legacyFrontEndChallengeId, | 
					
						
							|  |  |  |   [certTypes.backEnd]: legacyBackEndChallengeId, | 
					
						
							|  |  |  |   [certTypes.dataVis]: legacyDataVisId, | 
					
						
							| 
									
										
										
										
											2020-04-23 09:08:50 -04:00
										 |  |  |   [certTypes.infosecQa]: legacyInfosecQaId, | 
					
						
							| 
									
										
										
										
											2020-06-16 01:21:30 +09:00
										 |  |  |   [certTypes.fullStack]: legacyFullStackId, | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |   [certTypes.respWebDesign]: respWebDesignId, | 
					
						
							|  |  |  |   [certTypes.frontEndLibs]: frontEndLibsId, | 
					
						
							|  |  |  |   [certTypes.jsAlgoDataStruct]: jsAlgoDataStructId, | 
					
						
							|  |  |  |   [certTypes.dataVis2018]: dataVis2018Id, | 
					
						
							|  |  |  |   [certTypes.apisMicroservices]: apisMicroservicesId, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |   [certTypes.qaV7]: qaV7Id, | 
					
						
							|  |  |  |   [certTypes.infosecV7]: infosecV7Id, | 
					
						
							|  |  |  |   [certTypes.sciCompPyV7]: sciCompPyV7Id, | 
					
						
							|  |  |  |   [certTypes.dataAnalysisPyV7]: dataAnalysisPyV7Id, | 
					
						
							|  |  |  |   [certTypes.machineLearningPyV7]: machineLearningPyV7Id | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const certText = { | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |   [certTypes.frontEnd]: 'Legacy Front End', | 
					
						
							|  |  |  |   [certTypes.backEnd]: 'Legacy Back End', | 
					
						
							|  |  |  |   [certTypes.dataVis]: 'Legacy Data Visualization', | 
					
						
							| 
									
										
										
										
											2020-04-23 09:08:50 -04:00
										 |  |  |   [certTypes.infosecQa]: 'Legacy Information Security and Quality Assurance', | 
					
						
							| 
									
										
										
										
											2020-06-16 01:21:30 +09:00
										 |  |  |   [certTypes.fullStack]: 'Legacy Full Stack', | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |   [certTypes.respWebDesign]: 'Responsive Web Design', | 
					
						
							|  |  |  |   [certTypes.frontEndLibs]: 'Front End Libraries', | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   [certTypes.jsAlgoDataStruct]: 'JavaScript Algorithms and Data Structures', | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |   [certTypes.dataVis2018]: 'Data Visualization', | 
					
						
							|  |  |  |   [certTypes.apisMicroservices]: 'APIs and Microservices', | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |   [certTypes.qaV7]: 'Quality Assurance', | 
					
						
							|  |  |  |   [certTypes.infosecV7]: 'Information Security', | 
					
						
							|  |  |  |   [certTypes.sciCompPyV7]: 'Scientific Computing with Python', | 
					
						
							|  |  |  |   [certTypes.dataAnalysisPyV7]: 'Data Analysis with Python', | 
					
						
							|  |  |  |   [certTypes.machineLearningPyV7]: 'Machine Learning with Python' | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | const completionHours = { | 
					
						
							|  |  |  |   [certTypes.frontEnd]: 400, | 
					
						
							|  |  |  |   [certTypes.backEnd]: 400, | 
					
						
							|  |  |  |   [certTypes.dataVis]: 400, | 
					
						
							| 
									
										
										
										
											2020-04-23 09:08:50 -04:00
										 |  |  |   [certTypes.infosecQa]: 300, | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   [certTypes.fullStack]: 1800, | 
					
						
							|  |  |  |   [certTypes.respWebDesign]: 300, | 
					
						
							|  |  |  |   [certTypes.frontEndLibs]: 300, | 
					
						
							|  |  |  |   [certTypes.jsAlgoDataStruct]: 300, | 
					
						
							|  |  |  |   [certTypes.dataVis2018]: 300, | 
					
						
							|  |  |  |   [certTypes.apisMicroservices]: 300, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |   [certTypes.qaV7]: 300, | 
					
						
							|  |  |  |   [certTypes.infosecV7]: 300, | 
					
						
							|  |  |  |   [certTypes.sciCompPyV7]: 400, | 
					
						
							|  |  |  |   [certTypes.dataAnalysisPyV7]: 400, | 
					
						
							|  |  |  |   [certTypes.machineLearningPyV7]: 400 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-23 10:01:21 +02:00
										 |  |  | function getCertById(anId, allChallenges) { | 
					
						
							|  |  |  |   return allChallenges | 
					
						
							|  |  |  |     .filter(({ id }) => id === anId) | 
					
						
							|  |  |  |     .map(({ id, tests, name, challengeType }) => ({ | 
					
						
							|  |  |  |       id, | 
					
						
							|  |  |  |       tests, | 
					
						
							|  |  |  |       name, | 
					
						
							|  |  |  |       challengeType | 
					
						
							| 
									
										
										
										
											2020-07-15 17:58:26 +02:00
										 |  |  |     }))[0]; | 
					
						
							| 
									
										
										
										
											2016-01-11 15:58:37 -08:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | const superBlocks = Object.keys(superBlockCertTypeMap); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  | function sendCertifiedEmail( | 
					
						
							|  |  |  |   { | 
					
						
							| 
									
										
										
										
											2018-07-02 09:06:35 +01:00
										 |  |  |     email = '', | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |     name, | 
					
						
							|  |  |  |     username, | 
					
						
							| 
									
										
										
										
											2017-12-21 01:15:23 +00:00
										 |  |  |     isRespWebDesignCert, | 
					
						
							|  |  |  |     isFrontEndLibsCert, | 
					
						
							|  |  |  |     isJsAlgoDataStructCert, | 
					
						
							|  |  |  |     isDataVisCert, | 
					
						
							|  |  |  |     isApisMicroservicesCert, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |     isQaCertV7, | 
					
						
							|  |  |  |     isInfosecCertV7, | 
					
						
							|  |  |  |     isSciCompPyCertV7, | 
					
						
							|  |  |  |     isDataAnalysisPyCertV7, | 
					
						
							|  |  |  |     isMachineLearningPyCertV7 | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |   }, | 
					
						
							|  |  |  |   send$ | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  |   if ( | 
					
						
							| 
									
										
										
										
											2017-08-26 00:27:05 +02:00
										 |  |  |     !isEmail(email) || | 
					
						
							| 
									
										
										
										
											2017-12-21 01:15:23 +00:00
										 |  |  |     !isRespWebDesignCert || | 
					
						
							|  |  |  |     !isFrontEndLibsCert || | 
					
						
							|  |  |  |     !isJsAlgoDataStructCert || | 
					
						
							|  |  |  |     !isDataVisCert || | 
					
						
							|  |  |  |     !isApisMicroservicesCert || | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |     !isQaCertV7 || | 
					
						
							|  |  |  |     !isInfosecCertV7 || | 
					
						
							|  |  |  |     !isSciCompPyCertV7 || | 
					
						
							|  |  |  |     !isDataAnalysisPyCertV7 || | 
					
						
							|  |  |  |     !isMachineLearningPyCertV7 | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |   ) { | 
					
						
							|  |  |  |     return Observable.just(false); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const notifyUser = { | 
					
						
							|  |  |  |     type: 'email', | 
					
						
							|  |  |  |     to: email, | 
					
						
							| 
									
										
										
										
											2018-11-19 22:51:25 -07:00
										 |  |  |     from: 'quincy@freecodecamp.org', | 
					
						
							| 
									
										
										
										
											2017-09-08 10:52:40 -07:00
										 |  |  |     subject: dedent`
 | 
					
						
							|  |  |  |       Congratulations on completing all of the | 
					
						
							| 
									
										
										
										
											2018-05-21 16:21:15 +01:00
										 |  |  |       freeCodeCamp certifications! | 
					
						
							| 
									
										
										
										
											2017-09-08 10:52:40 -07:00
										 |  |  |     `,
 | 
					
						
							| 
									
										
										
										
											2020-02-08 13:29:10 -05:00
										 |  |  |     text: renderCertifiedEmail({ | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |       username, | 
					
						
							|  |  |  |       name | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2017-08-26 00:27:05 +02:00
										 |  |  |   return send$(notifyUser).map(() => true); | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  | function getUserIsCertMap(user) { | 
					
						
							|  |  |  |   const { | 
					
						
							|  |  |  |     isRespWebDesignCert = false, | 
					
						
							|  |  |  |     isJsAlgoDataStructCert = false, | 
					
						
							|  |  |  |     isFrontEndLibsCert = false, | 
					
						
							|  |  |  |     is2018DataVisCert = false, | 
					
						
							|  |  |  |     isApisMicroservicesCert = false, | 
					
						
							|  |  |  |     isInfosecQaCert = false, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |     isQaCertV7 = false, | 
					
						
							|  |  |  |     isInfosecCertV7 = false, | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  |     isFrontEndCert = false, | 
					
						
							|  |  |  |     isBackEndCert = false, | 
					
						
							|  |  |  |     isDataVisCert = false, | 
					
						
							| 
									
										
										
										
											2020-02-25 00:10:32 +05:30
										 |  |  |     isFullStackCert = false, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |     isSciCompPyCertV7 = false, | 
					
						
							|  |  |  |     isDataAnalysisPyCertV7 = false, | 
					
						
							|  |  |  |     isMachineLearningPyCertV7 = false | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  |   } = user; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     isRespWebDesignCert, | 
					
						
							|  |  |  |     isJsAlgoDataStructCert, | 
					
						
							|  |  |  |     isFrontEndLibsCert, | 
					
						
							|  |  |  |     is2018DataVisCert, | 
					
						
							|  |  |  |     isApisMicroservicesCert, | 
					
						
							|  |  |  |     isInfosecQaCert, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |     isQaCertV7, | 
					
						
							|  |  |  |     isInfosecCertV7, | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  |     isFrontEndCert, | 
					
						
							|  |  |  |     isBackEndCert, | 
					
						
							|  |  |  |     isDataVisCert, | 
					
						
							| 
									
										
										
										
											2020-02-25 00:10:32 +05:30
										 |  |  |     isFullStackCert, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |     isSciCompPyCertV7, | 
					
						
							|  |  |  |     isDataAnalysisPyCertV7, | 
					
						
							|  |  |  |     isMachineLearningPyCertV7 | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | function createVerifyCert(certTypeIds, app) { | 
					
						
							|  |  |  |   const { Email } = app.models; | 
					
						
							|  |  |  |   return function verifyCert(req, res, next) { | 
					
						
							|  |  |  |     const { | 
					
						
							|  |  |  |       body: { superBlock }, | 
					
						
							|  |  |  |       user | 
					
						
							|  |  |  |     } = req; | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |     log(superBlock); | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |     let certType = superBlockCertTypeMap[superBlock]; | 
					
						
							|  |  |  |     log(certType); | 
					
						
							| 
									
										
										
										
											2020-07-15 17:58:26 +02:00
										 |  |  |     return Observable.of(certTypeIds[certType]) | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |       .flatMap(challenge => { | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |         const certName = certText[certType]; | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         if (user[certType]) { | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |           return Observable.just(alreadyClaimedMessage(certName)); | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-08-02 20:13:10 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-03 22:11:26 +09:00
										 |  |  |         // certificate doesn't exist or
 | 
					
						
							|  |  |  |         // connection error
 | 
					
						
							|  |  |  |         if (!challenge) { | 
					
						
							|  |  |  |           reportError(`Error claiming ${certName}`); | 
					
						
							|  |  |  |           return Observable.just(failureMessage(certName)); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const { id, tests, challengeType } = challenge; | 
					
						
							| 
									
										
										
										
											2020-07-15 17:58:26 +02:00
										 |  |  |         if (!canClaim(tests, user.completedChallenges)) { | 
					
						
							| 
									
										
										
										
											2020-06-03 22:11:26 +09:00
										 |  |  |           return Observable.just(notCertifiedMessage(certName)); | 
					
						
							| 
									
										
										
										
											2018-08-02 20:13:10 +05:30
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2020-07-15 17:58:26 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         const updateData = { | 
					
						
							|  |  |  |           [certType]: true, | 
					
						
							| 
									
										
										
										
											2020-06-03 22:11:26 +09:00
										 |  |  |           completedChallenges: [ | 
					
						
							|  |  |  |             ...user.completedChallenges, | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               id, | 
					
						
							|  |  |  |               completedDate: new Date(), | 
					
						
							|  |  |  |               challengeType | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           ] | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2018-08-02 20:13:10 +05:30
										 |  |  | 
 | 
					
						
							|  |  |  |         if (!user.name) { | 
					
						
							|  |  |  |           return Observable.just(noNameMessage); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |         // set here so sendCertifiedEmail works properly
 | 
					
						
							|  |  |  |         // not used otherwise
 | 
					
						
							|  |  |  |         user[certType] = true; | 
					
						
							| 
									
										
										
										
											2018-11-12 16:58:34 +00:00
										 |  |  |         const updatePromise = new Promise((resolve, reject) => | 
					
						
							|  |  |  |           user.updateAttributes(updateData, err => { | 
					
						
							|  |  |  |             if (err) { | 
					
						
							|  |  |  |               return reject(err); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return resolve(); | 
					
						
							|  |  |  |           }) | 
					
						
							|  |  |  |         ); | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |         return Observable.combineLatest( | 
					
						
							|  |  |  |           // update user data
 | 
					
						
							| 
									
										
										
										
											2018-11-12 16:58:34 +00:00
										 |  |  |           Observable.fromPromise(updatePromise), | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |           // If user has committed to nonprofit,
 | 
					
						
							|  |  |  |           // this will complete their pledge
 | 
					
						
							|  |  |  |           completeCommitment$(user), | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |           // sends notification email is user has all 6 certs
 | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |           // if not it noop
 | 
					
						
							|  |  |  |           sendCertifiedEmail(user, Email.send$), | 
					
						
							| 
									
										
										
										
											2019-02-18 00:55:22 +03:00
										 |  |  |           (_, pledgeOrMessage) => ({ pledgeOrMessage }) | 
					
						
							|  |  |  |         ).map(({ pledgeOrMessage }) => { | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |           if (typeof pledgeOrMessage === 'string') { | 
					
						
							|  |  |  |             log(pledgeOrMessage); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2019-02-18 00:55:22 +03:00
										 |  |  |           log('Certificates updated'); | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |           return successMessage(user.username, certName); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |       .subscribe(message => { | 
					
						
							|  |  |  |         return res.status(200).json({ | 
					
						
							| 
									
										
										
										
											2018-09-25 12:51:17 +01:00
										 |  |  |           response: { | 
					
						
							|  |  |  |             type: message.includes('Congratulations') ? 'success' : 'info', | 
					
						
							|  |  |  |             message | 
					
						
							|  |  |  |           }, | 
					
						
							| 
									
										
										
										
											2020-06-10 08:54:19 +02:00
										 |  |  |           isCertMap: getUserIsCertMap(user), | 
					
						
							|  |  |  |           // send back the completed challenges
 | 
					
						
							|  |  |  |           // NOTE: we could just send back the latest challenge, but this
 | 
					
						
							|  |  |  |           // ensures the challenges are synced.
 | 
					
						
							|  |  |  |           completedChallenges: user.completedChallenges | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |         }); | 
					
						
							|  |  |  |       }, next); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function createShowCert(app) { | 
					
						
							|  |  |  |   const { User } = app.models; | 
					
						
							| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   function findUserByUsername$(username, fields) { | 
					
						
							|  |  |  |     return observeQuery(User, 'findOne', { | 
					
						
							|  |  |  |       where: { username }, | 
					
						
							|  |  |  |       fields | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |   return function showCert(req, res, next) { | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |     let { username, cert } = req.params; | 
					
						
							|  |  |  |     username = username.toLowerCase(); | 
					
						
							|  |  |  |     const certType = superBlockCertTypeMap[cert]; | 
					
						
							|  |  |  |     const certId = certIds[certType]; | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |     const certTitle = certText[certType]; | 
					
						
							|  |  |  |     const completionTime = completionHours[certType] || 300; | 
					
						
							|  |  |  |     return findUserByUsername$(username, { | 
					
						
							|  |  |  |       isCheater: true, | 
					
						
							|  |  |  |       isFrontEndCert: true, | 
					
						
							|  |  |  |       isBackEndCert: true, | 
					
						
							|  |  |  |       isFullStackCert: true, | 
					
						
							|  |  |  |       isRespWebDesignCert: true, | 
					
						
							|  |  |  |       isFrontEndLibsCert: true, | 
					
						
							|  |  |  |       isJsAlgoDataStructCert: true, | 
					
						
							|  |  |  |       isDataVisCert: true, | 
					
						
							|  |  |  |       is2018DataVisCert: true, | 
					
						
							|  |  |  |       isApisMicroservicesCert: true, | 
					
						
							|  |  |  |       isInfosecQaCert: true, | 
					
						
							| 
									
										
										
										
											2020-06-18 00:00:31 +09:00
										 |  |  |       isQaCertV7: true, | 
					
						
							|  |  |  |       isInfosecCertV7: true, | 
					
						
							|  |  |  |       isSciCompPyCertV7: true, | 
					
						
							|  |  |  |       isDataAnalysisPyCertV7: true, | 
					
						
							|  |  |  |       isMachineLearningPyCertV7: true, | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |       isHonest: true, | 
					
						
							|  |  |  |       username: true, | 
					
						
							|  |  |  |       name: true, | 
					
						
							|  |  |  |       completedChallenges: true, | 
					
						
							|  |  |  |       profileUI: true | 
					
						
							|  |  |  |     }).subscribe(user => { | 
					
						
							|  |  |  |       if (!user) { | 
					
						
							|  |  |  |         return res.json({ | 
					
						
							|  |  |  |           messages: [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							| 
									
										
										
										
											2019-02-18 19:32:49 +00:00
										 |  |  |               message: | 
					
						
							|  |  |  |                 'We could not find a user with the username "' + username + '"' | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |             } | 
					
						
							|  |  |  |           ] | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |       const { isLocked, showCerts } = user.profileUI; | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |       if (!user.name) { | 
					
						
							|  |  |  |         return res.json({ | 
					
						
							|  |  |  |           messages: [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							|  |  |  |               message: dedent`
 | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |               This user needs to add their name to their account | 
					
						
							| 
									
										
										
										
											2018-05-20 04:07:41 +01:00
										 |  |  |               in order for others to be able to view their certification. | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |             `
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |             } | 
					
						
							|  |  |  |           ] | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |       if (user.isCheater) { | 
					
						
							|  |  |  |         return res.json({ | 
					
						
							|  |  |  |           messages: [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							|  |  |  |               message: | 
					
						
							|  |  |  |                 'This user is not eligible for freeCodeCamp.org ' + | 
					
						
							|  |  |  |                 'certifications at this time' | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           ] | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |       if (isLocked) { | 
					
						
							|  |  |  |         return res.json({ | 
					
						
							|  |  |  |           messages: [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							|  |  |  |               message: dedent`
 | 
					
						
							| 
									
										
										
										
											2019-10-14 20:07:53 +03:00
										 |  |  |               ${username} has chosen to make their portfolio | 
					
						
							|  |  |  |                 private. They will need to make their portfolio public | 
					
						
							| 
									
										
										
										
											2018-05-20 04:07:41 +01:00
										 |  |  |                 in order for others to be able to view their certification. | 
					
						
							|  |  |  |             `
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |             } | 
					
						
							|  |  |  |           ] | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-05-20 04:07:41 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |       if (!showCerts) { | 
					
						
							|  |  |  |         return res.json({ | 
					
						
							|  |  |  |           messages: [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							|  |  |  |               message: dedent`
 | 
					
						
							| 
									
										
										
										
											2018-05-20 04:07:41 +01:00
										 |  |  |               ${username} has chosen to make their certifications | 
					
						
							|  |  |  |                 private. They will need to make their certifications public | 
					
						
							|  |  |  |                 in order for others to be able to view them. | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |             `
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |             } | 
					
						
							|  |  |  |           ] | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |       if (!user.isHonest) { | 
					
						
							|  |  |  |         return res.json({ | 
					
						
							|  |  |  |           messages: [ | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							|  |  |  |               message: dedent`
 | 
					
						
							| 
									
										
										
										
											2018-02-27 14:03:06 +00:00
										 |  |  |               ${username} has not yet agreed to our Academic Honesty Pledge. | 
					
						
							|  |  |  |             `
 | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |           ] | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (user[certType]) { | 
					
						
							|  |  |  |         const { completedChallenges = [] } = user; | 
					
						
							| 
									
										
										
										
											2019-04-02 21:04:14 +03:00
										 |  |  |         const certChallenge = _.find( | 
					
						
							|  |  |  |           completedChallenges, | 
					
						
							|  |  |  |           ({ id }) => certId === id | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |         let { completedDate = new Date() } = certChallenge || {}; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-08-09 21:27:26 +03:00
										 |  |  |         // the challenge id has been rotated for isDataVisCert
 | 
					
						
							| 
									
										
										
										
											2019-04-02 21:04:14 +03:00
										 |  |  |         if (certType === 'isDataVisCert' && !certChallenge) { | 
					
						
							|  |  |  |           let oldDataVisIdChall = _.find( | 
					
						
							|  |  |  |             completedChallenges, | 
					
						
							| 
									
										
										
										
											2020-03-26 11:57:15 +03:00
										 |  |  |             ({ id }) => oldDataVizId === id | 
					
						
							| 
									
										
										
										
											2019-04-02 21:04:14 +03:00
										 |  |  |           ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if (oldDataVisIdChall) { | 
					
						
							|  |  |  |             completedDate = oldDataVisIdChall.completedDate || completedDate; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-26 11:57:15 +03:00
										 |  |  |         // if fullcert is not found, return the latest completedDate
 | 
					
						
							|  |  |  |         if (certType === 'isFullStackCert' && !certChallenge) { | 
					
						
							| 
									
										
										
										
											2020-05-06 15:24:12 +02:00
										 |  |  |           completedDate = getFallbackFrontEndDate( | 
					
						
							|  |  |  |             completedChallenges, | 
					
						
							|  |  |  |             completedDate | 
					
						
							|  |  |  |           ); | 
					
						
							| 
									
										
										
										
											2020-03-26 11:57:15 +03:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |         const { username, name } = user; | 
					
						
							|  |  |  |         return res.json({ | 
					
						
							|  |  |  |           certTitle, | 
					
						
							|  |  |  |           username, | 
					
						
							|  |  |  |           name, | 
					
						
							| 
									
										
										
										
											2020-08-20 13:16:10 +02:00
										 |  |  |           date: completedDate, | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |           completionTime | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return res.json({ | 
					
						
							|  |  |  |         messages: [ | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             type: 'info', | 
					
						
							| 
									
										
										
										
											2019-07-19 19:28:04 +05:30
										 |  |  |             message: `
 | 
					
						
							|  |  |  | It looks like user ${username} is not ${certText[certType]} certified | 
					
						
							|  |  |  |           `
 | 
					
						
							| 
									
										
										
										
											2018-09-04 14:54:23 +01:00
										 |  |  |           } | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }, next); | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2015-10-02 11:47:36 -07:00
										 |  |  | } |