mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 05:37:22 +01:00
api: add pagination for guilds
start adding apiMessages add apiMessages lib with tests use apiMessage and fix tests fix content tests guilds pagination: add api docs guilds pagination: improve api docs
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
TAVERN_ID,
|
TAVERN_ID,
|
||||||
} from '../../../../../website/server/models/group';
|
} from '../../../../../website/server/models/group';
|
||||||
|
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||||
|
|
||||||
describe('GET /groups', () => {
|
describe('GET /groups', () => {
|
||||||
let user;
|
let user;
|
||||||
@@ -14,6 +15,7 @@ describe('GET /groups', () => {
|
|||||||
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
|
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
|
||||||
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
|
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
|
||||||
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
|
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
|
||||||
|
const GUILD_PER_PAGE = 30;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await resetHabiticaDB();
|
await resetHabiticaDB();
|
||||||
@@ -98,6 +100,60 @@ describe('GET /groups', () => {
|
|||||||
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS);
|
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('public guilds pagination', () => {
|
||||||
|
it('req.query.paginate must be a boolean string', async () => {
|
||||||
|
await expect(user.get('/groups?paginate=aString&type=publicGuilds'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('req.query.paginate can only be true when req.query.type includes publicGuilds', async () => {
|
||||||
|
await expect(user.get('/groups?paginate=true&type=notPublicGuilds'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: apiMessages('guildsOnlyPaginate'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('req.query.page can\'t be negative', async () => {
|
||||||
|
await expect(user.get('/groups?paginate=true&page=-1&type=publicGuilds'))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 30 guilds per page ordered by number of members', async () => {
|
||||||
|
await user.update({balance: 9000});
|
||||||
|
let groups = await Promise.all(_.times(60, (i) => {
|
||||||
|
return generateGroup(user, {
|
||||||
|
name: `public guild ${i} - is member`,
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
// update group number 32 and not the first to make sure sorting works
|
||||||
|
await groups[32].update({name: 'guild with most members', memberCount: 199});
|
||||||
|
await groups[33].update({name: 'guild with less members', memberCount: -100});
|
||||||
|
|
||||||
|
let page0 = await expect(user.get('/groups?type=publicGuilds&paginate=true'))
|
||||||
|
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
|
||||||
|
expect(page0[0].name).to.equal('guild with most members');
|
||||||
|
|
||||||
|
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
|
||||||
|
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
|
||||||
|
let page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
|
||||||
|
.to.eventually.have.a.lengthOf(1 + 2); // 1 created now, 2 by other tests
|
||||||
|
expect(page2[2].name).to.equal('guild with less members');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('returns all the user\'s guilds when guilds passed in as query', async () => {
|
it('returns all the user\'s guilds when guilds passed in as query', async () => {
|
||||||
await expect(user.get('/groups?type=guilds'))
|
await expect(user.get('/groups?type=guilds'))
|
||||||
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS);
|
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS);
|
||||||
|
|||||||
31
test/api/v3/unit/libs/apiMessages.js
Normal file
31
test/api/v3/unit/libs/apiMessages.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||||
|
|
||||||
|
describe('API Messages', () => {
|
||||||
|
const message = 'Only public guilds support pagination.';
|
||||||
|
it('returns an API message', () => {
|
||||||
|
expect(apiMessages('guildsOnlyPaginate')).to.equal(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if the API message does not exist', () => {
|
||||||
|
expect(() => apiMessages('iDoNotExist')).to.throw;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clones the passed variables', () => {
|
||||||
|
let vars = {a: 1};
|
||||||
|
sandbox.stub(_, 'clone').returns({});
|
||||||
|
apiMessages('guildsOnlyPaginate', vars);
|
||||||
|
expect(_.clone).to.have.been.called.once;
|
||||||
|
expect(_.clone).to.have.been.calledWith(vars);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pass the message through _.template', () => {
|
||||||
|
let vars = {a: 1};
|
||||||
|
let stub = sinon.stub().returns('string');
|
||||||
|
sandbox.stub(_, 'template').returns(stub);
|
||||||
|
apiMessages('guildsOnlyPaginate', vars);
|
||||||
|
expect(_.template).to.have.been.called.once;
|
||||||
|
expect(_.template).to.have.been.calledWith(message);
|
||||||
|
expect(stub).to.have.been.called.once;
|
||||||
|
expect(stub).to.have.been.calledWith(vars);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@ import translator from '../../website/common/script/content/translation';
|
|||||||
describe('Translator', () => {
|
describe('Translator', () => {
|
||||||
it('returns error message if string is not properly formatted', () => {
|
it('returns error message if string is not properly formatted', () => {
|
||||||
let improperlyFormattedString = translator('petName', {attr: 0})();
|
let improperlyFormattedString = translator('petName', {attr: 0})();
|
||||||
expect(improperlyFormattedString).to.eql(STRING_ERROR_MSG);
|
expect(improperlyFormattedString).to.match(STRING_ERROR_MSG);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an error message if string does not exist', () => {
|
it('returns an error message if string does not exist', () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ require('./globals.helper');
|
|||||||
import i18n from '../../website/common/script/i18n';
|
import i18n from '../../website/common/script/i18n';
|
||||||
i18n.translations = require('../../website/server/libs/i18n').translations;
|
i18n.translations = require('../../website/server/libs/i18n').translations;
|
||||||
|
|
||||||
export const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.';
|
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
|
||||||
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
||||||
|
|
||||||
export function expectValidTranslationString (attribute) {
|
export function expectValidTranslationString (attribute) {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export async function resetHabiticaDB () {
|
|||||||
name: 'HabitRPG',
|
name: 'HabitRPG',
|
||||||
type: 'guild',
|
type: 'guild',
|
||||||
privacy: 'public',
|
privacy: 'public',
|
||||||
|
memberCount: 0,
|
||||||
}, (insertErr2) => {
|
}, (insertErr2) => {
|
||||||
if (insertErr2) return reject(insertErr2);
|
if (insertErr2) return reject(insertErr2);
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ function t (stringName) {
|
|||||||
try {
|
try {
|
||||||
return template(string)(clonedVars);
|
return template(string)(clonedVars);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return 'Error processing the string. Please see Help > Report a Bug.';
|
return `Error processing the string "${stringName}". Please see Help > Report a Bug.`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let stringNotFound;
|
let stringNotFound;
|
||||||
@@ -57,7 +57,7 @@ function t (stringName) {
|
|||||||
string: stringName,
|
string: stringName,
|
||||||
});
|
});
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return 'Error processing the string. Please see Help > Report a Bug.';
|
return 'Error processing the string "stringNotFound". Please see Help > Report a Bug.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import payments from '../../libs/payments';
|
|||||||
import stripePayments from '../../libs/stripePayments';
|
import stripePayments from '../../libs/stripePayments';
|
||||||
import amzLib from '../../libs/amazonPayments';
|
import amzLib from '../../libs/amazonPayments';
|
||||||
import shared from '../../../common';
|
import shared from '../../../common';
|
||||||
|
import apiMessages from '../../libs/apiMessages';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine GroupBodyInvalid
|
* @apiDefine GroupBodyInvalid
|
||||||
@@ -252,6 +252,8 @@ api.createGroupPlan = {
|
|||||||
* @apiGroup Group
|
* @apiGroup Group
|
||||||
*
|
*
|
||||||
* @apiParam {String} type The type of groups to retrieve. Must be a query string representing a list of values like 'tavern,party'. Possible values are party, guilds, privateGuilds, publicGuilds, tavern
|
* @apiParam {String} type The type of groups to retrieve. Must be a query string representing a list of values like 'tavern,party'. Possible values are party, guilds, privateGuilds, publicGuilds, tavern
|
||||||
|
* @apiParam {String="true","false"} [paginate] Public guilds support pagination. When true guilds are returned in groups of 30
|
||||||
|
* @apiParam {Number} [page] When pagination is enabled for public guilds this parameter can be used to specify the page number (the initial page is number 0 and not required)
|
||||||
*
|
*
|
||||||
* @apiParamExample {json} Private Guilds, Tavern:
|
* @apiParamExample {json} Private Guilds, Tavern:
|
||||||
* {
|
* {
|
||||||
@@ -259,6 +261,9 @@ api.createGroupPlan = {
|
|||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @apiError (400) {BadRequest} groupTypesRequired Group types are required
|
* @apiError (400) {BadRequest} groupTypesRequired Group types are required
|
||||||
|
* @apiError (400) {BadRequest} guildsPaginateBooleanString Paginate query parameter must be a boolean (true or false)
|
||||||
|
* @apiError (400) {BadRequest} guildsPageInteger Page query parameter must be a positive integer
|
||||||
|
* @apiError (400) {BadRequest} guildsOnlyPaginate Only public guilds support pagination
|
||||||
*
|
*
|
||||||
* @apiSuccess {Object[]} data An array of the requested groups (See <a href="https://github.com/HabitRPG/habitica/blob/develop/website/server/models/group.js" target="_blank">/website/server/models/group.js</a>)
|
* @apiSuccess {Object[]} data An array of the requested groups (See <a href="https://github.com/HabitRPG/habitica/blob/develop/website/server/models/group.js" target="_blank">/website/server/models/group.js</a>)
|
||||||
*
|
*
|
||||||
@@ -276,15 +281,27 @@ api.getGroups = {
|
|||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
req.checkQuery('type', res.t('groupTypesRequired')).notEmpty();
|
req.checkQuery('type', res.t('groupTypesRequired')).notEmpty();
|
||||||
|
// pagination options, can only be used with public guilds
|
||||||
|
req.checkQuery('paginate').optional().isIn(['true', 'false'], apiMessages('guildsPaginateBooleanString'));
|
||||||
|
req.checkQuery('page').optional().isInt({min: 0}, apiMessages('guildsPageInteger'));
|
||||||
|
|
||||||
let validationErrors = req.validationErrors();
|
let validationErrors = req.validationErrors();
|
||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
let types = req.query.type.split(',');
|
let types = req.query.type.split(',');
|
||||||
|
|
||||||
|
let paginate = req.query.paginate === 'true' ? true : false;
|
||||||
|
if (paginate && !_.includes(types, 'publicGuilds')) {
|
||||||
|
throw new BadRequest(apiMessages('guildsOnlyPaginate'));
|
||||||
|
}
|
||||||
|
|
||||||
let groupFields = basicGroupFields.concat(' description memberCount balance');
|
let groupFields = basicGroupFields.concat(' description memberCount balance');
|
||||||
let sort = '-memberCount';
|
let sort = '-memberCount';
|
||||||
|
|
||||||
let results = await Group.getGroups({user, types, groupFields, sort});
|
let results = await Group.getGroups({
|
||||||
|
user, types, groupFields, sort,
|
||||||
|
paginate, page: req.query.page,
|
||||||
|
});
|
||||||
res.respond(200, results);
|
res.respond(200, results);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
21
website/server/libs/apiMessages.js
Normal file
21
website/server/libs/apiMessages.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// A map of messages used by the API that don't need to be translated and
|
||||||
|
// so are not placed into /common/locales
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
// When this file grows, it can be split into multiple ones.
|
||||||
|
const messages = {
|
||||||
|
guildsOnlyPaginate: 'Only public guilds support pagination.',
|
||||||
|
guildsPaginateBooleanString: 'req.query.paginate must be a boolean string.',
|
||||||
|
guildsPageInteger: 'req.query.page must be an integer greater than or equal to 0.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (msgKey, vars = {}) {
|
||||||
|
let message = messages[msgKey];
|
||||||
|
if (!message) throw new Error(`Error processing the API message "${msgKey}".`);
|
||||||
|
|
||||||
|
let clonedVars = vars ? _.clone(vars) : {};
|
||||||
|
|
||||||
|
// TODO cache the result of template() ? More memory usage, faster output
|
||||||
|
return _.template(message)(clonedVars);
|
||||||
|
}
|
||||||
@@ -204,8 +204,14 @@ schema.statics.getGroup = async function getGroup (options = {}) {
|
|||||||
|
|
||||||
export const VALID_QUERY_TYPES = ['party', 'guilds', 'privateGuilds', 'publicGuilds', 'tavern'];
|
export const VALID_QUERY_TYPES = ['party', 'guilds', 'privateGuilds', 'publicGuilds', 'tavern'];
|
||||||
|
|
||||||
|
const GUILDS_PER_PAGE = 30; // number of guilds to return per page when using pagination
|
||||||
|
|
||||||
schema.statics.getGroups = async function getGroups (options = {}) {
|
schema.statics.getGroups = async function getGroups (options = {}) {
|
||||||
let {user, types, groupFields = basicFields, sort = '-memberCount', populateLeader = false} = options;
|
let {
|
||||||
|
user, types, groupFields = basicFields,
|
||||||
|
sort = '-memberCount', populateLeader = false,
|
||||||
|
paginate = false, page = 0, // optional pagination for public guilds
|
||||||
|
} = options;
|
||||||
let queries = [];
|
let queries = [];
|
||||||
|
|
||||||
// Throw error if an invalid type is supplied
|
// Throw error if an invalid type is supplied
|
||||||
@@ -247,6 +253,7 @@ schema.statics.getGroups = async function getGroups (options = {}) {
|
|||||||
privacy: 'public',
|
privacy: 'public',
|
||||||
}).select(groupFields);
|
}).select(groupFields);
|
||||||
if (populateLeader === true) publicGuildsQuery.populate('leader', nameFields);
|
if (populateLeader === true) publicGuildsQuery.populate('leader', nameFields);
|
||||||
|
if (paginate === true) publicGuildsQuery.limit(GUILDS_PER_PAGE).skip(page * GUILDS_PER_PAGE);
|
||||||
publicGuildsQuery.sort(sort).lean().exec();
|
publicGuildsQuery.sort(sort).lean().exec();
|
||||||
queries.push(publicGuildsQuery);
|
queries.push(publicGuildsQuery);
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user