diff --git a/README.md b/README.md index 68b31e96fb..d621fd4b5e 100644 --- a/README.md +++ b/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 ------- diff --git a/app.js b/app.js old mode 100644 new mode 100755 index f03046711b..a40b25f074 --- a/app.js +++ b/app.js @@ -470,6 +470,11 @@ app.post( storyController.commentOnCommentSubmit ); +app.put( + '/stories/comment/:id/edit', + storyController.commentEdit +); + app.get( '/stories/submit', storyController.submitNew diff --git a/controllers/bonfire.js b/controllers/bonfire.js index ee8b26a5ce..dbe60a0da0 100644 --- a/controllers/bonfire.js +++ b/controllers/bonfire.js @@ -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, diff --git a/controllers/story.js b/controllers/story.js old mode 100644 new mode 100755 index c434e4752a..9047279e3f --- a/controllers/story.js +++ b/controllers/story.js @@ -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); + } + }); + } diff --git a/public/js/main.js b/public/js/main.js index 8a7be2aa82..59905c18e3 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -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); + }); } }); } diff --git a/seed_data/bonfires.json b/seed_data/bonfires.json index 0f7373798a..31df4b8c1d 100644 --- a/seed_data/bonfires.json +++ b/seed_data/bonfires.json @@ -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');" diff --git a/seed_data/coursewares.json b/seed_data/coursewares.json index 7571780697..c34bc8654f 100644 --- a/seed_data/coursewares.json +++ b/seed_data/coursewares.json @@ -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:

Hello CodePen!

. The text's color should be Bootstrap blue.", + "Verify that bootstrap is active by adding the following code to your HTML: <h1 class='text-primary'>Hello CodePen!</h1>. 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: $(document).ready(function() { $('.text-primary').text('Hi CodePen!') });. 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." diff --git a/views/stories/comments.jade b/views/stories/comments.jade old mode 100644 new mode 100755 index 3d8b424271..07412b59be --- a/views/stories/comments.jade +++ b/views/stories/comments.jade @@ -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 = "Edit · "; + } + } $(div) .html( '
' + @@ -36,6 +42,7 @@ '
' + '
' + "Reply · " + + editButton + "commented " + moment(commentDetails.commentOn).fromNow() + " by " + "@" + commentDetails.author.username + "" + '
' + @@ -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 @@ "
" + "" + "" + - "" + + "" + "" + "
" + "
" + @@ -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); - });// + }); } } }) diff --git a/views/stories/index.jade b/views/stories/index.jade index 19cb055ec5..39d3efcea2 100644 --- a/views/stories/index.jade +++ b/views/stories/index.jade @@ -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;