mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +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 {
|
||||
TAVERN_ID,
|
||||
} from '../../../../../website/server/models/group';
|
||||
import apiMessages from '../../../../../website/server/libs/apiMessages';
|
||||
|
||||
describe('GET /groups', () => {
|
||||
let user;
|
||||
@@ -14,6 +15,7 @@ describe('GET /groups', () => {
|
||||
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
|
||||
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
|
||||
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
|
||||
const GUILD_PER_PAGE = 30;
|
||||
|
||||
before(async () => {
|
||||
await resetHabiticaDB();
|
||||
@@ -98,6 +100,60 @@ describe('GET /groups', () => {
|
||||
.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 () => {
|
||||
await expect(user.get('/groups?type=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', () => {
|
||||
it('returns error message if string is not properly formatted', () => {
|
||||
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', () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ require('./globals.helper');
|
||||
import i18n from '../../website/common/script/i18n';
|
||||
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 function expectValidTranslationString (attribute) {
|
||||
|
||||
@@ -74,6 +74,7 @@ export async function resetHabiticaDB () {
|
||||
name: 'HabitRPG',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
memberCount: 0,
|
||||
}, (insertErr2) => {
|
||||
if (insertErr2) return reject(insertErr2);
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ function t (stringName) {
|
||||
try {
|
||||
return template(string)(clonedVars);
|
||||
} 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 {
|
||||
let stringNotFound;
|
||||
@@ -57,7 +57,7 @@ function t (stringName) {
|
||||
string: stringName,
|
||||
});
|
||||
} 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 amzLib from '../../libs/amazonPayments';
|
||||
import shared from '../../../common';
|
||||
|
||||
import apiMessages from '../../libs/apiMessages';
|
||||
|
||||
/**
|
||||
* @apiDefine GroupBodyInvalid
|
||||
@@ -252,6 +252,8 @@ api.createGroupPlan = {
|
||||
* @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="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:
|
||||
* {
|
||||
@@ -259,6 +261,9 @@ api.createGroupPlan = {
|
||||
* }
|
||||
*
|
||||
* @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>)
|
||||
*
|
||||
@@ -276,15 +281,27 @@ api.getGroups = {
|
||||
let user = res.locals.user;
|
||||
|
||||
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();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
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 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);
|
||||
},
|
||||
};
|
||||
|
||||
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'];
|
||||
|
||||
const GUILDS_PER_PAGE = 30; // number of guilds to return per page when using pagination
|
||||
|
||||
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 = [];
|
||||
|
||||
// Throw error if an invalid type is supplied
|
||||
@@ -247,6 +253,7 @@ schema.statics.getGroups = async function getGroups (options = {}) {
|
||||
privacy: 'public',
|
||||
}).select(groupFields);
|
||||
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();
|
||||
queries.push(publicGuildsQuery);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user