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:
@@ -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
@@ -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
@@ -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;
|
||||
});
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user