Merge branch 'master' of https://github.com/FreeCodeCamp/freecodecamp
This commit is contained in:
11
README.md
11
README.md
@ -149,17 +149,6 @@ List of Packages
|
||||
| supertest | HTTP assertion library. |
|
||||
| multiline | Multi-line strings for the generator. |
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
### 0.1.0 (December 24, 2014)
|
||||
- Improved how unique emails and usernames are handled (with Express-validator)
|
||||
- Added a tweet button to challenge completion model
|
||||
- Refactored all views to get rid of any hard-coded challenge information (to make for a better forking experience)
|
||||
- Installed Helmet to maximize security of application
|
||||
- Added .env and removed all trace of API keys from git history
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
|
5
app.js
Normal file → Executable file
5
app.js
Normal file → Executable file
@ -470,6 +470,11 @@ app.post(
|
||||
storyController.commentOnCommentSubmit
|
||||
);
|
||||
|
||||
app.put(
|
||||
'/stories/comment/:id/edit',
|
||||
storyController.commentEdit
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/stories/submit',
|
||||
storyController.submitNew
|
||||
|
@ -109,8 +109,8 @@ exports.returnIndividualBonfire = function(req, res, next) {
|
||||
dashedName: dashedName,
|
||||
name: bonfire.name,
|
||||
difficulty: Math.floor(+bonfire.difficulty),
|
||||
brief: bonfire.description[0],
|
||||
details: bonfire.description.slice(1),
|
||||
brief: bonfire.description.shift(),
|
||||
details: bonfire.description,
|
||||
tests: bonfire.tests,
|
||||
challengeSeed: bonfire.challengeSeed,
|
||||
points: req.user ? req.user.points : undefined,
|
||||
|
337
controllers/story.js
Normal file → Executable file
337
controllers/story.js
Normal file → Executable file
@ -57,7 +57,7 @@ exports.recentJSON = function(req, res, next) {
|
||||
};
|
||||
|
||||
exports.hot = function(req, res) {
|
||||
return res.render('stories/index', {
|
||||
return res.render('stories/index', {
|
||||
title: 'Hot stories currently trending on Camper News',
|
||||
page: 'hot'
|
||||
});
|
||||
@ -120,7 +120,7 @@ exports.returnIndividualStory = function(req, res, next) {
|
||||
|
||||
var storyName = dashedName.replace(/\-/g, ' ');
|
||||
|
||||
Story.find({'storyLink': new RegExp(storyName, 'i')}, function(err, story) {
|
||||
Story.find({'storyLink': storyName}, function(err, story) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
@ -328,149 +328,198 @@ exports.storySubmission = function(req, res, next) {
|
||||
if (link.search(/^https?:\/\//g) === -1) {
|
||||
link = 'http://' + link;
|
||||
}
|
||||
var story = new Story({
|
||||
headline: sanitizeHtml(data.headline, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"'),
|
||||
timePosted: Date.now(),
|
||||
link: link,
|
||||
description: sanitizeHtml(data.description, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"'),
|
||||
rank: 1,
|
||||
upVotes: [({
|
||||
upVotedBy: req.user._id,
|
||||
upVotedByUsername: req.user.profile.username
|
||||
})],
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
image: data.image,
|
||||
storyLink: storyLink,
|
||||
metaDescription: data.storyMetaDescription,
|
||||
originalStoryAuthorEmail: req.user.email
|
||||
});
|
||||
|
||||
story.save(function(err) {
|
||||
if (err) {
|
||||
return res.status(500);
|
||||
}
|
||||
res.send(JSON.stringify({
|
||||
storyLink: story.storyLink.replace(/\s/g, '-').toLowerCase()
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
exports.commentSubmit = function(req, res, next) {
|
||||
var data = req.body.data;
|
||||
if (!req.user) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
var sanitizedBody = sanitizeHtml(data.body,
|
||||
{
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (data.body !== sanitizedBody) {
|
||||
req.flash('errors', {
|
||||
msg: 'HTML is not allowed'
|
||||
});
|
||||
return res.send(true);
|
||||
}
|
||||
var comment = new Comment({
|
||||
associatedPost: data.associatedPost,
|
||||
originalStoryLink: data.originalStoryLink,
|
||||
originalStoryAuthorEmail: req.user.email,
|
||||
body: sanitizedBody,
|
||||
rank: 0,
|
||||
upvotes: 0,
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
topLevel: true,
|
||||
commentOn: Date.now()
|
||||
});
|
||||
|
||||
commentSave(comment, Story, res, next);
|
||||
};
|
||||
|
||||
exports.commentOnCommentSubmit = function(req, res, next) {
|
||||
var data = req.body.data;
|
||||
if (!req.user) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
|
||||
var sanitizedBody = sanitizeHtml(data.body,
|
||||
{
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (data.body !== sanitizedBody) {
|
||||
req.flash('errors', {
|
||||
msg: 'HTML is not allowed'
|
||||
});
|
||||
return res.send(true);
|
||||
}
|
||||
var comment = new Comment({
|
||||
associatedPost: data.associatedPost,
|
||||
body: sanitizedBody,
|
||||
rank: 0,
|
||||
upvotes: 0,
|
||||
originalStoryLink: data.originalStoryLink,
|
||||
originalStoryAuthorEmail: data.originalStoryAuthorEmail,
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
topLevel: false,
|
||||
commentOn: Date.now()
|
||||
});
|
||||
commentSave(comment, Comment, res, next);
|
||||
};
|
||||
|
||||
function commentSave(comment, Context, res, next) {
|
||||
comment.save(function(err, data) {
|
||||
Story.count({'storyLink': new RegExp('^' + storyLink + '(?: [0-9]+)?$', 'i')}, function (err, storyCount) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
return res.status(500);
|
||||
}
|
||||
try {
|
||||
Context.find({'_id': comment.associatedPost}, function (err, associatedStory) {
|
||||
|
||||
// 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 story = new Story({
|
||||
headline: sanitizeHtml(data.headline, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"'),
|
||||
timePosted: Date.now(),
|
||||
link: link,
|
||||
description: sanitizeHtml(data.description, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"'),
|
||||
rank: 1,
|
||||
upVotes: [({
|
||||
upVotedBy: req.user._id,
|
||||
upVotedByUsername: req.user.profile.username
|
||||
})],
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
image: data.image,
|
||||
storyLink: storyLink,
|
||||
metaDescription: data.storyMetaDescription,
|
||||
originalStoryAuthorEmail: req.user.email
|
||||
});
|
||||
story.save(function (err) {
|
||||
if (err) {
|
||||
return res.status(500);
|
||||
}
|
||||
res.send(JSON.stringify({
|
||||
storyLink: story.storyLink.replace(/\s/g, '-').toLowerCase()
|
||||
}));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.commentSubmit = function(req, res, next) {
|
||||
var data = req.body.data;
|
||||
if (!req.user) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
var sanitizedBody = sanitizeHtml(data.body,
|
||||
{
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (data.body !== sanitizedBody) {
|
||||
req.flash('errors', {
|
||||
msg: 'HTML is not allowed'
|
||||
});
|
||||
return res.send(true);
|
||||
}
|
||||
var comment = new Comment({
|
||||
associatedPost: data.associatedPost,
|
||||
originalStoryLink: data.originalStoryLink,
|
||||
originalStoryAuthorEmail: req.user.email,
|
||||
body: sanitizedBody,
|
||||
rank: 0,
|
||||
upvotes: 0,
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
topLevel: true,
|
||||
commentOn: Date.now()
|
||||
});
|
||||
|
||||
commentSave(comment, Story, res, next);
|
||||
};
|
||||
|
||||
exports.commentOnCommentSubmit = function(req, res, next) {
|
||||
var data = req.body.data;
|
||||
if (!req.user) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
|
||||
var sanitizedBody = sanitizeHtml(data.body,
|
||||
{
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (data.body !== sanitizedBody) {
|
||||
req.flash('errors', {
|
||||
msg: 'HTML is not allowed'
|
||||
});
|
||||
return res.send(true);
|
||||
}
|
||||
var comment = new Comment({
|
||||
associatedPost: data.associatedPost,
|
||||
body: sanitizedBody,
|
||||
rank: 0,
|
||||
upvotes: 0,
|
||||
originalStoryLink: data.originalStoryLink,
|
||||
originalStoryAuthorEmail: data.originalStoryAuthorEmail,
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
topLevel: false,
|
||||
commentOn: Date.now()
|
||||
});
|
||||
commentSave(comment, Comment, res, next);
|
||||
};
|
||||
|
||||
exports.commentEdit = function(req, res, next) {
|
||||
|
||||
Comment.find({'_id': req.params.id}, function(err, cmt) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
cmt = cmt.pop();
|
||||
|
||||
if (!req.user && cmt.author.userId !== req.user._id) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
|
||||
|
||||
var sanitizedBody = sanitizeHtml(req.body.body, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (req.body.body !== sanitizedBody) {
|
||||
req.flash('errors', {
|
||||
msg: 'HTML is not allowed'
|
||||
});
|
||||
return res.send(true);
|
||||
}
|
||||
|
||||
cmt.body = sanitizedBody;
|
||||
cmt.commentOn = Date.now();
|
||||
cmt.save(function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
associatedStory = associatedStory.pop();
|
||||
if (associatedStory) {
|
||||
associatedStory.comments.push(data._id);
|
||||
associatedStory.save(function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.send(true);
|
||||
});
|
||||
}
|
||||
User.findOne({'profile.username': associatedStory.author.username}, function(err, recipient) {
|
||||
res.send(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
function commentSave(comment, Context, res, next) {
|
||||
comment.save(function(err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
try {
|
||||
Context.find({'_id': comment.associatedPost}, function (err, associatedStory) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
var recipients = '';
|
||||
if (data.originalStoryAuthorEmail && (data.originalStoryAuthorEmail !== recipient.email)) {
|
||||
recipients = data.originalStoryAuthorEmail + ',' + recipient.email;
|
||||
} else {
|
||||
recipients = recipient.email;
|
||||
}
|
||||
associatedStory = associatedStory.pop();
|
||||
if (associatedStory) {
|
||||
associatedStory.comments.push(data._id);
|
||||
associatedStory.save(function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.send(true);
|
||||
});
|
||||
}
|
||||
User.findOne({'profile.username': associatedStory.author.username}, function(err, recipient) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
var recipients = '';
|
||||
if (data.originalStoryAuthorEmail && (data.originalStoryAuthorEmail !== recipient.email)) {
|
||||
recipients = data.originalStoryAuthorEmail + ',' + recipient.email;
|
||||
} else {
|
||||
recipients = recipient.email;
|
||||
}
|
||||
var transporter = nodemailer.createTransport({
|
||||
service: 'Mandrill',
|
||||
auth: {
|
||||
@ -494,11 +543,11 @@ function commentSave(comment, Context, res, next) {
|
||||
return err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
// delete comment
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// delete comment
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -298,7 +298,6 @@ $(document).ready(function() {
|
||||
{
|
||||
data: {
|
||||
associatedPost: storyId,
|
||||
originalStoryLink: originalStoryLink,
|
||||
body: data
|
||||
}
|
||||
})
|
||||
@ -314,7 +313,7 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
var profileValidation = angular.module('profileValidation',
|
||||
['ui.bootstrap', 'ngLodash']);
|
||||
['ui.bootstrap']);
|
||||
profileValidation.controller('profileValidationController', ['$scope', '$http',
|
||||
function($scope, $http) {
|
||||
$http.get('/account/api').success(function(data) {
|
||||
@ -393,12 +392,12 @@ profileValidation.directive('uniqueUsername', ['$http', function($http) {
|
||||
}]);
|
||||
|
||||
profileValidation.directive('existingUsername',
|
||||
['$http', 'lodash', function($http, lodash) {
|
||||
['$http', function($http) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function (scope, element, attrs, ngModel) {
|
||||
element.bind("keyup", function (event) {
|
||||
element.bind('keyup', function (event) {
|
||||
if (element.val().length > 0) {
|
||||
ngModel.$setValidity('exists', false);
|
||||
} else {
|
||||
@ -406,14 +405,11 @@ profileValidation.directive('existingUsername',
|
||||
ngModel.$setPristine();
|
||||
}
|
||||
if (element.val()) {
|
||||
var debo = lodash.debounce(function() {
|
||||
$http
|
||||
.get('/api/checkExistingUsername/' + element.val())
|
||||
.success(function (data) {
|
||||
ngModel.$setValidity('exists', data);
|
||||
});
|
||||
}, 2000);
|
||||
debo();
|
||||
$http
|
||||
.get('/api/checkExistingUsername/' + element.val())
|
||||
.success(function (data) {
|
||||
ngModel.$setValidity('exists', data);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -728,8 +728,9 @@
|
||||
"Create a function that takes two or more arrays and returns an array of the symmetric difference of the provided arrays.",
|
||||
"The mathematical term symmetric difference refers to the elements in two sets that are in either the first or second set, but not in both."
|
||||
],
|
||||
"challengeSeed": "function sym(args) {\n return arr;\r\n}\n\nsym([1, 2, 3], [5, 2, 1, 4]);",
|
||||
"challengeSeed": "function sym(args) {\n return arguments;\r\n}\n\nsym([1, 2, 3], [5, 2, 1, 4]);",
|
||||
"tests": [
|
||||
"expect(sym([1, 2, 3], [5, 2, 1, 4])).to.eqls([3, 5, 4])",
|
||||
"assert.deepEqual(sym([1, 2, 5], [2, 3, 5], [3, 4, 5]), [1, 4, 5], 'should return the symmetric difference of the given arrays');",
|
||||
"assert.deepEqual(sym([1, 1, 2, 5], [2, 2, 3, 5], [3, 4, 5, 5]), [1, 4, 5], 'should return an array of unique values');",
|
||||
"assert.deepEqual(sym([1, 1]), [1], 'should return an array of unique values');"
|
||||
|
@ -641,7 +641,7 @@
|
||||
"Click your user image in the top right corner, then click the \"New pen\" button that drops down.",
|
||||
"Drag the windows around and press the buttons in the lower-right hand corner to change the orientation to suit your preference.",
|
||||
"Click the gear next to CSS. Then in the \"External CSS File or Another Pen\" text field, type \"bootstrap\" and scroll down until you see the latest version of Bootstrap. Click it.",
|
||||
"Verify that bootstrap is active by adding the following code to your HTML: <code><h1 class='text-primary'>Hello CodePen!</h1></code>. The text's color should be Bootstrap blue.",
|
||||
"Verify that bootstrap is active by adding the following code to your HTML: <code><h1 class='text-primary'>Hello CodePen!</h1></code>. The text's color should be Bootstrap blue.",
|
||||
"Click the gear next the JavaScript. Click the \"Latest version of...\" select box and choose jQuery.",
|
||||
"Now add the following code to your JavaScript: <code>$(document).ready(function() { $('.text-primary').text('Hi CodePen!') });</code>. Click the \"Save\" button at the top. Your \"Hello CodePen!\" should change to \"Hi CodePen!\". This means that jQuery is working.",
|
||||
"Now you're ready for your first Zipline. Click the \"I've completed this challenge\" button and include a link to your CodePen. If you pair programmed, you should also include the Free Code Camp username of your pair."
|
||||
|
44
views/stories/comments.jade
Normal file → Executable file
44
views/stories/comments.jade
Normal file → Executable file
@ -21,7 +21,13 @@
|
||||
success: function (data, textStatus, xhr) {
|
||||
commentDetails = data;
|
||||
var div = document.createElement('div');
|
||||
|
||||
var editButton = "";
|
||||
// todo
|
||||
if (commentDetails.author.username === DF105CFA89562196E702912B3818C6A5B46E80D262442FDF29976621E5AF0D23) {
|
||||
if ((Date.now() - commentDetails.commentOn) < 600000){
|
||||
editButton = "<a class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost edit-btn' id='" + commentDetails._id + "'>Edit</a> · ";
|
||||
}
|
||||
}
|
||||
$(div)
|
||||
.html(
|
||||
'<div class="media media-news">' +
|
||||
@ -36,6 +42,7 @@
|
||||
'<h6>' +
|
||||
'<div class="clearfix comment-a-comment negative-15">' +
|
||||
"<a class='btn btn-no-shadow btn-primary btn-xs btn-primary-ghost' id='" + commentDetails._id + "'>Reply</a> · " +
|
||||
editButton +
|
||||
"commented " + moment(commentDetails.commentOn).fromNow() + " by " +
|
||||
"<a href='/" + commentDetails.author.username + "'>@" + commentDetails.author.username + "</a>" +
|
||||
'</div>' +
|
||||
@ -55,10 +62,13 @@
|
||||
complete: function () {
|
||||
sentinel--;
|
||||
if (!sentinel) {
|
||||
$('.comment-a-comment').on('click', 'a', function () {
|
||||
$('.comment-a-comment').on('click', 'a', function() {
|
||||
var editOrComment = 'comment';
|
||||
if ($(this).hasClass("edit-btn")){
|
||||
editOrComment = 'edit';
|
||||
}
|
||||
if (!isLoggedIn) {
|
||||
console.log('not logged in');
|
||||
//window.location.href = '/signin';
|
||||
window.location.href = '/signin';
|
||||
return;
|
||||
}
|
||||
$(this).unbind('click');
|
||||
@ -72,7 +82,7 @@
|
||||
"<div class='input-group'>" +
|
||||
"<input class='form-control big-text-field field-responsive' id='comment-to-comment-textinput' type='text' maxlength='140' autofocus />" +
|
||||
"<span class='input-group-btn'>" +
|
||||
"<button id='submit-comment-to-comment' class='btn btn-big btn-primary btn-responsive'>Send</button>" +
|
||||
"<button id='submit-comment-to-" + editOrComment + "' class='btn btn-big btn-primary btn-responsive'>Send</button>" +
|
||||
"</span>" +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
@ -109,9 +119,31 @@
|
||||
});
|
||||
|
||||
};
|
||||
// todo
|
||||
var submitCommentForEditToCommentHandler = function submitCommentForEditToCommentHandler() {
|
||||
$('#submit-comment-to-edit').unbind('click');
|
||||
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: '/stories/comment/' + commentId + '/edit',
|
||||
data: {
|
||||
associatedPost: commentId,
|
||||
originalStoryLink: originalStoryLink,
|
||||
body: $('#comment-to-comment-textinput').val()
|
||||
},
|
||||
dataType: "json",
|
||||
success: function (msg) {
|
||||
window.location.reload();
|
||||
},
|
||||
error: function (err){
|
||||
$('#submit-comment-to-edit').bind('click', submitCommentForEditToCommentHandler);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$('#submit-comment-to-edit').on('click', submitCommentForEditToCommentHandler)
|
||||
$('#submit-comment-to-comment').on('click', submitCommentToCommentHandler);
|
||||
});//
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -4,7 +4,8 @@ block content
|
||||
if (user)
|
||||
script.
|
||||
var isLoggedIn = true;
|
||||
var B3BA669EC5C1DD70FB478221E067A7E1B686929C569F5E73561B69C8F42129B = !{JSON.stringify(user._id)}
|
||||
var B3BA669EC5C1DD70FB478221E067A7E1B686929C569F5E73561B69C8F42129B = !{JSON.stringify(user._id)};
|
||||
var DF105CFA89562196E702912B3818C6A5B46E80D262442FDF29976621E5AF0D23 = !{JSON.stringify(user.profile.username)};
|
||||
else
|
||||
script.
|
||||
var isLoggedIn = false;
|
||||
|
Reference in New Issue
Block a user