From bf2801946582ec6621fe726448f5fd3d1472f818 Mon Sep 17 00:00:00 2001 From: Vera Pavel Date: Thu, 11 Sep 2014 11:09:12 -0400 Subject: [PATCH 1/3] making export to csv export everything (issue #2947) --- src/controllers/challenges.js | 403 +--------------------------------- 1 file changed, 1 insertion(+), 402 deletions(-) diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index 9545f4adae..0dda1ff90e 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -1,402 +1 @@ -// @see ../routes for routing - -var _ = require('lodash'); -var nconf = require('nconf'); -var async = require('async'); -var shared = require('habitrpg-shared'); -var User = require('./../models/user').model; -var Group = require('./../models/group').model; -var Challenge = require('./../models/challenge').model; -var logging = require('./../logging'); -var csv = require('express-csv'); -var api = module.exports; - - -/* - ------------------------------------------------------------------------ - Challenges - ------------------------------------------------------------------------ -*/ - -api.list = function(req, res, next) { - var user = res.locals.user; - async.waterfall([ - function(cb){ - // Get all available groups I belong to - Group.find({members: {$in: [user._id]}}).select('_id').exec(cb); - }, - function(gids, cb){ - // and their challenges - Challenge.find({ - $or:[ - {leader: user._id}, - {members:{$in:[user._id]}}, // all challenges I belong to (is this necessary? thought is a left a group, but not its challenge) - {group:{$in:gids}}, // all challenges in my groups - {group: 'habitrpg'} // public group - ] - }) - .select('name leader description group memberCount prize official') - .select({members:{$elemMatch:{$in:[user._id]}}}) - .populate('group', '_id name') - .populate('leader', 'profile.name') - .sort('-official -timestamp') - .exec(cb); - } - ], function(err, challenges){ - if (err) return next(err); - _.each(challenges, function(c){ - c._isMember = c.members.length > 0; - }) - res.json(challenges); - }); -} - -// GET -api.get = function(req, res, next) { - // TODO use mapReduce() or aggregate() here to - // 1) Find the sum of users.tasks.values within the challnege (eg, {'profile.name':'tyler', 'sum': 100}) - // 2) Sort by the sum - // 3) Limit 30 (only show the 30 users currently in the lead) - Challenge.findById(req.params.cid) - .populate('members', 'profile.name _id') - .exec(function(err, challenge){ - if(err) return next(err); - if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'}); - res.json(challenge); - }) -} - -api.csv = function(req, res, next) { - var cid = req.params.cid; - var challenge; - async.waterfall([ - function(cb){ - Challenge.findById(cid,cb) - }, - function(_challenge,cb) { - challenge = _challenge; - if (!challenge) return cb('Challenge ' + cid + ' not found'); - var elemMatch = {$elemMatch:{'challenge.id':cid}}; - User.find( - {_id:{'$in':challenge.members}}, - {todos:elemMatch,habits:elemMatch,dailys:elemMatch,rewards:elemMatch,_id:1,'profile.name':1}, - cb); - } - ],function(err,users){ - if(err) return next(err); - var output = ['UUID','name']; - _.each(challenge.tasks,function(t){ - output.push(t.type+':'+t.text); - output.push('Value'); - output.push('Notes'); - }) - output = [output]; - _.each(users, function(u){ - var uData = [u._id,u.profile.name]; - _.each(u.tasks,function(t){ - uData = uData.concat(['', t.value, t.notes]); - }) - output.push(uData); - }); - res.header('Content-disposition', 'attachment; filename='+cid+'.csv'); - res.csv(output); - }) -} - -api.getMember = function(req, res, next) { - var cid = req.params.cid, uid = req.params.uid; - User.findById(uid) - .select('profile.name habits dailys todos rewards') - .exec(function(err, member){ - if(err) return next(err); - if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'}); - - // Prune un-related tasks. See http://goo.gl/9fnwei - // See http://stackoverflow.com/a/18546277/362790 for fix - _.each(['habits','dailys','todos', 'rewards'], function(type){ - member[type] = _.filter(member[type], function(task){ - return task.challenge && task.challenge.id && task.challenge.id == cid; - }); - }); - - res.json(member); - }) -} - -// CREATE -api.create = function(req, res, next){ - var user = res.locals.user; - var group, chal; - - // First, make sure they've selected a legit group, and store it for later - var waterfall = [ - function(cb){ - Group.findById(req.body.group).exec(cb); - }, - function(_group, cb){ - if (!_group) return cb("Group." + req.body.group + " not found"); - group = _group; - cb(null); - } - ]; - - // If they're adding a prize, do some validation - if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'}); - if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'}); - if (+req.body.prize > 0) { - waterfall.push(function(cb){ - var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0); - var prizeCost = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid... - if (prizeCost > user.balance + groupBalance) - return cb("You can't afford this prize. Purchase more gems or lower the prize amount.") - - if (groupBalance >= prizeCost) { - // Group pays for all of prize - group.balance -= prizeCost; - } else if (groupBalance > 0) { - // User pays remainder of prize cost after group - var remainder = prizeCost - group.balance; - group.balance = 0; - user.balance -= remainder; - } else { - // User pays for all of prize - user.balance -= prizeCost; - } - cb(null) - }); - } - - waterfall = waterfall.concat([ - function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb` - req.body.leader = user._id; - req.body.official = user.contributor.admin && req.body.official; - var chal = new Challenge(req.body); // FIXME sanitize - chal.members.push(user._id); - chal.save(cb) - }, - function(_chal, num, cb){ - chal = _chal; - group.challenges.push(chal._id); - group.save(cb); - }, - function(_group, num, cb) { - // Auto-join creator to challenge (see members.push above) - chal.syncToUser(user, cb); - } - ]); - async.waterfall(waterfall, function(err){ - if (err) return next(err); - res.json(chal); - }); -} - -// UPDATE -api.update = function(req, res, next){ - var cid = req.params.cid; - var user = res.locals.user; - var before; - 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(_before, cb) { - if (!_before) return cb('Challenge ' + cid + ' not found'); - if (_before.leader != user._id) return cb("You don't have permissions to edit this challenge"); - // Update the challenge, since syncing will need the updated challenge. But store `before` we're going to do some - // before-save / after-save comparison to determine if we need to sync to users - before = _before; - var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' ')); - Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb); - }, - function(saved, cb) { - // after saving, we're done as far as the client's concerned. We kick of syncing (heavy task) in the background - cb(null, saved); - - // Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers - if (before.isOutdated(req.body)) { - User.find({_id: {$in: saved.members}}, function(err, users){ - logging.info('Challenge updated, sync to subscribers'); - if (err) throw err; - _.each(users, function(user){ - saved.syncToUser(user); - }) - }) - } - - } - ], function(err, saved){ - if(err) next(err); - res.json(saved); - }) -} - -/** - * Called by either delete() or selectWinner(). Will delete the challenge and set the "broken" property on all users' subscribed tasks - * @param {cid} the challenge id - * @param {broken} the object representing the broken status of the challenge. Eg: - * {broken: 'CHALLENGE_DELETED', id: CHALLENGE_ID} - * {broken: 'CHALLENGE_CLOSED', id: CHALLENGE_ID, winner: USER_NAME} - */ -function closeChal(cid, broken, cb) { - var removed; - async.waterfall([ - function(cb2){ - Challenge.findOneAndRemove({_id:cid}, cb2) - }, - function(_removed, cb2) { - removed = _removed; - var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1; - Group.findByIdAndUpdate(_removed.group, pull); - User.find({_id:{$in: removed.members}}, cb2); - }, - function(users, cb2) { - var parallel = []; - _.each(users, function(user){ - var tag = _.find(user.tags, {id:cid}); - if (tag) tag.challenge = undefined; - _.each(user.tasks, function(task){ - if (task.challenge && task.challenge.id == removed._id) { - _.merge(task.challenge, broken); - } - }) - parallel.push(function(cb3){ - user.save(cb3); - }) - }) - async.parallel(parallel, cb2); - } - ], cb); -} - -/** - * Delete & close - */ -api['delete'] = function(req, res, next){ - var user = res.locals.user; - var cid = req.params.cid; - async.waterfall([ - function(cb){ - Challenge.findById(cid, cb); - }, - function(chal, cb){ - if (!chal) return cb('Challenge ' + cid + ' not found'); - if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge"); - closeChal(req.params.cid, {broken: 'CHALLENGE_DELETED'}, cb); - } - ], function(err){ - if (err) return next(err); - res.send(200); - }); -} - -/** - * Select Winner & Close - */ -api.selectWinner = function(req, res, next) { - if (!req.query.uid) return res.json(401, {err: 'Must select a winner'}); - var user = res.locals.user; - var cid = req.params.cid; - var chal; - async.waterfall([ - function(cb){ - Challenge.findById(cid, cb); - }, - function(_chal, cb){ - chal = _chal; - if (!chal) return cb('Challenge ' + cid + ' not found'); - if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge"); - User.findById(req.query.uid, cb) - }, - function(winner, cb){ - if (!winner) return cb('Winner ' + req.query.uid + ' not found.'); - _.defaults(winner.achievements, {challenges: []}); - winner.achievements.challenges.push(chal.name); - winner.balance += chal.prize/4; - winner.save(cb); - }, - function(saved, num, cb) { - closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb); - } - ], function(err){ - if (err) return next(err); - res.send(200); - }) -} - -api.join = function(req, res, next){ - var user = res.locals.user; - var cid = req.params.cid; - - async.waterfall([ - function(cb) { - Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb); - }, - function(chal, cb) { - - // Trigger updating challenge member count in the background. We can't do it above because we don't have - // _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above. - Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec(); - - if (!~user.challenges.indexOf(cid)) - user.challenges.unshift(cid); - // Add all challenge's tasks to user's tasks - chal.syncToUser(user, function(err){ - if (err) return cb(err); - cb(null, chal); // we want the saved challenge in the return results, due to ng-resource - }); - } - ], function(err, chal){ - if(err) return next(err); - chal._isMember = true; - res.json(chal); - }); -} - - -api.leave = function(req, res, next){ - 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){ - - // Trigger updating challenge member count in the background. We can't do it above because we don't have - // _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above. - if (chal) - Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec(); - - var i = user.challenges.indexOf(cid) - if (~i) user.challenges.splice(i,1); - user.unlink({cid:cid, keep:keep}, function(err){ - if (err) return cb(err); - cb(null, chal); - }) - } - ], function(err, chal){ - if(err) return next(err); - if (chal) chal._isMember = false; - res.json(chal); - }); -} - -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)'}); - user.unlink({cid:cid, keep:req.query.keep, tid:tid}, function(err, saved){ - if (err) return next(err); - res.send(200); - }); -} +// @see ../routes for routing var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); var shared = require('habitrpg-shared'); var User = require('./../models/user').model; var Group = require('./../models/group').model; var Challenge = require('./../models/challenge').model; var logging = require('./../logging'); var csv = require('express-csv'); var api = module.exports; /* ------------------------------------------------------------------------ Challenges ------------------------------------------------------------------------ */ api.list = function(req, res, next) { var user = res.locals.user; async.waterfall([ function(cb){ // Get all available groups I belong to Group.find({members: {$in: [user._id]}}).select('_id').exec(cb); }, function(gids, cb){ // and their challenges Challenge.find({ $or:[ {leader: user._id}, {members:{$in:[user._id]}}, // all challenges I belong to (is this necessary? thought is a left a group, but not its challenge) {group:{$in:gids}}, // all challenges in my groups {group: 'habitrpg'} // public group ] }) .select('name leader description group memberCount prize official') .select({members:{$elemMatch:{$in:[user._id]}}}) .populate('group', '_id name') .populate('leader', 'profile.name') .sort('-official -timestamp') .exec(cb); } ], function(err, challenges){ if (err) return next(err); _.each(challenges, function(c){ c._isMember = c.members.length > 0; }) res.json(challenges); }); } // GET api.get = function(req, res, next) { // TODO use mapReduce() or aggregate() here to // 1) Find the sum of users.tasks.values within the challnege (eg, {'profile.name':'tyler', 'sum': 100}) // 2) Sort by the sum // 3) Limit 30 (only show the 30 users currently in the lead) Challenge.findById(req.params.cid) .populate('members', 'profile.name _id') .exec(function(err, challenge){ if(err) return next(err); if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'}); res.json(challenge); }) } api.csv = function(req, res, next) { var cid = req.params.cid; var challenge; async.waterfall([ function(cb){ Challenge.findById(cid,cb) }, function(_challenge,cb) { challenge = _challenge; if (!challenge) return cb('Challenge ' + cid + ' not found'); User.aggregate([ {$match:{'_id':{ '$in': challenge.members}}}, //yes, we want members {$project:{'profile.name':1,tasks:{$setUnion:["$habits","$dailys","$todos","$rewards"]}}}, {$unwind:"$tasks"}, {$match:{"tasks.challenge.id":cid}}, {$sort:{'tasks.type':1,'tasks.id':1}}, {$group:{_id:"$_id", "tasks":{$push:"$tasks"},"name":{$first:"$profile.name"}}} ], cb); } ],function(err,users){ if(err) return next(err); var output = ['UUID','name']; _.each(challenge.tasks,function(t){ //output.push(t.id+':'+t.type+':'+t.text); //don't have the order right yet output.push('Task') output.push('Value'); output.push('Notes'); }) output = [output]; _.each(users, function(u){ var uData = [u._id,u.name]; _.each(u.tasks,function(t){ uData = uData.concat([t.type+':'+t.text, t.value, t.notes]); }) output.push(uData); }); res.header('Content-disposition', 'attachment; filename='+cid+'.csv'); res.csv(output); }) } api.getMember = function(req, res, next) { var cid = req.params.cid, uid = req.params.uid; User.findById(uid) .select('profile.name habits dailys todos rewards') .exec(function(err, member){ if(err) return next(err); if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'}); // Prune un-related tasks. See http://goo.gl/9fnwei // See http://stackoverflow.com/a/18546277/362790 for fix _.each(['habits','dailys','todos', 'rewards'], function(type){ member[type] = _.filter(member[type], function(task){ return task.challenge && task.challenge.id && task.challenge.id == cid; }); }); res.json(member); }) } // CREATE api.create = function(req, res, next){ var user = res.locals.user; var group, chal; // First, make sure they've selected a legit group, and store it for later var waterfall = [ function(cb){ Group.findById(req.body.group).exec(cb); }, function(_group, cb){ if (!_group) return cb("Group." + req.body.group + " not found"); group = _group; cb(null); } ]; // If they're adding a prize, do some validation if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'}); if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'}); if (+req.body.prize > 0) { waterfall.push(function(cb){ var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0); var prizeCost = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid... if (prizeCost > user.balance + groupBalance) return cb("You can't afford this prize. Purchase more gems or lower the prize amount.") if (groupBalance >= prizeCost) { // Group pays for all of prize group.balance -= prizeCost; } else if (groupBalance > 0) { // User pays remainder of prize cost after group var remainder = prizeCost - group.balance; group.balance = 0; user.balance -= remainder; } else { // User pays for all of prize user.balance -= prizeCost; } cb(null) }); } waterfall = waterfall.concat([ function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb` req.body.leader = user._id; req.body.official = user.contributor.admin && req.body.official; var chal = new Challenge(req.body); // FIXME sanitize chal.members.push(user._id); chal.save(cb) }, function(_chal, num, cb){ chal = _chal; group.challenges.push(chal._id); group.save(cb); }, function(_group, num, cb) { // Auto-join creator to challenge (see members.push above) chal.syncToUser(user, cb); } ]); async.waterfall(waterfall, function(err){ if (err) return next(err); res.json(chal); }); } // UPDATE api.update = function(req, res, next){ var cid = req.params.cid; var user = res.locals.user; var before; 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(_before, cb) { if (!_before) return cb('Challenge ' + cid + ' not found'); if (_before.leader != user._id) return cb("You don't have permissions to edit this challenge"); // Update the challenge, since syncing will need the updated challenge. But store `before` we're going to do some // before-save / after-save comparison to determine if we need to sync to users before = _before; var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' ')); Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb); }, function(saved, cb) { // after saving, we're done as far as the client's concerned. We kick of syncing (heavy task) in the background cb(null, saved); // Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers if (before.isOutdated(req.body)) { User.find({_id: {$in: saved.members}}, function(err, users){ logging.info('Challenge updated, sync to subscribers'); if (err) throw err; _.each(users, function(user){ saved.syncToUser(user); }) }) } } ], function(err, saved){ if(err) next(err); res.json(saved); }) } /** * Called by either delete() or selectWinner(). Will delete the challenge and set the "broken" property on all users' subscribed tasks * @param {cid} the challenge id * @param {broken} the object representing the broken status of the challenge. Eg: * {broken: 'CHALLENGE_DELETED', id: CHALLENGE_ID} * {broken: 'CHALLENGE_CLOSED', id: CHALLENGE_ID, winner: USER_NAME} */ function closeChal(cid, broken, cb) { var removed; async.waterfall([ function(cb2){ Challenge.findOneAndRemove({_id:cid}, cb2) }, function(_removed, cb2) { removed = _removed; var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1; Group.findByIdAndUpdate(_removed.group, pull); User.find({_id:{$in: removed.members}}, cb2); }, function(users, cb2) { var parallel = []; _.each(users, function(user){ var tag = _.find(user.tags, {id:cid}); if (tag) tag.challenge = undefined; _.each(user.tasks, function(task){ if (task.challenge && task.challenge.id == removed._id) { _.merge(task.challenge, broken); } }) parallel.push(function(cb3){ user.save(cb3); }) }) async.parallel(parallel, cb2); } ], cb); } /** * Delete & close */ api['delete'] = function(req, res, next){ var user = res.locals.user; var cid = req.params.cid; async.waterfall([ function(cb){ Challenge.findById(cid, cb); }, function(chal, cb){ if (!chal) return cb('Challenge ' + cid + ' not found'); if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge"); closeChal(req.params.cid, {broken: 'CHALLENGE_DELETED'}, cb); } ], function(err){ if (err) return next(err); res.send(200); }); } /** * Select Winner & Close */ api.selectWinner = function(req, res, next) { if (!req.query.uid) return res.json(401, {err: 'Must select a winner'}); var user = res.locals.user; var cid = req.params.cid; var chal; async.waterfall([ function(cb){ Challenge.findById(cid, cb); }, function(_chal, cb){ chal = _chal; if (!chal) return cb('Challenge ' + cid + ' not found'); if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge"); User.findById(req.query.uid, cb) }, function(winner, cb){ if (!winner) return cb('Winner ' + req.query.uid + ' not found.'); _.defaults(winner.achievements, {challenges: []}); winner.achievements.challenges.push(chal.name); winner.balance += chal.prize/4; winner.save(cb); }, function(saved, num, cb) { closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb); } ], function(err){ if (err) return next(err); res.send(200); }) } api.join = function(req, res, next){ var user = res.locals.user; var cid = req.params.cid; async.waterfall([ function(cb) { Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb); }, function(chal, cb) { // Trigger updating challenge member count in the background. We can't do it above because we don't have // _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above. Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec(); if (!~user.challenges.indexOf(cid)) user.challenges.unshift(cid); // Add all challenge's tasks to user's tasks chal.syncToUser(user, function(err){ if (err) return cb(err); cb(null, chal); // we want the saved challenge in the return results, due to ng-resource }); } ], function(err, chal){ if(err) return next(err); chal._isMember = true; res.json(chal); }); } api.leave = function(req, res, next){ 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){ // Trigger updating challenge member count in the background. We can't do it above because we don't have // _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above. if (chal) Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec(); var i = user.challenges.indexOf(cid) if (~i) user.challenges.splice(i,1); user.unlink({cid:cid, keep:keep}, function(err){ if (err) return cb(err); cb(null, chal); }) } ], function(err, chal){ if(err) return next(err); if (chal) chal._isMember = false; res.json(chal); }); } 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)'}); user.unlink({cid:cid, keep:req.query.keep, tid:tid}, function(err, saved){ if (err) return next(err); res.send(200); }); } \ No newline at end of file From 35a83517393b7f60bf0b9621c079ee0427c76a70 Mon Sep 17 00:00:00 2001 From: Vera Pavel Date: Fri, 12 Sep 2014 17:51:22 -0400 Subject: [PATCH 2/3] Revert "making export to csv export everything (issue #2947)" This reverts commit bf2801946582ec6621fe726448f5fd3d1472f818. --- src/controllers/challenges.js | 403 +++++++++++++++++++++++++++++++++- 1 file changed, 402 insertions(+), 1 deletion(-) diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index 0dda1ff90e..9545f4adae 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -1 +1,402 @@ -// @see ../routes for routing var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); var shared = require('habitrpg-shared'); var User = require('./../models/user').model; var Group = require('./../models/group').model; var Challenge = require('./../models/challenge').model; var logging = require('./../logging'); var csv = require('express-csv'); var api = module.exports; /* ------------------------------------------------------------------------ Challenges ------------------------------------------------------------------------ */ api.list = function(req, res, next) { var user = res.locals.user; async.waterfall([ function(cb){ // Get all available groups I belong to Group.find({members: {$in: [user._id]}}).select('_id').exec(cb); }, function(gids, cb){ // and their challenges Challenge.find({ $or:[ {leader: user._id}, {members:{$in:[user._id]}}, // all challenges I belong to (is this necessary? thought is a left a group, but not its challenge) {group:{$in:gids}}, // all challenges in my groups {group: 'habitrpg'} // public group ] }) .select('name leader description group memberCount prize official') .select({members:{$elemMatch:{$in:[user._id]}}}) .populate('group', '_id name') .populate('leader', 'profile.name') .sort('-official -timestamp') .exec(cb); } ], function(err, challenges){ if (err) return next(err); _.each(challenges, function(c){ c._isMember = c.members.length > 0; }) res.json(challenges); }); } // GET api.get = function(req, res, next) { // TODO use mapReduce() or aggregate() here to // 1) Find the sum of users.tasks.values within the challnege (eg, {'profile.name':'tyler', 'sum': 100}) // 2) Sort by the sum // 3) Limit 30 (only show the 30 users currently in the lead) Challenge.findById(req.params.cid) .populate('members', 'profile.name _id') .exec(function(err, challenge){ if(err) return next(err); if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'}); res.json(challenge); }) } api.csv = function(req, res, next) { var cid = req.params.cid; var challenge; async.waterfall([ function(cb){ Challenge.findById(cid,cb) }, function(_challenge,cb) { challenge = _challenge; if (!challenge) return cb('Challenge ' + cid + ' not found'); User.aggregate([ {$match:{'_id':{ '$in': challenge.members}}}, //yes, we want members {$project:{'profile.name':1,tasks:{$setUnion:["$habits","$dailys","$todos","$rewards"]}}}, {$unwind:"$tasks"}, {$match:{"tasks.challenge.id":cid}}, {$sort:{'tasks.type':1,'tasks.id':1}}, {$group:{_id:"$_id", "tasks":{$push:"$tasks"},"name":{$first:"$profile.name"}}} ], cb); } ],function(err,users){ if(err) return next(err); var output = ['UUID','name']; _.each(challenge.tasks,function(t){ //output.push(t.id+':'+t.type+':'+t.text); //don't have the order right yet output.push('Task') output.push('Value'); output.push('Notes'); }) output = [output]; _.each(users, function(u){ var uData = [u._id,u.name]; _.each(u.tasks,function(t){ uData = uData.concat([t.type+':'+t.text, t.value, t.notes]); }) output.push(uData); }); res.header('Content-disposition', 'attachment; filename='+cid+'.csv'); res.csv(output); }) } api.getMember = function(req, res, next) { var cid = req.params.cid, uid = req.params.uid; User.findById(uid) .select('profile.name habits dailys todos rewards') .exec(function(err, member){ if(err) return next(err); if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'}); // Prune un-related tasks. See http://goo.gl/9fnwei // See http://stackoverflow.com/a/18546277/362790 for fix _.each(['habits','dailys','todos', 'rewards'], function(type){ member[type] = _.filter(member[type], function(task){ return task.challenge && task.challenge.id && task.challenge.id == cid; }); }); res.json(member); }) } // CREATE api.create = function(req, res, next){ var user = res.locals.user; var group, chal; // First, make sure they've selected a legit group, and store it for later var waterfall = [ function(cb){ Group.findById(req.body.group).exec(cb); }, function(_group, cb){ if (!_group) return cb("Group." + req.body.group + " not found"); group = _group; cb(null); } ]; // If they're adding a prize, do some validation if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'}); if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'}); if (+req.body.prize > 0) { waterfall.push(function(cb){ var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0); var prizeCost = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid... if (prizeCost > user.balance + groupBalance) return cb("You can't afford this prize. Purchase more gems or lower the prize amount.") if (groupBalance >= prizeCost) { // Group pays for all of prize group.balance -= prizeCost; } else if (groupBalance > 0) { // User pays remainder of prize cost after group var remainder = prizeCost - group.balance; group.balance = 0; user.balance -= remainder; } else { // User pays for all of prize user.balance -= prizeCost; } cb(null) }); } waterfall = waterfall.concat([ function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb` req.body.leader = user._id; req.body.official = user.contributor.admin && req.body.official; var chal = new Challenge(req.body); // FIXME sanitize chal.members.push(user._id); chal.save(cb) }, function(_chal, num, cb){ chal = _chal; group.challenges.push(chal._id); group.save(cb); }, function(_group, num, cb) { // Auto-join creator to challenge (see members.push above) chal.syncToUser(user, cb); } ]); async.waterfall(waterfall, function(err){ if (err) return next(err); res.json(chal); }); } // UPDATE api.update = function(req, res, next){ var cid = req.params.cid; var user = res.locals.user; var before; 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(_before, cb) { if (!_before) return cb('Challenge ' + cid + ' not found'); if (_before.leader != user._id) return cb("You don't have permissions to edit this challenge"); // Update the challenge, since syncing will need the updated challenge. But store `before` we're going to do some // before-save / after-save comparison to determine if we need to sync to users before = _before; var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' ')); Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb); }, function(saved, cb) { // after saving, we're done as far as the client's concerned. We kick of syncing (heavy task) in the background cb(null, saved); // Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers if (before.isOutdated(req.body)) { User.find({_id: {$in: saved.members}}, function(err, users){ logging.info('Challenge updated, sync to subscribers'); if (err) throw err; _.each(users, function(user){ saved.syncToUser(user); }) }) } } ], function(err, saved){ if(err) next(err); res.json(saved); }) } /** * Called by either delete() or selectWinner(). Will delete the challenge and set the "broken" property on all users' subscribed tasks * @param {cid} the challenge id * @param {broken} the object representing the broken status of the challenge. Eg: * {broken: 'CHALLENGE_DELETED', id: CHALLENGE_ID} * {broken: 'CHALLENGE_CLOSED', id: CHALLENGE_ID, winner: USER_NAME} */ function closeChal(cid, broken, cb) { var removed; async.waterfall([ function(cb2){ Challenge.findOneAndRemove({_id:cid}, cb2) }, function(_removed, cb2) { removed = _removed; var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1; Group.findByIdAndUpdate(_removed.group, pull); User.find({_id:{$in: removed.members}}, cb2); }, function(users, cb2) { var parallel = []; _.each(users, function(user){ var tag = _.find(user.tags, {id:cid}); if (tag) tag.challenge = undefined; _.each(user.tasks, function(task){ if (task.challenge && task.challenge.id == removed._id) { _.merge(task.challenge, broken); } }) parallel.push(function(cb3){ user.save(cb3); }) }) async.parallel(parallel, cb2); } ], cb); } /** * Delete & close */ api['delete'] = function(req, res, next){ var user = res.locals.user; var cid = req.params.cid; async.waterfall([ function(cb){ Challenge.findById(cid, cb); }, function(chal, cb){ if (!chal) return cb('Challenge ' + cid + ' not found'); if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge"); closeChal(req.params.cid, {broken: 'CHALLENGE_DELETED'}, cb); } ], function(err){ if (err) return next(err); res.send(200); }); } /** * Select Winner & Close */ api.selectWinner = function(req, res, next) { if (!req.query.uid) return res.json(401, {err: 'Must select a winner'}); var user = res.locals.user; var cid = req.params.cid; var chal; async.waterfall([ function(cb){ Challenge.findById(cid, cb); }, function(_chal, cb){ chal = _chal; if (!chal) return cb('Challenge ' + cid + ' not found'); if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge"); User.findById(req.query.uid, cb) }, function(winner, cb){ if (!winner) return cb('Winner ' + req.query.uid + ' not found.'); _.defaults(winner.achievements, {challenges: []}); winner.achievements.challenges.push(chal.name); winner.balance += chal.prize/4; winner.save(cb); }, function(saved, num, cb) { closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb); } ], function(err){ if (err) return next(err); res.send(200); }) } api.join = function(req, res, next){ var user = res.locals.user; var cid = req.params.cid; async.waterfall([ function(cb) { Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb); }, function(chal, cb) { // Trigger updating challenge member count in the background. We can't do it above because we don't have // _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above. Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec(); if (!~user.challenges.indexOf(cid)) user.challenges.unshift(cid); // Add all challenge's tasks to user's tasks chal.syncToUser(user, function(err){ if (err) return cb(err); cb(null, chal); // we want the saved challenge in the return results, due to ng-resource }); } ], function(err, chal){ if(err) return next(err); chal._isMember = true; res.json(chal); }); } api.leave = function(req, res, next){ 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){ // Trigger updating challenge member count in the background. We can't do it above because we don't have // _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above. if (chal) Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec(); var i = user.challenges.indexOf(cid) if (~i) user.challenges.splice(i,1); user.unlink({cid:cid, keep:keep}, function(err){ if (err) return cb(err); cb(null, chal); }) } ], function(err, chal){ if(err) return next(err); if (chal) chal._isMember = false; res.json(chal); }); } 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)'}); user.unlink({cid:cid, keep:req.query.keep, tid:tid}, function(err, saved){ if (err) return next(err); res.send(200); }); } \ No newline at end of file +// @see ../routes for routing + +var _ = require('lodash'); +var nconf = require('nconf'); +var async = require('async'); +var shared = require('habitrpg-shared'); +var User = require('./../models/user').model; +var Group = require('./../models/group').model; +var Challenge = require('./../models/challenge').model; +var logging = require('./../logging'); +var csv = require('express-csv'); +var api = module.exports; + + +/* + ------------------------------------------------------------------------ + Challenges + ------------------------------------------------------------------------ +*/ + +api.list = function(req, res, next) { + var user = res.locals.user; + async.waterfall([ + function(cb){ + // Get all available groups I belong to + Group.find({members: {$in: [user._id]}}).select('_id').exec(cb); + }, + function(gids, cb){ + // and their challenges + Challenge.find({ + $or:[ + {leader: user._id}, + {members:{$in:[user._id]}}, // all challenges I belong to (is this necessary? thought is a left a group, but not its challenge) + {group:{$in:gids}}, // all challenges in my groups + {group: 'habitrpg'} // public group + ] + }) + .select('name leader description group memberCount prize official') + .select({members:{$elemMatch:{$in:[user._id]}}}) + .populate('group', '_id name') + .populate('leader', 'profile.name') + .sort('-official -timestamp') + .exec(cb); + } + ], function(err, challenges){ + if (err) return next(err); + _.each(challenges, function(c){ + c._isMember = c.members.length > 0; + }) + res.json(challenges); + }); +} + +// GET +api.get = function(req, res, next) { + // TODO use mapReduce() or aggregate() here to + // 1) Find the sum of users.tasks.values within the challnege (eg, {'profile.name':'tyler', 'sum': 100}) + // 2) Sort by the sum + // 3) Limit 30 (only show the 30 users currently in the lead) + Challenge.findById(req.params.cid) + .populate('members', 'profile.name _id') + .exec(function(err, challenge){ + if(err) return next(err); + if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'}); + res.json(challenge); + }) +} + +api.csv = function(req, res, next) { + var cid = req.params.cid; + var challenge; + async.waterfall([ + function(cb){ + Challenge.findById(cid,cb) + }, + function(_challenge,cb) { + challenge = _challenge; + if (!challenge) return cb('Challenge ' + cid + ' not found'); + var elemMatch = {$elemMatch:{'challenge.id':cid}}; + User.find( + {_id:{'$in':challenge.members}}, + {todos:elemMatch,habits:elemMatch,dailys:elemMatch,rewards:elemMatch,_id:1,'profile.name':1}, + cb); + } + ],function(err,users){ + if(err) return next(err); + var output = ['UUID','name']; + _.each(challenge.tasks,function(t){ + output.push(t.type+':'+t.text); + output.push('Value'); + output.push('Notes'); + }) + output = [output]; + _.each(users, function(u){ + var uData = [u._id,u.profile.name]; + _.each(u.tasks,function(t){ + uData = uData.concat(['', t.value, t.notes]); + }) + output.push(uData); + }); + res.header('Content-disposition', 'attachment; filename='+cid+'.csv'); + res.csv(output); + }) +} + +api.getMember = function(req, res, next) { + var cid = req.params.cid, uid = req.params.uid; + User.findById(uid) + .select('profile.name habits dailys todos rewards') + .exec(function(err, member){ + if(err) return next(err); + if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'}); + + // Prune un-related tasks. See http://goo.gl/9fnwei + // See http://stackoverflow.com/a/18546277/362790 for fix + _.each(['habits','dailys','todos', 'rewards'], function(type){ + member[type] = _.filter(member[type], function(task){ + return task.challenge && task.challenge.id && task.challenge.id == cid; + }); + }); + + res.json(member); + }) +} + +// CREATE +api.create = function(req, res, next){ + var user = res.locals.user; + var group, chal; + + // First, make sure they've selected a legit group, and store it for later + var waterfall = [ + function(cb){ + Group.findById(req.body.group).exec(cb); + }, + function(_group, cb){ + if (!_group) return cb("Group." + req.body.group + " not found"); + group = _group; + cb(null); + } + ]; + + // If they're adding a prize, do some validation + if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'}); + if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'}); + if (+req.body.prize > 0) { + waterfall.push(function(cb){ + var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0); + var prizeCost = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid... + if (prizeCost > user.balance + groupBalance) + return cb("You can't afford this prize. Purchase more gems or lower the prize amount.") + + if (groupBalance >= prizeCost) { + // Group pays for all of prize + group.balance -= prizeCost; + } else if (groupBalance > 0) { + // User pays remainder of prize cost after group + var remainder = prizeCost - group.balance; + group.balance = 0; + user.balance -= remainder; + } else { + // User pays for all of prize + user.balance -= prizeCost; + } + cb(null) + }); + } + + waterfall = waterfall.concat([ + function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb` + req.body.leader = user._id; + req.body.official = user.contributor.admin && req.body.official; + var chal = new Challenge(req.body); // FIXME sanitize + chal.members.push(user._id); + chal.save(cb) + }, + function(_chal, num, cb){ + chal = _chal; + group.challenges.push(chal._id); + group.save(cb); + }, + function(_group, num, cb) { + // Auto-join creator to challenge (see members.push above) + chal.syncToUser(user, cb); + } + ]); + async.waterfall(waterfall, function(err){ + if (err) return next(err); + res.json(chal); + }); +} + +// UPDATE +api.update = function(req, res, next){ + var cid = req.params.cid; + var user = res.locals.user; + var before; + 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(_before, cb) { + if (!_before) return cb('Challenge ' + cid + ' not found'); + if (_before.leader != user._id) return cb("You don't have permissions to edit this challenge"); + // Update the challenge, since syncing will need the updated challenge. But store `before` we're going to do some + // before-save / after-save comparison to determine if we need to sync to users + before = _before; + var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' ')); + Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb); + }, + function(saved, cb) { + // after saving, we're done as far as the client's concerned. We kick of syncing (heavy task) in the background + cb(null, saved); + + // Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers + if (before.isOutdated(req.body)) { + User.find({_id: {$in: saved.members}}, function(err, users){ + logging.info('Challenge updated, sync to subscribers'); + if (err) throw err; + _.each(users, function(user){ + saved.syncToUser(user); + }) + }) + } + + } + ], function(err, saved){ + if(err) next(err); + res.json(saved); + }) +} + +/** + * Called by either delete() or selectWinner(). Will delete the challenge and set the "broken" property on all users' subscribed tasks + * @param {cid} the challenge id + * @param {broken} the object representing the broken status of the challenge. Eg: + * {broken: 'CHALLENGE_DELETED', id: CHALLENGE_ID} + * {broken: 'CHALLENGE_CLOSED', id: CHALLENGE_ID, winner: USER_NAME} + */ +function closeChal(cid, broken, cb) { + var removed; + async.waterfall([ + function(cb2){ + Challenge.findOneAndRemove({_id:cid}, cb2) + }, + function(_removed, cb2) { + removed = _removed; + var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1; + Group.findByIdAndUpdate(_removed.group, pull); + User.find({_id:{$in: removed.members}}, cb2); + }, + function(users, cb2) { + var parallel = []; + _.each(users, function(user){ + var tag = _.find(user.tags, {id:cid}); + if (tag) tag.challenge = undefined; + _.each(user.tasks, function(task){ + if (task.challenge && task.challenge.id == removed._id) { + _.merge(task.challenge, broken); + } + }) + parallel.push(function(cb3){ + user.save(cb3); + }) + }) + async.parallel(parallel, cb2); + } + ], cb); +} + +/** + * Delete & close + */ +api['delete'] = function(req, res, next){ + var user = res.locals.user; + var cid = req.params.cid; + async.waterfall([ + function(cb){ + Challenge.findById(cid, cb); + }, + function(chal, cb){ + if (!chal) return cb('Challenge ' + cid + ' not found'); + if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge"); + closeChal(req.params.cid, {broken: 'CHALLENGE_DELETED'}, cb); + } + ], function(err){ + if (err) return next(err); + res.send(200); + }); +} + +/** + * Select Winner & Close + */ +api.selectWinner = function(req, res, next) { + if (!req.query.uid) return res.json(401, {err: 'Must select a winner'}); + var user = res.locals.user; + var cid = req.params.cid; + var chal; + async.waterfall([ + function(cb){ + Challenge.findById(cid, cb); + }, + function(_chal, cb){ + chal = _chal; + if (!chal) return cb('Challenge ' + cid + ' not found'); + if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge"); + User.findById(req.query.uid, cb) + }, + function(winner, cb){ + if (!winner) return cb('Winner ' + req.query.uid + ' not found.'); + _.defaults(winner.achievements, {challenges: []}); + winner.achievements.challenges.push(chal.name); + winner.balance += chal.prize/4; + winner.save(cb); + }, + function(saved, num, cb) { + closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb); + } + ], function(err){ + if (err) return next(err); + res.send(200); + }) +} + +api.join = function(req, res, next){ + var user = res.locals.user; + var cid = req.params.cid; + + async.waterfall([ + function(cb) { + Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb); + }, + function(chal, cb) { + + // Trigger updating challenge member count in the background. We can't do it above because we don't have + // _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above. + Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec(); + + if (!~user.challenges.indexOf(cid)) + user.challenges.unshift(cid); + // Add all challenge's tasks to user's tasks + chal.syncToUser(user, function(err){ + if (err) return cb(err); + cb(null, chal); // we want the saved challenge in the return results, due to ng-resource + }); + } + ], function(err, chal){ + if(err) return next(err); + chal._isMember = true; + res.json(chal); + }); +} + + +api.leave = function(req, res, next){ + 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){ + + // Trigger updating challenge member count in the background. We can't do it above because we don't have + // _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above. + if (chal) + Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec(); + + var i = user.challenges.indexOf(cid) + if (~i) user.challenges.splice(i,1); + user.unlink({cid:cid, keep:keep}, function(err){ + if (err) return cb(err); + cb(null, chal); + }) + } + ], function(err, chal){ + if(err) return next(err); + if (chal) chal._isMember = false; + res.json(chal); + }); +} + +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)'}); + user.unlink({cid:cid, keep:req.query.keep, tid:tid}, function(err, saved){ + if (err) return next(err); + res.send(200); + }); +} From 70a409cb4a1dc2414576367bf68bdbc54bee7895 Mon Sep 17 00:00:00 2001 From: Vera Pavel Date: Fri, 12 Sep 2014 20:44:51 -0400 Subject: [PATCH 3/3] fix challenge export to csv to export all tasks (issue #2947) --- src/controllers/challenges.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index 9545f4adae..b4ed368245 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -76,25 +76,31 @@ api.csv = function(req, res, next) { function(_challenge,cb) { challenge = _challenge; if (!challenge) return cb('Challenge ' + cid + ' not found'); - var elemMatch = {$elemMatch:{'challenge.id':cid}}; - User.find( - {_id:{'$in':challenge.members}}, - {todos:elemMatch,habits:elemMatch,dailys:elemMatch,rewards:elemMatch,_id:1,'profile.name':1}, - cb); + User.aggregate([ + {$match:{'_id':{ '$in': challenge.members}}}, //yes, we want members + {$project:{'profile.name':1,tasks:{$setUnion:["$habits","$dailys","$todos","$rewards"]}}}, + {$unwind:"$tasks"}, + {$match:{"tasks.challenge.id":cid}}, + {$sort:{'tasks.type':1,'tasks.id':1}}, + {$group:{_id:"$_id", "tasks":{$push:"$tasks"},"name":{$first:"$profile.name"}}} + + ], cb); } ],function(err,users){ if(err) return next(err); var output = ['UUID','name']; _.each(challenge.tasks,function(t){ - output.push(t.type+':'+t.text); + //output.push(t.type+':'+t.text); + //not the right order yet + output.push('Task'); output.push('Value'); output.push('Notes'); }) output = [output]; _.each(users, function(u){ - var uData = [u._id,u.profile.name]; + var uData = [u._id,u.name]; _.each(u.tasks,function(t){ - uData = uData.concat(['', t.value, t.notes]); + uData = uData.concat([t.type+':'+t.text, t.value, t.notes]); }) output.push(uData); });