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!