Merge pull request #256 from terakilobyte/ux-improvements

User will now get a point for news/stories interactions
This commit is contained in:
Quincy Larson
2015-03-28 06:12:18 -07:00
8 changed files with 1131 additions and 1025 deletions

3
app.js
View File

@ -398,6 +398,8 @@ app.post(
app.all('/account', passportConf.isAuthenticated);
app.get('/account/api', userController.getAccountAngular);
app.get('/user/streak', userController.getStreak);
/**
* API routes
*/
@ -449,7 +451,6 @@ app.post('/account/password', userController.postUpdatePassword);
app.post('/account/delete', userController.postDeleteAccount);
app.get('/account/unlink/:provider', userController.getOauthUnlink);
app.get('/sitemap.xml', resourcesController.sitemap);
/**
* OAuth sign-in routes.
*/

View File

@ -236,6 +236,7 @@ exports.completedBonfire = function (req, res) {
} else {
var index = req.user.uncompletedBonfires.indexOf(bonfireHash);
if (index > -1) {
req.user.progressTimestamps.push(Date.now() || 0);
req.user.uncompletedBonfires.splice(index, 1)
}
@ -243,6 +244,7 @@ exports.completedBonfire = function (req, res) {
index = pairedWith.uncompletedBonfires.indexOf(bonfireHash);
if (index > -1) {
pairedWith.progressTimestamps.push(Date.now() || 0);
pairedWith.uncompletedBonfires.splice(index, 1);
@ -285,6 +287,7 @@ exports.completedBonfire = function (req, res) {
var index = req.user.uncompletedBonfires.indexOf(bonfireHash);
if (index > -1) {
req.user.progressTimestamps.push(Date.now() || 0);
req.user.uncompletedBonfires.splice(index, 1)
}

View File

@ -3,7 +3,8 @@ var _ = require('lodash'),
Courseware = require('./../models/Courseware'),
User = require('./../models/User'),
resources = require('./resources'),
R = require('ramda');
R = require('ramda'),
moment = require('moment');
/**
* Courseware controller
@ -253,20 +254,11 @@ exports.completedCourseware = function (req, res, next) {
var index = req.user.completedCoursewares.indexOf(coursewareHash);
if (index === -1) {
req.user.progressTimestamps.push(Date.now() || 0);
req.user.uncompletedCoursewares.splice(index, 1);
}
req.user.save(function (err, user) {
if (err) {
return next(err);
}
if (user) {
res.send(true);
}
});
};
exports.completedBasejump = function (req, res, next) {
var isCompletedWith = req.body.bonfireInfo.completedWith || undefined;
var isCompletedDate = Math.round(+new Date());
@ -275,75 +267,93 @@ exports.completedBasejump = function (req, res, next) {
if(!solutionLink) {
// flash error and redirect
}
if (user) {
res.send(true);
}
};
};
exports.completedZiplineOrBasejump = function (req, res, next) {
var isCompletedWith = req.body.bonfireInfo.completedWith || false;
var isCompletedDate = Math.round(+new Date());
var coursewareHash = req.body.coursewareInfo.coursewareHash;
var solutionLink = req.body.coursewareInfo.solutionLink;
if (!solutionLink) {
// flash error and redirect
return next(new Error('No solution provided'));
}
if (isCompletedWith) {
var paired = User.find({"profile.username": isCompletedWith.toLowerCase()}).limit(1);
var paired = User.find({'profile.username': isCompletedWith.toLowerCase()}).limit(1);
paired.exec(function (err, pairedWith) {
if (err) {
return err;
return next(err);
} else {
var index = req.user.uncompletedBonfires.indexOf(bonfireHash);
var index = req.user.uncompletedCoursewares.indexOf(coursewareHash);
if (index > -1) {
req.user.progressTimestamps.push(Date.now() || 0);
req.user.uncompletedBonfires.splice(index, 1)
req.user.uncompletedCoursewares.splice(index, 1);
}
pairedWith = pairedWith.pop();
index = pairedWith.uncompletedBonfires.indexOf(bonfireHash);
index = pairedWith.uncompletedCoursewares.indexOf(coursewareHash);
if (index > -1) {
pairedWith.progressTimestamps.push(Date.now() || 0);
pairedWith.uncompletedBonfires.splice(index, 1);
pairedWith.uncompletedCoursewares.splice(index, 1);
}
pairedWith.completedBonfires.push({
_id: bonfireHash,
pairedWith.completedCoursewares.push({
_id: coursewareHash,
completedWith: req.user._id,
completedDate: isCompletedDate,
solution: isSolution
solution: solutionLink
});
req.user.completedBonfires.push({
_id: bonfireHash,
req.user.completedCoursewares.push({
_id: coursewareHash,
completedWith: pairedWith._id,
completedDate: isCompletedDate,
solution: isSolution
solution: solutionLink
});
req.user.save(function (err, user) {
if (err) {
return next(err);
}
pairedWith.save(function (err, paired) {
if (err) {
throw err;
return next(err);
}
if (user && paired) {
res.send(true);
return res.send(true);
}
})
});
});
}
})
});
} else {
req.user.completedBonfires.push({
_id: bonfireHash,
req.user.completedCoursewares.push({
_id: coursewareHash,
completedWith: null,
completedDate: isCompletedDate,
solution: isSolution
solution: solutionLink
});
var index = req.user.uncompletedCourse.indexOf(bonfireHash);
var index = req.user.uncompletedCourse.indexOf(coursewareHash);
if (index > -1) {
req.user.progressTimestamps.push(Date.now() || 0);
req.user.uncompletedBonfires.splice(index, 1)
req.user.uncompletedCoursewares.splice(index, 1);
}
req.user.save(function (err, user) {
if (err) {
throw err;
return next(err);
}
if (user) {
debug('Saving user');
res.send(true)
return res.send(true);
}
});
}

View File

@ -172,6 +172,7 @@ module.exports = {
var date2 = new Date();
var progressTimestamps = req.user.progressTimestamps;
var now = Date.now() || 0;
if (req.user.pointsNeedMigration) {
var challengesHash = req.user.challengesHash;
for (var key in challengesHash) {
@ -180,23 +181,33 @@ module.exports = {
}
}
var timeStamps = [];
R.keys(req.user.challengesHash).forEach(function(key) {
"use strict";
var timeStamp = parseInt(challengesHash[key], 10);
timeStamps.push({timeStamp: timeStamp.length !== 13 ? (+timeStamp) : (+timeStamp * 1000)});
var oldChallengeKeys = R.keys(req.user.challengesHash);
var updatedTimesFromOldChallenges = oldChallengeKeys.map(function(timeStamp) {
if (timeStamp.toString().length !== 13) {
timeStamp *= 1000;
}
return timeStamp;
});
req.user.completedCoursewares = Array.zip(timeStamps, coursewares,
var newTimeStamps = R.map(function(timeStamp) {
if (timeStamp.toString().length !== 13) {
timeStamp *= 1000;
}
return timeStamp;
}, req.user.progressTimestamps);
req.user.progressTimestamps = newTimeStamps;
req.user.completedCoursewares = Array.zip(updatedTimesFromOldChallenges, coursewares,
function(left, right) {
"use strict";
return ({
completedDate: left.timeStamp,
_id: right._id,
name: right.name
});
}).filter(function(elem) {
"use strict";
return elem.completedDate !== 0;
});
req.user.pointsNeedMigration = false;

View File

@ -1,3 +1,4 @@
/* eslint-disable no-catch-shadow, no-unused-vars */
var R = require('ramda'),
debug = require('debug')('freecc:cntr:story'),
Story = require('./../models/Story'),
@ -24,11 +25,10 @@ function hotRank(timeValue, rank) {
}
exports.hotJSON = function(req, res) {
exports.hotJSON = function(req, res, next) {
var story = Story.find({}).sort({'timePosted': -1}).limit(1000);
story.exec(function(err, stories) {
if (err) {
res.send(500);
return next(err);
}
@ -38,7 +38,8 @@ exports.hotJSON = function(req, res) {
return res.json(stories.map(function(elem) {
return elem;
}).sort(function(a, b) {
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));
});
@ -48,36 +49,35 @@ exports.recentJSON = function(req, res, next) {
var story = Story.find({}).sort({'timePosted': -1}).limit(100);
story.exec(function(err, stories) {
if (err) {
res.status(500);
return next(err);
}
res.json(stories);
return res.json(stories);
});
};
exports.hot = function(req, res) {
res.render('stories/index', {
return res.render('stories/index', {
title: 'Hot stories currently trending on Camper News',
page: 'hot'
});
};
exports.submitNew = function(req, res) {
res.render('stories/index', {
return res.render('stories/index', {
title: 'Submit a new story to Camper News',
page: 'submit'
});
};
exports.search = function(req, res) {
res.render('stories/index', {
return res.render('stories/index', {
title: 'Search the archives of Camper News',
page: 'search'
});
};
exports.recent = function(req, res) {
res.render('stories/index', {
return res.render('stories/index', {
title: 'Recently submitted stories on Camper News',
page: 'recent'
});
@ -104,7 +104,7 @@ exports.preSubmit = function(req, res) {
var image = data.image || '';
var description = data.description || '';
return res.render('stories/index', {
title: "Confirm your Camper News story submission",
title: 'Confirm your Camper News story submission',
page: 'storySubmission',
storyURL: data.url,
storyTitle: title,
@ -121,7 +121,7 @@ exports.returnIndividualStory = function(req, res, next) {
Story.find({'storyLink': new RegExp(storyName, 'i')}, function(err, story) {
if (err) {
next(err);
return next(err);
}
@ -143,7 +143,7 @@ exports.returnIndividualStory = function(req, res, next) {
try {
var votedObj = story.upVotes.filter(function(a) {
return a['upVotedByUsername'] === req.user['profile']['username'];
})
});
if (votedObj.length > 0) {
userVoted = true;
}
@ -168,11 +168,14 @@ exports.returnIndividualStory = function(req, res, next) {
});
};
exports.getStories = function(req, res) {
exports.getStories = function(req, res, next) {
MongoClient.connect(secrets.db, function(err, database) {
if (err) {
return next(err);
}
database.collection('stories').find({
"$text": {
"$search": req.body.data.searchValue
'$text': {
'$search': req.body.data.searchValue
}
}, {
headline: 1,
@ -187,19 +190,22 @@ exports.getStories = function(req, res) {
storyLink: 1,
metaDescription: 1,
textScore: {
$meta: "textScore"
$meta: 'textScore'
}
}, {
sort: {
textScore: {
$meta: "textScore"
$meta: 'textScore'
}
}
}).toArray(function(err, items) {
if (err) {
return next(err);
}
if (items !== null && items.length !== 0) {
return res.json(items);
}
return res.status(404);
return res.sendStatus(404);
});
});
};
@ -208,7 +214,6 @@ exports.upvote = function(req, res, next) {
var data = req.body.data;
Story.find({'_id': data.id}, function(err, story) {
if (err) {
res.status(500);
return next(err);
}
story = story.pop();
@ -221,6 +226,15 @@ exports.upvote = function(req, res, next) {
);
story.markModified('rank');
story.save();
User.find({'_id': story.author.userId}, function(err, user) {
'use strict';
if (err) {
return next(err);
}
user = user.pop();
user.progressTimestamps.push(Date.now());
user.save();
});
return res.send(story);
});
};
@ -229,7 +243,6 @@ exports.comments = function(req, res, next) {
var data = req.params.id;
Comment.find({'_id': data}, function(err, comment) {
if (err) {
res.status(500);
return next(err);
}
comment = comment.pop();
@ -237,9 +250,9 @@ exports.comments = function(req, res, next) {
});
};
exports.newStory = function(req, res) {
exports.newStory = function(req, res, next) {
if (!req.user) {
return res.status(500);
return next(new Error('Must be logged in'));
}
var url = req.body.data.url;
var cleanURL = sanitizeHtml(url, {
@ -261,7 +274,7 @@ exports.newStory = function(req, res) {
}
Story.find({'link': url}, function(err, story) {
if (err) {
return res.status(500);
return next(err);
}
if (story.length) {
req.flash('errors', {
@ -296,10 +309,10 @@ exports.newStory = function(req, res) {
}
};
exports.storySubmission = function(req, res) {
exports.storySubmission = function(req, res, next) {
var data = req.body.data;
if (req.user._id.toString() !== data.author.userId.toString()) {
return res.status(500);
return next(new Error('Not authorized'));
}
var storyLink = data.headline
.replace(/\'/g, '')
@ -332,9 +345,12 @@ exports.storySubmission = function(req, res) {
metaDescription: data.storyMetaDescription
});
req.user.progressTimestamps.push(Date.now());
req.user.save();
story.save(function(err) {
if (err) {
return res.status(500);
return next(err);
}
res.send(JSON.stringify({
storyLink: story.storyLink.replace(/\s/g, '-').toLowerCase()
@ -342,10 +358,10 @@ exports.storySubmission = function(req, res) {
});
};
exports.commentSubmit = function(req, res) {
exports.commentSubmit = function(req, res, next) {
var data = req.body.data;
if (req.user._id.toString() !== data.author.userId.toString()) {
return res.status(500);
return next(new Error('Not authorized'));
}
var sanitizedBody = sanitizeHtml(data.body,
{
@ -368,14 +384,14 @@ exports.commentSubmit = function(req, res) {
topLevel: true,
commentOn: Date.now()
});
commentSave(comment, Story, res);
commentSave(comment, Story, res, next);
};
exports.commentOnCommentSubmit = function(req, res) {
exports.commentOnCommentSubmit = function(req, res, next) {
var data = req.body.data;
if (req.user._id.toString() !== data.author.userId.toString()) {
return res.status(500);
return next(new Error('Not authorized'));
}
var sanitizedBody = sanitizeHtml(data.body,
@ -399,25 +415,25 @@ exports.commentOnCommentSubmit = function(req, res) {
topLevel: false,
commentOn: Date.now()
});
commentSave(comment, Comment, res);
commentSave(comment, Comment, res, next);
};
function commentSave(comment, Context, res) {
function commentSave(comment, Context, res, next) {
comment.save(function(err, data) {
if (err) {
return res.status(500);
return next(err);
}
try {
Context.find({'_id': comment.associatedPost}, function (err, associatedStory) {
if (err) {
return res.status(500);
return next(err);
}
associatedStory = associatedStory.pop();
if (associatedStory) {
associatedStory.comments.push(data._id);
associatedStory.save(function (err) {
if (err) {
res.status(500);
return next(err);
}
res.send(true);
});
@ -425,7 +441,7 @@ function commentSave(comment, Context, res) {
});
} catch (e) {
// delete comment
return res.status(500);
return next(err);
}
});
}

View File

@ -6,9 +6,9 @@ var _ = require('lodash'),
User = require('../models/User'),
secrets = require('../config/secrets'),
moment = require('moment'),
Challenge = require('./../models/Challenge'),
debug = require('debug')('freecc:cntr:challenges'),
resources = require('./resources');
resources = require('./resources'),
R = require('ramda');
@ -18,7 +18,9 @@ var _ = require('lodash'),
*/
exports.getSignin = function(req, res) {
if (req.user) return res.redirect('/');
if (req.user) {
return res.redirect('/');
}
res.render('account/signin', {
title: 'Free Code Camp Login'
});
@ -41,13 +43,17 @@ exports.postSignin = function(req, res, next) {
}
passport.authenticate('local', function(err, user, info) {
if (err) return next(err);
if (err) {
return next(err);
}
if (!user) {
req.flash('errors', { msg: info.message });
return res.redirect('/signin');
}
req.logIn(user, function(err) {
if (err) return next(err);
if (err) {
return next(err);
}
req.flash('success', { msg: 'Success! You are logged in.' });
res.redirect(req.session.returnTo || '/');
});
@ -70,7 +76,9 @@ exports.signout = function(req, res) {
*/
exports.getEmailSignin = function(req, res) {
if (req.user) return res.redirect('/');
if (req.user) {
return res.redirect('/');
}
res.render('account/email-signin', {
title: 'Sign in to your Free Code Camp Account'
});
@ -82,7 +90,9 @@ exports.getEmailSignin = function(req, res) {
*/
exports.getEmailSignup = function(req, res) {
if (req.user) return res.redirect('/');
if (req.user) {
return res.redirect('/');
}
res.render('account/email-signup', {
title: 'Create Your Free Code Camp Account'
});
@ -174,7 +184,7 @@ exports.postEmailSignup = function(req, res, next) {
'Greetings from San Francisco!\n\n',
'Thank you for joining our community.\n',
'Feel free to email us at this address if you have any questions about Free Code Camp.\n',
"And if you have a moment, check out our blog: blog.freecodecamp.com.\n",
'And if you have a moment, check out our blog: blog.freecodecamp.com.\n',
'Good luck with the challenges!\n\n',
'- the Volunteer Camp Counselor Team'
].join('')
@ -190,10 +200,45 @@ exports.postEmailSignup = function(req, res, next) {
* For Calendar display
*/
exports.getStreak = function(req, res) {
var completedStreak = req.user.challengesHash;
exports.getStreak = function(req, res, next) {
req.user.progressTimestamps = req.user.progressTimestamps.sort(function(a, b) {
return a - b;
});
var timeObject = Object.create(null);
R.forEach(function(time) {
timeObject[moment(time).format('YYYY-MM-DD')] = time;
}, req.user.progressTimestamps);
var tmpLongest = 1;
var timeKeys = R.keys(timeObject);
for (var i = 1; i <= timeKeys.length; i++) {
if (moment(timeKeys[i - 1]).add(1, 'd').toString()
=== moment(timeKeys[i]).toString()) {
tmpLongest++;
if (tmpLongest > req.user.currentStreak) {
req.user.currentStreak = tmpLongest;
}
if ( req.user.currentStreak > req.user.longestStreak) {
req.user.longestStreak = req.user.currentStreak;
}
}
}
req.user.save(function(err) {
if (err) {
return next(err);
}
});
s
var payload = {
longest: req.user.longestStreak,
timeObject: timeObject
};
return res.send(payload);
};
/**
* GET /account
@ -272,7 +317,7 @@ exports.returnUser = function(req, res, next) {
var data = {};
var progressTimestamps = user.progressTimestamps;
for (var i = 0; i < progressTimestamps.length; i++) {
data[progressTimestamps[i].toString()] = 1;
data[(progressTimestamps[i] / 1000).toString()] = 1;
}
res.render('account/show', {

View File

@ -1,7 +1,9 @@
var bcrypt = require('bcrypt-nodejs');
var crypto = require('crypto');
var mongoose = require('mongoose');
require('mongoose-long')(mongoose);
var Long = mongoose.Types.Long;
var userSchema = new mongoose.Schema({
email: {
type: String,
@ -21,7 +23,7 @@ var userSchema = new mongoose.Schema({
type: Number,
default: 0
},
progressTimestamps: { type: Array, default: [Date] },
progressTimestamps: [],
challengesCompleted: { type: Array, default: [] },
pointsNeedMigration: { type: Boolean, default: true },
challengesHash: {
@ -332,9 +334,30 @@ var userSchema = new mongoose.Schema({
resetPasswordToken: String,
resetPasswordExpires: Date,
uncompletedBonfires: Array,
completedBonfires: Array,
completedBonfires: [
{
_id: String,
completedWith: String,
completedDate: Long,
solution: String
}
],
uncompletedCoursewares: Array,
completedCoursewares: Array
completedCoursewares: [
{
completedDate: Long,
_id: String,
name: String
}
],
currentStreak: {
type: Number,
default: 0
},
longestStreak: {
type: Number,
default: 0
}
});
/**

View File

@ -47,11 +47,9 @@
"method-override": "^2.3.0",
"moment": "^2.8.4",
"mongodb": "^1.4.33",
"mongoose": "^3.8.19",
"mongoose-text-search": "0.0.2",
"mongoose": "^4.0.0",
"morgan": "^1.5.0",
"newrelic": "^1.13.3",
"node": "0.0.0",
"nodemailer": "^1.3.0",
"passport": "^0.2.1",
"passport-facebook": "^1.0.3",
@ -76,7 +74,6 @@
"chai": "^1.10.0",
"gulp": "^3.8.8",
"gulp-inject": "^1.0.2",
"gulp-minify-css": "^0.5.1",
"gulp-nodemon": "^1.0.4",
"mocha": "^2.0.1",
"multiline": "^1.0.1",