From c5671550f11db74367c14b72d6d981dca16a7d28 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 28 Oct 2013 17:31:27 -0700 Subject: [PATCH] challenges: add group.memberCount and ability to specifiy field projections in GET request /api/v1/groups?type=guild&fields=name,description,chat --- .../20131028_task_subdocs_and_tags_cleanup.js | 5 + package.json | 2 +- public/js/services/groupServices.js | 2 +- src/controllers/groups.js | 97 +++++++++++-------- src/models/group.js | 4 + views/options/profile.jade | 6 +- 6 files changed, 71 insertions(+), 45 deletions(-) diff --git a/migrations/20131028_task_subdocs_and_tags_cleanup.js b/migrations/20131028_task_subdocs_and_tags_cleanup.js index f01cf967da..469d45a088 100644 --- a/migrations/20131028_task_subdocs_and_tags_cleanup.js +++ b/migrations/20131028_task_subdocs_and_tags_cleanup.js @@ -38,3 +38,8 @@ db.users.find().forEach(function(user){ // Remove old groups.*.challenges, they're not compatible with the new system db.groups.update({},{$pull:{challenges:1}},{multi:true}); +db.groups.find().forEach(function(group){ + db.groups.update({_id:group._id}, { + $set:{memberCount: _.size(group.members)} + }) +}); diff --git a/package.json b/package.json index 07a05f0925..56ecf29414 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lodash": "~1.3.1", "async": "~0.2.9", "optimist": "~0.5.2", - "mongoose": "~3.6.18", + "mongoose": "~3.6.20", "stylus": "~0.37.0", "grunt": "~0.4.1", "grunt-contrib-uglify": "~0.2.4", diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 47c1b610d8..8c6d9a4281 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -34,7 +34,7 @@ angular.module('groupServices', ['ngResource']). }; // But we don't defer triggering Party, since we always need it for the header if nothing else - Group.query({type:'party'}, function(_groups){ + Group.query({type:'party', fields:'members'}, function(_groups){ partyQ.resolve(_groups[0]); Members.populate(_groups[0]); }) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index cb36ffa7b0..3b563e047d 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -16,8 +16,8 @@ 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; function removeSelf(group, user){ group.members = _.filter(group.members, function(m){return m._id != user._id}); @@ -31,65 +31,83 @@ api.getMember = function(req, res) { }) } +function sanitizeMemberFields(fields, fallback) { + if (!fields) return fallback; + if (~fields.indexOf(',')) fields = fields.replace(/\,/g, ' '); + var intersection = _.intersection( (partyFields + ' items').split(' '), (fields).split(' ') ); + return _.isEmpty(_.filter(intersection, function(f){return f!=''})) ? fallback : intersection; +} + /** - * Get groups. If req.query.type privided, returned as an array (so ngResource can use). If not, returned as + * Get groups. If req.query.type provided, 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 + * + * We can request specific fields to keep things slim. Not specified gives you full detail. Request them as: + * - simple: ?fields=title,description,chat,challenges,memberCount + * - sub-fields: ?fields=title,description,members&fields.members=profile.name */ api.getGroups = function(req, res, next) { var user = res.locals.user; - - // if ?minimal=true, just send down names - if (req.query.minimal) { - return Group.find({members: {'$in': [user._id]}}).select('name _id').exec(function(err, groups){ - if (err) return res.json(500, {err:err}); - res.json(groups); - }); - } - var type = req.query.type && req.query.type.split(','); + var fields = req.query.fields && req.query.fields.replace(/\,/g, ' ') + var challengeFields = 'name description'; // public challenge fields, request specific challenge for more details // 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({ - path: 'members', - //match: {_id: {$ne: user._id}}, //fixme this causes it to hang?? - select: partyFields - }) - .exec(cb); + var query = Group.findOne({type: 'party', members: {'$in': [user._id]}}); + if (fields) { + query.select(fields); + if (~fields.indexOf('members')) + query.populate('members', sanitizeMemberFields(req.query['fields.members'], partyFields)); + if (~fields.indexOf('challenges')) + query.populate('challenges', challengeFields); + } + query.exec(function(err, group){ + if (err) return cb(err); + // Remove self from party (match in `populate()` hangs - mongoose bug) + removeSelf(group, user); + cb(null, group); + }); }, + guilds: function(cb) { - if (type && !~type.indexOf('guilds')) return cb(null, []); - Group.find({type: 'guild', members: {'$in': [user._id]}}).populate('members', usernameFields).exec(cb); -// Group.find({type: 'guild', members: {'$in': [user._id]}}, cb); + if (type && !~type.indexOf('guilds')) return cb(null, {}); + var query = Group.find({type: 'guild', members: {'$in': [user._id]}}); + if (fields) { + query.select(fields); + if (~fields.indexOf('members')) + query.populate('members', sanitizeMemberFields(req.query['fields.members'], 'profile.name')); + if (~fields.indexOf('challenges')) + query.populate('challenges', challengeFields); + } + query.exec(cb); }, + tavern: function(cb) { if (type && !~type.indexOf('tavern')) return cb(null, {}); - Group.findOne({_id: 'habitrpg'}, cb); + var query = Group.findById('habitrpg'); + if (fields) { + query.select(fields); + if (~fields.indexOf('challenges')) + query.populate('challenges', challengeFields); + } + query.exec(cb); }, + "public": function(cb) { if (type && !~type.indexOf('public')) return cb(null, []); - Group.find({privacy: 'public'}, {name:1, description:1, members:1}, cb); + Group.find({privacy: 'public'}) + .select('name description memberCount') + .sort('-memberCount') + .exec(cb); } + }, 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) { @@ -97,7 +115,6 @@ api.getGroups = function(req, res, next) { return m.concat(_.isArray(results[t]) ? results[t] : [results[t]]); }, []); } - res.json(results); }) }; @@ -157,7 +174,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 : 'profile.name'; Group.findById(group._id).populate('members', fields).exec(cb); } ], function(err, results){ diff --git a/src/models/group.js b/src/models/group.js index 6aed645bea..c297f31acc 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -26,6 +26,8 @@ var GroupSchema = new Schema({ # }] */ + memberCount: {type: Number, 'default': 0}, + challengeCount: {type: Number, 'default': 0}, balance: Number, logo: String, leaderMessage: String, @@ -59,6 +61,8 @@ function removeDuplicates(doc){ GroupSchema.pre('save', function(next){ removeDuplicates(this); + this.memberCount = _.size(this.members); + this.challengeCount = _.size(this.challenges); next(); }) diff --git a/views/options/profile.jade b/views/options/profile.jade index 8ee97c5fdb..7a223b6121 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -1,6 +1,6 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') .row-fluid - .span4.border-right + .span4 menu(type='list') // body-type li.customize-menu @@ -18,7 +18,7 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') button.f_armor_0_v1.customize-option(type='button', ng-click='set("preferences.armorSet","v1")') button.f_armor_0_v2.customize-option(type='button', ng-click='set("preferences.armorSet","v2")') - .span4.border-right + .span4 // hair li.customize-menu menu(label='Hair') @@ -28,7 +28,7 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') button(class='{{user.preferences.gender}}_hair_white customize-option', type='button', ng-click='set("preferences.hair","white")') button(class='{{user.preferences.gender}}_hair_red customize-option', type='button', ng-click='set("preferences.hair","red")') - .span4.border-right + .span4 // skin li.customize-menu menu(label='Basic Skins')