From 6512b9375531031d9a18137d6d119b45f4bae9e0 Mon Sep 17 00:00:00 2001
From: jewong
Date: Wed, 30 Mar 2011 22:26:09 -0600
Subject: [PATCH 1/2] Add follow user functionality
---
db/fixtures/dbSetup.sql | 1 +
db/fixtures/followedUsers.sql | 1 +
db/fixtures/schema.sql | 79 ++++++------
lib/routes.js | 13 +-
node_modules/followUser.js | 54 ++++++++
node_modules/personalFeed.js | 126 ++++++++++++-------
node_modules/recentActivity.js | 134 +++++++++++++++++---
node_modules/user.js | 98 ++++++++-------
node_modules/users.js | 221 ++++++++++++++++++++++++++++-----
node_modules/viewIssue.js | 10 +-
node_modules/viewProfile.js | 66 ++++++----
test/unitTests/test-users.js | 110 ++++++++++++++++
views/personalFeed.html | 17 ++-
views/viewProfile.html | 10 ++
14 files changed, 721 insertions(+), 219 deletions(-)
create mode 100755 db/fixtures/followedUsers.sql
create mode 100755 node_modules/followUser.js
create mode 100755 test/unitTests/test-users.js
diff --git a/db/fixtures/dbSetup.sql b/db/fixtures/dbSetup.sql
index 1028d02..d7dbf51 100644
--- a/db/fixtures/dbSetup.sql
+++ b/db/fixtures/dbSetup.sql
@@ -10,4 +10,5 @@
.read ./fixtures/follows.sql
.read ./fixtures/updateIssueVotes.sql
.read ./fixtures/messages.sql
+.read ./fixtures/followedUsers.sql
.quit
diff --git a/db/fixtures/followedUsers.sql b/db/fixtures/followedUsers.sql
new file mode 100755
index 0000000..d91a800
--- /dev/null
+++ b/db/fixtures/followedUsers.sql
@@ -0,0 +1 @@
+INSERT INTO followed_users (id, follower_user_id, user_id) VALUES (1, 2, 1);
\ No newline at end of file
diff --git a/db/fixtures/schema.sql b/db/fixtures/schema.sql
index 61513b8..58ec83a 100644
--- a/db/fixtures/schema.sql
+++ b/db/fixtures/schema.sql
@@ -1,44 +1,44 @@
DROP TABLE IF EXISTS users;
CREATE TABLE users (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- email TEXT NOT NULL,
- facebook_account TEXT,
- twitter_account TEXT,
- password TEXT NOT NULL,
- neighborhood TEXT,
- postal_code TEXT,
- website TEXT,
- created DATETIME DEFAULT CURRENT_TIMESTAMP,
- reputation_score INTEGER,
- isEditor INTEGER
- );
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ email TEXT NOT NULL,
+ facebook_account TEXT,
+ twitter_account TEXT,
+ password TEXT NOT NULL,
+ neighborhood TEXT,
+ postal_code TEXT,
+ website TEXT,
+ created DATETIME DEFAULT CURRENT_TIMESTAMP,
+ reputation_score INTEGER,
+ isEditor INTEGER
+);
DROP TABLE IF EXISTS issues;
CREATE TABLE issues (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- created DATETIME DEFAULT CURRENT_TIMESTAMP,
- lastModified DATETIME DEFAULT CURRENT_TIMESTAMP,
- status TEXT,
- title TEXT,
- description TEXT,
- link TEXT,
- location TEXT,
- likes INTEGER,
- dislikes INTEGER
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ created DATETIME DEFAULT CURRENT_TIMESTAMP,
+ lastModified DATETIME DEFAULT CURRENT_TIMESTAMP,
+ status TEXT,
+ title TEXT,
+ description TEXT,
+ link TEXT,
+ location TEXT,
+ likes INTEGER,
+ dislikes INTEGER
);
DROP TABLE IF EXISTS comments;
CREATE TABLE comments (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- issue_id INTEGER NOT NULL,
- created DATETIME DEFAULT CURRENT_TIMESTAMP,
- content TEXT,
- likes INTEGER DEFAULT 0,
- dislikes INTEGER DEFAULT 0
- );
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ issue_id INTEGER NOT NULL,
+ created DATETIME DEFAULT CURRENT_TIMESTAMP,
+ content TEXT,
+ likes INTEGER DEFAULT 0,
+ dislikes INTEGER DEFAULT 0
+);
DROP TABLE IF EXISTS tags;
CREATE TABLE tags (
@@ -66,6 +66,13 @@ CREATE TABLE follows (
user_id INTEGER NOT NULL,
issue_id INTEGER NOT NULL
);
+
+DROP TABLE IF EXISTS followed_users;
+CREATE TABLE followed_users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ follower_user_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL
+);
DROP TABLE IF EXISTS interests;
CREATE TABLE interests (
@@ -101,10 +108,10 @@ CREATE TABLE sent_msgs (
-- Table used for storing the user votes on comments
DROP TABLE IF EXISTS cmntvotes;
CREATE TABLE cmntvotes (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- comment_id INTEGER NOT NULL
- );
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id INTEGER NOT NULL,
+ comment_id INTEGER NOT NULL
+);
DROP TABLE IF EXISTS sessions;
CREATE TABLE sessions (
diff --git a/lib/routes.js b/lib/routes.js
index 41f6442..cc092d0 100755
--- a/lib/routes.js
+++ b/lib/routes.js
@@ -1,4 +1,4 @@
-/*
+/* route.js
* Add new routes to the router
*
* Route paths can be either strings or regular expressions.
@@ -11,7 +11,6 @@
var Router = require('./router').Router;
var ViewIssue = require('viewIssue').ViewIssue;
var ViewProfile = require('viewProfile').ViewProfile;
-var TestModule = require('testModule').TestModule;
var EditIssue = require('editIssue').EditIssue;
var SaveIssue = require('saveIssue').SaveIssue;
var signupModule = require('signupModule').SignupModule;
@@ -25,6 +24,7 @@ var SaveInterest = require('saveInterest').SaveInterest;
var PersonalFeed = require('personalFeed');
var MessageCenter = require('messageInterface');
var VoteComments = require('voteComments').VoteComments;
+var followUser = require('followUser');
//var viewMessage = require('messageInterface').viewMessage;
//var NewMessage = require('messageInterface').NewMessage;
@@ -33,25 +33,23 @@ var r = new Router();
/*
* Define routes here!
*/
+r.add('/', ListIssues.display);
r.add('/signup', signupModule.handleSignup);
r.add('/editProfile', editProfileModule.buildEditProfilePage);
r.add('/saveInterest', SaveInterest.handleSave);
r.add('/handleEditProfile', editProfileModule.handleEditProfile);
-r.add('/foo', TestModule.foo);
-r.add('/fooBar', TestModule.fooBar);
r.add('/viewIssue', ViewIssue.display);
r.add('/addcomments', ViewIssue.addcomments)
r.add('/followIssue', FollowIssue.followIssue);
r.add('/unfollowIssue', FollowIssue.unfollowIssue);
+r.add('/followUser', followUser.follow);
+r.add('/unfollowUser', followUser.unfollow);
r.add('/listIssues', ListIssues.display);
r.add('/viewProfile', ViewProfile.display);
r.add('/editIssue', EditIssue.display);
r.add('/saveIssue', SaveIssue.display);
r.add('/index.html', ListIssues.display);
-r.add('/', ListIssues.display);
r.add('/voteIssues', VoteIssues.display);
-r.add(/^\/home$/, 'about.html');
-r.add('/static', 'static.html');
r.add('/fieldValidations.js', 'fieldValidations.js');
r.add('/add', AddIssue.add);
r.add('/addIssue', AddIssue.display);
@@ -63,6 +61,5 @@ r.add('/sendMessage', MessageCenter.SendMessage.send);
r.add('/voteComments', VoteComments.display);
r.add('/viewMessage', MessageCenter.viewMessage.display);
r.add('/newMessage', MessageCenter.NewMessage.display);
-r.add('/', ListIssues.display);
exports.router = r;
diff --git a/node_modules/followUser.js b/node_modules/followUser.js
new file mode 100755
index 0000000..766c2bf
--- /dev/null
+++ b/node_modules/followUser.js
@@ -0,0 +1,54 @@
+/* followUser.js
+ * Provides functionality for following users
+ */
+
+var dbAccess = require('dbAccess'),
+ users = require('users'),
+ url = require('url');
+
+/* follow
+ * Exposes functionality for following another user when logged in
+ *
+ * @param req A node http request object
+ * @param res A node http response object
+ */
+exports.follow = function(req, res) {
+ req.getUser(function(error, user) {
+ if (error || !user) {
+ throw error;
+ }
+ else {
+ var userId = user.id;
+ var urlQuery = url.parse(req.url, true).query;
+ var userIdToFollow = urlQuery.id;
+ users.followUser(userId, userIdToFollow, function(error) {
+ if (error) console.log(error);
+ res.writeHeader(200, {'Content-Type': 'text/html'});
+ res.end('');
+ });
+ }
+ });
+}
+
+/* unfollow
+ * Exposes functionality for unfollowing another user when logged in
+ *
+ * @param req A node http request object
+ * @param res A node http response object
+ */
+exports.unfollow = function(req, res) {
+ req.getUser(function(error, user) {
+ if (error || !user) {
+ throw error;
+ }
+ else {
+ var userId = user.id;
+ var urlQuery = url.parse(req.url, true).query;
+ var userIdToFollow = urlQuery.id;
+ users.unfollowUser(userId, userIdToFollow, function(error) {
+ res.writeHeader(200, {'Content-Type': 'text/html'});
+ res.end('');
+ });
+ }
+ });
+}
diff --git a/node_modules/personalFeed.js b/node_modules/personalFeed.js
index a6c8705..534b080 100755
--- a/node_modules/personalFeed.js
+++ b/node_modules/personalFeed.js
@@ -15,64 +15,106 @@ var NUM_ACTIVITIES = 20; // The number of activites to show
* Produces the response for a personal feed when a user is logged in.
* If no user is defined, it will just show the recent activity on the site
*
- * 'res' A node Http.Response object
- * 'user' The user (optional)
+ * @param res A node http response object
+ * @param user The user (optional)
*/
function generateActivityFeed(res, user) {
+ var isLoggedIn = user ? true : false;
function generateResponse(error, results) {
- step(
- function loadData() {
- var userIds = [];
- for (r in results) {
- userIds.push(results[r].contents.user_id);
- }
- getUserNames(userIds, this.parallel());
- },
- function handleCallback(err, userNames) {
- if (err) throw error;
+ var userIds = [];
+
+ for (r in results.recentActivity) {
+ userIds.push(results.recentActivity[r].contents.user_id);
+ }
+
+ if (results.followedUsers) {
+ for (r in results.followedUsers) {
+ userIds.push(results.followedUsers[r].contents.user_id);
+ }
+ }
+
+ getUserNames(userIds, function(err, userNames) {
+ var errorOccured = false;
+ if (err) errorOccured = true;
- var activityList = [];
- var currentDate = new Date();
- for (var i = 0; i < results.length; i++) {
+ var activityList = [];
+ var currentDate = new Date();
+ for (var i = 0; i < results.recentActivity.length; i++) {
+ var activity;
+ var activityType = results.recentActivity[i].type;
+ activity = {
+ type: results.recentActivity[i].type,
+ isIssue: (results.recentActivity[i].type == 'issue'),
+ isComment: (results.recentActivity[i].type == 'comment'),
+ id: results.recentActivity[i].contents.id,
+ title: (results.recentActivity[i].contents.title) ? results.recentActivity[i].contents.title.abbreviate(60) : '',
+ userId: results.recentActivity[i].contents.user_id,
+ username: userNames[results.recentActivity[i].contents.user_id],
+ howLongAgo: currentDate.howLongAgo(results.recentActivity[i].contents.created),
+ issueId: (activityType == 'comment') ? results.recentActivity[i].contents.issue_id : results.recentActivity[i].contents.id,
+ comment: (activityType == 'comment' && results.recentActivity[i].contents.content) ? results.recentActivity[i].contents.content.abbreviate(60) : ''
+ };
+ activityList.push(activity);
+ }
+
+ var followedUsersList = [];
+ if (results.followedUsers) {
+ for (var i = 0; i < results.followedUsers.length; i++) {
var activity;
- var activityType = results[i].type;
+ var activityType = results.followedUsers[i].type;
activity = {
- type: results[i].type,
- isIssue: (results[i].type == 'issue') ? true : false,
- isComment: (results[i].type == 'comment') ? true : false,
- id: results[i].contents.id,
- title: (results[i].contents.title) ? results[i].contents.title.abbreviate(60) : '',
- userId: results[i].contents.user_id,
- username: userNames[results[i].contents.user_id],
- howLongAgo: currentDate.howLongAgo(results[i].contents.created),
- issueId: (activityType == 'comment') ? results[i].contents.issue_id : results[i].contents.id,
- comment: (activityType == 'comment' && results[i].contents.content) ? results[i].contents.content.abbreviate(60) : ''
+ type: results.followedUsers[i].type,
+ isIssue: (results.followedUsers[i].type == 'issue'),
+ isComment: (results.followedUsers[i].type == 'comment'),
+ id: results.followedUsers[i].contents.id,
+ title: (results.followedUsers[i].contents.title) ? results.followedUsers[i].contents.title.abbreviate(60) : '',
+ userId: results.followedUsers[i].contents.user_id,
+ username: userNames[results.followedUsers[i].contents.user_id],
+ howLongAgo: currentDate.howLongAgo(results.followedUsers[i].contents.created),
+ issueId: (activityType == 'comment') ? results.followedUsers[i].contents.issue_id : results.followedUsers[i].contents.id,
+ comment: (activityType == 'comment' && results.followedUsers[i].contents.content) ? results.followedUsers[i].contents.content.abbreviate(60) : ''
};
- activityList.push(activity);
- }
-
- var variables = {
- content: activityList
+ followedUsersList.push(activity);
}
+ }
- res.render('views/personalFeed.html', variables);
+ var variables = {
+ is_logged_in: isLoggedIn,
+ error: errorOccured,
+ feed: activityList,
+ follow_feed: followedUsersList
}
- );
+
+ res.render('views/personalFeed.html', variables);
+ });
}
- if (user) {
- recentActivity.getUserRecentActivityList(user.id, NUM_ACTIVITIES, generateResponse);
+ if (isLoggedIn) {
+ step(
+ function loadData() {
+ recentActivity.getUserRecentActivityList(user.id, NUM_ACTIVITIES, this.parallel());
+ recentActivity.getFollowedUsersFeed(user.id, NUM_ACTIVITIES, this.parallel());
+ },
+ function handleCallback(err, recAct, flwUsrs) {
+ var allResults = { recentActivity: recAct, followedUsers: flwUsrs };
+ generateResponse(err, allResults);
+ }
+ );
+ //recentActivity.getUserRecentActivityList(user.id, NUM_ACTIVITIES, generateResponse);
}
else {
- recentActivity.getRecentActivityList(NUM_ACTIVITIES, generateResponse);
+ recentActivity.getRecentActivityList(NUM_ACTIVITIES, function(err, results) {
+ var allResults = { recentActivity: results };
+ generateResponse(err, allResults);
+ });
}
}
/* getUserNames
* Gets the names of users based off a list of user ids
*
- * 'userIds' A list of users ids
- * 'callback' A callback (error, results) where results is an array with user names
+ * @param userIds A list of users ids
+ * @param callback A callback (error, results) where results is an array with user names
*
* TODO: Improve the performance and efficiency of this function
*/
@@ -91,8 +133,8 @@ function getUserNames(userIds, callback) {
/* contains
* Checks if an array contains an object
*
- * 'a' The array
- * 'obj' The object to look for in the array
+ * @param a The array
+ * @param obj The object to look for in the array
*/
function contains(a, obj) {
var i = a.length;
@@ -107,8 +149,8 @@ function contains(a, obj) {
/* display
* Determines what to render
*
- * 'req' A node http request object
- * 'res' A node http response object
+ * @param req A node http request object
+ * @param res A node http response object
*/
exports.display = function(req, res) {
req.getUser(function(error, user) {
diff --git a/node_modules/recentActivity.js b/node_modules/recentActivity.js
index 82a5f05..4c61bdc 100755
--- a/node_modules/recentActivity.js
+++ b/node_modules/recentActivity.js
@@ -4,7 +4,8 @@
*/
var dbAccess = require('dbAccess'),
- step = require('step'),
+ step = require('step'),
+ users = require('users'),
util = require('util');
var MAX_RESULTS = 100;
@@ -12,8 +13,8 @@ var MAX_RESULTS = 100;
/* getLatestIssues
* Returns the latest issues. A maximum number of issues to return can be specified
*
- * 'numIssues' The maximum number of issues to return
- * 'callback' A callback with (error, results)
+ * @param numIssues The maximum number of issues to return
+ * @param callback A callback with (error, results)
*/
function getLatestIssues(numIssues, callback) {
var maxIssues = (numIssues < 0) ? -1 : numIssues;
@@ -22,11 +23,36 @@ function getLatestIssues(numIssues, callback) {
});
}
+/* getLatestIssuesByUser
+ * Returns the latest issues by a specified user. A maximum number of issues to return can be specified
+ *
+ * @param userId The user id of the author of issues to return
+ * @param numIssues The maximum number of issues to return
+ * @param callback A callback with (error, results)
+ */
+function getLatestIssuesByUsers(userIds, numIssues, callback) {
+ if (userIds.length <= 0) {
+ callback(null, []);
+ }
+ var userIdList = '';
+ for (var i = 0; i < userIds.length; i++) {
+ userIdList += 'user_id="' + userIds[i] + '"';
+ if (i != userIds.length - 1) {
+ userIdList += ' OR ';
+ }
+ }
+ var maxIssues = (numIssues < 0) ? -1 : numIssues;
+ dbAccess.find('issues', { conditions: [ userIdList ],
+ orderby: 'created DESC', limit: maxIssues }, function(error, results) {
+ callback(error, results);
+ });
+}
+
/* getLatestComments
- * Returns the latest comments. A maximum number of comments to return can be specified
+ * Returns the latest comments. A maximum number of comments to return can be specified.
*
- * 'numComments' The maximum number of issues to return
- * 'callback' A callback with (error, results)
+ * @param numComments The maximum number of comments to return
+ * @param callback A callback with (error, results)
*/
function getLatestComments(numComments, callback) {
var maxComments = (numComments < 0) ? -1 : numComments;
@@ -35,11 +61,36 @@ function getLatestComments(numComments, callback) {
});
}
+/* getLatestCommentsByUser
+ * Returns the latest comments by a specified user. A maximum number of comments to return can be specified.
+ *
+ * @param userIds The user id of the author of comments to return
+ * @param numComments The maximum number of comments to return
+ * @param callback A callback with (error, results)
+ */
+function getLatestCommentsByUsers(userIds, numComments, callback) {
+ if (userIds.length <= 0) {
+ callback(null, []);
+ }
+ var userIdList = '';
+ for (var i = 0; i < userIds.length; i++) {
+ userIdList += 'user_id="' + userIds[i] + '"';
+ if (i != userIds.length - 1) {
+ userIdList += ' OR ';
+ }
+ }
+ var maxComments = (numComments < 0) ? -1 : numComments;
+ dbAccess.find('comments', { conditions: [ userIdList ],
+ orderby: 'created DESC', limit: numComments }, function(error, results) {
+ callback(error, results);
+ });
+}
+
/* getInterests
* Gets the interests for a user
*
- * 'userId' The id of the user
- * 'callback' A callback with (error, results)
+ * @param userId The id of the user
+ * @param callback A callback with (error, results)
*/
function getInterests(userId, callback) {
dbAccess.find('interests', { conditions: ['user_id="' + userId + '"'] }, function(error, results) {
@@ -50,21 +101,21 @@ function getInterests(userId, callback) {
/* getUserLocation
* Gets the location/neighborhood of a user
*
- * 'userId' The id of the user
- * 'callback' A callback with (error, results) where results is the user's neighborhood
+ * @param userId The id of the user
+ * @param callback A callback with (error, results) where results is the user's neighborhood
*/
function getUserLocation(userId, callback) {
dbAccess.find('users', { properties: [ 'neighborhood' ], conditions: ['id="' + userId + '"'] },
function(error, results) {
callback(error, results);
- });
+ });
}
/* getRecentActivityList
* Returns a list of recent activity (issues and comments)
*
- * 'maxResults' The maximum number of results to return
- * 'callback' A callback which returns an object with the following properties:
+ * @param maxResults The maximum number of results to return
+ * @param callback A callback which returns an object with the following properties:
* type : The type of results (e.g. 'issue' or 'comment')
* contents : The result contents
*/
@@ -100,9 +151,9 @@ exports.getRecentActivityList = function(maxResults, callback) {
/* getUserRecentActivityList
* Returns a list of recent activity (e.g. issues and comments) for a user
*
- * 'userId' The id of the user
- * 'maxResults' The maximum number of results to return
- * 'callback' A callback which returns an object with the following properties:
+ * @param userId The id of the user
+ * @param maxResults The maximum number of results to return
+ * @param callback A callback which returns an object with the following properties:
* type : The type of results (e.g. 'issue' or 'comment')
* contents : The result contents
* relevance : The relevance score
@@ -155,11 +206,54 @@ exports.getUserRecentActivityList = function(userId, maxResults, callback) {
);
}
+/* getFollowedUsersFeed
+ * Returns a list of activity from followed users by a given user
+ *
+ * @param userId The id of the user
+ * @param maxResults The maximum number of results to return
+ * @param callback A callback which returns an object with the following properties:
+ * type : The type of results (e.g. 'issue' or 'comment')
+ * contents : The result contents
+ */
+exports.getFollowedUsersFeed = function(userId, maxResults, callback) {
+ users.getFollowedUsers(userId, function(error, results) {
+ if (error) return callback(error, results);
+ step(
+ function loadData() {
+ getLatestIssuesByUsers(results, MAX_RESULTS, this.parallel());
+ getLatestCommentsByUsers(results, MAX_RESULTS, this.parallel());
+ },
+ function handleCallback(err, issues, comments) {
+ if (err) throw err;
+
+ var all_results = [];
+
+ function dateComparator(a, b) {
+ var a = new Date(a.contents.created), b = new Date(b.contents.created);
+ return (b.getTime() - a.getTime());
+ }
+
+ for (i in issues) {
+ all_results.push({ 'type' : 'issue', 'contents' : issues[i] });
+ }
+ for (c in comments) {
+ all_results.push({ 'type' : 'comment', 'contents' : comments[c] });
+ }
+
+ all_results.sort(dateComparator);
+ all_results = all_results.slice(0, maxResults);
+ callback(null, all_results);
+ }
+ );
+ });
+
+}
+
/* rankIssueToLocation
* Determines the relevance of an issue based on the frequency of matches a user's location
*
- * 'issue' The issue
- * 'neighborhood' The user's neighborhood
+ * @param issue The issue
+ * @param neighborhood The user's neighborhood
*/
function rankIssueToLocation(issue, neighborhood) {
var rank = 0;
@@ -172,8 +266,8 @@ function rankIssueToLocation(issue, neighborhood) {
/* rankCommentToLocation
* Determines the relevance of a comment based on the frequency of matches a user's location
*
- * 'issue' The comment
- * 'neighborhood' The user's neighborhood
+ * @param issue The comment
+ * @param neighborhood The user's neighborhood
*/
function rankCommentToLocation(comment, neighborhood) {
var rank = 0;
diff --git a/node_modules/user.js b/node_modules/user.js
index da1c54b..50dcc42 100644
--- a/node_modules/user.js
+++ b/node_modules/user.js
@@ -1,22 +1,23 @@
var crypto = require('crypto'),
-dbAccess = require('./dbAccess'),
-queryString = require('querystring');
+ dbAccess = require('./dbAccess'),
+ queryString = require('querystring');
-// Maybe this should use a salt to secure the password in the database.
-exports.md5hash = function(string){
+// TODO: Maybe this should use a salt to secure the password in the database.
+exports.md5hash = function(string) {
hash = crypto.createHash('md5');
hash.update(string);
return hash.digest('hex');
}
-/*
-* Checks if username and password match.
-*
-* 'username': The username address of the user.
-* 'password': The password to check.
-* 'callback': returns (error, user), where error is null unless
-* an error occured and user is null if the password hashes don't
-* match or the user record otherwise.
-*/
+
+/* authenticate
+ * Checks if username and password match.
+ *
+ * @param username The username address of the user.
+ * @param password The password to check.
+ * @param callback returns (error, user), where error is null unless
+ * an error occured and user is null if the password hashes don't
+ * match or the user record otherwise.
+ */
exports.authenticate = function(username, password, callback){
dbAccess.find('users', { conditions:['name="' + username + '"']}, function(error, records){
if(error)
@@ -35,73 +36,74 @@ exports.authenticate = function(username, password, callback){
});
}
-function parseCookieString(cookieString){
+/* parseCookieString
+ * Gets name-value pairs from a cookie string
+ *
+ * @param cookieString The cookie string to parse
+ * @returns A hash for name-values pairs in the cookie
+ */
+function parseCookieString(cookieString) {
cookiesSplit = cookieString.split(';');
- cookiesHash = {}
- for(var i in cookiesSplit){
- //remove whitespce
- cookiesSplit[i] = cookiesSplit[i].replace(/^\s+|\s+$/g,"");
- cookieParsed = queryString.parse(cookiesSplit[i])
- name = Object.keys(cookieParsed)[0]
+ cookiesHash = {};
+ for (var i in cookiesSplit) {
+ cookiesSplit[i] = cookiesSplit[i].replace(/^\s+|\s+$/g, '');
+ cookieParsed = queryString.parse(cookiesSplit[i]);
+ name = Object.keys(cookieParsed)[0];
cookiesHash[name] = cookieParsed[name];
}
return cookiesHash;
}
-/*
-* given the request object this will return the user or null if
-* not logged in.
-* @param {Object} req The request object.
-* @param {Function} callback Expects a callback of the form: callback(error, user)
-*/
-exports.getUserFromRequest = function(req, callback){
-
+/* getUserFromRequest
+ * Given the request object this will return the user or null if not logged in.
+ * @param {Object} req The request object.
+ * @param {Function} callback Expects a callback of the form: callback(error, user)
+ */
+exports.getUserFromRequest = function(req, callback) {
//if user is cached don't call the database again. simply return the cache
- if(req.user){
+ if (req.user) {
callback(null, req.user);
return;
}
- if(!req.headers.cookie)
+ if (!req.headers.cookie)
{
return callback(null, null);
}
cookies = req.headers.cookie;
-
cookiesHash = parseCookieString(cookies);
-
session = cookiesHash.session;
- var query = "SELECT * FROM users JOIN sessions ON sessions.user_id=users.id WHERE sessions.session_hash='"+session+"' ORDER BY id DESC LIMIT 1;";
+ var query = "SELECT * FROM users JOIN sessions ON sessions.user_id=users.id WHERE sessions.session_hash='" + session + "' ORDER BY id DESC LIMIT 1;";
- dbAccess.runQuery(query, function(error, records){
- if(error)
- return callback(error, null);
+ dbAccess.runQuery(query, function(error, records) {
+ if (error)
+ return callback(error, null);
- if(records.length == 0)
- return callback(null, null); // no user with that session found.
+ if (records.length == 0)
+ return callback(null, null); // no user with that session found.
// Found User log in session, now use ID to find user
dbAccess.find('users', { conditions:['id="' + records[0].user_id + '"']}, function(error, rows){
if(error)
- return callback(error, null);
+ return callback(error, null);
if(rows.length == 0)
- return callback(null, null); // no user found.
+ return callback(null, null); // no user found.
callback(null, rows[0]);
});
});
}
-/*
-* This method is expected to be added to the request object.
-* To get the current user based on the session id call req.getUser
-* and the second parameter of the callback should be the user.
-* To check if the user is logged in call req.getUser and check
-* if the callback user is null.
-*/
-exports.getUser = function(callback){
+/* getUser
+ * This method is expected to be added to the request object.
+ * To get the current user based on the session id call req.getUser
+ * and the second parameter of the callback should be the user.
+ * To check if the user is logged in call req.getUser and check
+ * if the callback user is null.
+ */
+exports.getUser = function(callback) {
exports.getUserFromRequest(this, callback);
}
diff --git a/node_modules/users.js b/node_modules/users.js
index 11a2208..fa8bc6a 100644
--- a/node_modules/users.js
+++ b/node_modules/users.js
@@ -1,44 +1,199 @@
-/*
- * Back end functions for anything related to users
+/* users.js
+ * Provides functionality for managing and retrieving information about users
*/
var dbAccess = require('dbAccess');
-// Returns the userid of given username, or -1 if no user exists by that username
-var getIdFromUsername = exports.getIdFromUsername = function(username, callback) {
- // TODO: Make this check the username instead of real name once that is implemented
- var q = "SELECT id FROM users WHERE name = '" + username + "';";
- util.log(q);
- dbAccess.runQuery(q, function(error, results) {
- if (error) {
- util.log('Error running query in users.getIdFromUsername. ' + error);
- if (callback instanceof Function) callback(error);
+/* getIdFromUsername
+ * Gets the id of a user based on the username
+ *
+ * @param username The username to lookup
+ * @param callback The callback with (error, results)
+ */
+exports.getIdFromUsername = function(username, callback) {
+ getUserProperties(['id'], ['name="' + username +'"'], function(error, results) {
+ if (error) {
+ if (callback instanceof Function) callback(error);
}
- else if (results.length != 1) {
- var error = "Error in users.getIdFromUser.";
- util.log(error);
- if (callback instanceof Function) callback(error);
+ else {
+ if (callback instanceof Function) callback(error, results.id);
}
- else {
- if (callback instanceof Function) callback(undefined, results[0].id);
- }
- });
+ });
}
-var getUsernameFromId = exports.getUsernameFromId = function(userid, callback) {
- // TODO: Make this get the username instead of real name once that is implemented
- var q = "SELECT name FROM users WHERE id = '" + userid + "';";
- dbAccess.runQuery(q, function(error, results) {
- if(error) {
- util.log("Error finding username from userid." + error);
- if (callback instanceof Function) callback(error);
+/* getUsernameFromId
+ * Gets the username of a user from a userid
+ *
+ * @param userid The user id to lookup the username
+ * @param callback The callback with (error, results)
+ */
+exports.getUsernameFromId = function(userid, callback) {
+ getUserProperties(['name'], ['id="' + userid +'"'], function(error, results) {
+ if (error) {
+ if (callback instanceof Function) callback(error);
}
- else if (results.length != 1) {
- var error = "Error in users.getUsernameFromId. " + q;
- util.log(error);
- if (callback instanceof Function) callback(error);
+ else {
+ if (callback instanceof Function) callback(error, results.name);
}
- else
- if (callback instanceof Function) callback(undefined, results[0].name);
+ });
+}
+
+/* getUserFollowers
+ * Gets the all the followers for a user
+ *
+ * @param userId The user id
+ * @param callback The callback with (error, results)
+ */
+exports.getUserFollowers = function(userId, callback) {
+ dbAccess.find('followed_users', { properties: ['follower_user_id'],
+ conditions: ['user_id="' + userId + '"'] }, function(error, results) {
+ if (error) {
+ if (callback instanceof Function) callback(error, results);
+ }
+ else {
+ var followerUserIds = [];
+ for (i in results) {
+ followerUserIds.push(results[i].follower_user_id);
+ }
+ if (callback instanceof Function) callback(undefined, followerUserIds);
+ }
+ });
+}
+
+/* getFollowedUsers
+ * Gets the users follwed by a given user
+ *
+ * @param userId The user id
+ * @param callback The callback with (error, results)
+ */
+exports.getFollowedUsers = function(userId, callback) {
+ dbAccess.find('followed_users', { properties: ['user_id'],
+ conditions: ['follower_user_id="' + userId + '"'] }, function(error, results) {
+ if (error) {
+ if (callback instanceof Function) callback(error, results);
+ }
+ else {
+ var followedUserIds = [];
+ for (i in results) {
+ followedUserIds.push(results[i].user_id);
+ }
+ if (callback instanceof Function) callback(undefined, followedUserIds);
+ }
+ });
+}
+
+/* isUserFollowedByUser
+ * Determines if a given user is being followed a specified follower
+ *
+ * @param userId The id of the user who is being followed
+ * @param followerId The id of the user who is the follower
+ * @param callback The callback with (error, results) where results is:
+ * true if user is being followed by the specified follower user id; false otherwise
+ */
+exports.isUserFollowedByUser = function(userId, followerId, callback) {
+ if (userId == followerId) {
+ if (callback instanceof Function) callback(undefined, false);
+ }
+ else {
+ exports.getUserFollowers(userId, function(error, results) {
+ if (error) {
+ if (callback instanceof Function) callback(error, results);
+ }
+ else {
+ var isUserFollowed = false;
+ for (i in results) {
+ if (results[i] == followerId) {
+ isUserFollowed = true;
+ break;
+ }
+ }
+ if (callback instanceof Function) callback(undefined, isUserFollowed);
+ }
+ });
+ }
+}
+
+/* followUser
+ * Follows a user
+ *
+ * @param userId The id of the user who wants to follow another user
+ * @param followedUserId The id of the target user who is to be followed
+ * @param callback The callback with (error)
+ */
+exports.followUser = function(userId, followedUserId, callback) {
+ if (userId == followedUserId) {
+ var msg = 'Error following user: a user cannot follow themselves.';
+ if (callback instanceof Function) callback(msg);
+ }
+ else {
+ exports.isUserFollowedByUser(followedUserId, userId, function(error, results) {
+ if (error) {
+ if (callback instanceof Function) callback(error);
+ }
+ else if (results) {
+ if (callback instanceof Function) callback('User is already being followed');
+ }
+ else {
+ dbAccess.create('followed_users', { values:['follower_user_id="' + userId +'"',
+ 'user_id="' + followedUserId + '"'] }, function(error, id) {
+ callback(error);
+ });
+ }
+ });
+ }
+}
+
+/* unfollowUser
+ * Unfollows a user
+ *
+ * @param userId The id of the user
+ * @param followedUserId The id of the user to be unfollowed
+ * @param callback The callback with (error)
+ */
+exports.unfollowUser = function(userId, followedUserId, callback) {
+ if (userId === followedUserId) {
+ var msg = 'Error unfollowing user: a user cannot unfollow themselves.';
+ if (callback instanceof Function) callback(msg);
+ }
+ else {
+ exports.isUserFollowedByUser(userId, followedUserId, function(error, results) {
+ if (error) {
+ if (callback instanceof Function) callback(error);
+ }
+ else if (results) {
+ // User not being followed, so we don't need to do anything
+ if (callback instanceof Function) callback(undefined);
+ }
+ else {
+ // Found follow relationship: remove it
+ dbAccess.remove('followed_users', { conditions:['follower_user_id="' + userId +'"',
+ 'user_id="' + followedUserId + '"'] }, function(error, id) {
+ callback(error);
+ });
+ }
+ });
+ }
+}
+
+/* getUserProperties
+ * Helper method for retrieve user properties
+ *
+ * @param props The user properties to retrieve
+ * @param conds The user conditions to enforce
+ * @param callback The callback with (error, results) from the database
+ */
+function getUserProperties(props, conds, callback) {
+ dbAccess.find('users', { properties: props, conditions: conds },
+ function(error, results) {
+ if (error) {
+ if (callback instanceof Function) callback(error);
+ }
+ else if (results.length != 1) {
+ var msg = 'Error retrieving user.' + results.length + ' users found.';
+ if (callback instanceof Function) callback(error);
+ }
+ else {
+ if (callback instanceof Function) callback(undefined, results[0]);
+ }
});
-}
\ No newline at end of file
+}
diff --git a/node_modules/viewIssue.js b/node_modules/viewIssue.js
index 791ce36..8260c3d 100644
--- a/node_modules/viewIssue.js
+++ b/node_modules/viewIssue.js
@@ -1,8 +1,8 @@
var dbAccess = require('dbAccess'),
- url = require('url'),
querystring = require('querystring'),
reputation = require('reputation'),
- url = require('url');
+ url = require('url'),
+ users = require('users');
var g_UserId = -1;
var g_UserVote;
@@ -88,7 +88,7 @@ function displayPage(response, issue) {
};
}
-ViewIssue.addcomments = function (req,res) {
+ViewIssue.addcomments = function(req, res) {
var requestString = "";
if (g_UserId == -1) {
@@ -98,7 +98,7 @@ ViewIssue.addcomments = function (req,res) {
}
else {
req.on('data', function (chunk) {
- requestString = requestString+chunk;
+ requestString = requestString + chunk;
});
req.on('end', function() {
var decodedBody = querystring.parse(requestString);
@@ -128,7 +128,7 @@ function findIssueCreator(response, issueId) {
return function(error, rows) {
if (error) throw error;
var issue = rows[0];
- if(issue == undefined) {
+ if (issue == undefined) {
variables = { found: false, issue_id: issueId }
response.render('views/viewIssue.html', variables);
}
diff --git a/node_modules/viewProfile.js b/node_modules/viewProfile.js
index e2ba903..d245dee 100644
--- a/node_modules/viewProfile.js
+++ b/node_modules/viewProfile.js
@@ -3,6 +3,8 @@
*/
var dbAccess = require('dbAccess'),
+ step = require('step');
+ users = require('users');
url = require('url');
var ViewProfile = exports.ViewProfile = function() {};
@@ -14,44 +16,58 @@ var ViewProfile = exports.ViewProfile = function() {};
*/
ViewProfile.display = function(request, response) {
var parsedURL = url.parse(request.url, true);
- var id = parsedURL.query.id;
+ var profileUserId = parsedURL.query.id;
request.getUser(function(error, user) {
if (error) throw error;
var loggedIn = false;
+ var loggedInUserId = null;
- if (!id && user) {
- id = user.id;
+ if (user) {
loggedIn = true;
+ loggedInUserId = user.id;
+ }
+
+ if (!profileUserId) {
+ profileUserId = user.id;
}
- dbAccess.find('users', { conditions:['id="' + id + '"'] }, function(error, users) {
+ dbAccess.find('users', { conditions:['id="' + profileUserId + '"'] }, function(error, userResults) {
if (error) throw error;
// If the user_id specified is not found in the db - error
- if(users.length != 1) { // Properly handle if the user_id is not valid
- variables = { found: false, user_id: id };
+ if (userResults.length != 1) { // Properly handle if the user_id is not valid
+ variables = { found: false, user_id: profileUserId };
response.render('views/viewProfile.html', variables);
}
else { // If user_id is found, display personal information and the list of issues
- user = users[0];
- dbAccess.find('issues', { conditions:['user_id="' + user.id + '"'], orderby: 'created desc' },
- function(error, issues) {
- variables = {
- found: true,
- name: user.name,
- neighborhood: user.neighborhood || '',
- postal_code: user.postal_code || '',
- facebook_account: user.facebook_account || 'None',
- twitter_account: user.twitter_account || 'None',
- has_website: user.website,
- website: user.website || 'None',
- issues_list: issues,
- has_issues: issues.length > 0,
- is_loggedin: loggedIn,
- reputation: user.reputation_score
- };
- response.render('views/viewProfile.html', variables);
- });
+ user = userResults[0];
+ step(
+ function loadData() {
+ dbAccess.find('issues', { conditions:['user_id="' + user.id + '"'], orderby: 'created desc' },
+ this.parallel());
+ users.isUserFollowedByUser(profileUserId, loggedInUserId, this.parallel());
+ },
+ function handleCallback(err, issues, isFollowed) {
+ variables = {
+ found: true,
+ name: user.name,
+ neighborhood: user.neighborhood || '',
+ postal_code: user.postal_code || '',
+ facebook_account: user.facebook_account || 'None',
+ twitter_account: user.twitter_account || 'None',
+ has_website: user.website,
+ website: user.website || 'None',
+ issues_list: issues,
+ has_issues: issues.length > 0,
+ is_loggedin: loggedIn,
+ reputation: user.reputation_score,
+ user_id: user.id,
+ is_followed: isFollowed,
+ is_viewing_own_profile: (loggedInUserId == profileUserId)
+ };
+ response.render('views/viewProfile.html', variables);
+ }
+ );
}
});
});
diff --git a/test/unitTests/test-users.js b/test/unitTests/test-users.js
new file mode 100755
index 0000000..1580e7e
--- /dev/null
+++ b/test/unitTests/test-users.js
@@ -0,0 +1,110 @@
+/* Tests for users.js
+ */
+
+var users = require('../../node_modules/users');
+var testCase = require('nodeunit/nodeunit').testCase;
+
+module.exports = testCase({
+ setUp: function(callback) {
+ callback();
+ },
+
+ tearDown: function(callback) {
+ callback();
+ },
+
+ testGetIdFromUsername: function(test) {
+ users.getIdFromUsername('Virginia Snyder', function(error, id) {
+ test.ifError(error);
+ test.equals(id, 1);
+ test.done();
+ });
+ },
+
+ testGetUsernameFromId: function(test) {
+ users.getUsernameFromId(1, function(error, name) {
+ test.ifError(error);
+ test.equals(name, 'Virginia Snyder');
+ test.done();
+ });
+ },
+
+ testGetUserFollowers: function(test) {
+ users.getUserFollowers(1, function(error, results) {
+ test.ifError(error);
+ test.equals(results.length, 1);
+ test.equals(results[0], 2);
+ test.done();
+ });
+ },
+
+ testGetFollowedUsers: function(test) {
+ users.getFollowedUsers(2, function(error, results) {
+ test.ifError(error);
+ test.equals(results.length, 1);
+ test.equals(results[0], 1);
+ test.done();
+ });
+ },
+
+ testIsUserFollowedByUser: function(test) {
+ users.isUserFollowedByUser(1, 2, function(error, result) {
+ test.ifError(error);
+ test.equals(result, true);
+ test.done();
+ });
+ },
+
+ testIsUserNotFollowedByUser: function(test) {
+ users.isUserFollowedByUser(2, 1, function(error, result) {
+ test.ifError(error);
+ test.equals(result, false);
+ test.done();
+ });
+ },
+
+ testFollowUser: function(test) {
+ users.followUser(3, 5, function(err1) {
+ test.ifError(err1);
+ users.isUserFollowedByUser(5, 3, function(err2, isFollowed1) {
+ test.ifError(err2);
+ test.equals(isFollowed1, true);
+ users.unfollowUser(3, 5, function(err3) {
+ test.ifError(err3);
+ users.isUserFollowedByUser(5, 3, function(err4, isFollowed2) {
+ test.ifError(err4);
+ test.equals(isFollowed2, false);
+ test.done();
+ });
+ });
+ });
+ });
+ },
+
+ testFollowOwnUser: function(test) {
+ users.followUser(5, 5, function(err1) {
+ test.notEqual(err1, null);
+ test.done();
+ });
+ },
+
+ testFollowingUserTwice: function(test) {
+ users.followUser(3, 5, function(err1) {
+ test.ifError(err1);
+ users.followUser(3, 5, function(err1) {
+ test.notEqual(err1, null);
+ users.unfollowUser(3, 5, function(err3) {
+ test.ifError(err3);
+ test.done();
+ });
+ });
+ });
+ },
+
+ testUnfollowOnUnfollowedUser: function(test) {
+ users.unfollowUser(5, 3, function(err1) {
+ test.ifError(err1);
+ test.done();
+ });
+ }
+});
diff --git a/views/personalFeed.html b/views/personalFeed.html
index 10d8070..c40040b 100755
--- a/views/personalFeed.html
+++ b/views/personalFeed.html
@@ -1,12 +1,25 @@
Activity Feed
-{{#content}}
+{{#feed}}
{{#isIssue}}
{{username}} posted an {{type}} {{howLongAgo}} ({{title}})
{{/isIssue}}
{{#isComment}}
{{username}} posted a {{type}} {{howLongAgo}} ({{comment}})
{{/isComment}}
-{{/content}}
+{{/feed}}
+{{#is_logged_in}}
+Followed Users
+
+ {{#follow_feed}}
+ {{#isIssue}}
+ {{username}} posted an {{type}} {{howLongAgo}} ({{title}})
+ {{/isIssue}}
+ {{#isComment}}
+ {{username}} posted a {{type}} {{howLongAgo}} ({{comment}})
+ {{/isComment}}
+ {{/follow_feed}}
+
+{{/is_logged_in}}
diff --git a/views/viewProfile.html b/views/viewProfile.html
index db9f6f3..2f8e7cb 100644
--- a/views/viewProfile.html
+++ b/views/viewProfile.html
@@ -25,6 +25,16 @@ Personal Information{{#is_loggedin}} Edit{{/is_log
{{website}}
{{/has_website}}
+ {{^is_viewing_own_profile}}
+ {{#is_loggedin}}
+ {{#is_followed}}
+ Unfollow User
+ {{/is_followed}}
+ {{^is_followed}}
+ Follow User
+ {{/is_followed}}
+ {{/is_loggedin}}
+ {{/is_viewing_own_profile}}
Issues
{{#has_issues}}
From 791f1ccdf49fb90fe674ba3e79f1b090cfddd1cf Mon Sep 17 00:00:00 2001
From: jewong
Date: Thu, 31 Mar 2011 21:35:20 -0600
Subject: [PATCH 2/2] Fix broken dbAccess sqlite require, fix view profile
---
node_modules/dbAccess.js | 2 +-
views/viewProfile.html | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/node_modules/dbAccess.js b/node_modules/dbAccess.js
index 31d2af1..3d30f82 100644
--- a/node_modules/dbAccess.js
+++ b/node_modules/dbAccess.js
@@ -17,7 +17,7 @@
* Example: .. values: 'name="Bob"' ...
*/
-var sqlite = require('../../node-sqlite/sqlite'),
+var sqlite = require('sqlite'),
//db = new sqlite.Database(),
qs = require('querystring'),
util = require('util'),
diff --git a/views/viewProfile.html b/views/viewProfile.html
index 8b116fd..608c297 100644
--- a/views/viewProfile.html
+++ b/views/viewProfile.html
@@ -92,7 +92,6 @@ Issues
This user is not an author of any issues.
{{/is_loggedin}}
{{/has_issues}}
-{{/found}}
{{^found}}
User not found!