api v3 adapt v2: correctly import dependencies, port create, get, list groups

This commit is contained in:
Matteo Pagliazzi
2016-04-04 23:18:49 +02:00
parent f6fc50f6c2
commit 0a40c56973
17 changed files with 258 additions and 123 deletions

View File

@@ -4,11 +4,11 @@ import {
resetHabiticaDB,
} from '../../../helpers/api-integration/v2';
xdescribe('GET /groups', () => {
describe('GET /groups', () => {
const NUMBER_OF_PUBLIC_GUILDS = 3;
const NUMBER_OF_USERS_GUILDS = 2;
let user;
let leader;
before(async () => {
// Set up a world with a mixture of public and private guilds
@@ -16,7 +16,7 @@ xdescribe('GET /groups', () => {
await resetHabiticaDB();
user = await generateUser();
let leader = await generateUser({ balance: 10 });
leader = await generateUser({ balance: 10 });
await generateGroup(leader, {
name: 'public guild - is member',
@@ -90,8 +90,8 @@ xdescribe('GET /groups', () => {
context('guilds passed in as query', () => {
it('returns all guilds user is a part of ', async () => {
await expect(user.get('/groups', null, {type: 'guilds'}))
.to.eventually.have.a.lengthOf(NUMBER_OF_USERS_GUILDS);
await expect(leader.get('/groups', null, {type: 'guilds'}))
.to.eventually.have.a.lengthOf(4);
});
});
});

View File

@@ -8,7 +8,7 @@ import {
each,
} from 'lodash';
xdescribe('GET /groups/:id', () => {
describe('GET /groups/:id', () => {
let typesOfGroups = {};
typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' };
typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' };

View File

@@ -4,7 +4,7 @@ import {
translate as t,
} from '../../../helpers/api-integration/v2';
xdescribe('POST /groups', () => {
describe('POST /groups', () => {
context('All groups', () => {
let leader;

View File

@@ -4,7 +4,11 @@ import {
generateGroup,
generateUser,
} from '../../../helpers/api-integration/v2';
import { find } from 'lodash';
import {
find,
map,
} from 'lodash';
import Q from 'q';
xdescribe('DELETE /user', () => {
let user;
@@ -19,6 +23,18 @@ xdescribe('DELETE /user', () => {
})).to.eventually.eql(false);
});
it('deletes the user\'s tasks', async () => {
// gets the user's todos ids
let ids = user.todos.map(todo => todo._id);
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
await user.del('/user');
await Q.all(map(ids, id => {
return expect(checkExistence('tasks', id)).to.eventually.eql(false);
}));
});
context('user has active subscription', () => {
it('does not delete account');
});

View File

@@ -42,7 +42,7 @@ describe('DELETE /user', () => {
});
});
it('deletes the user', async () => {
it('deletes the user\'s tasks', async () => {
// gets the user's tasks ids
let ids = [];
each(user.tasksOrder, (idsForOrder) => {
@@ -60,7 +60,7 @@ describe('DELETE /user', () => {
}));
});
it('delete the user\'s tasks', async () => {
it('deletes the user', async () => {
await user.del('/user', {
password,
});

View File

@@ -72,18 +72,18 @@ export async function createAndPopulateGroup (settings = {}) {
let groupLeader = await generateUser(leaderDetails);
let group = await generateGroup(groupLeader, groupDetails);
const groupMembershipTypes = {
party: { 'party._id': group._id},
guild: { guilds: [group._id] },
};
let members = await Q.all(
times(numberOfMembers, () => {
return generateUser();
return generateUser(groupMembershipTypes[group.type]);
})
);
let memberIds = members.map((member) => {
return member._id;
});
memberIds.push(groupLeader._id);
await group.update({ members: memberIds });
await group.update({ memberCount: numberOfMembers + 1});
let invitees = await Q.all(
times(numberOfInvites, () => {

View File

@@ -7,8 +7,13 @@ var utils = require('../../libs/api-v2/utils');
var nconf = require('nconf');
var request = require('request');
var FirebaseTokenGenerator = require('firebase-token-generator');
var User = require('../../models/user').model;
var EmailUnsubscription = require('../../models/emailUnsubscription').model;
import {
model as User,
} from '../../models/user';
import {
model as EmailUnsubscription,
} from '../../models/emailUnsubscription';
var analytics = utils.analytics;
var i18n = require('./../../libs/api-v2/i18n');

View File

@@ -4,9 +4,15 @@ var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
var shared = require('../../../../common');
var User = require('./../../models/user').model;
var Group = require('./../../models/group').model;
var Challenge = require('./../../models/challenge').model;
import {
model as User,
} from '../../models/user';
import {
model as Group,
} from '../../models/group';
import {
model as Challenge,
} from '../../models/challenge';
var logging = require('./../../libs/api-v2/logging');
var csvStringify = require('csv-stringify');
var utils = require('../../libs/api-v2/utils');

View File

@@ -1,5 +1,7 @@
var _ = require('lodash');
var Coupon = require('./../../models/coupon').model;
import {
model as Coupon,
} from '../../models/coupon';
var api = module.exports;
var csvStringify = require('csv-stringify');
var async = require('async');

View File

@@ -5,7 +5,9 @@ var nconf = require('nconf');
var moment = require('moment');
var js2xmlparser = require("js2xmlparser");
var pd = require('pretty-data').pd;
var User = require('../../models/user').model;
import {
model as User,
} from '../../models/user';
// Avatar screenshot/static-page includes
//var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres

View File

@@ -11,10 +11,20 @@ var async = require('async');
var Q = require('q');
var utils = require('./../../libs/api-v2/utils');
var shared = require('../../../../common');
var User = require('./../../models/user').model;
var Group = require('./../../models/group').model;
var Challenge = require('./../../models/challenge').model;
var EmailUnsubscription = require('./../../models/emailUnsubscription').model;
import {
model as User,
} from './../../models/user';
import {
model as Group,
} from './../../models/group';
import {
model as Challenge,
} from './../../models/challenge';
import {
model as EmailUnsubscription,
} from './../../models/emailUnsubscription';
var isProd = nconf.get('NODE_ENV') === 'production';
var api = module.exports;
var pushNotify = require('./pushNotifications');
@@ -70,31 +80,41 @@ api.list = function(req, res, next) {
// unecessary given our ui-router setup
party: function(cb){
if (!~type.indexOf('party')) return cb(null, {});
Group.findOne({type: 'party', members: {'$in': [user._id]}})
Group.findOne({_id: user.party._id, type: 'party'})
.select(groupFields).exec(function(err, party){
if (err) return cb(err);
cb(null, (party === null ? [] : [party])); // return as an array for consistent ngResource use
if (!party) return cb(null, []);
party.getTransformedData({cb: function (err, transformedParty) {
if (err) return cb(err);
cb(null, (transformedParty === null ? [] : [transformedParty])); // return as an array for consistent ngResource use
}});
});
},
guilds: function(cb) {
if (!~type.indexOf('guilds')) return cb(null, []);
Group.find({members: {'$in': [user._id]}, type:'guild'})
.select(groupFields).sort(sort).exec(cb);
Group.find({_id: {'$in': user.guilds}, type:'guild'})
.select(groupFields).sort(sort).exec(function (err, guilds) {
if (err) return cb(err);
async.map(guilds, function (guild, cb1) {
guild.getTransformedData({cb: cb1})
}, function(err, guildsTransormed) {
cb(err, guildsTransormed);
});
});
},
'public': function(cb) {
if (!~type.indexOf('public')) return cb(null, []);
Group.find({privacy: 'public'})
.select(groupFields + ' members')
.select(groupFields)
.sort(sort)
.lean()
.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;
if (user.guilds.indexOf(g._id) !== -1) g._isMember = true;
});
cb(null, groups);
});
@@ -105,7 +125,10 @@ api.list = function(req, res, next) {
if (!~type.indexOf('tavern')) return cb(null, {});
Group.findById('habitrpg').select(groupFields).exec(function(err, tavern){
if (err) return cb(err);
cb(null, [tavern]); // return as an array for consistent ngResource use
tavern.getTransformedData({cb: function (err, transformedTavern) {
if (err) return cb(err);
cb(null, ([transformedTavern])); // return as an array for consistent ngResource use
}});
});
}
@@ -132,14 +155,24 @@ api.list = function(req, res, next) {
api.get = function(req, res, next) {
var user = res.locals.user;
var gid = req.params.gid;
let isUserGuild = user.guilds.indexOf(gid) !== -1;
var q = (gid == 'party')
? Group.findOne({type: 'party', members: {'$in': [user._id]}})
: Group.findOne({$or:[
{_id:gid, privacy:'public'},
{_id:gid, privacy:'private', members: {$in:[user._id]}} // if the group is private, only return if they have access
]});
populateQuery(gid, q);
var q;
if (gid === 'party' || gid === user.party._id) {
q = Group.findOne({_id: user.party._id, type: 'party'})
} else {
if (isUserGuild) {
q = Group.findOne({type: 'guild', _id: gid});
} else {
q = Group.findOne({type: 'guild', privacy: 'public', _id: gid});
}
}
q.populate('leader', nameFields);
//populateQuery(gid, q);
q.exec(function(err, group){
if (err) return next(err);
if(!group){
@@ -150,34 +183,27 @@ api.get = function(req, res, next) {
return res.json(group);
}
if (!user.contributor.admin) {
_purgeFlagInfoFromChat(group, user);
}
//Since we have a limit on how many members are populate to the group, we want to make sure the user is always in the group
var userInGroup = _.find(group.members, function(member){ return member._id == user._id; });
//If the group is private or the group is a party, then the user must be a member of the group based on access restrictions above
if (group.privacy === 'private' || gid === 'party') {
//If the user is not in the group query, remove a user and add the current user
if (!userInGroup) {
group.members.splice(0,1);
group.members.push(user);
}
res.json(group);
} else if ( group.privacy === "public" ) { //The group is public, we must do an extra check to see if the user is already in the group query
//We must see how to check if a user is a member of a public group, so we requery
var q2 = Group.findOne({ _id: group._id, privacy:'public', members: {$in:[user._id]} });
q2.exec(function(err, group2){
group.getTransformedData({
cb: function (err, transformedGroup) {
if (err) return next(err);
if (group2 && !userInGroup) {
group.members.splice(0,1);
group.members.push(user);
}
res.json(group);
});
}
gid = null;
if (!user.contributor.admin) {
_purgeFlagInfoFromChat(transformedGroup, user);
}
//Since we have a limit on how many members are populate to the group, we want to make sure the user is always in the group
var userInGroup = _.find(transformedGroup.members, function(member){ return member._id == user._id; });
if ((gid === 'party' || isUserGuild) && !userInGroup) {
transformedGroup.members.splice(0,1);
transformedGroup.members.push(user);
}
res.json(transformedGroup);
},
populateMembers: group.type === 'party' ? partyFields : nameFields,
populateInvites: nameFields,
populateChallenges: challengeFields,
});
});
};
@@ -185,10 +211,12 @@ api.get = function(req, res, next) {
api.create = function(req, res, next) {
var group = new Group(req.body);
var user = res.locals.user;
group.members = [user._id];
//group.members = [user._id];
group.leader = user._id;
if (!group.name) group.name = 'group name';
if(group.type === 'guild'){
user.guilds.push(group._id);
if(user.balance < 1) return res.status(401).json({err: shared.i18n.t('messageInsufficientGems')});
group.balance = 1;
@@ -200,33 +228,31 @@ api.create = function(req, res, next) {
function(saved,ct,cb){
firebase.updateGroupData(saved);
firebase.addUserToGroup(saved._id, user._id);
saved.populate('members', nameFields, cb);
saved.getTransformedData({
populateMembers: nameFields,
cb,
})
}
],function(err,saved){
],function(err,groupTransformed){
if (err) return next(err);
res.json(saved);
res.json(groupTransformed);
group = user = null;
});
} else{
async.waterfall([
function(cb){
Group.findOne({type:'party',members:{$in:[user._id]}},cb);
},
function(found, cb){
if (found) return cb(shared.i18n.t('messageGroupAlreadyInParty'));
group.save(cb);
},
function(saved, count, cb){
firebase.updateGroupData(saved);
firebase.addUserToGroup(saved._id, user._id);
saved.populate('members', nameFields, cb);
}
], function(err, populated){
if (err === shared.i18n.t('messageGroupAlreadyInParty')) return res.status(400).json({err:err});
if (user.party._id) return res.status(400).json({err:shared.i18n.t('messageGroupAlreadyInParty')});
user.party._id = group._id;
user.save(function (err) {
if (err) return next(err);
group = user = null;
return res.json(populated);
group.save(function(err, saved) {
if (err) return next(err);
saved.getTransformedData({
populateMembers: nameFields,
cb (err, groupTransformed) {
res.json(groupTransformed);
},
});
});
})
}
}

View File

@@ -2,8 +2,12 @@ var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
var shared = require('../../../../common');
var User = require('./../../models/user').model;
var Group = require('./../../models/group').model;
import {
model as User,
} from '../../models/user';
import {
model as Group,
} from '../../models/group';
var api = module.exports;
api.ensureAdmin = function(req, res, next) {

View File

@@ -1,6 +1,11 @@
var User = require('mongoose').model('User');
var groups = require('../../models/group');
var partyFields = require('./groups').partyFields
import {
model as groups,
chatDefaults,
} from '../../models/group';
import {
model as User,
} from '../../models/user';
let partyFields = require('./groups').partyFields;
var api = module.exports;
var async = require('async');
var _ = require('lodash');
@@ -49,12 +54,12 @@ api.sendMessage = function(user, member, data){
}
msg += data.message ? data.message : '';
}
shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user));
shared.refPush(member.inbox.messages, chatDefaults(msg, user));
member.inbox.newMessages++;
member._v++;
member.markModified('inbox.messages');
shared.refPush(user.inbox.messages, _.defaults({sent:true}, groups.chatDefaults(msg, member)));
shared.refPush(user.inbox.messages, _.defaults({sent:true}, chatDefaults(msg, member)));
user.markModified('inbox.messages');
}

View File

@@ -1,5 +1,9 @@
var User = require('../../models/user').model;
var EmailUnsubscription = require('../../models/emailUnsubscription').model;
import {
model as User,
} from '../../models/user';
import {
model as EmailUnsubscription,
} from '../../models/emailUnsubscription';
var utils = require('../../libs/api-v2/utils');
var i18n = require('../../../../common').i18n;

View File

@@ -4,14 +4,21 @@ var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
var shared = require('../../../../common');
var User = require('./../../models/user').model;
import {
model as User,
} from '../../models/user';
import * as Tasks from '../../models/task';
import Q from 'q';
import {removeFromArray} from './../../libs/api-v3/collectionManipulators';
var utils = require('./../../libs/api-v2/utils');
var analytics = utils.analytics;
var Group = require('./../../models/group').model;
var Challenge = require('./../../models/challenge').model;
import {
basicFields as basicGroupFields,
model as Group,
} from '../../models/group';
import {
model as Challenge,
} from '../../models/challenge';
var moment = require('moment');
var logging = require('./../../libs/api-v2/logging');
var acceptablePUTPaths;
@@ -442,26 +449,28 @@ api.delete = function(req, res, next) {
return res.status(400).json({err:"You have an active subscription, cancel your plan before deleting your account."});
}
Group.find({
members: {
'$in': [user._id]
}
}, function(err, groups){
if(err) return next(err);
let types = ['party', 'publicGuilds', 'privateGuilds'];
let groupFields = basicGroupFields.concat(' leader memberCount');
async.each(groups, function(group, cb){
group.leave(user, 'remove-all', cb);
}, function(err){
if(err) return next(err);
user.remove(function(err){
if(err) return next(err);
firebase.deleteUser(user._id);
res.sendStatus(200);
});
});
});
Group.getGroups({user, types, groupFields})
.then(groups => {
return Q.all(groups.map((group) => {
return group.leave(user, 'remove-all');
}));
})
.then(() => {
return Tasks.Task.remove({
userId: user._id,
}).exec();
})
.then(() => {
return user.remove();
})
.then(() => {
firebase.deleteUser(user._id);
res.sendStatus(200);
})
.catch(next);
}
/*

View File

@@ -24,6 +24,7 @@ api.getFrontPage = {
},
};
// TODO remove api static page
let staticPages = ['front', 'privacy', 'terms', 'api', 'features',
'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines',
'old-news', 'press-kit', 'faq', 'overview', 'apps',

View File

@@ -22,7 +22,6 @@ let Schema = mongoose.Schema;
// NOTE once Firebase is enabled any change to groups' members in MongoDB will have to be run through the API
// changes made directly to the db will cause Firebase to get out of sync
export let schema = new Schema({
// TODO don't break validation on _id === 'habitrpg'
name: {type: String, required: true},
description: String,
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
@@ -65,7 +64,6 @@ export let schema = new Schema({
// 'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them.
// TODO when booting user, remove from .joined and check again if we can now start the quest
// TODO as long as quests are party only we can keep it here
// TODO are we sure we need this type of default for this to work?
members: {type: Schema.Types.Mixed, default: () => {
return {};
}},
@@ -669,6 +667,63 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
return Q.all(promises);
};
// API v2 compatibility methods
schema.methods.getTransformedData = function getTransformedData (options) {
let cb = options.cb;
let populateMembers = options.populateMembers;
let populateInvites = options.populateInvites;
let populateChallenges = options.populateChallenges;
let obj = this.toJSON();
let queryMembers = {};
let queryInvites = {};
if (this.type === 'guild') {
queryInvites['invitations.guilds.id'] = this._id;
} else {
queryInvites['invitations.party.id'] = this._id;
}
if (this.type === 'guild') {
queryMembers.guilds = this._id;
} else {
queryMembers['party._id'] = this._id;
}
let selectDataMembers = '_id';
let selectDataInvites = '_id';
let selectDataChallenges = '_id';
if (populateMembers) {
selectDataMembers += ` ${populateMembers}`;
}
if (populateInvites) {
selectDataInvites += ` ${populateInvites}`;
}
if (populateChallenges) {
selectDataChallenges += ` ${populateChallenges}`;
}
let membersQuery = User.find(queryMembers).select(selectDataMembers);
if (options.limitPopulation) membersQuery.limit(15);
Q.all([
membersQuery.exec(),
User.find(queryInvites).select(populateInvites).exec(),
Challenge.find({group: obj._id}).select(populateMembers).exec(),
])
.then((results) => {
obj.members = results[0];
obj.invites = results[1];
obj.challenges = results[2];
cb(null, obj);
})
.catch(cb);
};
// END API v2 compatibility methods
export const INVITES_LIMIT = 100;
export let model = mongoose.model('Group', schema);