From fa37dc865d7165d90508b1d9b483095e1f568bec Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 11 Feb 2016 22:33:54 -0800 Subject: [PATCH 1/7] Reduce initial user query load Refactor passport to own file --- package.json | 1 + server/component-passport.js | 114 +++++++++++++++++++++++++++++++++++ server/server.js | 73 +--------------------- 3 files changed, 118 insertions(+), 70 deletions(-) create mode 100644 server/component-passport.js diff --git a/package.json b/package.json index 739e202db7..186a18056c 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "normalize-url": "^1.3.1", "normalizr": "^2.0.0", "object.assign": "^4.0.3", + "passport": "^0.2.1", "passport-facebook": "^2.0.0", "passport-github": "^1.0.0", "passport-google-oauth2": "~0.1.6", diff --git a/server/component-passport.js b/server/component-passport.js new file mode 100644 index 0000000000..b9e1635af9 --- /dev/null +++ b/server/component-passport.js @@ -0,0 +1,114 @@ +import passport from 'passport'; +import { PassportConfigurator } from 'loopback-component-passport'; +import passportProviders from './passport-providers'; +import uuid from 'node-uuid'; +import { generateKey } from 'loopback-component-passport/lib/models/utils'; + +import { + setProfileFromGithub, + getSocialProvider, + getUsernameFromProvider +} from './utils/auth'; + +const passportOptions = { + emailOptional: true, + profileToUser(provider, profile) { + var emails = profile.emails; + // NOTE(berks): get email or set to null. + // MongoDB indexs email but can be sparse(blank) + var email = emails && emails[0] && emails[0].value ? + emails[0].value : + null; + + // create random username + // username will be assigned when camper signups for Github + var username = 'fcc' + uuid.v4().slice(0, 8); + var password = generateKey('password'); + var userObj = { + username: username, + password: password + }; + + if (email) { + userObj.email = email; + } + + if (!(/github/).test(provider)) { + userObj[getSocialProvider(provider)] = getUsernameFromProvider( + getSocialProvider(provider), + profile + ); + } + + if (/github/.test(provider)) { + setProfileFromGithub(userObj, profile, profile._json); + } + return userObj; + } +}; + +const fields = { + progressTimestamps: false, + completedChallenges: false, + challengeMap: false +}; + +PassportConfigurator.prototype.init = function passportInit(noSession) { + this.app.middleware('session:after', passport.initialize()); + + if (noSession) { + return; + } + + this.app.middleware('session:after', passport.session()); + + // Serialization and deserialization is only required if passport session is + // enabled + + passport.serializeUser((user, done) => { + done(null, user.id); + }); + + passport.deserializeUser((id, done) => { + + this.userModel.findById(id, { fields }, (err, user) => { + if (err || !user) { + return done(err, user); + } + this.app.dataSources.db.connector + .collection('user') + .aggregate([ + { $match: { _id: user.id } }, + { $project: { points: { $size: '$progressTimestamps' } } } + ], function(err, { points = 1 } = {}) { + if (err) { return done(err); } + user.points = points; + done(null, user); + }); + }); + }); +}; + +export default function setupPassport(app) { + const configurator = new PassportConfigurator(app); + + configurator.setupModels({ + userModel: app.models.user, + userIdentityModel: app.models.userIdentity, + userCredentialModel: app.models.userCredential + }); + + configurator.init(); + + Object.keys(passportProviders).map(function(strategy) { + var config = passportProviders[strategy]; + config.session = config.session !== false; + configurator.configureProvider( + strategy, + { + ...config, + ...passportOptions + } + ); + }); +} diff --git a/server/server.js b/server/server.js index 1f711cd92e..1d5f9aefda 100755 --- a/server/server.js +++ b/server/server.js @@ -3,96 +3,29 @@ var pmx = require('pmx'); pmx.init(); var _ = require('lodash'), - uuid = require('node-uuid'), - assign = require('lodash').assign, loopback = require('loopback'), boot = require('loopback-boot'), expressState = require('express-state'), path = require('path'), - passportProviders = require('./passport-providers'); + setupPassport = require('./component-passport'); -var setProfileFromGithub = require('./utils/auth').setProfileFromGithub; -var getSocialProvider = require('./utils/auth').getSocialProvider; -var getUsernameFromProvider = require('./utils/auth').getUsernameFromProvider; -var generateKey = - require('loopback-component-passport/lib/models/utils').generateKey; - -var isBeta = !!process.env.BETA; var app = loopback(); +var isBeta = !!process.env.BETA; expressState.extend(app); app.set('state namespace', '__fcc__'); - -var PassportConfigurator = - require('loopback-component-passport').PassportConfigurator; -var passportConfigurator = new PassportConfigurator(app); - app.set('port', process.env.PORT || 3000); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); app.use(loopback.token()); app.disable('x-powered-by'); -// adds passport initialization after session middleware phase is complete -passportConfigurator.init(); - boot(app, { appRootDir: __dirname, dev: process.env.NODE_ENV }); - -passportConfigurator.setupModels({ - userModel: app.models.user, - userIdentityModel: app.models.userIdentity, - userCredentialModel: app.models.userCredential -}); - -var passportOptions = { - emailOptional: true, - profileToUser: function(provider, profile) { - var emails = profile.emails; - // NOTE(berks): get email or set to null. - // MongoDB indexs email but can be sparse(blank) - var email = emails && emails[0] && emails[0].value ? - emails[0].value : - null; - - // create random username - // username will be assigned when camper signups for GitHub - var username = 'fcc' + uuid.v4().slice(0, 8); - var password = generateKey('password'); - var userObj = { - username: username, - password: password - }; - - if (email) { - userObj.email = email; - } - - if (!(/github/).test(provider)) { - userObj[getSocialProvider(provider)] = getUsernameFromProvider( - getSocialProvider(provider), - profile - ); - } - - if (/github/.test(provider)) { - setProfileFromGithub(userObj, profile, profile._json); - } - return userObj; - } -}; - -Object.keys(passportProviders).map(function(strategy) { - var config = passportProviders[strategy]; - config.session = config.session !== false; - passportConfigurator.configureProvider( - strategy, - assign(config, passportOptions) - ); -}); +setupPassport(app); app.start = _.once(function() { app.listen(app.get('port'), function() { From 40eff8d5ccfc10e767363446ca21fcbc18157acf Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 5 Apr 2016 16:53:13 -0700 Subject: [PATCH 2/7] Fix lint issues --- server/component-passport.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/component-passport.js b/server/component-passport.js index b9e1635af9..022104d541 100644 --- a/server/component-passport.js +++ b/server/component-passport.js @@ -75,7 +75,7 @@ PassportConfigurator.prototype.init = function passportInit(noSession) { if (err || !user) { return done(err, user); } - this.app.dataSources.db.connector + return this.app.dataSources.db.connector .collection('user') .aggregate([ { $match: { _id: user.id } }, @@ -83,7 +83,7 @@ PassportConfigurator.prototype.init = function passportInit(noSession) { ], function(err, { points = 1 } = {}) { if (err) { return done(err); } user.points = points; - done(null, user); + return done(null, user); }); }); }); From 7737bfedc1671b7fce34ecd89386146fd624e3f1 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 6 Apr 2016 16:54:45 -0700 Subject: [PATCH 3/7] Fix points query --- server/component-passport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/component-passport.js b/server/component-passport.js index 022104d541..db96edcb86 100644 --- a/server/component-passport.js +++ b/server/component-passport.js @@ -80,7 +80,7 @@ PassportConfigurator.prototype.init = function passportInit(noSession) { .aggregate([ { $match: { _id: user.id } }, { $project: { points: { $size: '$progressTimestamps' } } } - ], function(err, { points = 1 } = {}) { + ], function(err, [{ points = 1 } = {}]) { if (err) { return done(err); } user.points = points; return done(null, user); From 1308d312a417733db7503219a0679ecdbe75f2bf Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 6 Apr 2016 21:08:19 -0700 Subject: [PATCH 4/7] Add methods to get challengeMap Update boot/challenge.js to use new methods Update boot/user.js to use new methods --- common/models/user.js | 24 ++++++ server/boot/certificate.js | 3 +- server/boot/challenge.js | 134 +++++++++++++++++------------- server/boot/user.js | 66 +++++++-------- server/views/partials/navbar.jade | 2 +- 5 files changed, 132 insertions(+), 97 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index c772884066..4c48f5e3ab 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -441,4 +441,28 @@ module.exports = function(User) { } return this.constructor.update$({ id }, updateData, updateOptions); }; + User.prototype.getPoints$ = function getPoints$() { + const id = this.getId(); + const filter = { + where: { id }, + fields: { progressTimestamps: true } + }; + return this.constructor.findOne$(filter) + .map(user => { + this.progressTimestamps = user.progressTimestamps; + return user.progressTimestamps; + }); + }; + User.prototype.getChallengeMap$ = function getChallengeMap$() { + const id = this.getId(); + const filter = { + where: { id }, + fields: { challengeMap: true } + }; + return this.constructor.findOne$(filter) + .map(user => { + this.challengeMap = user.challengeMap; + return user.challengeMap; + }); + }; }; diff --git a/server/boot/certificate.js b/server/boot/certificate.js index 6775edd3a0..d9bb42d246 100644 --- a/server/boot/certificate.js +++ b/server/boot/certificate.js @@ -84,7 +84,8 @@ export default function certificate(app) { function verifyCert(certType, req, res, next) { const { user } = req; - return certTypeIds[certType] + return user.getChallengeMap() + .flatMap(() => certTypeIds[certType]) .flatMap(challenge => { const { id, diff --git a/server/boot/challenge.js b/server/boot/challenge.js index aa8938e6df..6496579ba3 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -142,7 +142,7 @@ function getRenderData$(user, challenge$, origChallengeName, solution) { return challenge$ .map(challenge => challenge.toJSON()) - .filter((challenge) => { + .filter(challenge => { return shouldNotFilterComingSoon(challenge) && challenge.type !== 'hike' && testChallengeName.test(challenge.name); @@ -500,8 +500,17 @@ module.exports = function(app) { function showChallenge(req, res, next) { const solution = req.query.solution; const challengeName = req.params.challengeName.replace(challengesRegex, ''); + const { user } = req; - getRenderData$(req.user, challenge$, challengeName, solution) + Observable.defer(() => { + if (user && user.getChallengeMap$) { + return user.getChallengeMap$().map(user); + } + return Observable.just(null); + }) + .flatMap(user => { + return getRenderData$(user, challenge$, challengeName, solution); + }) .subscribe( ({ type, redirectUrl, message, data }) => { if (message) { @@ -546,48 +555,46 @@ module.exports = function(app) { return res.sendStatus(403); } - const completedDate = Date.now(); - const { - id, - name, - challengeType, - solution, - timezone - } = req.body; + 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 { alreadyCompleted, updateData } = buildUserUpdate( + req.user, + id, + { + id, + challengeType, + solution, + name, + completedDate + }, + timezone + ); - const user = req.user; - const points = alreadyCompleted ? - user.progressTimestamps.length : - user.progressTimestamps.length + 1; + const user = req.user; + const points = alreadyCompleted ? user.points : user.points + 1; - return user.update$(updateData) - .doOnNext(({ count }) => log('%s documents updated', count)) - .subscribe( - () => {}, - next, - function() { - if (type === 'json') { - return res.json({ - points, - alreadyCompleted - }); - } - return res.sendStatus(200); - } - ); + 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); } function completedZiplineOrBasejump(req, res, next) { @@ -635,31 +642,38 @@ module.exports = function(app) { } - const { - alreadyCompleted, - updateData - } = buildUserUpdate(req.user, completedChallenge.id, completedChallenge); + 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.progressTimestamps.length : - user.progressTimestamps.length + 1 + 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); }); - } - return res.status(200).send(true); }) .subscribe(() => {}, next); } - function showMap(showAside, { user = {} }, res, next) { - const { challengeMap = {} } = user; - - return getSuperBlocks$(challenge$, challengeMap) + function showMap(showAside, { user }, res, next) { + return Observable.defer(() => { + if (user && typeof user.getChallengeMap$ === 'function') { + return user.getChallengeMap$(); + } + console.log('foo'); + return Observable.just({}); + }) + .doOnNext(challengeMap => console.log('challengeMap', challengeMap)) + .flatMap(challengeMap => getSuperBlocks$(challenge$, challengeMap)) .subscribe( superBlocks => { res.render('map/show', { diff --git a/server/boot/user.js b/server/boot/user.js index 37b56bd214..8d9984e2ab 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -420,63 +420,59 @@ module.exports = function(app) { } function toggleLockdownMode(req, res, next) { - return User.findById(req.accessToken.userId, function(err, user) { - if (err) { return next(err); } - return user.updateAttribute('isLocked', !user.isLocked, function(err) { - if (err) { return next(err); } - req.flash('info', { - msg: 'We\'ve successfully updated your Privacy preferences.' - }); - return res.redirect('/settings'); - }); - }); + const { user } = req; + user.update$({ isLocked: !user.isLocked }) + .subscribe( + () => { + req.flash('info', { + msg: 'We\'ve successfully updated your Privacy preferences.' + }); + return res.redirect('/settings'); + }, + next + ); } function toggleReceivesAnnouncementEmails(req, res, next) { - return User.findById(req.accessToken.userId, function(err, user) { - if (err) { return next(err); } - return user.updateAttribute( - 'sendMonthlyEmail', - !user.sendMonthlyEmail, - (err) => { - if (err) { return next(err); } + const { user } = req; + return user.update$({ sendMonthlyEmail: !user.sendMonthlyEmail }) + .subscribe( + () => { req.flash('info', { msg: 'We\'ve successfully updated your Email preferences.' }); return res.redirect('/settings'); - }); - }); + }, + next + ); } function toggleReceivesQuincyEmails(req, res, next) { - return User.findById(req.accessToken.userId, function(err, user) { - if (err) { return next(err); } - return user.updateAttribute('sendQuincyEmail', !user.sendQuincyEmail, - (err) => { - if (err) { return next(err); } + const { user } = req; + return user.update$({ sendQuincyEmail: !user.sendQuincyEmail }) + .subscribe( + () => { req.flash('info', { msg: 'We\'ve successfully updated your Email preferences.' }); return res.redirect('/settings'); - } + }, + next ); - }); } function toggleReceivesNotificationEmails(req, res, next) { - return User.findById(req.accessToken.userId, function(err, user) { - if (err) { return next(err); } - return user.updateAttribute( - 'sendNotificationEmail', - !user.sendNotificationEmail, - function(err) { - if (err) { return next(err); } + const { user } = req; + return user.update$({ sendNotificationEmail: !user.sendNotificationEmail }) + .subscribe( + () => { req.flash('info', { msg: 'We\'ve successfully updated your Email preferences.' }); return res.redirect('/settings'); - }); - }); + }, + next + ); } function postDeleteAccount(req, res, next) { diff --git a/server/views/partials/navbar.jade b/server/views/partials/navbar.jade index eb03371a1f..5e527ddc8d 100644 --- a/server/views/partials/navbar.jade +++ b/server/views/partials/navbar.jade @@ -28,7 +28,7 @@ nav.navbar.navbar-default.navbar-fixed-top.nav-height a(href='/login') Sign in else li.brownie-points-nav - a(href='/' + user.username) [ #{user.progressTimestamps.length} ] + a(href='/' + user.username) [ #{user.points} ] li.hidden-xs.hidden-sm.avatar a(href='/' + user.username) img.profile-picture.float-right(src='#{user.picture}') From 513d55b23da41a232166a4bc4484f1b840cacb87 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 6 Apr 2016 23:06:19 -0700 Subject: [PATCH 5/7] Add caching to news Deprecate story creation --- server/boot/story.js | 399 ++++--------------------------------------- 1 file changed, 32 insertions(+), 367 deletions(-) diff --git a/server/boot/story.js b/server/boot/story.js index 4d1ca77660..6849812874 100755 --- a/server/boot/story.js +++ b/server/boot/story.js @@ -1,27 +1,11 @@ -var Rx = require('rx'), - assign = require('object.assign'), - sanitizeHtml = require('sanitize-html'), - moment = require('moment'), - debug = require('debug')('fcc:cntr:story'), - utils = require('../utils'), - observeMethod = require('../utils/rx').observeMethod, - saveUser = require('../utils/rx').saveUser, - saveInstance = require('../utils/rx').saveInstance, - validator = require('validator'); +import moment from 'moment'; -import { - ifNoUser401, - ifNoUserRedirectTo -} from '../utils/middleware'; +import { unDasherize } from '../utils'; +import { observeMethod } from '../utils/rx'; const foundationDate = 1413298800000; const time48Hours = 172800000; -const unDasherize = utils.unDasherize; -const dasherize = utils.dasherize; -const getURLTitle = utils.getURLTitle; -const sendNonUserToNews = ifNoUserRedirectTo('/news'); - function hotRank(timeValue, rank) { /* * Hotness ranking algorithm: http://amix.dk/blog/post/19588 @@ -39,81 +23,54 @@ function sortByRank(a, b) { hotRank(a.timePosted - foundationDate, a.rank); } -function cleanData(data, opts) { - var options = assign( - {}, - { - allowedTags: [], - allowedAttributes: [] - }, - opts || {} - ); - return sanitizeHtml(data, options).replace(/";/g, '"'); -} - module.exports = function(app) { - var router = app.loopback.Router(); - var User = app.models.User; - var findUserById = observeMethod(User, 'findById'); + const router = app.loopback.Router(); + const Story = app.models.Story; + const findStory = observeMethod(Story, 'find'); + const findOneStory = observeMethod(Story, 'findOne'); + const query = { + order: 'timePosted DESC', + limit: 1000 + }; + const storiesData$ = findStory(query) + .map(stories => { + const sliceVal = stories.length >= 100 ? 100 : stories.length; + return stories.sort(sortByRank).slice(0, sliceVal); + }) + .shareReplay(); - var Story = app.models.Story; - var findStory = observeMethod(Story, 'find'); - var findOneStory = observeMethod(Story, 'findOne'); - var findStoryById = observeMethod(Story, 'findById'); - var countStories = observeMethod(Story, 'count'); - - router.post('/news/userstories', userStories); + const redirectToNews = (req, res) => res.redirect('/news'); + const deprecated = (req, res) => res.sendStatus(410); + router.post('/news/userstories', deprecated); router.get('/news/hot', hotJSON); router.get('/news/feed', RSSFeed); router.get('/stories/hotStories', hotJSON); - router.get( - '/stories/submit', - sendNonUserToNews, - submitNew - ); - router.get( - '/stories/submit/new-story', - sendNonUserToNews, - preSubmit - ); - router.post('/stories/preliminary', ifNoUser401, newStory); - router.post('/stories/', ifNoUser401, storySubmission); - router.post('/stories/search', getStories); + router.get('/stories/submit', redirectToNews); + router.get('/stories/submit/new-story', redirectToNews); + router.post('/stories/preliminary', deprecated); + router.post('/stories/', deprecated); + router.post('/stories/search', deprecated); router.get('/news/:storyName', returnIndividualStory); - router.post('/stories/upvote/', ifNoUser401, upvote); - router.get('/stories/:storyName', redirectToNews); + router.post('/stories/upvote/', deprecated); + router.get('/stories/:storyName', replaceStoryWithNews); app.use(router); - function redirectToNews(req, res) { + function replaceStoryWithNews(req, res) { var url = req.originalUrl.replace(/^\/stories/, '/news'); return res.redirect(url); } function hotJSON(req, res, next) { - var query = { - order: 'timePosted DESC', - limit: 1000 - }; - findStory(query).subscribe( - function(stories) { - var sliceVal = stories.length >= 100 ? 100 : stories.length; - var data = stories.sort(sortByRank).slice(0, sliceVal); - res.json(data); - }, + storiesData$.subscribe( + stories => res.json(stories), next ); } function RSSFeed(req, res, next) { - var query = { - order: 'timePosted DESC', - limit: 1000 - }; - findStory(query).subscribe( - function(stories) { - var sliceVal = stories.length >= 100 ? 100 : stories.length; - var data = stories.sort(sortByRank).slice(0, sliceVal); + storiesData$.subscribe( + data => { res.set('Content-Type', 'text/xml'); res.render('feed', { title: 'FreeCodeCamp Camper News RSS Feed', @@ -126,51 +83,6 @@ module.exports = function(app) { ); } - function submitNew(req, res) { - if (!req.user.isGithubCool) { - req.flash('errors', { - msg: 'You must link GitHub with your account before you can post' + - ' on Camper News.' - }); - return res.redirect('/news'); - } - - return res.render('stories/index', { - title: 'Submit a new story to Camper News', - page: 'submit' - }); - } - - function preSubmit(req, res) { - var data = req.query; - if (typeof data.url !== 'string') { - req.flash('errors', { msg: 'No URL supplied with story' }); - return res.redirect('/news'); - } - var cleanedData = cleanData(data.url); - - if (data.url.replace(/&/g, '&') !== cleanedData) { - req.flash('errors', { - msg: 'The data for this post is malformed' - }); - return res.render('stories/index', { - page: 'stories/submit' - }); - } - - var title = data.title || ''; - var image = data.image || ''; - var description = data.description || ''; - return res.render('stories/index', { - title: 'Confirm your Camper News story submission', - page: 'storySubmission', - storyURL: data.url, - storyTitle: title, - storyImage: image, - storyMetaDescription: description - }); - } - function returnIndividualStory(req, res, next) { var dashedName = req.params.storyName; var storyName = unDasherize(dashedName); @@ -217,251 +129,4 @@ module.exports = function(app) { next ); } - - function userStories({ body: { search = '' } = {} }, res, next) { - if (!search || typeof search !== 'string') { - return res.sendStatus(404); - } - - return app.dataSources.db.connector - .collection('story') - .find({ - 'author.username': search.toLowerCase().replace('$', '') - }) - .toArray(function(err, items) { - if (err) { - return next(err); - } - if (items && items.length !== 0) { - return res.json(items.sort(sortByRank)); - } - return res.sendStatus(404); - }); - } - - function getStories({ body: { search = '' } = {} }, res, next) { - if (!search || typeof search !== 'string') { - return res.sendStatus(404); - } - - const query = { - '$text': { - // protect against NoSQL injection - '$search': search.replace('$', '') - } - }; - - const fields = { - headline: 1, - timePosted: 1, - link: 1, - description: 1, - rank: 1, - upVotes: 1, - author: 1, - image: 1, - storyLink: 1, - metaDescription: 1, - textScore: { - $meta: 'textScore' - } - }; - - const options = { - sort: { - textScore: { - $meta: 'textScore' - } - } - }; - - return app.dataSources.db.connector - .collection('story') - .find(query, fields, options) - .toArray(function(err, items) { - if (err) { - return next(err); - } - if (items && items.length !== 0) { - return res.json(items); - } - return res.sendStatus(404); - }); - } - - function upvote(req, res, next) { - const { id } = req.body; - var story$ = findStoryById(id).shareReplay(); - - story$.flatMap(function(story) { - // find story author - return findUserById(story.author.userId); - }) - .flatMap(function(user) { - // if user deletes account then this will not exist - if (user) { - user.progressTimestamps.push({ - timestamp: Date.now() - }); - } - return saveUser(user); - }) - .flatMap(function() { - return story$; - }) - .flatMap(function(story) { - debug('upvoting'); - story.rank += 1; - story.upVotes.push({ - upVotedBy: req.user.id, - upVotedByUsername: req.user.username - }); - return saveInstance(story); - }) - .subscribe( - function(story) { - return res.send(story); - }, - next - ); - } - - function newStory(req, res, next) { - if (!req.user.isGithubCool) { - req.flash('errors', { - msg: 'You must authenticate with GitHub to post to Camper News' - }); - return res.redirect('/news'); - } - var url = req.body.data.url; - - if (!validator.isURL('' + url)) { - req.flash('errors', { - msg: "The URL you submitted doesn't appear valid" - }); - return res.json({ - alreadyPosted: true, - storyURL: '/stories/submit' - }); - - } - if (url.search(/^https?:\/\//g) === -1) { - url = 'http://' + url; - } - - return findStory({ where: { link: url } }) - .map(function(stories) { - if (stories.length) { - return { - alreadyPosted: true, - storyURL: '/stories/' + stories.pop().storyLink - }; - } - return { - alreadyPosted: false, - storyURL: url - }; - }) - .flatMap(function(data) { - if (data.alreadyPosted) { - return Rx.Observable.just(data); - } - return Rx.Observable.fromNodeCallback(getURLTitle)(data.storyURL) - .map(function(story) { - return { - alreadyPosted: false, - storyURL: data.storyURL, - storyTitle: story.title, - storyImage: story.image, - storyMetaDescription: story.description - }; - }); - }) - .subscribe( - function(story) { - if (story.alreadyPosted) { - req.flash('errors', { - msg: "Someone's already posted that link. Here's the discussion." - }); - } - res.json(story); - }, - next - ); - } - - function storySubmission(req, res, next) { - if (req.user.isBanned) { - return res.json({ - isBanned: true - }); - } - var data = req.body.data; - - var storyLink = data.headline - .replace(/[^a-z0-9\s]/gi, '') - .replace(/\s+/g, ' ') - .toLowerCase() - .trim(); - - var link = data.link; - - if (link.search(/^https?:\/\//g) === -1) { - link = 'http://' + link; - } - - var query = { - storyLink: { - like: ('^' + storyLink + '(?: [0-9]+)?$'), - options: 'i' - } - }; - - var savedStory = countStories(query) - .flatMap(function(storyCount) { - // if duplicate storyLink add unique number - storyLink = (storyCount === 0) ? - storyLink : - storyLink + ' ' + storyCount; - - var link = data.link; - if (link.search(/^https?:\/\//g) === -1) { - link = 'http://' + link; - } - var newStory = new Story({ - headline: cleanData(data.headline), - timePosted: Date.now(), - link: link, - description: cleanData(data.description), - rank: 1, - upVotes: [({ - upVotedBy: req.user.id, - upVotedByUsername: req.user.username - })], - author: { - picture: req.user.picture, - userId: req.user.id, - username: req.user.username - }, - image: data.image, - storyLink: storyLink, - metaDescription: data.storyMetaDescription - }); - return saveInstance(newStory); - }); - - req.user.progressTimestamps.push({ - timestamp: Date.now() - }); - return saveUser(req.user) - .flatMap(savedStory) - .subscribe( - function(story) { - res.json({ - storyLink: dasherize(story.storyLink) - }); - }, - next - ); - } }; From 0d0f4253ec876082c3e2c58fc2a637a5440a7ffa Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 7 Apr 2016 09:39:52 -0700 Subject: [PATCH 6/7] Remove console.logs --- server/boot/challenge.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/boot/challenge.js b/server/boot/challenge.js index 6496579ba3..c194311404 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -669,10 +669,8 @@ module.exports = function(app) { if (user && typeof user.getChallengeMap$ === 'function') { return user.getChallengeMap$(); } - console.log('foo'); return Observable.just({}); }) - .doOnNext(challengeMap => console.log('challengeMap', challengeMap)) .flatMap(challengeMap => getSuperBlocks$(challenge$, challengeMap)) .subscribe( superBlocks => { From 258597f053ffbc6e140a1dee6e5b10314f824fd0 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 7 Apr 2016 10:12:29 -0700 Subject: [PATCH 7/7] Fix news jade pages Remove unused pages --- server/boot/randomAPIs.js | 7 - server/boot/story.js | 22 ++- .../deprecated.jade} | 0 server/views/{ => news}/feed.jade | 0 server/views/news/index.jade | 48 +++++++ server/views/stories/hot-stories.jade | 90 ------------- server/views/stories/index.jade | 32 ----- server/views/stories/news-nav.jade | 127 ------------------ server/views/stories/preliminary-submit.jade | 51 ------- server/views/stories/show.jade | 41 ------ server/views/stories/submit-story.jade | 41 ------ 11 files changed, 57 insertions(+), 402 deletions(-) rename server/views/{resources/camper-news-deprecated.jade => news/deprecated.jade} (100%) rename server/views/{ => news}/feed.jade (100%) create mode 100644 server/views/news/index.jade delete mode 100644 server/views/stories/hot-stories.jade delete mode 100644 server/views/stories/index.jade delete mode 100644 server/views/stories/news-nav.jade delete mode 100644 server/views/stories/preliminary-submit.jade delete mode 100644 server/views/stories/show.jade delete mode 100644 server/views/stories/submit-story.jade diff --git a/server/boot/randomAPIs.js b/server/boot/randomAPIs.js index f12a270843..2995082fb2 100644 --- a/server/boot/randomAPIs.js +++ b/server/boot/randomAPIs.js @@ -43,7 +43,6 @@ module.exports = function(app) { router.get('/how-nonprofit-projects-work', howNonprofitProjectsWork); router.get('/code-of-conduct', codeOfConduct); router.get('/academic-honesty', academicHonesty); - router.get('/news', news); router.get( '/the-fastest-web-page-on-the-internet', @@ -285,12 +284,6 @@ module.exports = function(app) { }); } - function news(req, res) { - res.render('resources/camper-news-deprecated', { - title: 'Camper News' - }); - } - function twitch(req, res) { res.redirect('https://twitch.tv/freecodecamp'); } diff --git a/server/boot/story.js b/server/boot/story.js index 6849812874..2a7a11be0b 100755 --- a/server/boot/story.js +++ b/server/boot/story.js @@ -41,6 +41,7 @@ module.exports = function(app) { const redirectToNews = (req, res) => res.redirect('/news'); const deprecated = (req, res) => res.sendStatus(410); + router.get('/news', showNews); router.post('/news/userstories', deprecated); router.get('/news/hot', hotJSON); router.get('/news/feed', RSSFeed); @@ -56,6 +57,10 @@ module.exports = function(app) { app.use(router); + function showNews(req, res) { + res.render('news/deprecated', { title: 'Camper News' }); + } + function replaceStoryWithNews(req, res) { var url = req.originalUrl.replace(/^\/stories/, '/news'); return res.redirect(url); @@ -72,7 +77,7 @@ module.exports = function(app) { storiesData$.subscribe( data => { res.set('Content-Type', 'text/xml'); - res.render('feed', { + res.render('news/feed', { title: 'FreeCodeCamp Camper News RSS Feed', description: 'RSS Feed for FreeCodeCamp Top 100 Hot Camper News', url: 'http://www.freecodecamp.com/news', @@ -105,25 +110,16 @@ module.exports = function(app) { return res.redirect('../stories/' + dashedNameFull); } - var username = req.user ? req.user.username : ''; - // true if any of votes are made by user - var userVoted = story.upVotes.some(function(upvote) { - return upvote.upVotedByUsername === username; - }); - - return res.render('stories/index', { - title: story.headline, + return res.render('news/index', { + title: story.headline || 'news', link: story.link, originalStoryLink: dashedName, author: story.author, rank: story.upVotes.length, - upVotes: story.upVotes, id: story.id, timeAgo: moment(story.timePosted).fromNow(), image: story.image, - page: 'show', - storyMetaDescription: story.metaDescription, - hasUserVoted: userVoted + storyMetaDescription: story.metaDescription }); }, next diff --git a/server/views/resources/camper-news-deprecated.jade b/server/views/news/deprecated.jade similarity index 100% rename from server/views/resources/camper-news-deprecated.jade rename to server/views/news/deprecated.jade diff --git a/server/views/feed.jade b/server/views/news/feed.jade similarity index 100% rename from server/views/feed.jade rename to server/views/news/feed.jade diff --git a/server/views/news/index.jade b/server/views/news/index.jade new file mode 100644 index 0000000000..c645be7c88 --- /dev/null +++ b/server/views/news/index.jade @@ -0,0 +1,48 @@ +extends ../layout +block content + h1.text-center Camper News + hr + .spacer + .row + .col-xs-12 + h2 We have discontinued Camper News in favor of our + a(href='http://reddit.com/r/freecodecamp')  Subreddit + | . + h3 Thank you to all of the campers who have contributed links over the past year. Our + a(href='http://reddit.com/r/freecodecamp')  Subreddit + |   is now the best place to share coding-related links. + .spacer + hr + #search-results + .spacer + #story-list + ul#stories + .spacer + h3.row + .col-xs-2.col-sm-1 + a(href="/" + author.username) + img(src="#{author.picture}", class='img-news') + .col-xs-10.col-sm-10 + .col-xs-12.negative-28 + a(href="#{link}", target="_blank") + h3= title + h6 + .col-xs-12.positive-15.hidden-element#image-display + .media + .media-left + img.url-preview.media-object(src="#{image}", alt="#{storyMetaDescription}") + .media-body + .col-xs-12.col-sm-12.col-md-6 + h4= storyMetaDescription + .col-xs-12 + .spacer + span#storyRank= rank + (rank > 1 ? " points" : " point") + |  ·  + span Posted #{timeAgo} + span  by  + a(href="/" + author.username) @#{author.username} + + script. + if (image) { + $('#image-display').removeClass('hidden-element') + } diff --git a/server/views/stories/hot-stories.jade b/server/views/stories/hot-stories.jade deleted file mode 100644 index 0666058399..0000000000 --- a/server/views/stories/hot-stories.jade +++ /dev/null @@ -1,90 +0,0 @@ -.spacer -.spacer -#story-list.story-list - script. - var getLinkedName = function getLinkedName(name) { - return name.trim().toLowerCase().replace(/\s/g, '-'); - } - $.ajax({ - url: '/news/hot', - type: 'GET' - }) - .success( - function(data) { - for (var i = 0; i < data.length; i++) { - var div = document.createElement('div'); - var linkedName = getLinkedName(data[i].storyLink); - var rank = data[i].rank; - var alreadyUpvoted = false; - if (typeof username !== 'undefined') { - alreadyUpvoted = data[i].upVotes.some(function(vote) { - return vote.upVotedByUsername === username - }); - } - - $(div) - .html( - "
" + - "
" + - "" + - "" + - "" + - "
" + - "
" + - "" + - data[i].headline + - "" + - "
" + - "
" + - rank + (rank > 1 ? " points" : " point") + " · posted " + - moment(data[i].timePosted).fromNow() + - " by @" + data[i].author.username + " " + - "
" + - "
" + - "
" + - (typeof username !== 'undefined' ? - "" : - "upvote") + - "" + - "
" + - "
" + - "" - ); - $(div).addClass('story-list news-box') - $(div).appendTo($('#story-list')); - $(div).find('.btn-upvote').each(function(idx, btn) { - var $btn = $(btn); - if (alreadyUpvoted) { - $btn.addClass('disabled'); - $btn.text('upvoted!'); - } - $btn.data('upVotes', data[i].upVotes); - }); - } - }); diff --git a/server/views/stories/index.jade b/server/views/stories/index.jade deleted file mode 100644 index 56958a7a08..0000000000 --- a/server/views/stories/index.jade +++ /dev/null @@ -1,32 +0,0 @@ -extends ../layout -block content - if (user) - script. - var isLoggedIn = true; - var userId = !{JSON.stringify(user.id)}; - var username = !{JSON.stringify(user.username)}; - else - script. - var isLoggedIn = false; - script. - var challengeName = 'Camper News'; - var page = !{JSON.stringify(page)}; - h1.text-center Camper News - hr - .spacer - include news-nav - .spacer - if (page === 'hot') - include hot-stories - if (page === 'submit') - if (user) - include preliminary-submit - else - .spacer - .text-center - a.btn.btn-cta.signup-btn.btn-primary(href="/login") Sign in to post your story (it's free) - .spacer - if (page === 'storySubmission') - include submit-story - if (page === 'show') - include show diff --git a/server/views/stories/news-nav.jade b/server/views/stories/news-nav.jade deleted file mode 100644 index 345b2eb895..0000000000 --- a/server/views/stories/news-nav.jade +++ /dev/null @@ -1,127 +0,0 @@ -.row - .col-xs-12 - h2 We are retiring Camper News in favor of our - a(href='http://reddit.com/r/freecodecamp') Subreddit - | . - h3 Thank you to all of the campers who have contributed links over the past year. We will keep Camper News accessible until May. Our - a(href='http://reddit.com/r/freecodecamp') Subreddit - |   is now the best place to share coding-related links. - .spacer - hr - .spacer - .col-xs-12.col-sm-3 - span - a.btn.btn-primary.btn-bigger.btn-block.btn-responsive(href='/stories/submit' class="#{ page === 'hot' ? '' : 'hidden' }") Submit a link - span - a.btn.btn-success.btn-bigger.btn-block.btn-responsive(href='/news/' class="#{ (page !== 'hot') ? '' : 'hidden' }") All - .visible-xs - .button-spacer - .col-xs-12.col-sm-9 - .input-group - input#searchArea.big-text-field.field-responsive.form-control(type='text', placeholder='Search term or @username') - span.input-group-btn - button#searchbutton.btn.btn-bigger.btn-primary.btn-responsive(type='button') Search - .spacer - -#search-results - -.spacer -#story-list - ul#stories -script. - $('#searchArea').keypress(function (event) { - if (event.keyCode === 13 || event.which === 13) { - executeSearch(); - $('#searchArea').focus(); - event.preventDefault(); - } - }); - $('#searchbutton').on('click', function () { - executeSearch(); - }); - function executeSearch() { - $('#stories').empty(); - var searchTerm = $('#searchArea').val(), - url = '/stories/search'; - if (searchTerm.match(/^\@\w+$/)) { - url = '/news/userstories'; - searchTerm = searchTerm.match(/^\@\w+$/)[0].split('@')[1]; - } - var getLinkedName = function getLinkedName(name) { - return name.toLowerCase().replace(/\s/g, '-'); - } - $.post(url, { search: searchTerm }) - .fail(function(xhr, textStatus, errorThrown) { - $('#search-results').empty(); - var div = document.createElement("div"); - $(div).html("

No Results Found

"); - $(div).appendTo($('#search-results')); - }) - .done(function(data, textStatus, xhr) { - $('#search-results').empty(); - var spacer = document.createElement('div'); - $(spacer).html("
"); - $(spacer).appendTo($('#search-results')); - for (var i = 0; i < data.length; i++) { - var div = document.createElement('div'); - var linkedName = getLinkedName(data[i].storyLink); - var rank = data[i].rank; - $(div).html( - "
" + - "
" + - "" + - "" + - "" + - "
" + - "
" + - "" + - data[i].headline + - "" + - "
" + - "
" + - "
" + - rank + (rank > 1 ? " points" : " point") + " · posted " + - moment(data[i].timePosted).fromNow() + - " by " + - "@" + data[i].author.username + - " " + - "
" + - "
" + - "
" + - "more info" + - "
" + - "
" + - "
" + - "") - $(div).addClass('story-list news-box-search') - $(div).appendTo($('#search-results')); - } - var hr = document.createElement("div"); - $(hr).html("

End search results

") - $(hr).appendTo($('#search-results')); - }); - } diff --git a/server/views/stories/preliminary-submit.jade b/server/views/stories/preliminary-submit.jade deleted file mode 100644 index 70af2d94ef..0000000000 --- a/server/views/stories/preliminary-submit.jade +++ /dev/null @@ -1,51 +0,0 @@ -form.input-group(id='URLSubmit' name='URLSubmit') - input#story-url.big-text-field.field-responsive.form-control(placeholder='Paste your link here', name='link', type='url', required, autofocus) - span.input-group-btn - button#preliminary-story-submit.btn.btn-bigger.btn-primary.btn-responsive(type='submit') Submit -.spacer - - script. - $('#story-url').on('keypress', function(e) { - if (e.keyCode === 13) { - $('#preliminary-story-submit').click(); - } - }); - function preliminaryStorySubmit(e) { - if (!$('#URLSubmit')[0].checkValidity()) { - return null; - } - e.preventDefault(); - - var storyURL = $('#story-url').val(); - $('#preliminary-story-submit').attr('disabled', 'disabled'); - - $.post('/stories/preliminary', - { - data: { - url: storyURL - } - }) - .fail(function (xhr, textStatus, errorThrown) { - $('#preliminary-story-submit').attr('disabled', false); - }) - .done(function (data, textStatus, xhr) { - if (data.alreadyPosted) { - window.location = data.storyURL; - } else { - window.location = '/stories/submit/new-story?url=' + - encodeURIComponent(data.storyURL) + - '&title=' + encodeURIComponent(data.storyTitle) + - '&image=' + encodeURIComponent(data.storyImage) + - '&description=' + encodeURIComponent(data.storyMetaDescription); - } - }); - } - - $('#preliminary-story-submit').on('click', preliminaryStorySubmit); - - arr = $( "h3 input:checked" ) - .map(function() { - return this.id; - }) - .get() - .join('&'); diff --git a/server/views/stories/show.jade b/server/views/stories/show.jade deleted file mode 100644 index cced0f41f5..0000000000 --- a/server/views/stories/show.jade +++ /dev/null @@ -1,41 +0,0 @@ -script. - var storyId = !{JSON.stringify(id)}; - var originalStoryLink = !{JSON.stringify(originalStoryLink)}; - var upVotes = !{JSON.stringify(upVotes)}; - var image = !{JSON.stringify(image)}; - var hasUserVoted = !{JSON.stringify(hasUserVoted)}; - -h3.row - .col-xs-2.col-sm-1 - a(href="/" + author.username) - img(src="#{author.picture}", class='img-news') - .col-xs-10.col-sm-10 - .col-xs-12.negative-28 - a(href="#{link}", target="_blank") - h3= title - h6 - .col-xs-12.positive-15.hidden-element#image-display - .media - .media-left - img.url-preview.media-object(src="#{image}", alt="#{storyMetaDescription}") - .media-body - .col-xs-12.col-sm-12.col-md-6 - h4= storyMetaDescription - .col-xs-12 - .spacer - if !hasUserVoted - a#upvote.btn.btn-no-shadow.btn-primary.btn-xs.btn-primary-ghost Upvote - |  ·  - else - a#upvote.btn.disabled.btn-no-shadow.btn-primary.btn-xs.btn-primary-ghost Upvoted! - |  ·  - span#storyRank= rank + (rank > 1 ? " points" : " point") - |  ·  - span Posted #{timeAgo} - span  by  - a(href="/" + author.username) @#{author.username} - -script. - if (image) { - $('#image-display').removeClass('hidden-element') - } diff --git a/server/views/stories/submit-story.jade b/server/views/stories/submit-story.jade deleted file mode 100644 index 66b91fc4f1..0000000000 --- a/server/views/stories/submit-story.jade +++ /dev/null @@ -1,41 +0,0 @@ -.spacer -.col-xs-12 - script. - var main = window.main || { init: [] }; - main.storyURL = !{JSON.stringify(storyURL)}; - main.storyTitle = !{JSON.stringify(storyTitle)}; - main.storyImage = !{JSON.stringify(storyImage)}; - main.storyMetaDescription = !{JSON.stringify(storyMetaDescription)}; - - form.form-horizontal.control-label-story-submission#story-submission-form(name="submitStory") - .col-xs-12 - .form-group - .col-xs-12.col-md-1 - label.control-label.control-label-story-submission(for='name') Link - .col-xs-12.col-md-11 - input#story-url.form-control(name='Link', disabled, value='#{storyURL}') - .form-group - .col-xs-12.col-md-1 - label.control-label.control-label-story-submission(for='name') Title - .col-xs-12.col-md-11 - input#story-title.form-control(value='#{storyTitle}', name='Title', maxlength='90', autocomplete="off", autofocus, required) - .form-group - .col-xs-12.col-md-offset-1 - span.pull-left#textarea_feedback - .form-group - .col-xs-11.col-md-offset-1 - .hidden-element#image-display - .media - .media-left - img.url-preview.media-object(src="#{storyImage}", alt="#{storyMetaDescription}") - .media-body - .col-xs-12 - p= storyMetaDescription - .spacer - .row - .form-group - button.btn.btn-bigger.btn-block.btn-primary#story-submit(type='submit', onclick="return false;") Submit - script. - if (main.storyImage) { - $('#image-display').removeClass('hidden-element'); - }