From d9d769a0e1c036c102ee7a86ecebfa69fe92f06a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 29 Oct 2013 15:25:50 -0700 Subject: [PATCH] challenges: much better implemntation of ui-router for nested groups. No need to specify fields or populate members at /groups - instead, provide that functionality at /groups/:gid --- public/js/app.js | 22 +++-- public/js/controllers/groupsCtrl.js | 1 - public/js/services/groupServices.js | 28 +++---- src/controllers/groups.js | 124 ++++++++++------------------ views/options/groups/index.jade | 76 +++++++++-------- 5 files changed, 110 insertions(+), 141 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index aa7f899e8d..b6a5e14a97 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -16,6 +16,7 @@ window.habitrpg = angular.module('habitrpg', .when('/options', '/options/profile/avatar') .when('/options/profile', '/options/profile/avatar') .when('/options/groups', '/options/groups/tavern') + .when('/options/groups/guilds', '/options/groups/guilds/public') .when('/options/inventory', '/options/inventory/inventory') // redirect states that don't match @@ -64,12 +65,6 @@ window.habitrpg = angular.module('habitrpg', url: "/tavern", templateUrl: "partials/options.groups.tavern.html", controller: 'TavernCtrl' - // TODO this doesn't work, seems ngResource doesn't get the .then() function -// resolve: { -// group: ['Groups', function(Groups){ -// //return Groups.fetchTavern(); -// }] -// } }) .state('options.groups.party', { url: '/party', @@ -81,6 +76,21 @@ window.habitrpg = angular.module('habitrpg', templateUrl: "partials/options.groups.guilds.html", controller: 'GuildsCtrl' }) + .state('options.groups.guilds.public', { + url: '/public', + templateUrl: "partials/options.groups.guilds.public.html" + }) + .state('options.groups.guilds.create', { + url: '/create', + templateUrl: "partials/options.groups.guilds.create.html" + }) + .state('options.groups.guilds.detail', { + url: '/:gid', + templateUrl: 'partials/options.groups.guilds.detail.html', + controller: ['$scope', 'Groups', '$stateParams', function($scope, Groups, $stateParams){ + $scope.group = Groups.Group.get({gid:$stateParams.gid}); + }] + }) // Options > Inventory .state('options.inventory', { diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index b0d1fba4e8..8045fb697d 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -205,7 +205,6 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A function($scope, Groups, User) { Groups.fetchTavern(); $scope.group = Groups.groups.tavern; - $scope.rest = function(){ User.user.flags.rest = !User.user.flags.rest; User.log({op:'set',data:{'flags.rest':User.user.flags.rest}}); diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 8c6d9a4281..849be47b44 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -10,7 +10,7 @@ angular.module('groupServices', ['ngResource']). var Group = $resource(API_URL + '/api/v1/groups/:gid', {gid:'@_id', messageId: '@_messageId'}, { - //'query': {method: "GET", isArray:false} + query: {method: "GET", isArray:false}, postChat: {method: "POST", url: API_URL + '/api/v1/groups/:gid/chat'}, deleteChatMessage: {method: "DELETE", url: API_URL + '/api/v1/groups/:gid/chat/:messageId'}, join: {method: "POST", url: API_URL + '/api/v1/groups/:gid/join'}, @@ -34,32 +34,24 @@ 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', fields:'members'}, function(_groups){ - partyQ.resolve(_groups[0]); - Members.populate(_groups[0]); + Group.get({gid:'party'}, function(party){ + partyQ.resolve(party); }) return { // Note the _.once() to make sure it can never be called again fetchGuilds: _.once(function(){ - $('#loading-indicator').show(); - Group.query({type:'guilds'}, function(_groups){ - $('#loading-indicator').hide(); - guildsQ.resolve(_groups); - Members.populate(_groups); - }) - Group.query({type:'public'}, function(_groups){ - publicQ.resolve(_groups); - Members.populate(_groups); + Group.query(function(_groups){ + guildsQ.resolve(_groups.guilds); + Members.populate(_groups.guilds); + publicQ.resolve(_groups['public']); + Members.populate(_groups['public']); }) }), fetchTavern: _.once(function(){ - $('#loading-indicator').show(); - Group.query({type:'tavern'}, function(_groups){ - $('#loading-indicator').hide(); - Members.populate(_groups[0]); - tavernQ.resolve(_groups[0]); + Group.get({gid:'habitrpg'}, function(tavern){ + tavernQ.resolve(tavern); }) }), diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 35383ed053..10ff4ef7d2 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -18,6 +18,7 @@ var api = module.exports; 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}); @@ -31,80 +32,31 @@ 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 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` - * - * 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 + * 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 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 + var groupFields = 'name description memberCount'; + var sort = '-memberCount'; async.parallel({ - party: function(cb) { - if (type && !~type.indexOf('party')) return cb(null, {}); - 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); - } else { - query.populate('members', partyFields); - } - 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); - }); + // unecessary given our ui-router setup + party: function(cb){ + return cb(null, [{}]); }, guilds: function(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); - } else { - query.populate('members', 'profile.name'); - } - query.exec(cb); + Group.find({members: {'$in': [user._id]}, type:'guild'}) + .select(groupFields).sort(sort).exec(cb); }, - tavern: function(cb) { - if (type && !~type.indexOf('tavern')) return cb(null, {}); - 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, []); + 'public': function(cb) { Group.find({privacy: 'public'}) - .select('name description memberCount members') - .sort('-memberCount') + .select(groupFields + ' members') + .sort(sort) .exec(function(err, groups){ if (err) return cb(err); _.each(groups, function(g){ @@ -114,41 +66,49 @@ api.getGroups = function(req, res, next) { }); cb(null, groups); }); + }, + + // unecessary given our ui-router setup + tavern: function(cb) { + return cb(null, [{}]); } }, function(err, results){ if (err) return res.json(500, {err: err}); - - // 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', 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', 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', 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); + }) + } - }) }; diff --git a/views/options/groups/index.jade b/views/options/groups/index.jade index 0bd8241630..e1541bc448 100644 --- a/views/options/groups/index.jade +++ b/views/options/groups/index.jade @@ -23,43 +23,51 @@ script(type='text/ng-template', id='partials/options.groups.party.html') {{user.id}} include ./create-group +script(type='text/ng-template', id='partials/options.groups.guilds.public.html') + div(ng-repeat='invitation in user.invitations.guilds') + h3 You're Invited To {{invitation.name}} + a.btn.btn-success(data-type='guild', ng-click='join(invitation)') Accept + a.btn.btn-danger(ng-click='reject(invitation)') Reject + // Public Groups + .options-group.option-large.whatever-options + input.option-content(type='text',ng-model='guildSearch', placeholder='Search') + table.table.table-striped + tr(ng-repeat='group in groups.public | filter:guildSearch') + td + ul.pull-right.challenge-accordion-header-specs + li {{group.memberCount}} member(s) + li + // join / leave + a.btn.btn-small.btn-danger(ng-show='group_.isMember', ng-click='leave(group)') + i.icon-ban-circle + | Leave + a.btn.btn-small.btn-success(ng-hide='group._isMember', ng-click='join(group)') + i.icon-ok + | Join + h4 {{group.name}} + p {{group.description}} + +script(type='text/ng-template', id='partials/options.groups.guilds.detail.html') + include ./group + +script(type='text/ng-template', id='partials/options.groups.guilds.create.html') + include ./create-group + script(type='text/ng-template', id='partials/options.groups.guilds.html') ul.nav.nav-tabs - li.active - a(data-target='#groups-public-guilds', data-toggle='tab') Public Guilds - li(ng-repeat='group in groups.guilds') - a(data-target='#groups-guild-{{group._id}}', data-toggle='tab') {{group.name}} - li - a(data-target='#groups-create-guild', data-toggle='tab') Create Guild - .tab-content - .tab-pane.active#groups-public-guilds - div(ng-repeat='invitation in user.invitations.guilds') - h3 You're Invited To {{invitation.name}} - a.btn.btn-success(data-type='guild', ng-click='join(invitation)') Accept - a.btn.btn-danger(ng-click='reject(invitation)') Reject - // Public Groups - .options-group.option-large.whatever-options - input.option-content(type='text',ng-model='guildSearch', placeholder='Search') - table.table.table-striped - tr(ng-repeat='group in groups.public | filter:guildSearch') - td - ul.pull-right.challenge-accordion-header-specs - li {{group.memberCount}} member(s) - li - // join / leave - a.btn.btn-small.btn-danger(ng-show='group_.isMember', ng-click='leave(group)') - i.icon-ban-circle - | Leave - a.btn.btn-small.btn-success(ng-hide='group._isMember', ng-click='join(group)') - i.icon-ok - | Join - h4 {{group.name}} - p {{group.description}} - .tab-pane(id='groups-guild-{{group._id}}', ng-repeat='group in groups.guilds') - include ./group + li(ng-class="{ active: $state.includes('options.groups.guilds.public') }") + a(ui-sref='options.groups.guilds.public') + | Public Guilds + li(ng-class="{ active: $stateParams.gid == group._id }", ng-repeat='group in groups.guilds') + a(ui-sref="options.groups.guilds.detail({gid:group._id})") + | {{group.name}} + li(ng-class="{ active: $state.includes('options.groups.guilds.create') }") + a(ui-sref='options.groups.guilds.create') + | Create Guild - .tab-pane#groups-create-guild - include ./create-group + .tab-content + .tab-pane.active + div(ui-view) script(type='text/ng-template', id='partials/options.groups.html') ul.nav.nav-tabs