Merge pull request #6927 from FreeCodeCamp/feature/reduce-user-query-load
Reduce initial user query load
This commit is contained in:
@ -441,4 +441,28 @@ module.exports = function(User) {
|
|||||||
}
|
}
|
||||||
return this.constructor.update$({ id }, updateData, updateOptions);
|
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;
|
||||||
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
"normalize-url": "^1.3.1",
|
"normalize-url": "^1.3.1",
|
||||||
"normalizr": "^2.0.0",
|
"normalizr": "^2.0.0",
|
||||||
"object.assign": "^4.0.3",
|
"object.assign": "^4.0.3",
|
||||||
|
"passport": "^0.2.1",
|
||||||
"passport-facebook": "^2.0.0",
|
"passport-facebook": "^2.0.0",
|
||||||
"passport-github": "^1.0.0",
|
"passport-github": "^1.0.0",
|
||||||
"passport-google-oauth2": "~0.1.6",
|
"passport-google-oauth2": "~0.1.6",
|
||||||
|
@ -84,7 +84,8 @@ export default function certificate(app) {
|
|||||||
|
|
||||||
function verifyCert(certType, req, res, next) {
|
function verifyCert(certType, req, res, next) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
return certTypeIds[certType]
|
return user.getChallengeMap()
|
||||||
|
.flatMap(() => certTypeIds[certType])
|
||||||
.flatMap(challenge => {
|
.flatMap(challenge => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
@ -142,7 +142,7 @@ function getRenderData$(user, challenge$, origChallengeName, solution) {
|
|||||||
|
|
||||||
return challenge$
|
return challenge$
|
||||||
.map(challenge => challenge.toJSON())
|
.map(challenge => challenge.toJSON())
|
||||||
.filter((challenge) => {
|
.filter(challenge => {
|
||||||
return shouldNotFilterComingSoon(challenge) &&
|
return shouldNotFilterComingSoon(challenge) &&
|
||||||
challenge.type !== 'hike' &&
|
challenge.type !== 'hike' &&
|
||||||
testChallengeName.test(challenge.name);
|
testChallengeName.test(challenge.name);
|
||||||
@ -500,8 +500,17 @@ module.exports = function(app) {
|
|||||||
function showChallenge(req, res, next) {
|
function showChallenge(req, res, next) {
|
||||||
const solution = req.query.solution;
|
const solution = req.query.solution;
|
||||||
const challengeName = req.params.challengeName.replace(challengesRegex, '');
|
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(
|
.subscribe(
|
||||||
({ type, redirectUrl, message, data }) => {
|
({ type, redirectUrl, message, data }) => {
|
||||||
if (message) {
|
if (message) {
|
||||||
@ -546,6 +555,8 @@ module.exports = function(app) {
|
|||||||
return res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return req.user.getChallengeMap$()
|
||||||
|
.flatMap(() => {
|
||||||
const completedDate = Date.now();
|
const completedDate = Date.now();
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@ -569,16 +580,11 @@ module.exports = function(app) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const user = req.user;
|
const user = req.user;
|
||||||
const points = alreadyCompleted ?
|
const points = alreadyCompleted ? user.points : user.points + 1;
|
||||||
user.progressTimestamps.length :
|
|
||||||
user.progressTimestamps.length + 1;
|
|
||||||
|
|
||||||
return user.update$(updateData)
|
return user.update$(updateData)
|
||||||
.doOnNext(({ count }) => log('%s documents updated', count))
|
.doOnNext(({ count }) => log('%s documents updated', count))
|
||||||
.subscribe(
|
.map(() => {
|
||||||
() => {},
|
|
||||||
next,
|
|
||||||
function() {
|
|
||||||
if (type === 'json') {
|
if (type === 'json') {
|
||||||
return res.json({
|
return res.json({
|
||||||
points,
|
points,
|
||||||
@ -586,8 +592,9 @@ module.exports = function(app) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}
|
});
|
||||||
);
|
})
|
||||||
|
.subscribe(() => {}, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
function completedZiplineOrBasejump(req, res, next) {
|
function completedZiplineOrBasejump(req, res, next) {
|
||||||
@ -635,10 +642,12 @@ module.exports = function(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return user.getChallengeMap$()
|
||||||
|
.flatMap(() => {
|
||||||
const {
|
const {
|
||||||
alreadyCompleted,
|
alreadyCompleted,
|
||||||
updateData
|
updateData
|
||||||
} = buildUserUpdate(req.user, completedChallenge.id, completedChallenge);
|
} = buildUserUpdate(user, completedChallenge.id, completedChallenge);
|
||||||
|
|
||||||
return user.update$(updateData)
|
return user.update$(updateData)
|
||||||
.doOnNext(({ count }) => log('%s documents updated', count))
|
.doOnNext(({ count }) => log('%s documents updated', count))
|
||||||
@ -646,20 +655,23 @@ module.exports = function(app) {
|
|||||||
if (type === 'json') {
|
if (type === 'json') {
|
||||||
return res.send({
|
return res.send({
|
||||||
alreadyCompleted,
|
alreadyCompleted,
|
||||||
points: alreadyCompleted ?
|
points: alreadyCompleted ? user.points : user.points + 1
|
||||||
user.progressTimestamps.length :
|
|
||||||
user.progressTimestamps.length + 1
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return res.status(200).send(true);
|
return res.status(200).send(true);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.subscribe(() => {}, next);
|
.subscribe(() => {}, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMap(showAside, { user = {} }, res, next) {
|
function showMap(showAside, { user }, res, next) {
|
||||||
const { challengeMap = {} } = user;
|
return Observable.defer(() => {
|
||||||
|
if (user && typeof user.getChallengeMap$ === 'function') {
|
||||||
return getSuperBlocks$(challenge$, challengeMap)
|
return user.getChallengeMap$();
|
||||||
|
}
|
||||||
|
return Observable.just({});
|
||||||
|
})
|
||||||
|
.flatMap(challengeMap => getSuperBlocks$(challenge$, challengeMap))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
superBlocks => {
|
superBlocks => {
|
||||||
res.render('map/show', {
|
res.render('map/show', {
|
||||||
|
@ -43,7 +43,6 @@ module.exports = function(app) {
|
|||||||
router.get('/how-nonprofit-projects-work', howNonprofitProjectsWork);
|
router.get('/how-nonprofit-projects-work', howNonprofitProjectsWork);
|
||||||
router.get('/code-of-conduct', codeOfConduct);
|
router.get('/code-of-conduct', codeOfConduct);
|
||||||
router.get('/academic-honesty', academicHonesty);
|
router.get('/academic-honesty', academicHonesty);
|
||||||
router.get('/news', news);
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/the-fastest-web-page-on-the-internet',
|
'/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) {
|
function twitch(req, res) {
|
||||||
res.redirect('https://twitch.tv/freecodecamp');
|
res.redirect('https://twitch.tv/freecodecamp');
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,11 @@
|
|||||||
var Rx = require('rx'),
|
import moment from 'moment';
|
||||||
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 {
|
import { unDasherize } from '../utils';
|
||||||
ifNoUser401,
|
import { observeMethod } from '../utils/rx';
|
||||||
ifNoUserRedirectTo
|
|
||||||
} from '../utils/middleware';
|
|
||||||
|
|
||||||
const foundationDate = 1413298800000;
|
const foundationDate = 1413298800000;
|
||||||
const time48Hours = 172800000;
|
const time48Hours = 172800000;
|
||||||
|
|
||||||
const unDasherize = utils.unDasherize;
|
|
||||||
const dasherize = utils.dasherize;
|
|
||||||
const getURLTitle = utils.getURLTitle;
|
|
||||||
const sendNonUserToNews = ifNoUserRedirectTo('/news');
|
|
||||||
|
|
||||||
function hotRank(timeValue, rank) {
|
function hotRank(timeValue, rank) {
|
||||||
/*
|
/*
|
||||||
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
|
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
|
||||||
@ -39,83 +23,61 @@ function sortByRank(a, b) {
|
|||||||
hotRank(a.timePosted - foundationDate, a.rank);
|
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) {
|
module.exports = function(app) {
|
||||||
var router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
var User = app.models.User;
|
const Story = app.models.Story;
|
||||||
var findUserById = observeMethod(User, 'findById');
|
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;
|
const redirectToNews = (req, res) => res.redirect('/news');
|
||||||
var findStory = observeMethod(Story, 'find');
|
const deprecated = (req, res) => res.sendStatus(410);
|
||||||
var findOneStory = observeMethod(Story, 'findOne');
|
router.get('/news', showNews);
|
||||||
var findStoryById = observeMethod(Story, 'findById');
|
router.post('/news/userstories', deprecated);
|
||||||
var countStories = observeMethod(Story, 'count');
|
|
||||||
|
|
||||||
router.post('/news/userstories', userStories);
|
|
||||||
router.get('/news/hot', hotJSON);
|
router.get('/news/hot', hotJSON);
|
||||||
router.get('/news/feed', RSSFeed);
|
router.get('/news/feed', RSSFeed);
|
||||||
router.get('/stories/hotStories', hotJSON);
|
router.get('/stories/hotStories', hotJSON);
|
||||||
router.get(
|
router.get('/stories/submit', redirectToNews);
|
||||||
'/stories/submit',
|
router.get('/stories/submit/new-story', redirectToNews);
|
||||||
sendNonUserToNews,
|
router.post('/stories/preliminary', deprecated);
|
||||||
submitNew
|
router.post('/stories/', deprecated);
|
||||||
);
|
router.post('/stories/search', deprecated);
|
||||||
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('/news/:storyName', returnIndividualStory);
|
router.get('/news/:storyName', returnIndividualStory);
|
||||||
router.post('/stories/upvote/', ifNoUser401, upvote);
|
router.post('/stories/upvote/', deprecated);
|
||||||
router.get('/stories/:storyName', redirectToNews);
|
router.get('/stories/:storyName', replaceStoryWithNews);
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
function redirectToNews(req, res) {
|
function showNews(req, res) {
|
||||||
|
res.render('news/deprecated', { title: 'Camper News' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceStoryWithNews(req, res) {
|
||||||
var url = req.originalUrl.replace(/^\/stories/, '/news');
|
var url = req.originalUrl.replace(/^\/stories/, '/news');
|
||||||
return res.redirect(url);
|
return res.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hotJSON(req, res, next) {
|
function hotJSON(req, res, next) {
|
||||||
var query = {
|
storiesData$.subscribe(
|
||||||
order: 'timePosted DESC',
|
stories => res.json(stories),
|
||||||
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);
|
|
||||||
},
|
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RSSFeed(req, res, next) {
|
function RSSFeed(req, res, next) {
|
||||||
var query = {
|
storiesData$.subscribe(
|
||||||
order: 'timePosted DESC',
|
data => {
|
||||||
limit: 1000
|
|
||||||
};
|
|
||||||
findStory(query).subscribe(
|
|
||||||
function(stories) {
|
|
||||||
var sliceVal = stories.length >= 100 ? 100 : stories.length;
|
|
||||||
var data = stories.sort(sortByRank).slice(0, sliceVal);
|
|
||||||
res.set('Content-Type', 'text/xml');
|
res.set('Content-Type', 'text/xml');
|
||||||
res.render('feed', {
|
res.render('news/feed', {
|
||||||
title: 'FreeCodeCamp Camper News RSS Feed',
|
title: 'FreeCodeCamp Camper News RSS Feed',
|
||||||
description: 'RSS Feed for FreeCodeCamp Top 100 Hot Camper News',
|
description: 'RSS Feed for FreeCodeCamp Top 100 Hot Camper News',
|
||||||
url: 'http://www.freecodecamp.com/news',
|
url: 'http://www.freecodecamp.com/news',
|
||||||
@ -126,51 +88,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) {
|
function returnIndividualStory(req, res, next) {
|
||||||
var dashedName = req.params.storyName;
|
var dashedName = req.params.storyName;
|
||||||
var storyName = unDasherize(dashedName);
|
var storyName = unDasherize(dashedName);
|
||||||
@ -193,272 +110,16 @@ module.exports = function(app) {
|
|||||||
return res.redirect('../stories/' + dashedNameFull);
|
return res.redirect('../stories/' + dashedNameFull);
|
||||||
}
|
}
|
||||||
|
|
||||||
var username = req.user ? req.user.username : '';
|
return res.render('news/index', {
|
||||||
// true if any of votes are made by user
|
title: story.headline || 'news',
|
||||||
var userVoted = story.upVotes.some(function(upvote) {
|
|
||||||
return upvote.upVotedByUsername === username;
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.render('stories/index', {
|
|
||||||
title: story.headline,
|
|
||||||
link: story.link,
|
link: story.link,
|
||||||
originalStoryLink: dashedName,
|
originalStoryLink: dashedName,
|
||||||
author: story.author,
|
author: story.author,
|
||||||
rank: story.upVotes.length,
|
rank: story.upVotes.length,
|
||||||
upVotes: story.upVotes,
|
|
||||||
id: story.id,
|
id: story.id,
|
||||||
timeAgo: moment(story.timePosted).fromNow(),
|
timeAgo: moment(story.timePosted).fromNow(),
|
||||||
image: story.image,
|
image: story.image,
|
||||||
page: 'show',
|
storyMetaDescription: story.metaDescription
|
||||||
storyMetaDescription: story.metaDescription,
|
|
||||||
hasUserVoted: userVoted
|
|
||||||
});
|
|
||||||
},
|
|
||||||
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
|
next
|
||||||
|
@ -420,63 +420,59 @@ module.exports = function(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleLockdownMode(req, res, next) {
|
function toggleLockdownMode(req, res, next) {
|
||||||
return User.findById(req.accessToken.userId, function(err, user) {
|
const { user } = req;
|
||||||
if (err) { return next(err); }
|
user.update$({ isLocked: !user.isLocked })
|
||||||
return user.updateAttribute('isLocked', !user.isLocked, function(err) {
|
.subscribe(
|
||||||
if (err) { return next(err); }
|
() => {
|
||||||
req.flash('info', {
|
req.flash('info', {
|
||||||
msg: 'We\'ve successfully updated your Privacy preferences.'
|
msg: 'We\'ve successfully updated your Privacy preferences.'
|
||||||
});
|
});
|
||||||
return res.redirect('/settings');
|
return res.redirect('/settings');
|
||||||
});
|
},
|
||||||
});
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReceivesAnnouncementEmails(req, res, next) {
|
function toggleReceivesAnnouncementEmails(req, res, next) {
|
||||||
return User.findById(req.accessToken.userId, function(err, user) {
|
const { user } = req;
|
||||||
if (err) { return next(err); }
|
return user.update$({ sendMonthlyEmail: !user.sendMonthlyEmail })
|
||||||
return user.updateAttribute(
|
.subscribe(
|
||||||
'sendMonthlyEmail',
|
() => {
|
||||||
!user.sendMonthlyEmail,
|
|
||||||
(err) => {
|
|
||||||
if (err) { return next(err); }
|
|
||||||
req.flash('info', {
|
req.flash('info', {
|
||||||
msg: 'We\'ve successfully updated your Email preferences.'
|
msg: 'We\'ve successfully updated your Email preferences.'
|
||||||
});
|
});
|
||||||
return res.redirect('/settings');
|
return res.redirect('/settings');
|
||||||
});
|
},
|
||||||
});
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReceivesQuincyEmails(req, res, next) {
|
function toggleReceivesQuincyEmails(req, res, next) {
|
||||||
return User.findById(req.accessToken.userId, function(err, user) {
|
const { user } = req;
|
||||||
if (err) { return next(err); }
|
return user.update$({ sendQuincyEmail: !user.sendQuincyEmail })
|
||||||
return user.updateAttribute('sendQuincyEmail', !user.sendQuincyEmail,
|
.subscribe(
|
||||||
(err) => {
|
() => {
|
||||||
if (err) { return next(err); }
|
|
||||||
req.flash('info', {
|
req.flash('info', {
|
||||||
msg: 'We\'ve successfully updated your Email preferences.'
|
msg: 'We\'ve successfully updated your Email preferences.'
|
||||||
});
|
});
|
||||||
return res.redirect('/settings');
|
return res.redirect('/settings');
|
||||||
}
|
},
|
||||||
|
next
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleReceivesNotificationEmails(req, res, next) {
|
function toggleReceivesNotificationEmails(req, res, next) {
|
||||||
return User.findById(req.accessToken.userId, function(err, user) {
|
const { user } = req;
|
||||||
if (err) { return next(err); }
|
return user.update$({ sendNotificationEmail: !user.sendNotificationEmail })
|
||||||
return user.updateAttribute(
|
.subscribe(
|
||||||
'sendNotificationEmail',
|
() => {
|
||||||
!user.sendNotificationEmail,
|
|
||||||
function(err) {
|
|
||||||
if (err) { return next(err); }
|
|
||||||
req.flash('info', {
|
req.flash('info', {
|
||||||
msg: 'We\'ve successfully updated your Email preferences.'
|
msg: 'We\'ve successfully updated your Email preferences.'
|
||||||
});
|
});
|
||||||
return res.redirect('/settings');
|
return res.redirect('/settings');
|
||||||
});
|
},
|
||||||
});
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function postDeleteAccount(req, res, next) {
|
function postDeleteAccount(req, res, next) {
|
||||||
|
114
server/component-passport.js
Normal file
114
server/component-passport.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
return 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;
|
||||||
|
return 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
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -3,96 +3,29 @@ var pmx = require('pmx');
|
|||||||
pmx.init();
|
pmx.init();
|
||||||
|
|
||||||
var _ = require('lodash'),
|
var _ = require('lodash'),
|
||||||
uuid = require('node-uuid'),
|
|
||||||
assign = require('lodash').assign,
|
|
||||||
loopback = require('loopback'),
|
loopback = require('loopback'),
|
||||||
boot = require('loopback-boot'),
|
boot = require('loopback-boot'),
|
||||||
expressState = require('express-state'),
|
expressState = require('express-state'),
|
||||||
path = require('path'),
|
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 app = loopback();
|
||||||
|
var isBeta = !!process.env.BETA;
|
||||||
|
|
||||||
expressState.extend(app);
|
expressState.extend(app);
|
||||||
app.set('state namespace', '__fcc__');
|
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('port', process.env.PORT || 3000);
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
app.set('view engine', 'jade');
|
app.set('view engine', 'jade');
|
||||||
app.use(loopback.token());
|
app.use(loopback.token());
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
|
|
||||||
// adds passport initialization after session middleware phase is complete
|
|
||||||
passportConfigurator.init();
|
|
||||||
|
|
||||||
boot(app, {
|
boot(app, {
|
||||||
appRootDir: __dirname,
|
appRootDir: __dirname,
|
||||||
dev: process.env.NODE_ENV
|
dev: process.env.NODE_ENV
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setupPassport(app);
|
||||||
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)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.start = _.once(function() {
|
app.start = _.once(function() {
|
||||||
app.listen(app.get('port'), function() {
|
app.listen(app.get('port'), function() {
|
||||||
|
48
server/views/news/index.jade
Normal file
48
server/views/news/index.jade
Normal file
@ -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')
|
||||||
|
}
|
@ -28,7 +28,7 @@ nav.navbar.navbar-default.navbar-fixed-top.nav-height
|
|||||||
a(href='/login') Sign in
|
a(href='/login') Sign in
|
||||||
else
|
else
|
||||||
li.brownie-points-nav
|
li.brownie-points-nav
|
||||||
a(href='/' + user.username) [ #{user.progressTimestamps.length} ]
|
a(href='/' + user.username) [ #{user.points} ]
|
||||||
li.hidden-xs.hidden-sm.avatar
|
li.hidden-xs.hidden-sm.avatar
|
||||||
a(href='/' + user.username)
|
a(href='/' + user.username)
|
||||||
img.profile-picture.float-right(src='#{user.picture}')
|
img.profile-picture.float-right(src='#{user.picture}')
|
||||||
|
@ -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(
|
|
||||||
"<div class='visible-xs row'>" +
|
|
||||||
"<div class='visible-xs col-sm-1 col-md-1'>" +
|
|
||||||
"<a href='" + data[i].link + "'>" +
|
|
||||||
"<img class='mobile-story-image img-responsive' src='" + (!!data[i].image ? data[i].image : data[i].author.picture) + "'/>" +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12 mobile-story-headline text-center'>" +
|
|
||||||
"<a href='" + data[i].link + "' target='_blank'>" +
|
|
||||||
data[i].headline +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12 text-center'>" +
|
|
||||||
rank + (rank > 1 ? " points" : " point") + " · posted " +
|
|
||||||
moment(data[i].timePosted).fromNow() +
|
|
||||||
" by <a href='/" + data[i].author.username + "'>@" + data[i].author.username + "</a> " +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12'>" +
|
|
||||||
"<br>" +
|
|
||||||
(typeof username !== 'undefined' ?
|
|
||||||
"<button id='" + data[i].id + "' class='btn btn-no-shadow btn-primary btn-block btn-primary-ghost btn-upvote'>upvote</button>" :
|
|
||||||
"<a href='/signin' class='btn btn-no-shadow btn-primary btn-block btn-primary-ghost btn-upvote'>upvote</a>") +
|
|
||||||
"<a class='btn btn-no-shadow btn-primary btn-block btn-primary-ghost hidden' href='/news/" + linkedName + "'>more info</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='hidden-xs row media-stories'>" +
|
|
||||||
"<div class='media'>" +
|
|
||||||
"<div class='media-left'>" +
|
|
||||||
"<a href='/" + data[i].author.username + "'>" +
|
|
||||||
"<img class='img-news' src='" + data[i].author.picture + "'/>" +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<h2 class='media-body'>" +
|
|
||||||
"<div class='media-body-wrapper'>" +
|
|
||||||
"<div class='story-headline'>" +
|
|
||||||
"<a href='" + data[i].link + "' target='_blank'>" +
|
|
||||||
data[i].headline +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='story-byline col-xs-12 wrappable'>" +
|
|
||||||
(typeof username !== 'undefined' ?
|
|
||||||
"<button id='" + data[i].id + "' class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost btn-upvote'>upvote</button>" :
|
|
||||||
"<a href='/signin' class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost'>upvote</a>") +
|
|
||||||
"<a class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost hidden' href='/news/" + linkedName + "'>more info</a> " +
|
|
||||||
rank + (rank > 1 ? " points" : " point") + " · posted " +
|
|
||||||
moment(data[i].timePosted).fromNow() +
|
|
||||||
" by <a href='/" + data[i].author.username + "'>@" + data[i].author.username + "</a> " +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</h2>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>"
|
|
||||||
);
|
|
||||||
$(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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -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
|
|
@ -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("<h3 class='text-center text-warning dotted-underline'><em>No Results Found</em></h3>");
|
|
||||||
$(div).appendTo($('#search-results'));
|
|
||||||
})
|
|
||||||
.done(function(data, textStatus, xhr) {
|
|
||||||
$('#search-results').empty();
|
|
||||||
var spacer = document.createElement('div');
|
|
||||||
$(spacer).html("<div class='spacer'></div>");
|
|
||||||
$(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(
|
|
||||||
"<div class='visible-xs row'>" +
|
|
||||||
"<div class='visible-xs col-sm-1 col-md-1'>" +
|
|
||||||
"<a href='" + data[i].link + "'>" +
|
|
||||||
"<img class='mobile-story-image img-responsive' src='" + (!!data[i].image ? data[i].image : data[i].author.picture) + "'/>" +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12 mobile-story-headline text-center'>" +
|
|
||||||
"<a href='" + data[i].link + "' target='_blank'>" +
|
|
||||||
data[i].headline +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='visible-xs'>" +
|
|
||||||
"<div class='col-xs-12 text-center'>" +
|
|
||||||
rank + (rank > 1 ? " points" : " point") + " · posted " +
|
|
||||||
moment(data[i].timePosted).fromNow() +
|
|
||||||
" by " +
|
|
||||||
"<a href='/" + data[i].author.username + "'>@" + data[i].author.username +
|
|
||||||
"</a> " +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='col-xs-12'>" +
|
|
||||||
"<br>" +
|
|
||||||
"<a class='btn btn-no-shadow btn-primary btn-block btn-primary-ghost' href='/news/" + linkedName + "'>more info</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='hidden-xs row media-stories'>" +
|
|
||||||
"<div class='media'>" +
|
|
||||||
"<div class='media-left'>" +
|
|
||||||
"<a href='/" + data[i].author.username + "'>" +
|
|
||||||
"<img class='img-news' src='" + data[i].author.picture + "'/>" +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<h2 class='media-body'>" +
|
|
||||||
"<div class='media-body-wrapper'>" +
|
|
||||||
"<div class='story-headline'>" +
|
|
||||||
"<a href='" + data[i].link + "' target='_blank'>" +
|
|
||||||
data[i].headline +
|
|
||||||
"</a>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='story-byline col-xs-12 wrappable'>" +
|
|
||||||
"<a class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost' href='/news/" + linkedName + "'>more info</a> · " +
|
|
||||||
rank + (rank > 1 ? " points" : " point") + " · posted " +
|
|
||||||
moment(data[i].timePosted).fromNow() +
|
|
||||||
" by <a href='/" + data[i].author.username + "'>@" + data[i].author.username +
|
|
||||||
"</a> " +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</h2>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>")
|
|
||||||
$(div).addClass('story-list news-box-search')
|
|
||||||
$(div).appendTo($('#search-results'));
|
|
||||||
}
|
|
||||||
var hr = document.createElement("div");
|
|
||||||
$(hr).html("<h3 class='text-center text-success dotted-underline'><em>End search results</em></h3>")
|
|
||||||
$(hr).appendTo($('#search-results'));
|
|
||||||
});
|
|
||||||
}
|
|
@ -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('&');
|
|
@ -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')
|
|
||||||
}
|
|
@ -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');
|
|
||||||
}
|
|
Reference in New Issue
Block a user