| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  | import _ from 'lodash'; | 
					
						
							| 
									
										
										
										
											2015-08-29 01:48:01 -07:00
										 |  |  | import dedent from 'dedent'; | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  | import moment from 'moment'; | 
					
						
							| 
									
										
										
										
											2015-08-19 23:11:21 -07:00
										 |  |  | import { Observable, Scheduler } from 'rx'; | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  | import debug from 'debug'; | 
					
						
							| 
									
										
										
										
											2016-01-09 20:08:01 -08:00
										 |  |  | import accepts from 'accepts'; | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  | import { isMongoId } from 'validator'; | 
					
						
							| 
									
										
										
										
											2015-11-09 17:27:56 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   dasherize, | 
					
						
							|  |  |  |   unDasherize, | 
					
						
							|  |  |  |   getMDNLinks, | 
					
						
							|  |  |  |   randomVerb, | 
					
						
							|  |  |  |   randomPhrase, | 
					
						
							|  |  |  |   randomCompliment | 
					
						
							|  |  |  | } from '../utils'; | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  | import { observeMethod } from '../utils/rx'; | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | import { | 
					
						
							|  |  |  |   ifNoUserSend | 
					
						
							|  |  |  | } from '../utils/middleware'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-11-09 17:27:56 -08:00
										 |  |  | import getFromDisk$ from '../utils/getFromDisk$'; | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  | import badIdMap from '../utils/bad-id-map'; | 
					
						
							| 
									
										
										
										
											2015-11-09 17:27:56 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-10-06 22:37:08 -07:00
										 |  |  | const isDev = process.env.NODE_ENV !== 'production'; | 
					
						
							|  |  |  | const isBeta = !!process.env.BETA; | 
					
						
							| 
									
										
										
										
											2016-01-27 11:34:44 -08:00
										 |  |  | const log = debug('fcc:challenges'); | 
					
						
							| 
									
										
										
										
											2016-01-01 21:10:08 -08:00
										 |  |  | const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i; | 
					
						
							| 
									
										
										
										
											2015-08-19 23:11:21 -07:00
										 |  |  | const challengeView = { | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |   0: 'challenges/showHTML', | 
					
						
							|  |  |  |   1: 'challenges/showJS', | 
					
						
							|  |  |  |   2: 'challenges/showVideo', | 
					
						
							|  |  |  |   3: 'challenges/showZiplineOrBasejump', | 
					
						
							|  |  |  |   4: 'challenges/showZiplineOrBasejump', | 
					
						
							|  |  |  |   5: 'challenges/showBonfire', | 
					
						
							|  |  |  |   7: 'challenges/showStep' | 
					
						
							| 
									
										
										
										
											2015-08-19 23:11:21 -07:00
										 |  |  | }; | 
					
						
							| 
									
										
										
										
											2015-06-20 13:35:26 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-09 19:16:47 -05:00
										 |  |  | function isChallengeCompleted(user, challengeId) { | 
					
						
							|  |  |  |   if (!user) { | 
					
						
							|  |  |  |     return false; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2016-02-10 10:35:40 -08:00
										 |  |  |   return !!user.challengeMap[challengeId]; | 
					
						
							| 
									
										
										
										
											2015-12-09 19:16:47 -05:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  | /* | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  | function numberWithCommas(x) { | 
					
						
							|  |  |  |   return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  | */ | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  | function buildUserUpdate( | 
					
						
							|  |  |  |   user, | 
					
						
							|  |  |  |   challengeId, | 
					
						
							|  |  |  |   completedChallenge, | 
					
						
							|  |  |  |   timezone | 
					
						
							|  |  |  | ) { | 
					
						
							|  |  |  |   const updateData = { $set: {} }; | 
					
						
							|  |  |  |   let finalChallenge; | 
					
						
							|  |  |  |   const { timezone: userTimezone, challengeMap = {} } = user; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const oldChallenge = challengeMap[challengeId]; | 
					
						
							|  |  |  |   const alreadyCompleted = !!oldChallenge; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (alreadyCompleted) { | 
					
						
							|  |  |  |     // add data from old challenge
 | 
					
						
							|  |  |  |     finalChallenge = { | 
					
						
							|  |  |  |       ...completedChallenge, | 
					
						
							|  |  |  |       completedDate: oldChallenge.completedDate, | 
					
						
							|  |  |  |       lastUpdated: completedChallenge.completedDate | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2016-02-10 12:01:00 -08:00
										 |  |  |   } else { | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |     updateData.$push = { | 
					
						
							| 
									
										
										
										
											2016-02-10 12:01:00 -08:00
										 |  |  |       progressTimestamps: { | 
					
						
							|  |  |  |         timestamp: Date.now(), | 
					
						
							|  |  |  |         completedChallenge: challengeId | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |     }; | 
					
						
							|  |  |  |     finalChallenge = completedChallenge; | 
					
						
							| 
									
										
										
										
											2015-06-20 19:52:37 -07:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-10-01 21:44:24 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |   updateData.$set = { | 
					
						
							|  |  |  |     [`challengeMap.${challengeId}`]: finalChallenge | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if ( | 
					
						
							| 
									
										
										
										
											2016-02-10 10:05:51 -08:00
										 |  |  |     timezone && | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |     timezone !== 'UTC' && | 
					
						
							|  |  |  |     (!userTimezone || userTimezone === 'UTC') | 
					
						
							|  |  |  |   ) { | 
					
						
							|  |  |  |     updateData.$set = { | 
					
						
							|  |  |  |       ...updateData.$set, | 
					
						
							|  |  |  |       timezone: userTimezone | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2016-01-09 20:08:01 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |   log('user update data', updateData); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return { alreadyCompleted, updateData }; | 
					
						
							| 
									
										
										
										
											2015-06-20 19:52:37 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-04 22:13:14 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // small helper function to determine whether to mark something as new
 | 
					
						
							|  |  |  | const dateFormat = 'MMM MMMM DD, YYYY'; | 
					
						
							|  |  |  | function shouldShowNew(element, block) { | 
					
						
							|  |  |  |   if (element) { | 
					
						
							|  |  |  |     return typeof element.releasedOn !== 'undefined' && | 
					
						
							| 
									
										
										
										
											2016-01-01 00:34:05 -06:00
										 |  |  |       moment(element.releasedOn, dateFormat).diff(moment(), 'days') >= -60; | 
					
						
							| 
									
										
										
										
											2015-12-04 22:13:14 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (block) { | 
					
						
							|  |  |  |     const newCount = block.reduce((sum, { markNew }) => { | 
					
						
							|  |  |  |       if (markNew) { | 
					
						
							|  |  |  |         return sum + 1; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return sum; | 
					
						
							|  |  |  |     }, 0); | 
					
						
							|  |  |  |     return newCount / block.length * 100 === 100; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2016-03-02 20:54:14 -08:00
										 |  |  |   return null; | 
					
						
							| 
									
										
										
										
											2015-12-04 22:13:14 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-14 13:18:54 -08:00
										 |  |  | // meant to be used with a filter method
 | 
					
						
							|  |  |  | // on an array or observable stream
 | 
					
						
							|  |  |  | // true if challenge should be passed through
 | 
					
						
							|  |  |  | // false if should filter challenge out of array or stream
 | 
					
						
							|  |  |  | function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) { | 
					
						
							|  |  |  |   return isDev || | 
					
						
							|  |  |  |     !isComingSoon || | 
					
						
							|  |  |  |     (isBeta && challengeIsBeta); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  | function getRenderData$(user, challenge$, origChallengeName, solution) { | 
					
						
							|  |  |  |   const challengeName = unDasherize(origChallengeName) | 
					
						
							|  |  |  |     .replace(challengesRegex, ''); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const testChallengeName = new RegExp(challengeName, 'i'); | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |   log('looking for %s', testChallengeName); | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return challenge$ | 
					
						
							| 
									
										
										
										
											2016-01-15 01:44:18 -08:00
										 |  |  |     .map(challenge => challenge.toJSON()) | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |     .filter(challenge => { | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  |       return shouldNotFilterComingSoon(challenge) && | 
					
						
							|  |  |  |         challenge.type !== 'hike' && | 
					
						
							|  |  |  |         testChallengeName.test(challenge.name); | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |     }) | 
					
						
							|  |  |  |     .last({ defaultValue: null }) | 
					
						
							|  |  |  |     .flatMap(challenge => { | 
					
						
							|  |  |  |       if (challenge && isDev) { | 
					
						
							|  |  |  |         return getFromDisk$(challenge); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return Observable.just(challenge); | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |     .flatMap(challenge => { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Handle not found
 | 
					
						
							|  |  |  |       if (!challenge) { | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |         log('did not find challenge for ' + origChallengeName); | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |         return Observable.just({ | 
					
						
							|  |  |  |           type: 'redirect', | 
					
						
							|  |  |  |           redirectUrl: '/map', | 
					
						
							|  |  |  |           message: dedent`
 | 
					
						
							| 
									
										
										
										
											2016-01-15 06:08:54 -08:00
										 |  |  |     We couldn't find a challenge with the name ${origChallengeName}. | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |     Please double check the name. | 
					
						
							|  |  |  |           `
 | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (dasherize(challenge.name) !== origChallengeName) { | 
					
						
							|  |  |  |         let redirectUrl = `/challenges/${dasherize(challenge.name)}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (solution) { | 
					
						
							|  |  |  |           redirectUrl += `?solution=${encodeURIComponent(solution)}`; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return Observable.just({ | 
					
						
							|  |  |  |           type: 'redirect', | 
					
						
							|  |  |  |           redirectUrl | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // save user does nothing if user does not exist
 | 
					
						
							|  |  |  |       return Observable.just({ | 
					
						
							|  |  |  |         data: { | 
					
						
							|  |  |  |           ...challenge, | 
					
						
							|  |  |  |           // identifies if a challenge is completed
 | 
					
						
							|  |  |  |           isCompleted: isChallengeCompleted(user, challenge.id), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // video challenges
 | 
					
						
							|  |  |  |           video: challenge.challengeSeed[0], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // bonfires specific
 | 
					
						
							|  |  |  |           bonfires: challenge, | 
					
						
							|  |  |  |           MDNkeys: challenge.MDNlinks, | 
					
						
							|  |  |  |           MDNlinks: getMDNLinks(challenge.MDNlinks), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // htmls specific
 | 
					
						
							|  |  |  |           verb: randomVerb(), | 
					
						
							|  |  |  |           phrase: randomPhrase(), | 
					
						
							| 
									
										
										
										
											2016-01-30 00:13:41 -08:00
										 |  |  |           compliment: randomCompliment(), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           // Google Analytics
 | 
					
						
							|  |  |  |           gaName: challenge.title + '~' + challenge.checksum | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // create a stream of an array of all the challenge blocks
 | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  | function getSuperBlocks$(challenge$, challengeMap) { | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |   return challenge$ | 
					
						
							|  |  |  |     // mark challenge completed
 | 
					
						
							|  |  |  |     .map(challengeModel => { | 
					
						
							|  |  |  |       const challenge = challengeModel.toJSON(); | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |       challenge.completed = !!challengeMap[challenge.id]; | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |       challenge.markNew = shouldShowNew(challenge); | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       if (challenge.type === 'hike') { | 
					
						
							|  |  |  |         challenge.url = '/videos/' + challenge.dashedName; | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         challenge.url = '/challenges/' + challenge.dashedName; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |       return challenge; | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |     // group challenges by block | returns a stream of observables
 | 
					
						
							|  |  |  |     .groupBy(challenge => challenge.block) | 
					
						
							|  |  |  |     // turn block group stream into an array
 | 
					
						
							|  |  |  |     .flatMap(block$ => block$.toArray()) | 
					
						
							|  |  |  |     .map(blockArray => { | 
					
						
							|  |  |  |       const completedCount = blockArray.reduce((sum, { completed }) => { | 
					
						
							|  |  |  |         if (completed) { | 
					
						
							|  |  |  |           return sum + 1; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return sum; | 
					
						
							|  |  |  |       }, 0); | 
					
						
							|  |  |  |       const isBeta = _.every(blockArray, 'isBeta'); | 
					
						
							|  |  |  |       const isComingSoon = _.every(blockArray, 'isComingSoon'); | 
					
						
							| 
									
										
										
										
											2016-01-13 02:27:08 -06:00
										 |  |  |       const isRequired = _.every(blockArray, 'isRequired'); | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         isBeta, | 
					
						
							|  |  |  |         isComingSoon, | 
					
						
							| 
									
										
										
										
											2016-01-13 14:59:51 -08:00
										 |  |  |         isRequired, | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |         name: blockArray[0].block, | 
					
						
							|  |  |  |         superBlock: blockArray[0].superBlock, | 
					
						
							|  |  |  |         dashedName: dasherize(blockArray[0].block), | 
					
						
							|  |  |  |         markNew: shouldShowNew(null, blockArray), | 
					
						
							|  |  |  |         challenges: blockArray, | 
					
						
							|  |  |  |         completed: completedCount / blockArray.length * 100, | 
					
						
							|  |  |  |         time: blockArray[0] && blockArray[0].time || '???' | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |     .toArray() | 
					
						
							|  |  |  |     .flatMap(blocks => Observable.from(blocks, null, null, Scheduler.default)) | 
					
						
							|  |  |  |     .groupBy(block => block.superBlock) | 
					
						
							|  |  |  |     .flatMap(blocks$ => blocks$.toArray()) | 
					
						
							|  |  |  |     .map(superBlockArray => ({ | 
					
						
							|  |  |  |       name: superBlockArray[0].superBlock, | 
					
						
							|  |  |  |       blocks: superBlockArray | 
					
						
							|  |  |  |     })) | 
					
						
							|  |  |  |     .toArray(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  | function getChallengeById$(challenge$, challengeId) { | 
					
						
							|  |  |  |   // return first challenge if no id is given
 | 
					
						
							|  |  |  |   if (!challengeId) { | 
					
						
							|  |  |  |     return challenge$ | 
					
						
							|  |  |  |       .map(challenge => challenge.toJSON()) | 
					
						
							|  |  |  |       .filter(shouldNotFilterComingSoon) | 
					
						
							|  |  |  |       // filter out hikes
 | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  |       .filter(({ superBlock }) => !(/^videos/gi).test(superBlock)) | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |       .first(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return challenge$ | 
					
						
							|  |  |  |     .map(challenge => challenge.toJSON()) | 
					
						
							|  |  |  |     // filter out challenges coming soon
 | 
					
						
							|  |  |  |     .filter(shouldNotFilterComingSoon) | 
					
						
							|  |  |  |     // filter out hikes
 | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  |     .filter(({ superBlock }) => !(/^videos/gi).test(superBlock)) | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |     .filter(({ id }) => id === challengeId); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function getNextChallenge$(challenge$, blocks$, challengeId) { | 
					
						
							|  |  |  |   return getChallengeById$(challenge$, challengeId) | 
					
						
							|  |  |  |     // now lets find the block it belongs to
 | 
					
						
							|  |  |  |     .flatMap(challenge => { | 
					
						
							|  |  |  |       // find the index of the block this challenge resides in
 | 
					
						
							|  |  |  |       const blockIndex$ = blocks$ | 
					
						
							|  |  |  |         .findIndex(({ name }) => name === challenge.block); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return blockIndex$ | 
					
						
							|  |  |  |         .flatMap(blockIndex => { | 
					
						
							|  |  |  |           // could not find block?
 | 
					
						
							|  |  |  |           if (blockIndex === -1) { | 
					
						
							|  |  |  |             return Observable.throw( | 
					
						
							|  |  |  |               'could not find challenge block for ' + challenge.block | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           const firstChallengeOfNextBlock$ = blocks$ | 
					
						
							|  |  |  |             .elementAt(blockIndex + 1, {}) | 
					
						
							|  |  |  |             .map(({ challenges = [] }) => challenges[0]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           return blocks$ | 
					
						
							|  |  |  |             .filter(shouldNotFilterComingSoon) | 
					
						
							|  |  |  |             .elementAt(blockIndex) | 
					
						
							|  |  |  |             .flatMap(block => { | 
					
						
							|  |  |  |               // find where our challenge lies in the block
 | 
					
						
							|  |  |  |               const challengeIndex$ = Observable.from( | 
					
						
							|  |  |  |                 block.challenges, | 
					
						
							|  |  |  |                 null, | 
					
						
							|  |  |  |                 null, | 
					
						
							|  |  |  |                 Scheduler.default | 
					
						
							|  |  |  |               ) | 
					
						
							|  |  |  |                 .findIndex(({ id }) => id === challengeId); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               // grab next challenge in this block
 | 
					
						
							|  |  |  |               return challengeIndex$ | 
					
						
							|  |  |  |                 .map(index => { | 
					
						
							|  |  |  |                   return block.challenges[index + 1]; | 
					
						
							|  |  |  |                 }) | 
					
						
							|  |  |  |                 .flatMap(nextChallenge => { | 
					
						
							|  |  |  |                   if (!nextChallenge) { | 
					
						
							|  |  |  |                     return firstChallengeOfNextBlock$; | 
					
						
							|  |  |  |                   } | 
					
						
							|  |  |  |                   return Observable.just(nextChallenge); | 
					
						
							|  |  |  |                 }); | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |     .first(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  | module.exports = function(app) { | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  |   const router = app.loopback.Router(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |   const challengesQuery = { | 
					
						
							|  |  |  |     order: [ | 
					
						
							| 
									
										
										
										
											2015-12-06 21:44:34 -08:00
										 |  |  |       'superOrder ASC', | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |       'order ASC', | 
					
						
							|  |  |  |       'suborder ASC' | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // challenge model
 | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  |   const Challenge = app.models.Challenge; | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |   // challenge find query stream
 | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  |   const findChallenge$ = observeMethod(Challenge, 'find'); | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |   // create a stream of all the challenges
 | 
					
						
							|  |  |  |   const challenge$ = findChallenge$(challengesQuery) | 
					
						
							| 
									
										
										
										
											2015-08-19 23:11:21 -07:00
										 |  |  |     .flatMap(challenges => Observable.from( | 
					
						
							|  |  |  |       challenges, | 
					
						
							|  |  |  |       null, | 
					
						
							|  |  |  |       null, | 
					
						
							|  |  |  |       Scheduler.default | 
					
						
							|  |  |  |     )) | 
					
						
							| 
									
										
										
										
											2015-10-06 22:37:08 -07:00
										 |  |  |     // filter out all challenges that have isBeta flag set
 | 
					
						
							|  |  |  |     // except in development or beta site
 | 
					
						
							|  |  |  |     .filter(challenge => isDev || isBeta || !challenge.isBeta) | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |     .shareReplay(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // create a stream of challenge blocks
 | 
					
						
							| 
									
										
										
										
											2015-12-11 15:45:48 -08:00
										 |  |  |   const blocks$ = challenge$ | 
					
						
							| 
									
										
										
										
											2015-08-16 14:19:30 -07:00
										 |  |  |     .map(challenge => challenge.toJSON()) | 
					
						
							| 
									
										
										
										
											2015-12-14 13:18:54 -08:00
										 |  |  |     .filter(shouldNotFilterComingSoon) | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |     // group challenges by block | returns a stream of observables
 | 
					
						
							|  |  |  |     .groupBy(challenge => challenge.block) | 
					
						
							|  |  |  |     // turn block group stream into an array
 | 
					
						
							| 
									
										
										
										
											2015-12-04 22:13:14 -08:00
										 |  |  |     .flatMap(blocks$ => blocks$.toArray()) | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |     // turn array into stream of object
 | 
					
						
							| 
									
										
										
										
											2015-12-11 15:45:48 -08:00
										 |  |  |     .map(blocksArray => ({ | 
					
						
							|  |  |  |       name: blocksArray[0].block, | 
					
						
							|  |  |  |       dashedName: dasherize(blocksArray[0].block), | 
					
						
							|  |  |  |       challenges: blocksArray, | 
					
						
							|  |  |  |       superBlock: blocksArray[0].superBlock, | 
					
						
							|  |  |  |       order: blocksArray[0].order | 
					
						
							| 
									
										
										
										
											2015-08-12 20:39:40 -07:00
										 |  |  |     })) | 
					
						
							| 
									
										
										
										
											2015-12-11 14:44:27 -08:00
										 |  |  |     // filter out hikes
 | 
					
						
							| 
									
										
										
										
											2015-12-04 22:37:15 -08:00
										 |  |  |     .filter(({ superBlock }) => { | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  |       return !(/^videos/gi).test(superBlock); | 
					
						
							| 
									
										
										
										
											2015-08-12 20:39:40 -07:00
										 |  |  |     }) | 
					
						
							|  |  |  |     .shareReplay(); | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |   const firstChallenge$ = challenge$ | 
					
						
							|  |  |  |     .first() | 
					
						
							|  |  |  |     .map(challenge => challenge.toJSON()) | 
					
						
							|  |  |  |     .shareReplay(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const lastChallenge$ = challenge$ | 
					
						
							|  |  |  |     .last() | 
					
						
							|  |  |  |     .map(challenge => challenge.toJSON()) | 
					
						
							|  |  |  |     .shareReplay(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  |   const send200toNonUser = ifNoUserSend(true); | 
					
						
							| 
									
										
										
										
											2015-06-22 16:43:31 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   router.post( | 
					
						
							|  |  |  |     '/completed-challenge/', | 
					
						
							|  |  |  |     send200toNonUser, | 
					
						
							|  |  |  |     completedChallenge | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  |   router.post( | 
					
						
							|  |  |  |     '/completed-zipline-or-basejump', | 
					
						
							|  |  |  |     send200toNonUser, | 
					
						
							|  |  |  |     completedZiplineOrBasejump | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-13 14:59:51 -08:00
										 |  |  |   router.get('/map', showMap.bind(null, false)); | 
					
						
							| 
									
										
										
										
											2016-01-14 16:03:55 -08:00
										 |  |  |   router.get('/map-aside', showMap.bind(null, true)); | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |   router.get( | 
					
						
							|  |  |  |     '/challenges/current-challenge', | 
					
						
							|  |  |  |     redirectToCurrentChallenge | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2015-06-20 11:43:12 -07:00
										 |  |  |   router.get( | 
					
						
							|  |  |  |     '/challenges/next-challenge', | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |     redirectToNextChallenge | 
					
						
							| 
									
										
										
										
											2015-06-20 11:43:12 -07:00
										 |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |   router.get('/challenges/:challengeName', showChallenge); | 
					
						
							| 
									
										
										
										
											2015-06-20 11:43:12 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-06-03 16:31:42 -07:00
										 |  |  |   app.use(router); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |   function redirectToCurrentChallenge(req, res, next) { | 
					
						
							| 
									
										
										
										
											2016-01-15 01:44:18 -08:00
										 |  |  |     let challengeId = req.query.id || req.cookies.currentChallengeId; | 
					
						
							|  |  |  |     // prevent serialized null/undefined from breaking things
 | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (badIdMap[challengeId]) { | 
					
						
							|  |  |  |       challengeId = badIdMap[challengeId]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-03-13 18:04:24 -07:00
										 |  |  |     if (!isMongoId('' + challengeId)) { | 
					
						
							| 
									
										
										
										
											2016-01-15 01:44:18 -08:00
										 |  |  |       challengeId = null; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |     getChallengeById$(challenge$, challengeId) | 
					
						
							|  |  |  |       .doOnNext(({ dashedName })=> { | 
					
						
							|  |  |  |         if (!dashedName) { | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |           log('no challenge found for %s', challengeId); | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |           req.flash('info', { | 
					
						
							|  |  |  |             msg: `We coudn't find a challenge with the id ${challengeId}` | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |           }); | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |           res.redirect('/map'); | 
					
						
							| 
									
										
										
										
											2015-10-05 16:38:58 -07:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |         res.redirect('/challenges/' + dashedName); | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |       }) | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |       .subscribe(() => {}, next); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function redirectToNextChallenge(req, res, next) { | 
					
						
							| 
									
										
										
										
											2016-01-15 01:44:18 -08:00
										 |  |  |     let challengeId = req.query.id || req.cookies.currentChallengeId; | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (badIdMap[challengeId]) { | 
					
						
							|  |  |  |       challengeId = badIdMap[challengeId]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-03-13 18:04:24 -07:00
										 |  |  |     if (!isMongoId('' + challengeId)) { | 
					
						
							| 
									
										
										
										
											2016-01-15 01:44:18 -08:00
										 |  |  |       challengeId = null; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     Observable.combineLatest( | 
					
						
							|  |  |  |       firstChallenge$, | 
					
						
							| 
									
										
										
										
											2016-01-14 23:06:06 -08:00
										 |  |  |       lastChallenge$ | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |     ) | 
					
						
							|  |  |  |       .flatMap(([firstChallenge, { id: lastChallengeId } ]) => { | 
					
						
							|  |  |  |         // no id supplied, load first challenge
 | 
					
						
							|  |  |  |         if (!challengeId) { | 
					
						
							|  |  |  |           return Observable.just(firstChallenge); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // camper just completed last challenge
 | 
					
						
							|  |  |  |         if (challengeId === lastChallengeId) { | 
					
						
							|  |  |  |           return Observable.just() | 
					
						
							|  |  |  |             .doOnCompleted(() => { | 
					
						
							|  |  |  |               req.flash('info', { | 
					
						
							| 
									
										
										
										
											2016-05-03 00:45:34 -07:00
										 |  |  |                 msg: 'You\'ve completed the last challenge!' | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |               }); | 
					
						
							|  |  |  |               return res.redirect('/map'); | 
					
						
							| 
									
										
										
										
											2015-08-12 11:44:29 -07:00
										 |  |  |             }); | 
					
						
							| 
									
										
										
										
											2015-06-20 11:43:12 -07:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-14 17:10:26 -08:00
										 |  |  |         return getNextChallenge$(challenge$, blocks$, challengeId); | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |       .doOnNext(({ dashedName } = {}) => { | 
					
						
							|  |  |  |         if (!dashedName) { | 
					
						
							|  |  |  |           log('no challenge found for %s', challengeId); | 
					
						
							|  |  |  |           res.redirect('/map'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         res.redirect('/challenges/' + dashedName); | 
					
						
							| 
									
										
										
										
											2016-01-14 15:15:44 -08:00
										 |  |  |       }) | 
					
						
							|  |  |  |       .subscribe(() => {}, next); | 
					
						
							| 
									
										
										
										
											2015-05-16 00:39:43 -04:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |   function showChallenge(req, res, next) { | 
					
						
							|  |  |  |     const solution = req.query.solution; | 
					
						
							| 
									
										
										
										
											2016-01-15 06:08:54 -08:00
										 |  |  |     const challengeName = req.params.challengeName.replace(challengesRegex, ''); | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |     const { user } = req; | 
					
						
							| 
									
										
										
										
											2016-01-12 22:26:19 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |     Observable.defer(() => { | 
					
						
							|  |  |  |       if (user && user.getChallengeMap$) { | 
					
						
							|  |  |  |         return user.getChallengeMap$().map(user); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return Observable.just(null); | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |       .flatMap(user => { | 
					
						
							|  |  |  |         return getRenderData$(user, challenge$, challengeName, solution); | 
					
						
							|  |  |  |       }) | 
					
						
							| 
									
										
										
										
											2015-08-19 23:11:21 -07:00
										 |  |  |       .subscribe( | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |         ({ type, redirectUrl, message, data }) => { | 
					
						
							|  |  |  |           if (message) { | 
					
						
							|  |  |  |             req.flash('info', { | 
					
						
							|  |  |  |               msg: message | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           if (type === 'redirect') { | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |             log('redirecting to %s', redirectUrl); | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |             return res.redirect(redirectUrl); | 
					
						
							| 
									
										
										
										
											2015-08-19 23:11:21 -07:00
										 |  |  |           } | 
					
						
							|  |  |  |           var view = challengeView[data.challengeType]; | 
					
						
							| 
									
										
										
										
											2016-01-15 01:44:18 -08:00
										 |  |  |           if (data.id) { | 
					
						
							| 
									
										
										
										
											2016-03-15 14:53:54 -05:00
										 |  |  |             res.cookie('currentChallengeId', data.id, { | 
					
						
							|  |  |  |               expires: new Date(2147483647000)}); | 
					
						
							| 
									
										
										
										
											2016-01-15 01:44:18 -08:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2016-03-02 20:54:14 -08:00
										 |  |  |           return res.render(view, data); | 
					
						
							| 
									
										
										
										
											2015-08-19 23:11:21 -07:00
										 |  |  |         }, | 
					
						
							|  |  |  |         next, | 
					
						
							|  |  |  |         function() {} | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   function completedChallenge(req, res, next) { | 
					
						
							| 
									
										
										
										
											2016-03-30 23:58:38 -04:00
										 |  |  |     req.checkBody('id', 'id must be an ObjectId').isMongoId(); | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |     req.checkBody('name', 'name must be at least 3 characters') | 
					
						
							|  |  |  |       .isString() | 
					
						
							|  |  |  |       .isLength({ min: 3 }); | 
					
						
							|  |  |  |     req.checkBody('challengeType', 'challengeType must be an integer') | 
					
						
							| 
									
										
										
										
											2016-03-02 21:53:42 -08:00
										 |  |  |       .isNumber(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-09 20:08:01 -08:00
										 |  |  |     const type = accepts(req).type('html', 'json', 'text'); | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |     const errors = req.validationErrors(true); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (errors) { | 
					
						
							|  |  |  |       if (type === 'json') { | 
					
						
							|  |  |  |         return res.status(403).send({ errors }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       log('errors', errors); | 
					
						
							|  |  |  |       return res.sendStatus(403); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |     return req.user.getChallengeMap$() | 
					
						
							|  |  |  |       .flatMap(() => { | 
					
						
							|  |  |  |         const completedDate = Date.now(); | 
					
						
							|  |  |  |         const { | 
					
						
							|  |  |  |           id, | 
					
						
							|  |  |  |           name, | 
					
						
							|  |  |  |           challengeType, | 
					
						
							|  |  |  |           solution, | 
					
						
							|  |  |  |           timezone | 
					
						
							|  |  |  |         } = req.body; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const { alreadyCompleted, updateData } = buildUserUpdate( | 
					
						
							|  |  |  |           req.user, | 
					
						
							|  |  |  |           id, | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             id, | 
					
						
							|  |  |  |             challengeType, | 
					
						
							|  |  |  |             solution, | 
					
						
							|  |  |  |             name, | 
					
						
							|  |  |  |             completedDate | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |           timezone | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const user = req.user; | 
					
						
							|  |  |  |         const points = alreadyCompleted ? user.points : user.points + 1; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return user.update$(updateData) | 
					
						
							|  |  |  |           .doOnNext(({ count }) => log('%s documents updated', count)) | 
					
						
							|  |  |  |           .map(() => { | 
					
						
							|  |  |  |             if (type === 'json') { | 
					
						
							|  |  |  |               return res.json({ | 
					
						
							|  |  |  |                 points, | 
					
						
							|  |  |  |                 alreadyCompleted | 
					
						
							|  |  |  |               }); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return res.sendStatus(200); | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |       .subscribe(() => {}, next); | 
					
						
							| 
									
										
										
										
											2015-05-20 21:50:31 -04:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-05-19 22:31:01 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  |   function completedZiplineOrBasejump(req, res, next) { | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |     const type = accepts(req).type('html', 'json', 'text'); | 
					
						
							|  |  |  |     req.checkBody('id', 'id must be an ObjectId').isMongoId(); | 
					
						
							|  |  |  |     req.checkBody('name', 'Name must be at least 3 characters') | 
					
						
							|  |  |  |       .isString() | 
					
						
							|  |  |  |       .isLength({ min: 3 }); | 
					
						
							|  |  |  |     req.checkBody('challengeType', 'must be a number') | 
					
						
							| 
									
										
										
										
											2016-03-02 21:53:42 -08:00
										 |  |  |       .isNumber(); | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |     req.checkBody('solution', 'solution must be a url').isURL(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const errors = req.validationErrors(true); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (errors) { | 
					
						
							|  |  |  |       if (type === 'json') { | 
					
						
							|  |  |  |         return res.status(403).send({ errors }); | 
					
						
							| 
									
										
										
										
											2016-01-19 22:30:01 -08:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |       log('errors', errors); | 
					
						
							|  |  |  |       return res.sendStatus(403); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |     const { user, body = {} } = req; | 
					
						
							| 
									
										
										
										
											2015-06-20 19:52:37 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |     const completedChallenge = _.pick( | 
					
						
							|  |  |  |       body, | 
					
						
							|  |  |  |       [ 'id', 'name', 'solution', 'githubLink', 'challengeType' ] | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     completedChallenge.challengeType = +completedChallenge.challengeType; | 
					
						
							|  |  |  |     completedChallenge.completedDate = Date.now(); | 
					
						
							| 
									
										
										
										
											2015-06-20 19:52:37 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-19 22:30:01 -08:00
										 |  |  |     if ( | 
					
						
							|  |  |  |       !completedChallenge.solution || | 
					
						
							|  |  |  |       // only basejumps require github links
 | 
					
						
							|  |  |  |       ( | 
					
						
							|  |  |  |         completedChallenge.challengeType === 4 && | 
					
						
							|  |  |  |         !completedChallenge.githubLink | 
					
						
							|  |  |  |       ) | 
					
						
							|  |  |  |     ) { | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  |       req.flash('errors', { | 
					
						
							|  |  |  |         msg: 'You haven\'t supplied the necessary URLs for us to inspect ' + | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |           'your work.' | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  |       }); | 
					
						
							|  |  |  |       return res.sendStatus(403); | 
					
						
							| 
									
										
										
										
											2015-05-19 22:31:01 -04:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-05-21 00:17:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-01-19 22:30:01 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |     return user.getChallengeMap$() | 
					
						
							|  |  |  |       .flatMap(() => { | 
					
						
							|  |  |  |         const { | 
					
						
							|  |  |  |           alreadyCompleted, | 
					
						
							|  |  |  |           updateData | 
					
						
							|  |  |  |         } = buildUserUpdate(user, completedChallenge.id, completedChallenge); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return user.update$(updateData) | 
					
						
							|  |  |  |           .doOnNext(({ count }) => log('%s documents updated', count)) | 
					
						
							|  |  |  |           .doOnNext(() => { | 
					
						
							|  |  |  |             if (type === 'json') { | 
					
						
							|  |  |  |               return res.send({ | 
					
						
							|  |  |  |                 alreadyCompleted, | 
					
						
							|  |  |  |                 points: alreadyCompleted ? user.points : user.points + 1 | 
					
						
							|  |  |  |               }); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             return res.status(200).send(true); | 
					
						
							| 
									
										
										
										
											2016-02-10 22:10:06 -08:00
										 |  |  |           }); | 
					
						
							|  |  |  |       }) | 
					
						
							| 
									
										
										
										
											2016-01-19 22:30:01 -08:00
										 |  |  |       .subscribe(() => {}, next); | 
					
						
							| 
									
										
										
										
											2015-05-19 22:31:01 -04:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-06-19 14:49:10 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |   function showMap(showAside, { user }, res, next) { | 
					
						
							|  |  |  |     return Observable.defer(() => { | 
					
						
							|  |  |  |       if (user && typeof user.getChallengeMap$ === 'function') { | 
					
						
							|  |  |  |         return user.getChallengeMap$(); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return Observable.just({}); | 
					
						
							|  |  |  |     }) | 
					
						
							|  |  |  |       .flatMap(challengeMap => getSuperBlocks$(challenge$, challengeMap)) | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  |       .subscribe( | 
					
						
							| 
									
										
										
										
											2016-01-11 22:47:49 -08:00
										 |  |  |         superBlocks => { | 
					
						
							| 
									
										
										
										
											2016-01-14 23:06:06 -08:00
										 |  |  |           res.render('map/show', { | 
					
						
							| 
									
										
										
										
											2015-12-04 22:13:14 -08:00
										 |  |  |             superBlocks, | 
					
						
							| 
									
										
										
										
											2016-01-14 23:06:06 -08:00
										 |  |  |             title: 'A Map to Learn to Code and Become a Software Engineer', | 
					
						
							|  |  |  |             showAside | 
					
						
							| 
									
										
										
										
											2015-08-09 22:14:31 -07:00
										 |  |  |           }); | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         next | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2015-06-19 14:49:10 -07:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-06-02 19:02:54 -07:00
										 |  |  | }; |