gMerge branch 'challenges' into develop

Conflicts:
	migrations/20131028_cleanup_deleted_tags.js
	src/controllers/groups.js
	views/options/groups/group.jade
	views/options/profile.jade
This commit is contained in:
Tyler Renelle
2013-10-29 15:38:47 -07:00
49 changed files with 1899 additions and 1424 deletions
+258
View File
@@ -0,0 +1,258 @@
// @see ../routes for routing
var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
var algos = require('habitrpg-shared/script/algos');
var helpers = require('habitrpg-shared/script/helpers');
var items = require('habitrpg-shared/script/items');
var User = require('./../models/user').model;
var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
var api = module.exports;
/*
------------------------------------------------------------------------
Challenges
------------------------------------------------------------------------
*/
// GET
api.get = function(req, res) {
var user = res.locals.user;
Challenge.find({$or:[{leader: user._id}, {members:{$in:[user._id]}}]})
.populate('members', 'profile.name habits dailys rewards todos')
.exec(function(err, challenges){
if(err) return res.json(500, {err:err});
// slim down the return members' tasks to only the ones in the challenge
_.each(challenges, function(challenge){
_.each(challenge.members, function(member){
_.each(['habits', 'dailys', 'todos', 'rewards'], function(type){
member[type] = _.where(member[type], function(task){
return task.challenge && task.challenge.id == challenge._id;
})
})
})
});
res.json(challenges);
})
}
// CREATE
api.create = function(req, res){
// FIXME sanitize
var challenge = new Challenge(req.body);
challenge.save(function(err, saved){
// Need to create challenge with refs (group, leader)? Or is this taken care of automatically?
// @see http://mongoosejs.com/docs/populate.html
if (err) return res.json(500, {err:err});
res.json(saved);
});
}
function keepAttrs(task) {
// only sync/compare important attrs
var keepAttrs = 'text notes up down priority repeat'.split(' ');
if (task.type=='reward') keepAttrs.push('value');
return _.pick(task, keepAttrs);
}
// UPDATE
api.update = function(req, res){
//FIXME sanitize
var cid = req.params.cid;
async.waterfall([
function(cb){
// We first need the original challenge data, since we're going to compare against new & decide to sync users
Challenge.findById(cid, cb);
},
function(chal, cb) {
// Update the challenge, and then just res.json success (note we're passing `cb` here).
// The syncing stuff is really heavy, and the client doesn't care - so we kick it off in the background
delete req.body._id;
Challenge.findByIdAndUpdate(cid, {$set:req.body}, cb);
// Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
function comparableData(obj) {
return (
_.chain(obj.habits.concat(obj.dailys).concat(obj.todos).concat(obj.rewards))
.sortBy('id') // we don't want to update if they're sort-order is different
.transform(function(result, task){
result.push(keepAttrs(task));
}))
.toString(); // for comparing arrays easily
}
if (comparableData(chal) !== comparableData(req.body)) {
User.find({_id: {$in: chal.members}}, function(err, users){
console.log('Challenge updated, sync to subscribers');
if (err) throw err;
_.each(users, function(user){
syncChalToUser(chal, user);
user.save();
})
})
}
}
], function(err, saved){
if(err) res.json(500, {err:err});
res.json(saved);
})
}
// DELETE
api['delete'] = function(req, res){
Challenge.findOneAndRemove({_id:req.params.cid}, function(err, removed){
if (err) return res.json(500, {err: err});
User.find({_id:{$in: removed.members}}, function(err, users){
if (err) throw err;
_.each(users, function(user){
_.each(user.tasks, function(task){
if (task.challenge && task.challenge.id == removed._id) {
task.challenge.broken = 'CHALLENGE_DELETED';
}
})
user.save();
})
})
})
}
/**
* Syncs all new tasks, deleted tasks, etc to the user object
* @param chal
* @param user
* @return nothing, user is modified directly. REMEMBER to save the user!
*/
var syncChalToUser = function(chal, user) {
if (!chal || !user) return;
// Sync tags
var tags = user.tags || [];
var i = _.findIndex(tags, {id: chal._id})
if (~i) {
if (tags[i].name !== chal.name) {
// update the name - it's been changed since
user.tags[i].name = chal.name;
}
} else {
user.tags.push({
id: chal._id,
name: chal.name,
challenge: true
});
}
tags = {};
tags[chal._id] = true;
// Sync new tasks and updated tasks
_.each(chal.tasks, function(task){
var type = task.type;
_.defaults(task, {tags: tags, challenge:{}});
_.defaults(task.challenge, {id:chal._id});
if (user.tasks[task.id]) {
_.merge(user.tasks[task.id], keepAttrs(task));
} else {
user[type+'s'].push(task);
}
})
// Flag deleted tasks as "broken"
_.each(user.tasks, function(task){
if (!chal.tasks[task.id]) task.challenge.broken = 'TASK_DELETED';
})
};
api.join = function(req, res){
var user = res.locals.user;
var cid = req.params.cid;
async.waterfall([
function(cb){
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb);
},
function(challenge, cb){
if (!~user.challenges.indexOf(cid))
user.challenges.unshift(cid);
// Add all challenge's tasks to user's tasks
syncChalToUser(challenge, user);
user.save(function(err){
if (err) return cb(err);
cb(null, challenge); // we want the saved challenge in the return results, due to ng-resource
});
}
], function(err, result){
if(err) return res.json(500,{err:err});
res.json(result);
});
}
function unlink(user, cid, keep, tid) {
switch (keep) {
case 'keep':
delete user.tasks[tid].challenge;
break;
case 'remove':
user[user.tasks[tid].type+'s'].id(tid).remove();
break;
case 'keep-all':
_.each(user.tasks, function(t){
if (t.challenge && t.challenge.id == cid) {
delete t.challenge;
}
});
break;
case 'remove-all':
_.each(user.tasks, function(t){
if (t.challenge && t.challenge.id == cid) {
user[t.type+'s'].id(t.id).remove();
}
})
break;
}
}
api.leave = function(req, res){
var user = res.locals.user;
var cid = req.params.cid;
// whether or not to keep challenge's tasks. strictly default to true if "keep-all" isn't provided
var keep = (/^remove-all/i).test(req.query.keep) ? 'remove-all' : 'keep-all';
async.waterfall([
function(cb){
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb);
},
function(chal, cb){
var i = user.challenges.indexOf(cid)
if (~i) user.challenges.splice(i,1);
unlink(user, chal._id, keep)
user.save(function(err){
if (err) return cb(err);
cb(null, chal);
})
}
], function(err, result){
if(err) return res.json(500,{err:err});
res.json(result);
});
}
api.unlink = function(req, res, next) {
// they're scoring the task - commented out, we probably don't need it due to route ordering in api.js
//var urlParts = req.originalUrl.split('/');
//if (_.contains(['up','down'], urlParts[urlParts.length -1])) return next();
var user = res.locals.user;
var tid = req.params.id;
var cid = user.tasks[tid].challenge.id;
if (!req.query.keep)
return res.json(400, {err: 'Provide unlink method as ?keep=keep-all (keep, keep-all, remove, remove-all)'});
unlink(user, cid, req.query.keep, tid);
user.save(function(err, saved){
if (err) return res.json(500,{err:err});
res.send(200);
});
}
+59 -60
View File
@@ -16,8 +16,9 @@ var api = module.exports;
------------------------------------------------------------------------
*/
var usernameFields = 'auth.local.username auth.facebook.displayName auth.facebook.givenName auth.facebook.familyName auth.facebook.name';
var partyFields = 'profile preferences items stats achievements party backer flags.rest auth.timestamps ' + usernameFields;
var itemFields = 'items.armor items.head items.shield items.weapon items.currentPet';
var partyFields = 'profile preferences stats achievements party backer flags.rest auth.timestamps ' + itemFields;
var nameFields = 'profile.name';
function removeSelf(group, user){
group.members = _.filter(group.members, function(m){return m._id != user._id});
@@ -32,83 +33,81 @@ api.getMember = function(req, res) {
}
/**
* Get groups. If req.query.type privided, returned as an array (so ngResource can use). If not, returned as
* object {guilds, public, party, tavern}. req.query.type can be comma-separated `type=guilds,party`
* @param req
* @param res
* @param next
* Fetch groups list. This no longer returns party or tavern, as those can be requested indivdually
* as /groups/party or /groups/tavern
*/
api.getGroups = function(req, res, next) {
api.getGroups = function(req, res) {
var user = res.locals.user;
var groupFields = 'name description memberCount';
var sort = '-memberCount';
var type = req.query.type && req.query.type.split(',');
// First get all groups
async.parallel({
party: function(cb) {
if (type && !~type.indexOf('party')) return cb(null, {});
Group
.findOne({type: 'party', members: {'$in': [user._id]}})
.populate('members invites', partyFields)
.exec(cb);
// unecessary given our ui-router setup
party: function(cb){
return cb(null, [{}]);
},
guilds: function(cb) {
if (type && !~type.indexOf('guilds')) return cb(null, []);
Group.find({type: 'guild', members: {'$in': [user._id]}}).populate('members invites', usernameFields).exec(cb);
// Group.find({type: 'guild', members: {'$in': [user._id]}}, cb);
Group.find({members: {'$in': [user._id]}, type:'guild'})
.select(groupFields).sort(sort).exec(cb);
},
'public': function(cb) {
Group.find({privacy: 'public'})
.select(groupFields + ' members')
.sort(sort)
.exec(function(err, groups){
if (err) return cb(err);
_.each(groups, function(g){
// To save some client-side performance, don't send down the full members arr, just send down temp var _isMember
if (~g.members.indexOf(user._id)) g._isMember = true;
g.members = undefined;
});
cb(null, groups);
});
},
// unecessary given our ui-router setup
tavern: function(cb) {
if (type && !~type.indexOf('tavern')) return cb(null, {});
Group.findOne({_id: 'habitrpg'}, cb);
},
"public": function(cb) {
if (type && !~type.indexOf('public')) return cb(null, []);
Group.find({privacy: 'public'}, {name:1, description:1, members:1}, cb);
return cb(null, [{}]);
}
}, function(err, results){
if (err) return res.json(500, {err: err});
// Remove self from party (see above failing `match` directive in `populate`
if (results.party) {
removeSelf(results.party, user);
}
// Sort public groups by members length (not easily doable in mongoose)
results.public = _.sortBy(results.public, function(group){
return -group.members.length;
});
// If they're requesting a specific type, let's return it as an array so that $ngResource
// can utilize it properly
if (type) {
results = _.reduce(type, function(m,t){
return m.concat(_.isArray(results[t]) ? results[t] : [results[t]]);
}, []);
}
res.json(results);
})
};
/**
* Get group
* TODO: implement requesting fields ?fields=chat,members
*/
api.getGroup = function(req, res, next) {
api.getGroup = function(req, res) {
var user = res.locals.user;
var gid = req.params.gid;
Group.findById(gid).populate('members invites', partyFields).exec(function(err, group){
if ( (group.type == 'guild' && group.privacy == 'private') || group.type == 'party') {
if(!_.find(group.members, {_id: user._id}))
return res.json(401, {err: "You don't have access to this group"});
}
// Remove self from party (see above failing `match` directive in `populate`
if (group.type == 'party') {
removeSelf(group, user);
}
res.json(group);
})
// This will be called for the header, we need extra members' details than usuals
if (gid == 'party') {
Group.findOne({type: 'party', members: {'$in': [user._id]}})
.populate('members invites', partyFields).exec(function(err, group){
if (err) return res.json(500,{err:err});
removeSelf(group, user);
res.json(group);
});
} else {
Group.findById(gid).populate('members invites', nameFields).exec(function(err, group){
if ( (group.type == 'guild' && group.privacy == 'private') || group.type == 'party') {
if(!_.find(group.members, {_id: user._id}))
return res.json(401, {err: "You don't have access to this group"});
}
// Remove self from party (see above failing `match` directive in `populate`
if (group.type == 'party') {
removeSelf(group, user);
}
res.json(group);
})
}
};
@@ -151,7 +150,7 @@ api.updateGroup = function(req, res, next) {
async.series([
function(cb){group.save(cb);},
function(cb){
var fields = group.type == 'party' ? partyFields : usernameFields;
var fields = group.type == 'party' ? partyFields : nameFields;
Group.findById(group._id).populate('members invites', fields).exec(cb);
}
], function(err, results){
@@ -178,7 +177,7 @@ api.postChat = function(req, res, next) {
contributor: user.backer && user.backer.contributor,
npc: user.backer && user.backer.npc,
text: req.query.message, // FIXME this should be body, but ngResource is funky
user: helpers.username(user.auth, user.profile.name),
user: user.profile.name,
timestamp: +(new Date)
};
+140 -307
View File
@@ -1,8 +1,5 @@
/* @see ./routes.coffee for routing*/
// fixme remove this junk, was coffeescript compiled (probably for IE8 compat)
var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
var url = require('url');
var ipn = require('paypal-ipn');
var _ = require('lodash');
@@ -16,6 +13,7 @@ var check = validator.check;
var sanitize = validator.sanitize;
var User = require('./../models/user').model;
var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
var api = module.exports;
// FIXME put this in a proper location
@@ -55,200 +53,121 @@ api.marketBuy = function(req, res, next){
---------------
*/
/*
// FIXME put this in helpers, so mobile & web can us it too
// FIXME actually, move to mongoose
*/
function taskSanitizeAndDefaults(task) {
var _ref;
if (task.id == null) {
task.id = helpers.uuid();
}
task.value = ~~task.value;
if (task.type == null) {
task.type = 'habit';
}
if (_.isString(task.text)) {
task.text = sanitize(task.text).xss();
}
if (_.isString(task.text)) {
task.notes = sanitize(task.notes).xss();
}
if (task.type === 'habit') {
if (!_.isBoolean(task.up)) {
task.up = true;
}
if (!_.isBoolean(task.down)) {
task.down = true;
}
}
if ((_ref = task.type) === 'daily' || _ref === 'todo') {
if (!_.isBoolean(task.completed)) {
task.completed = false;
}
}
if (task.type === 'daily') {
if (task.repeat == null) {
task.repeat = {
m: true,
t: true,
w: true,
th: true,
f: true,
s: true,
su: true
};
}
}
return task;
};
/*
Validate task
*/
api.verifyTaskExists = function(req, res, next) {
/* If we're updating, get the task from the user*/
var task;
task = res.locals.user.tasks[req.params.id];
if (_.isEmpty(task)) {
return res.json(400, {
err: "No task found."
});
}
// If we're updating, get the task from the user
var task = res.locals.user.tasks[req.params.id];
if (_.isEmpty(task)) return res.json(400, {err: "No task found."});
res.locals.task = task;
return next();
};
function addTask(user, task) {
taskSanitizeAndDefaults(task);
user.tasks[task.id] = task;
user["" + task.type + "Ids"].unshift(task.id);
return task;
};
/* Override current user.task with incoming values, then sanitize all values*/
function updateTask(user, id, incomingTask) {
return user.tasks[id] = taskSanitizeAndDefaults(_.defaults(incomingTask, user.tasks[id]));
};
function deleteTask(user, task) {
var i, ids;
delete user.tasks[task.id];
if ((ids = user["" + task.type + "Ids"]) && ~(i = ids.indexOf(task.id))) {
return ids.splice(i, 1);
}
user[task.type+'s'].id(task.id).remove();
};
function addTask(user, task) {
var type = task.type || 'habit'
user[type+'s'].unshift(task);
// FIXME will likely have to use taskSchema instead, so we can populate the defaults, add the _id, and return the added task
return user[task.type+'s'][0];
}
/*
API Routes
---------------
*/
var syncScoreToChallenge = function(task, delta){
if (!task.challenge || !task.challenge.id) return;
Challenge.findById(task.challenge.id, function(err, chal){
if (err) throw err;
var t = chal.tasks[task.id]
t.value += delta;
t.history.push({value: t.value, date: +new Date});
chal.save();
});
}
/**
This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login
Export it also so we can call it from deprecated.coffee
*/
api.scoreTask = function(req, res, next) {
// FIXME this is all uglified from coffeescript compile, clean this up
var delta, direction, existing, id, task, user, _ref, _ref1, _ref2, _ref3, _ref4;
_ref = req.params, id = _ref.id, direction = _ref.direction;
var id = req.params.id,
direction = req.params.direction,
user = res.locals.user,
task;
// Send error responses for improper API call
if (!id) {
return res.json(500, {
err: ':id required'
});
}
if (!id) return res.json(500, {err: ':id required'});
if (direction !== 'up' && direction !== 'down') {
return res.json(500, {
err: ":direction must be 'up' or 'down'"
});
if (direction == 'unlink') return next();
return res.json(500, {err: ":direction must be 'up' or 'down'"});
}
user = res.locals.user;
/* If exists already, score it*/
if ((existing = user.tasks[id])) {
/* Set completed if type is daily or todo and task exists*/
if ((_ref1 = existing.type) === 'daily' || _ref1 === 'todo') {
// If exists already, score it
var existing;
if (existing = user.tasks[id]) {
// Set completed if type is daily or todo and task exists
if (existing.type === 'daily' || existing.type === 'todo') {
existing.completed = direction === 'up';
}
} else {
/* If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it*/
// If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it
task = {
id: id,
value: 0,
type: ((_ref2 = req.body) != null ? _ref2.type : void 0) || 'habit',
text: ((_ref3 = req.body) != null ? _ref3.title : void 0) || id,
type: req.body.type || 'habit',
text: req.body.title || id,
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
};
if (task.type === 'habit') {
task.up = task.down = true;
}
if ((_ref4 = task.type) === 'daily' || _ref4 === 'todo') {
if (task.type === 'daily' || task.type === 'todo') {
task.completed = direction === 'up';
}
addTask(user, task);
}
task = user.tasks[id];
delta = algos.score(user, task, direction);
//user.markModified('flags');
var delta = algos.score(user, task, direction);
//user.markModified('flags');
user.save(function(err, saved) {
if (err) return res.json(500, {err: err});
res.json(200, _.extend({
delta: delta
}, saved.toJSON().stats));
});
// if it's a challenge task, sync the score
syncScoreToChallenge(task, delta);
};
/*
Get all tasks
*/
/**
* Get all tasks
*/
api.getTasks = function(req, res, next) {
var tasks, types, _ref;
types = (_ref = req.query.type) === 'habit' || _ref === 'todo' || _ref === 'daily' || _ref === 'reward' ? [req.query.type] : ['habit', 'todo', 'daily', 'reward'];
tasks = _.toArray(_.filter(res.locals.user.tasks, function(t) {
var _ref1;
return _ref1 = t.type, __indexOf.call(types, _ref1) >= 0;
}));
return res.json(200, tasks);
if (req.query.type) {
return res.json(user[req.query.type+'s']);
} else {
return res.json(_.toArray(user.tasks));
}
};
/*
Get Task
*/
/**
* Get Task
*/
api.getTask = function(req, res, next) {
var task;
task = res.locals.user.tasks[req.params.id];
if (_.isEmpty(task)) {
return res.json(400, {
err: "No task found."
});
}
var task = res.locals.user.tasks[req.params.id];
if (_.isEmpty(task)) return res.json(400, {err: "No task found."});
return res.json(200, task);
};
/*
Delete Task
*/
/**
* Delete Task
*/
api.deleteTask = function(req, res, next) {
deleteTask(res.locals.user, res.locals.task);
res.locals.user.save(function(err) {
@@ -263,113 +182,69 @@ api.deleteTask = function(req, res, next) {
api.updateTask = function(req, res, next) {
var id, user;
user = res.locals.user;
id = req.params.id;
updateTask(user, id, req.body);
return user.save(function(err, saved) {
if (err) {
return res.json(500, {
err: err
});
}
return res.json(200, _.findWhere(saved.toJSON().tasks, {
id: id
}));
var user = res.locals.user;
var task = user.tasks[req.params.id];
user[task.type+'s'][_.findIndex(user[task.type+'s'],{id:task.id})] = req.body;
user.save(function(err, saved) {
if (err) return res.json(500, {err: err})
return res.json(200, saved.tasks[id]);
});
};
/*
Update tasks (plural). This will update, add new, delete, etc all at once.
Should we keep this?
*/
/**
* Update tasks (plural). This will update, add new, delete, etc all at once.
* TODO Should we keep this?
*/
api.updateTasks = function(req, res, next) {
var tasks, user;
user = res.locals.user;
tasks = req.body;
var user = res.locals.user;
var tasks = req.body;
_.each(tasks, function(task, idx) {
if (task.id) {
/*delete*/
// delete
if (task.del) {
deleteTask(user, task);
task = {
deleted: true
};
task = {deleted: true};
} else {
/* Update*/
updateTask(user, task.id, task);
// Update
// updateTask(user, task.id, task); //FIXME
}
} else {
/* Create*/
// Create
task = addTask(user, task);
}
return tasks[idx] = task;
tasks[idx] = task;
});
return user.save(function(err, saved) {
if (err) {
return res.json(500, {
err: err
});
}
user.save(function(err, saved) {
if (err) return res.json(500, {err: err});
return res.json(201, tasks);
});
};
api.createTask = function(req, res, next) {
var task, user;
user = res.locals.user;
task = addTask(user, req.body);
return user.save(function(err) {
if (err) {
return res.json(500, {
err: err
});
}
var user = res.locals.user;
var task = addTask(user, req.body);
user.save(function(err, saved) {
if (err) return res.json(500, {err: err});
return res.json(201, task);
});
};
api.sortTask = function(req, res, next) {
var from, id, path, to, type, user, _ref;
id = req.params.id;
_ref = req.body, to = _ref.to, from = _ref.from, type = _ref.type;
user = res.locals.user;
path = "" + type + "Ids";
user[path].splice(to, 0, user[path].splice(from, 1)[0]);
return user.save(function(err, saved) {
if (err) {
return res.json(500, {
err: err
});
}
return res.json(200, saved.toJSON()[path]);
var id = req.params.id;
var to = req.body.to, from = req.body.from, type = req.body.type;
var user = res.locals.user;
user[type+'s'].splice(to, 0, user[type+'s'].splice(from, 1)[0]);
user.save(function(err, saved) {
if (err) return res.json(500, {err: err});
return res.json(200, saved.toJSON()[type+'s']);
});
};
api.clearCompleted = function(req, res, next) {
var completedIds, todoIds, user;
user = res.locals.user;
completedIds = _.pluck(_.where(user.tasks, {
type: 'todo',
completed: true
}), 'id');
todoIds = user.todoIds;
_.each(completedIds, function(id) {
delete user.tasks[id];
return true;
});
user.todoIds = _.difference(todoIds, completedIds);
var user = res.locals.user;
user.todos = _.where(user.todos, {completed: false});
return user.save(function(err, saved) {
if (err) {
return res.json(500, {
err: err
});
}
if (err) return res.json(500, {err: err});
return res.json(saved);
});
};
@@ -379,31 +254,21 @@ api.clearCompleted = function(req, res, next) {
Items
------------------------------------------------------------------------
*/
api.buy = function(req, res, next) {
var hasEnough, type, user;
user = res.locals.user;
type = req.params.type;
if (type !== 'weapon' && type !== 'armor' && type !== 'head' && type !== 'shield' && type !== 'potion') {
return res.json(400, {
err: ":type must be in one of: 'weapon', 'armor', 'head', 'shield', 'potion'"
});
return res.json(400, {err: ":type must be in one of: 'weapon', 'armor', 'head', 'shield', 'potion'"});
}
hasEnough = items.buyItem(user, type);
if (hasEnough) {
return user.save(function(err, saved) {
if (err) {
return res.json(500, {
err: err
});
}
if (err) return res.json(500, {err: err});
return res.json(200, saved.toJSON().items);
});
} else {
return res.json(200, {
err: "Not enough GP"
});
return res.json(200, {err: "Not enough GP"});
}
};
@@ -413,11 +278,9 @@ api.buy = function(req, res, next) {
------------------------------------------------------------------------
*/
/*
Get User
*/
/**
* Get User
*/
api.getUser = function(req, res, next) {
var user = res.locals.user.toJSON();
user.stats.toNextLevel = algos.tnl(user.stats.lvl);
@@ -430,12 +293,10 @@ api.getUser = function(req, res, next) {
return res.json(200, user);
};
/*
Update user
FIXME add documentation here
/**
* Update user
* FIXME add documentation here
*/
api.updateUser = function(req, res, next) {
var acceptableAttrs, errors, user;
user = res.locals.user;
@@ -483,8 +344,7 @@ api.updateUser = function(req, res, next) {
};
api.cron = function(req, res, next) {
var user;
user = res.locals.user;
var user = res.locals.user;
algos.cron(user);
if (user.isModified()) {
res.locals.wasModified = true;
@@ -494,52 +354,36 @@ api.cron = function(req, res, next) {
};
api.revive = function(req, res, next) {
var user;
user = res.locals.user;
var user = res.locals.user;
algos.revive(user);
return user.save(function(err, saved) {
if (err) {
return res.json(500, {
err: err
});
}
user.save(function(err, saved) {
if (err) return res.json(500, {err: err});
return res.json(200, saved);
});
};
api.reroll = function(req, res, next) {
var user;
user = res.locals.user;
if (user.balance < 1) {
return res.json(401, {
err: "Not enough tokens."
});
}
var user = res.locals.user;
if (user.balance < 1) return res.json(401, {err: "Not enough tokens."});
user.balance -= 1;
_.each(user.tasks, function(task) {
if (task.type !== 'reward') {
user.tasks[task.id].value = 0;
}
return true;
});
_.each(['habits','dailys','todos'], function(type){
_.each([user[type+'s']], function(task){
task.value = 0;
})
})
user.stats.hp = 50;
return user.save(function(err, saved) {
if (err) {
return res.json(500, {
err: err
});
}
user.save(function(err, saved) {
if (err) return res.json(500, {err: err});
return res.json(200, saved);
});
};
api.reset = function(req, res){
var user = res.locals.user;
user.tasks = {};
_.each(['habit', 'daily', 'todo', 'reward'], function(type) {
user[type + "Ids"] = [];
});
user.habits = [];
user.dailys = [];
user.todos = [];
user.rewards = [];
user.stats.hp = 50;
user.stats.lvl = 1;
@@ -675,9 +519,11 @@ api.deleteTag = function(req, res){
delete user.filters[tag.id];
user.tags.splice(i,1);
// remove tag from all tasks
_.each(user.tasks, function(task) {
delete user.tasks[task.id].tags[tag.id];
});
_.each(['habits','dailys','todos','rewards'], function(type){
_.each(user[type], function(task){
delete task.tags[tag.id];
})
})
user.save(function(err,saved){
if (err) return res.json(500, {err: err});
// Need to use this until we found a way to update the ui for tasks when a tag is deleted
@@ -695,22 +541,16 @@ api.deleteTag = function(req, res){
Run a bunch of updates all at once
------------------------------------------------------------------------
*/
api.batchUpdate = function(req, res, next) {
var actions, oldJson, oldSend, performAction, user, _ref;
user = res.locals.user;
oldSend = res.send;
oldJson = res.json;
performAction = function(action, cb) {
/*
# TODO come up with a more consistent approach here. like:
# req.body=action.data; delete action.data; _.defaults(req.params, action)
# Would require changing action.dir on mobile app
*/
var user = res.locals.user;
var oldSend = res.send;
var oldJson = res.json;
var performAction = function(action, cb) {
var _ref;
req.params.id = (_ref = action.data) != null ? _ref.id : void 0;
// TODO come up with a more consistent approach here. like:
// req.body=action.data; delete action.data; _.defaults(req.params, action)
// Would require changing action.dir on mobile app
req.params.id = action.data && action.data.id;
req.params.direction = action.dir;
req.params.type = action.type;
req.body = action.data;
@@ -764,27 +604,22 @@ api.batchUpdate = function(req, res, next) {
break;
}
};
/* Setup the array of functions we're going to call in parallel with async*/
actions = _.transform((_ref = req.body) != null ? _ref : [], function(result, action) {
// Setup the array of functions we're going to call in parallel with async
var actions = _.transform(req.body || [], function(result, action) {
if (!_.isEmpty(action)) {
return result.push(function(cb) {
return performAction(action, cb);
result.push(function(cb) {
performAction(action, cb);
});
}
});
/* call all the operations, then return the user object to the requester*/
return async.series(actions, function(err) {
var response;
// call all the operations, then return the user object to the requester
async.series(actions, function(err) {
res.json = oldJson;
res.send = oldSend;
if (err) {
return res.json(500, {
err: err
});
}
response = user.toJSON();
if (err) return res.json(500, {err: err});
var response = user.toJSON();
response.wasModified = res.locals.wasModified;
if (response._tmp && response._tmp.drop) response.wasModified = true;
@@ -794,7 +629,5 @@ api.batchUpdate = function(req, res, next) {
}else{
res.json(200, {_v: response._v});
}
return;
});
};
+34
View File
@@ -0,0 +1,34 @@
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var helpers = require('habitrpg-shared/script/helpers');
var _ = require('lodash');
var TaskSchema = require('./task').schema;
var ChallengeSchema = new Schema({
_id: {type: String, 'default': helpers.uuid},
name: String,
description: String,
habits: [TaskSchema],
dailys: [TaskSchema],
todos: [TaskSchema],
rewards: [TaskSchema],
leader: {type: String, ref: 'User'},
group: {type: String, ref: 'Group'},
// FIXME remove below, we don't need it since every time we load a challenge, we'll load it with the group ref. we don't need to look up challenges by type
//type: group.type, //type: {type: String,"enum": ['guild', 'party']},
//id: group._id
//},
timestamp: {type: Date, 'default': Date.now},
members: [{type: String, ref: 'User'}]
}, {
minimize: 'false'
});
ChallengeSchema.virtual('tasks').get(function () {
var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
var tasks = _.object(_.pluck(tasks,'id'), tasks);
return tasks;
});
module.exports.schema = ChallengeSchema;
module.exports.model = mongoose.model("Challenge", ChallengeSchema);
+14 -32
View File
@@ -7,37 +7,14 @@ var GroupSchema = new Schema({
_id: {type: String, 'default': helpers.uuid},
name: String,
description: String,
leader: {
type: String,
ref: 'User'
},
members: [
{
type: String,
ref: 'User'
}
],
invites: [
{
type: String,
ref: 'User'
}
],
type: {
type: String,
"enum": ['guild', 'party']
},
privacy: {
type: String,
"enum": ['private', 'public']
},
_v: {
Number: Number,
'default': 0
},
leader: {type: String, ref: 'User'},
members: [{type: String, ref: 'User'}],
invites: [{type: String, ref: 'User'}],
type: {type: String, "enum": ['guild', 'party']},
privacy: {type: String, "enum": ['private', 'public']},
_v: {Number: Number,'default': 0},
websites: Array,
chat: Array,
/*
# [{
# timestamp: Date
@@ -49,15 +26,17 @@ var GroupSchema = new Schema({
# }]
*/
memberCount: {type: Number, 'default': 0},
challengeCount: {type: Number, 'default': 0},
balance: Number,
logo: String,
leaderMessage: String
leaderMessage: String,
challenges: [{type:'String', ref:'Challenge'}]
}, {
strict: 'throw',
strict: 'throw',
minimize: false // So empty objects are returned
});
/**
* Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration
* to remove duplicates, then take these fucntions out
@@ -81,12 +60,15 @@ function removeDuplicates(doc){
GroupSchema.pre('save', function(next){
removeDuplicates(this);
this.memberCount = _.size(this.members);
this.challengeCount = _.size(this.challenges);
next();
})
GroupSchema.methods.toJSON = function(){
var doc = this.toObject();
removeDuplicates(doc);
doc._isMember = this._isMember;
return doc;
}
+48
View File
@@ -0,0 +1,48 @@
// User.js
// =======
// Defines the user data model (schema) for use via the API.
// Dependencies
// ------------
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var helpers = require('habitrpg-shared/script/helpers');
var _ = require('lodash');
// Task Schema
// -----------
var TaskSchema = new Schema({
history: [{date:Date, value:Number}],
_id:{type: String,'default': helpers.uuid},
text: String,
notes: {type: String, 'default': ''},
tags: {type: Schema.Types.Mixed, 'default': {}}, //{ "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true },
type: {type:String, 'default': 'habit'}, // habit, daily
up: {type: Boolean, 'default': true},
down: {type: Boolean, 'default': true},
value: {type: Number, 'default': 0},
completed: {type: Boolean, 'default': false},
priority: {type: String, 'default': '!'}, //'!!' // FIXME this should be a number or something
repeat: {type: Schema.Types.Mixed, 'default': {m:1, t:1, w:1, th:1, f:1, s:1, su:1} },
streak: {type: Number, 'default': 0},
challenge: {
id: {type: 'String', ref:'Challenge'},
broken: String // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, etc
// group: {type: 'Strign', ref: 'Group'} // if we restore this, rename `id` above to `challenge`
}
}, {
minimize: 'false'
});
TaskSchema.methods.toJSON = function() {
var doc = this.toObject();
doc.id = doc._id;
return doc;
}
TaskSchema.virtual('id').get(function(){
return this._id;
})
module.exports.schema = TaskSchema;
module.exports.model = mongoose.model("Task", TaskSchema);
+29 -55
View File
@@ -8,6 +8,7 @@ var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var helpers = require('habitrpg-shared/script/helpers');
var _ = require('lodash');
var TaskSchema = require('./task').schema;
// User Schema
// -----------
@@ -63,10 +64,6 @@ var UserSchema = new Schema({
},
balance: Number,
habitIds: Array,
dailyIds: Array,
todoIds: Array,
rewardIds: Array,
filters: {type: Schema.Types.Mixed, 'default': {}},
purchased: {
@@ -204,79 +201,56 @@ var UserSchema = new Schema({
}
],
// ### Tasks Definition
// We can't define `tasks` until we move off Derby, since Derby requires dictionary of objects. When we're off, migrate
// to array of subdocs
challenges: [{type: 'String', ref:'Challenge'}],
tasks: Schema.Types.Mixed
/*
# history: {date, value}
# id
# notes
# tags { "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true },
# text
# type
# up
# down
# value
# completed
# priority: '!!'
# repeat {m: true, t: true}
# streak
*/
habits: [TaskSchema],
dailys: [TaskSchema],
todos: [TaskSchema],
rewards: [TaskSchema],
}, {
strict: true,
minimize: false // So empty objects are returned
});
// Legacy Derby Function?
// ----------------------
// Derby requires a strange storage format for somethign called "refLists". Here we hook into loading the data, so we
// can provide a more "expected" storage format for our various helper methods. Since the attributes are passed by reference,
// the underlying data will be modified too - so when we save back to the database, it saves it in the way Derby likes.
// This will go away after the rewrite is complete
function transformTaskLists(doc) {
_.each(['habit', 'daily', 'todo', 'reward'], function(type) {
// we use _.transform instead of a simple _.where in order to maintain sort-order
doc[type + "s"] = _.reduce(doc[type + "Ids"], function(m, tid) {
if (!doc.tasks[tid]) return m; // FIXME tmp hotfix, people still have null tasks?
if (!doc.tasks[tid].tags) doc.tasks[tid].tags = {}; // FIXME remove this when we switch tasks to subdocs and can define tags default in schema
m.push(doc.tasks[tid]);
return m;
}, []);
});
}
UserSchema.post('init', function(doc) {
transformTaskLists(doc);
});
UserSchema.methods.toJSON = function() {
var doc = this.toObject();
doc.id = doc._id;
transformTaskLists(doc); // we need to also transform for our server-side routes
// FIXME? Is this a reference to `doc.filters` or just disabled code? Remove?
/*
// Remove some unecessary data as far as client consumers are concerned
//_.each(['habit', 'daily', 'todo', 'reward'], function(type) {
// delete doc["#{type}Ids"]
//});
//delete doc.tasks
*/
doc.filters = {};
doc._tmp = this._tmp; // be sure to send down drop notifs
// TODO why isnt' this happening automatically given the TaskSchema.methods.toJSON above?
_.each(['habits','dailys','todos','rewards'], function(type){
_.each(doc[type],function(task){
task.id = task._id;
})
})
return doc;
};
UserSchema.virtual('tasks').get(function () {
var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
var tasks = _.object(_.pluck(tasks,'id'), tasks);
return tasks;
});
// FIXME - since we're using special @post('init') above, we need to flag when the original path was modified.
// Custom setter/getter virtuals?
UserSchema.pre('save', function(next) {
this.markModified('tasks');
//this.markModified('tasks');
if (!this.profile.name) {
var fb = this.auth.facebook;
this.profile.name =
(this.auth.local && this.auth.local.username) ||
(fb && (fb.displayName || fb.name || fb.username || (fb.first_name && fb.first_name + ' ' + fb.last_name))) ||
'Anonymous';
}
//our own version incrementer
this._v++;
next();
+13
View File
@@ -3,6 +3,7 @@ var router = new express.Router();
var user = require('../controllers/user');
var groups = require('../controllers/groups');
var auth = require('../controllers/auth');
var challenges = require('../controllers/challenges');
/*
---------- /api/v1 API ------------
@@ -36,6 +37,7 @@ router["delete"]('/user/task/:id', auth.auth, cron, verifyTaskExists, user.delet
router.post('/user/task', auth.auth, cron, user.createTask);
router.put('/user/task/:id/sort', auth.auth, cron, verifyTaskExists, user.sortTask);
router.post('/user/clear-completed', auth.auth, cron, user.clearCompleted);
router.post('/user/task/:id/unlink', auth.auth, challenges.unlink); // removing cron since they may want to remove task first
/* Items*/
router.post('/user/buy/:type', auth.auth, cron, user.buy);
@@ -78,4 +80,15 @@ router.get('/members/:uid', groups.getMember);
// Market
router.post('/market/buy', auth.auth, user.marketBuy);
/* Challenges */
// Note: while challenges belong to groups, and would therefore make sense as a nested resource
// (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab
// without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource
router.get('/challenges', auth.auth, challenges.get)
router.post('/challenges', auth.auth, challenges.create)
router.post('/challenges/:cid', auth.auth, challenges.update)
router['delete']('/challenges/:cid', auth.auth, challenges['delete'])
router.post('/challenges/:cid/join', auth.auth, challenges.join)
router.post('/challenges/:cid/leave', auth.auth, challenges.leave)
module.exports = router;
-8
View File
@@ -11,14 +11,6 @@ router.get('/', function(req, res) {
});
});
router.get('/partials/tasks', function(req, res) {
res.render('tasks/index', {env: res.locals.habitrpg});
});
router.get('/partials/options', function(req, res) {
res.render('options', {env: res.locals.habitrpg});
});
// -------- Marketing --------
router.get('/splash.html', function(req, res) {
+1
View File
@@ -27,6 +27,7 @@ process.on("uncaughtException", function(error) {
mongoose = require('mongoose');
require('./models/user'); //load up the user schema - TODO is this necessary?
require('./models/group');
require('./models/challenge');
mongoose.connect(nconf.get('NODE_DB_URI'), function(err) {
if (err) throw err;
console.info('Connected with Mongoose');