Merge pull request #180 from terakilobyte/master

MOre news updates
This commit is contained in:
Quincy Larson
2015-03-09 16:39:04 -07:00
12 changed files with 579 additions and 464 deletions

2
app.js
View File

@ -324,7 +324,7 @@ app.get(
); );
app.get( app.get(
'/stories/submit/:newStory', '/stories/submit/new-story',
storyController.preSubmit storyController.preSubmit
); );

View File

@ -4,7 +4,6 @@ var User = require('../models/User'),
Story = require('./../models/Story'), Story = require('./../models/Story'),
Comment = require('./../models/Comment'), Comment = require('./../models/Comment'),
resources = require('./resources.json'), resources = require('./resources.json'),
questions = resources.questions,
steps = resources.steps, steps = resources.steps,
secrets = require('./../config/secrets'), secrets = require('./../config/secrets'),
bonfires = require('../seed_data/bonfires.json'), bonfires = require('../seed_data/bonfires.json'),
@ -154,7 +153,7 @@ module.exports = {
}, },
bloggerCalls: function(req, res) { bloggerCalls: function(req, res) {
request('https://www.googleapis.com/blogger/v3/blogs/2421288658305323950/posts?key=' + secrets.blogger.key, function (err, status, blog) { request('https://www.googleapis.com/blogger/v3/blogs/2421288658305323950/posts?key=' + secrets.blogger.key, function (err, status, blog) {
var blog = blog.length > 100 ? JSON.parse(blog) : ""; blog = blog.length > 100 ? JSON.parse(blog) : '';
res.send({ res.send({
blog1Title: blog ? blog["items"][0]["title"] : "Can't connect to Blogger", blog1Title: blog ? blog["items"][0]["title"] : "Can't connect to Blogger",
blog1Link: blog ? blog["items"][0]["url"] : "http://blog.freecodecamp.com", blog1Link: blog ? blog["items"][0]["url"] : "http://blog.freecodecamp.com",
@ -225,10 +224,6 @@ module.exports = {
return compliments[Math.floor(Math.random() * compliments.length)]; return compliments[Math.floor(Math.random() * compliments.length)];
}, },
numberOfBonfires: function() {
return bonfires.length - 1;
},
allBonfireIds: function() { allBonfireIds: function() {
return bonfires.map(function(elem) { return bonfires.map(function(elem) {
return { return {
@ -290,14 +285,19 @@ module.exports = {
return process.env.NODE_ENV; return process.env.NODE_ENV;
}, },
getURLTitle: function(url, callback) { getURLTitle: function(url, callback) {
debug('got url in meta scraping function', url);
(function () { (function () {
var result = {title: ''}; var result = {title: '', image: '', url: '', description: ''};
request(url, function (error, response, body) { request(url, function (error, response, body) {
if (!error && response.statusCode === 200) { if (!error && response.statusCode === 200) {
var $ = cheerio.load(body); var $ = cheerio.load(body);
var title = $('title').text(); var metaDescription = $("meta[name='description']");
result.title = title; var metaImage = $("meta[property='og:image']");
var urlImage = metaImage.attr('content') ? metaImage.attr('content') : '';
var description = metaDescription.attr('content') ? metaDescription.attr('content') : '';
result.title = $('title').text();
result.image = urlImage;
result.description = description;
callback(null, result); callback(null, result);
} else { } else {
callback('failed'); callback('failed');

View File

@ -8,9 +8,9 @@ var R = require('ramda'),
mongodb = require('mongodb'), mongodb = require('mongodb'),
MongoClient = mongodb.MongoClient, MongoClient = mongodb.MongoClient,
secrets = require('../config/secrets'), secrets = require('../config/secrets'),
User = require('./../models/User'); sanitizeHtml = require('sanitize-html');
function hotRank(timeValue, rank, headline) { function hotRank(timeValue, rank) {
/* /*
* Hotness ranking algorithm: http://amix.dk/blog/post/19588 * Hotness ranking algorithm: http://amix.dk/blog/post/19588
* tMS = postedOnDate - foundationTime; * tMS = postedOnDate - foundationTime;
@ -24,21 +24,20 @@ function hotRank(timeValue, rank, headline) {
} }
exports.hotJSON = function(req, res, next) { exports.hotJSON = function(req, res) {
var story = Story.find({}).sort({'timePosted': -1}).limit(1000); var story = Story.find({}).sort({'timePosted': -1}).limit(1000);
story.exec(function(err, stories) { story.exec(function(err, stories) {
if (err) { if (err) {
throw err; res.send(500);
return next(err);
} }
var foundationDate = 1413298800000; var foundationDate = 1413298800000;
var sliceVal = stories.length >= 100 ? 100 : stories.length; var sliceVal = stories.length >= 100 ? 100 : stories.length;
var rankedStories = stories; return res.json(stories.map(function(elem) {
return res.json(rankedStories.map(function(elem) {
return elem; return elem;
}).sort(function(a, b) { }).sort(function(a, b) {
debug('a rank and b rank', hotRank(a.timePosted - foundationDate, a.rank, a.headline), hotRank(b.timePosted - foundationDate, b.rank, b.headline));
return hotRank(b.timePosted - foundationDate, b.rank, b.headline) - hotRank(a.timePosted - foundationDate, a.rank, a.headline); return hotRank(b.timePosted - foundationDate, b.rank, b.headline) - hotRank(a.timePosted - foundationDate, a.rank, a.headline);
}).slice(0, sliceVal)); }).slice(0, sliceVal));
@ -49,48 +48,61 @@ exports.recentJSON = function(req, res, next) {
var story = Story.find({}).sort({'timePosted': -1}).limit(100); var story = Story.find({}).sort({'timePosted': -1}).limit(100);
story.exec(function(err, stories) { story.exec(function(err, stories) {
if (err) { if (err) {
throw err; res.status(500);
return next(err);
} }
res.json(stories); res.json(stories);
}); });
}; };
exports.hot = function(req, res, next) { exports.hot = function(req, res) {
res.render('stories/index', { res.render('stories/index', {
page: 'hot' page: 'hot'
}); });
}; };
exports.submitNew = function(req,res, next) { exports.submitNew = function(req, res) {
res.render('stories/index', { res.render('stories/index', {
page: 'submit' page: 'submit'
}); });
}; };
exports.search = function(req, res, next) { exports.search = function(req, res) {
res.render('stories/index', { res.render('stories/index', {
page: 'search' page: 'search'
}); });
}; };
exports.recent = function(req, res, next) { exports.recent = function(req, res) {
res.render('stories/index', { res.render('stories/index', {
page: 'recent' page: 'recent'
}); });
}; };
exports.preSubmit = function(req, res, next) { exports.preSubmit = function(req, res) {
var data = req.params.newStory; var data = req.query;
var cleanData = sanitizeHtml(data.url);
if (data.url.replace(/&/g, '&') !== cleanData) {
debug('data and cleandata', data, cleanData, data.url === cleanData);
req.flash('errors', {
msg: 'The data for this post is malformed'
});
return res.render('stories/index', {
page: 'stories/submit'
});
}
data = data.replace(/url=/gi, '').replace(/&title=/gi, ',').split(','); var title = data.title || '';
var url = data[0]; var image = data.image || '';
var title = data[1]; var description = data.description || '';
res.render('stories/index', { return res.render('stories/index', {
page: 'storySubmission', page: 'storySubmission',
storyURL: url, storyURL: data.url,
storyTitle: title storyTitle: title,
storyImage: image,
storyMetaDescription: description
}); });
}; };
@ -132,15 +144,15 @@ exports.returnIndividualStory = function(req, res, next) {
user: req.user, user: req.user,
timeAgo: moment(story.timePosted).fromNow(), timeAgo: moment(story.timePosted).fromNow(),
image: story.image, image: story.image,
page: 'show' page: 'show',
storyMetaDescription: story.metaDescription
}); });
}); });
}; };
exports.getStories = function(req, res, next) { exports.getStories = function(req, res) {
MongoClient.connect(secrets.db, function(err, database) { MongoClient.connect(secrets.db, function(err, database) {
var db = database; database.collection('stories').find({
db.collection('stories').find({
"$text": { "$text": {
"$search": req.body.data.searchValue "$search": req.body.data.searchValue
} }
@ -155,6 +167,7 @@ exports.getStories = function(req, res, next) {
comments: 1, comments: 1,
image: 1, image: 1,
storyLink: 1, storyLink: 1,
metaDescription: 1,
textScore: { textScore: {
$meta: "textScore" $meta: "textScore"
} }
@ -177,7 +190,8 @@ exports.upvote = function(req, res, next) {
var data = req.body.data; var data = req.body.data;
Story.find({'_id': data.id}, function(err, story) { Story.find({'_id': data.id}, function(err, story) {
if (err) { if (err) {
throw err; res.status(500);
return next(err);
} }
story = story.pop(); story = story.pop();
story.rank++; story.rank++;
@ -197,59 +211,68 @@ exports.comments = function(req, res, next) {
var data = req.params.id; var data = req.params.id;
Comment.find({'_id': data}, function(err, comment) { Comment.find({'_id': data}, function(err, comment) {
if (err) { if (err) {
throw err; res.status(500);
return next(err);
} }
comment = comment.pop(); comment = comment.pop();
return res.send(comment); return res.send(comment);
}); });
}; };
exports.newStory = function(req, res, next) { exports.newStory = function(req, res) {
var url = req.body.data.url; var url = req.body.data.url;
var cleanURL = sanitizeHtml(url);
if (cleanURL !== 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) { if (url.search(/^https?:\/\//g) === -1) {
url = 'http://' + url; url = 'http://' + url;
} }
debug('In pre submit with a url', url);
Story.find({'link': url}, function(err, story) { Story.find({'link': url}, function(err, story) {
debug('Attempting to find a story');
if (err) { if (err) {
debug('oops');
return res.status(500); return res.status(500);
} }
if (story.length) { if (story.length) {
debug('Found a story already, here\'s the return from find', story);
req.flash('errors', { req.flash('errors', {
msg: "Someone's already posted that link. Here's the discussion." msg: "Someone's already posted that link. Here's the discussion."
}); });
debug('Redirecting the user with', story[0].storyLink);
return res.json({ return res.json({
alreadyPosted: true, alreadyPosted: true,
storyURL: story.pop().storyLink storyURL: '/stories/' + story.pop().storyLink
}); });
} }
resources.getURLTitle(url, processResponse); resources.getURLTitle(url, processResponse);
}); });
function processResponse(err, storyTitle) { function processResponse(err, story) {
if (err) { if (err) {
res.json({ res.json({
alreadyPosted: false, alreadyPosted: false,
storyURL: url, storyURL: url,
storyTitle: '' storyTitle: '',
storyImage: '',
storyMetaDescription: ''
}); });
} else { } else {
storyTitle = storyTitle ? storyTitle : '';
res.json({ res.json({
alreadyPosted: false, alreadyPosted: false,
storyURL: url, storyURL: url,
storyTitle: storyTitle.title storyTitle: story.title,
storyImage: story.image,
storyMetaDescription: story.description
}); });
} }
} }
}; };
exports.storySubmission = function(req, res, next) { exports.storySubmission = function(req, res) {
var data = req.body.data; var data = req.body.data;
var storyLink = data.headline var storyLink = data.headline
.replace(/\'/g, '') .replace(/\'/g, '')
@ -263,19 +286,20 @@ exports.storySubmission = function(req, res, next) {
link = 'http://' + link; link = 'http://' + link;
} }
var story = new Story({ var story = new Story({
headline: data.headline, headline: sanitizeHtml(data.headline),
timePosted: Date.now(), timePosted: Date.now(),
link: link, link: link,
description: data.description, description: sanitizeHtml(data.description),
rank: 1, rank: 1,
upVotes: data.upVotes, upVotes: data.upVotes,
author: data.author, author: data.author,
comments: [], comments: [],
image: data.image, image: data.image,
storyLink: storyLink storyLink: storyLink,
metaDescription: data.storyMetaDescription
}); });
story.save(function(err, data) { story.save(function(err) {
if (err) { if (err) {
return res.status(500); return res.status(500);
} }
@ -285,12 +309,22 @@ exports.storySubmission = function(req, res, next) {
}); });
}; };
exports.commentSubmit = function(req, res, next) { exports.commentSubmit = function(req, res) {
debug('comment submit fired');
var data = req.body.data; var data = req.body.data;
var sanitizedBody = sanitizeHtml(data.body,
{
allowedTags: [],
allowedAttributes: []
});
if (data.body !== sanitizedBody) {
req.flash('errors', {
msg: 'HTML is not allowed'
});
return res.send(true);
}
var comment = new Comment({ var comment = new Comment({
associatedPost: data.associatedPost, associatedPost: data.associatedPost,
body: data.body, body: sanitizedBody,
rank: 0, rank: 0,
upvotes: 0, upvotes: 0,
author: data.author, author: data.author,
@ -301,13 +335,22 @@ exports.commentSubmit = function(req, res, next) {
commentSave(comment, Story, res); commentSave(comment, Story, res);
}; };
exports.commentOnCommentSubmit = function(req, res, next) { exports.commentOnCommentSubmit = function(req, res) {
debug('comment on comment submit');
var idToFind = req.params.id;
var data = req.body.data; var data = req.body.data;
var sanitizedBody = sanitizeHtml(data.body,
{
allowedTags: [],
allowedAttributes: []
});
if (data.body !== sanitizedBody) {
req.flash('errors', {
msg: 'HTML is not allowed'
});
return res.send(true);
}
var comment = new Comment({ var comment = new Comment({
associatedPost: data.associatedPost, associatedPost: data.associatedPost,
body: data.body, body: sanitizedBody,
rank: 0, rank: 0,
upvotes: 0, upvotes: 0,
author: data.author, author: data.author,
@ -331,7 +374,7 @@ function commentSave(comment, Context, res) {
associatedStory = associatedStory.pop(); associatedStory = associatedStory.pop();
if (associatedStory) { if (associatedStory) {
associatedStory.comments.push(data._id); associatedStory.comments.push(data._id);
associatedStory.save(function (err, data) { associatedStory.save(function (err) {
if (err) { if (err) {
res.status(500); res.status(500);
} }

View File

@ -7,10 +7,10 @@ var _ = require('lodash'),
secrets = require('../config/secrets'), secrets = require('../config/secrets'),
moment = require('moment'), moment = require('moment'),
Challenge = require('./../models/Challenge'), Challenge = require('./../models/Challenge'),
debug = require('debug')('freecc:cntr:challenges') debug = require('debug')('freecc:cntr:challenges'),
resources = require('./resources'); resources = require('./resources');
//TODO(Berks): Refactor to use module.exports = {} pattern.
/** /**
* GET /signin * GET /signin
@ -99,9 +99,25 @@ exports.postEmailSignup = function(req, res, next) {
if (errors) { if (errors) {
req.flash('errors', errors); req.flash('errors', errors);
return res.redirect('/email-signup'); return res.redirect('/email-signup');
debug(errors);
} }
var possibleUserData = req.body;
if (possibleUserData.password.length < 8) {
req.flash('errors', {
msg: 'Your password is too short'
});
return res.redirect('email-signup');
}
if (possibleUserData.username.length < 5 || possibleUserData.length > 20) {
req.flash('errors', {
msg: 'Your username must be between 5 and 20 characters'
});
return res.redirect('email-signup');
}
var user = new User({ var user = new User({
email: req.body.email.trim(), email: req.body.email.trim(),
password: req.body.password, password: req.body.password,
@ -111,18 +127,30 @@ exports.postEmailSignup = function(req, res, next) {
} }
}); });
User.findOne({ email: req.body.email }, function(err, existingUser) { User.findOne({ email: req.body.email }, function(err, existingEmail) {
if (err) { return next(err); } if (err) {
return next(err);
}
if (existingUser) { if (existingEmail) {
req.flash('errors', { req.flash('errors', {
msg: 'Account with that email address already exists.' msg: 'Account with that email address already exists.'
}); });
return res.redirect('/email-signup'); return res.redirect('/email-signup');
} }
User.findOne({'profile.username': req.body.username }, function(err, existingUsername) {
if (err) {
return next(err);
}
if (existingUsername) {
req.flash('errors', {
msg: 'Account with that username already exists.'
});
return res.redirect('/email-signup');
}
user.save(function(err) { user.save(function(err) {
if (err) { return next(err); } if (err) { return next(err); }
req.logIn(user, function(err) { req.logIn(user, function(err) {
if (err) { return next(err); } if (err) { return next(err); }
res.redirect('/email-signup'); res.redirect('/email-signup');
@ -152,6 +180,7 @@ exports.postEmailSignup = function(req, res, next) {
if (err) { return err; } if (err) { return err; }
}); });
}); });
});
}; };
/** /**
@ -169,7 +198,7 @@ exports.getAccount = function(req, res) {
* Angular API Call * Angular API Call
*/ */
exports.getAccountAngular = function(req, res) { exports.getAccountAngular = function(req, res) {
res.json({ res.json({
user: req.user user: req.user
}); });
@ -292,6 +321,8 @@ exports.updateProgress = function(req, res) {
*/ */
exports.postUpdateProfile = function(req, res, next) { exports.postUpdateProfile = function(req, res, next) {
// What does this do?
User.findById(req.user.id, function(err, user) { User.findById(req.user.id, function(err, user) {
if (err) return next(err); if (err) return next(err);
var errors = req.validationErrors(); var errors = req.validationErrors();
@ -322,7 +353,6 @@ exports.postUpdateProfile = function(req, res, next) {
}); });
return res.redirect('/account'); return res.redirect('/account');
} }
var user = req.user;
user.email = req.body.email.trim() || ''; user.email = req.body.email.trim() || '';
user.profile.name = req.body.name.trim() || ''; user.profile.name = req.body.name.trim() || '';
user.profile.username = req.body.username.trim() || ''; user.profile.username = req.body.username.trim() || '';

View File

@ -14,6 +14,11 @@ var storySchema = new mongoose.Schema({
type: String, type: String,
unique: false unique: false
}, },
metaDescription: {
type: String,
default: '',
unique: false
},
description: { description: {
type: String, type: String,
unique: false unique: false

View File

@ -61,6 +61,7 @@
"passport-twitter": "^1.0.2", "passport-twitter": "^1.0.2",
"ramda": "^0.10.0", "ramda": "^0.10.0",
"request": "^2.53.0", "request": "^2.53.0",
"sanitize-html": "^1.6.1",
"sitemap": "^0.7.4", "sitemap": "^0.7.4",
"uglify-js": "^2.4.15", "uglify-js": "^2.4.15",
"validator": "^3.22.1", "validator": "^3.22.1",

View File

@ -821,6 +821,11 @@ iframe.iphone {
height: 50px; height: 50px;
} }
.url-preview {
max-width: 250px;
max-height: 250px;
}
//.media ~ .media .media-body-wrapper:nth-child(odd) { //.media ~ .media .media-body-wrapper:nth-child(odd) {
// background-color: #e5e5e5; // background-color: #e5e5e5;
//} //}

View File

@ -164,7 +164,7 @@ $(document).ready(function() {
headline: headline, headline: headline,
timePosted: Date.now(), timePosted: Date.now(),
description: description, description: description,
storyMetaDescription: storyMetaDescription,
rank: 1, rank: 1,
upVotes: [userDataForUpvote], upVotes: [userDataForUpvote],
author: { author: {
@ -173,7 +173,7 @@ $(document).ready(function() {
username: user.profile.username username: user.profile.username
}, },
comments: [], comments: [],
image: '' image: storyImage
} }
}) })
.fail(function (xhr, textStatus, errorThrown) { .fail(function (xhr, textStatus, errorThrown) {

View File

@ -40,7 +40,7 @@ block content
| Your usernames must be 20 characters or fewer. | Your usernames must be 20 characters or fewer.
.form-group .form-group
.col-sm-6.col-sm-offset-3 .col-sm-6.col-sm-offset-3
input.form-control(type='password', ng-model='password', name='password', id='password', placeholder='password', required, ng-minlength=5) input.form-control(type='password', ng-model='password', name='password', id='password', placeholder='password', required, ng-minlength=8)
.col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.password.$error.minlength && !signupForm.password.$pristine") .col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.password.$error.minlength && !signupForm.password.$pristine")
alert(type='danger') alert(type='danger')
span.ion-close-circled span.ion-close-circled

View File

@ -34,11 +34,13 @@
}) })
.done(function (data, textStatus, xhr) { .done(function (data, textStatus, xhr) {
if (data.alreadyPosted) { if (data.alreadyPosted) {
window.location = '/stories/' + data.storyURL; window.location = data.storyURL;
} else { } else {
window.location = '/stories/submit/url=' + window.location = '/stories/submit/new-story?url=' +
encodeURIComponent(data.storyURL) + encodeURIComponent(data.storyURL) +
'&title=' + encodeURIComponent(data.storyTitle); '&title=' + encodeURIComponent(data.storyTitle) +
'&image=' + encodeURIComponent(data.storyImage) +
'&description=' + encodeURIComponent(data.storyMetaDescription);
} }
}); });
} }

View File

@ -5,6 +5,7 @@
var comments = !{JSON.stringify(comments)}; var comments = !{JSON.stringify(comments)};
var upVotes = !{JSON.stringify(upVotes)}; var upVotes = !{JSON.stringify(upVotes)};
var user = !{JSON.stringify(user)}; var user = !{JSON.stringify(user)};
var image = !{JSON.stringify(image)};
.spacer .spacer
h3.row.col-xs-12 h3.row.col-xs-12
@ -25,7 +26,14 @@
a(href="#{link}") a(href="#{link}")
h3= title h3= title
h6 h6
.col-xs-12.negative-28 .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
h4= description h4= description
.negative-5 .negative-5
span Posted #{timeAgo} span Posted #{timeAgo}
@ -44,6 +52,9 @@
span.spacer.pull-left#textarea_feedback span.spacer.pull-left#textarea_feedback
script. script.
if (image) {
$('#image-display').removeClass('hidden-element')
}
$('#reply-to-main-post').on('click', function() { $('#reply-to-main-post').on('click', function() {
$('#initial-comment-submit').removeClass('hidden-element'); $('#initial-comment-submit').removeClass('hidden-element');
$(this).unbind('click'); $(this).unbind('click');

View File

@ -3,6 +3,8 @@
script. script.
var storyURL = !{JSON.stringify(storyURL)}; var storyURL = !{JSON.stringify(storyURL)};
var storyTitle = !{JSON.stringify(storyTitle)}; var storyTitle = !{JSON.stringify(storyTitle)};
var storyImage = !{JSON.stringify(storyImage)};
var storyMetaDescription = !{JSON.stringify(storyMetaDescription)};
form.form-horizontal.control-label-story-submission#story-submission-form form.form-horizontal.control-label-story-submission#story-submission-form
.col-xs-12 .col-xs-12
.form-group .form-group
@ -20,13 +22,29 @@
label.control-label.control-label-story-submission(for='name') Description label.control-label.control-label-story-submission(for='name') Description
.col-xs-12.col-md-11 .col-xs-12.col-md-11
input#description-box.form-control(name="comment-box", placeholder="Start off the discussion with a description of your post" maxlength='140') input#description-box.form-control(name="comment-box", placeholder="Start off the discussion with a description of your post" maxlength='140')
span.pull-left#textarea_feedback
.spacer
.form-group .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-big.btn-block.btn-primary#story-submit Submit button.btn.btn-big.btn-block.btn-primary#story-submit Submit
script. script.
$('#story-url').val(storyURL).attr('disabled', 'disabled'); $('#story-url').val(storyURL).attr('disabled', 'disabled');
$('#story-title').val(storyTitle); $('#story-title').val(storyTitle);
if (storyImage) {
$('#image-display').removeClass('hidden-element');
}
var text_max = 140; var text_max = 140;
$('#textarea_feedback').html(text_max + ' characters remaining'); $('#textarea_feedback').html(text_max + ' characters remaining');
$('#description-box').keyup(function () { $('#description-box').keyup(function () {