Merge branch 'api-v3-groups' into api-v3-members
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
||||
"groupMemberNotFound": "User not found among group's members",
|
||||
"keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
|
||||
"keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
|
||||
"canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
|
||||
"inviteMissingEmail": "Missing email address in invite.",
|
||||
"onlyGroupLeaderChal": "Only the group leader can create challenges",
|
||||
@@ -48,6 +49,10 @@
|
||||
"challengeNotFound": "Challenge not found.",
|
||||
"onlyLeaderDeleteChal": "Only the challenge leader can delete it.",
|
||||
"winnerNotFound": "Winner with id \"<%= userId %>\" not found or not part of the challenge.",
|
||||
"noCompletedTodosChallenge": "\"includeComepletedTodos\" is not supported when fetching a challenge tasks.",
|
||||
"userTasksNoChallengeId": "When \"tasksOwner\" is \"user\" \"challengeId\" can't be passed.",
|
||||
"onlyChalLeaderEditTasks": "Tasks belonging to a challenge can only be edited by the leader.",
|
||||
"invalidTasksOwner": "\"tasksOwner\" must be \"user\" or \"challenge\".",
|
||||
"partyMustbePrivate": "Parties must be private",
|
||||
"userAlreadyInGroup": "User already in that group.",
|
||||
"userAlreadyInvitedToGroup": "User already invited to that group.",
|
||||
@@ -56,5 +61,6 @@
|
||||
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
|
||||
"uuidsMustBeAnArray": "UUIDs invites must be a an Array.",
|
||||
"emailsMustBeAnArray": "Email invites must be a an Array.",
|
||||
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time"
|
||||
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
|
||||
"cantOnlyUnlinkChalTask": "Only challenges tasks can be unlinked."
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import scoreTask from './scoreTask';
|
||||
import preenUserHistory from './preenHistory';
|
||||
import { preenUserHistory } from './preening';
|
||||
import common from '../../';
|
||||
import {
|
||||
shouldDo,
|
||||
@@ -65,7 +65,7 @@ export default function cron (options = {}) {
|
||||
gemCapExtra: 0,
|
||||
});
|
||||
|
||||
user.markModified('purchased.plan'); // TODO necessary?
|
||||
user.markModified('purchased.plan');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export default function cron (options = {}) {
|
||||
// preen user history so that it doesn't become a performance problem
|
||||
// also for subscribed users but differentyly
|
||||
// premium subscribers can keep their full history.
|
||||
preenUserHistory(user, tasksByType);
|
||||
preenUserHistory(user, tasksByType, user.preferences.timezoneOffset);
|
||||
|
||||
if (perfect) {
|
||||
user.achievements.perfect++;
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
|
||||
function _preen (newHistory, history, amount, groupBy) {
|
||||
_.chain(history)
|
||||
.groupBy(h => moment(h.date).format(groupBy))
|
||||
.sortBy((h, k) => k)
|
||||
.slice(-amount)
|
||||
.pop()
|
||||
.each((group) => {
|
||||
newHistory.push({
|
||||
date: moment(group[0].date).toDate(),
|
||||
value: _.reduce(group, (m, obj) => m + obj.value, 0) / group.length,
|
||||
});
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
// Free users:
|
||||
// Preen history for users with > 7 history entries
|
||||
// This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
|
||||
// of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 1 entry each week
|
||||
// of this month; 1 entry for each month of this year; 1 entry per previous year: [day*7 week*4 month*12 year*infinite]
|
||||
//
|
||||
// Subscribers:
|
||||
// TODO implement
|
||||
|
||||
// TODO Probably the description ^ is not too correct, this method actually takes 1 value each for the last 50 years,
|
||||
// then the X last months, where X is the month we're in (september = 8 starting from 0)
|
||||
// and all the days in this month
|
||||
// Allowing for multiple values in a single day for habits we probably want something different:
|
||||
// For free users:
|
||||
// - At max 30 values for today (max 30)
|
||||
// - 1 value each for the previous 61 days (2 months)
|
||||
// - 1 value each for the previous 10 months (max 10)
|
||||
// - 1 value each for the previous 50 years
|
||||
// - Total: 30+61+10+ a few years ~= 105
|
||||
//
|
||||
// For subscribed users
|
||||
// - At max 30 values for today (max 30)
|
||||
// - 1 value each for the previous 364 days (max 364)
|
||||
// - 1 value each for the previous 12 months (max 12)
|
||||
// - 1 value each for the previous 50 years
|
||||
// - Total: 30+364+12+ a few years ~= 410
|
||||
//
|
||||
export function preenHistory (history) {
|
||||
// TODO remember to add this to migration
|
||||
/* history = _.filter(history, function(h) {
|
||||
return !!h;
|
||||
}); */
|
||||
let newHistory = [];
|
||||
|
||||
_preen(newHistory, history, 50, 'YYYY');
|
||||
_preen(newHistory, history, moment().format('MM'), 'YYYYMM');
|
||||
|
||||
let thisMonth = moment().format('YYYYMM');
|
||||
newHistory = newHistory.concat(history.filter(h => {
|
||||
return moment(h.date).format('YYYYMM') === thisMonth;
|
||||
}));
|
||||
|
||||
return newHistory;
|
||||
}
|
||||
|
||||
export function preenUserHistory (user, tasksByType, minHistLen = 7) {
|
||||
tasksByType.habits.concat(tasksByType.dailys).forEach((task) => {
|
||||
if (task.history.length > minHistLen) {
|
||||
task.history = preenHistory(user, task.history);
|
||||
task.markModified('history');
|
||||
}
|
||||
});
|
||||
|
||||
if (user.history.exp.length > minHistLen) {
|
||||
user.history.exp = preenHistory(user, user.history.exp);
|
||||
user.markModified('history.exp');
|
||||
}
|
||||
|
||||
if (user.history.todos.length > minHistLen) {
|
||||
user.history.todos = preenHistory(user, user.history.todos);
|
||||
user.markModified('history.todos');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
// Aggregate entries
|
||||
function _aggregate (history, aggregateBy) {
|
||||
return _.chain(history)
|
||||
.groupBy(entry => { // group entries by aggregateBy
|
||||
return moment(entry.date).format(aggregateBy);
|
||||
})
|
||||
.sortBy((entry, key) => key) // sort by date
|
||||
.map(entries => {
|
||||
return {
|
||||
date: Number(entries[0].date),
|
||||
value: _.reduce(entries, (previousValue, entry) => {
|
||||
return previousValue + entry.value;
|
||||
}, 0) / entries.length,
|
||||
};
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
/* Preen an array of history entries
|
||||
Free users:
|
||||
- 1 value for each day of the past 60 days (no compression)
|
||||
- 1 value each month for the previous 10 months
|
||||
- 1 value each year for the previous years
|
||||
Subscribers and challenges:
|
||||
- 1 value for each day of the past 365 days (no compression)
|
||||
- 1 value each month for the previous 12 months
|
||||
- 1 value each year for the previous years
|
||||
*/
|
||||
export function preenHistory (history, isSubscribed, timezoneOffset) {
|
||||
// history = _.filter(history, historyEntry => Boolean(historyEntry)); // Filter missing entries TODO add to migration
|
||||
let now = timezoneOffset ? moment().zone(timezoneOffset) : moment();
|
||||
// Date after which to begin compressing data
|
||||
let cutOff = now.subtract(isSubscribed ? 365 : 60, 'days').startOf('day');
|
||||
|
||||
// Keep uncompressed entries (modifies history)
|
||||
let newHistory = _.remove(history, entry => {
|
||||
let date = moment(entry.date);
|
||||
return date.isSame(cutOff) || date.isAfter(cutOff);
|
||||
});
|
||||
|
||||
// Date after which to begin compressing data by year
|
||||
let monthsCutOff = cutOff.subtract(isSubscribed ? 12 : 10, 'months').startOf('day');
|
||||
let aggregateByMonth = _.remove(history, entry => {
|
||||
let date = moment(entry.date);
|
||||
return date.isSame(monthsCutOff) || date.isAfter(monthsCutOff);
|
||||
});
|
||||
// Aggregate remaining entries by month and year
|
||||
if (aggregateByMonth.length > 0) newHistory.unshift(..._aggregate(aggregateByMonth, 'YYYYMM'));
|
||||
if (history.length > 0) newHistory.unshift(..._aggregate(history, 'YYYY'));
|
||||
|
||||
return newHistory;
|
||||
}
|
||||
|
||||
// Preen history for users and tasks. This code runs only on the server.
|
||||
export function preenUserHistory (user, tasksByType) {
|
||||
let isSubscribed = user.isSubscribed();
|
||||
let timezoneOffset = user.preferences.timezoneOffset;
|
||||
let minHistoryLength = isSubscribed ? 365 : 60;
|
||||
|
||||
function _processTask (task) {
|
||||
if (task.history && task.history.length > minHistoryLength) {
|
||||
task.history = preenHistory(task.history, isSubscribed, timezoneOffset);
|
||||
task.markModified('history');
|
||||
}
|
||||
}
|
||||
|
||||
tasksByType.habits.forEach(_processTask);
|
||||
tasksByType.dailys.forEach(_processTask);
|
||||
|
||||
if (user.history.exp.length > minHistoryLength) {
|
||||
user.history.exp = preenHistory(user.history.exp, isSubscribed, timezoneOffset);
|
||||
user.markModified('history.exp');
|
||||
}
|
||||
|
||||
if (user.history.todos.length > minHistoryLength) {
|
||||
user.history.todos = preenHistory(user.history.todos, isSubscribed, timezoneOffset);
|
||||
user.markModified('history.todos');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET /groups/:groupId/chat', () => {
|
||||
let user;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /chat/:chatId/flag', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /chat/:chatId/like', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /chat', () => {
|
||||
let user;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /group', () => {
|
||||
let user;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /group/:groupId/join', () => {
|
||||
@@ -15,22 +16,28 @@ describe('POST /group/:groupId/join', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('Accepting invitation to a guild', () => {
|
||||
context('Accepting invitation to a private guild', () => {
|
||||
let user, invitedUser, guild;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({balance: 1});
|
||||
guild = await user.post('/groups', {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
let { group, groupLeader, invitees } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
invites: 1,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
invitedUser = invitees[0];
|
||||
});
|
||||
|
||||
it('returns error when user is not invited to private guild', async () => {
|
||||
let joiningUser = await generateUser();
|
||||
let userWithoutInvite = await generateUser();
|
||||
|
||||
await expect(joiningUser.post(`/groups/${guild._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
await expect(userWithoutInvite.post(`/groups/${guild._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageGroupRequiresInvite'),
|
||||
@@ -38,26 +45,21 @@ describe('POST /group/:groupId/join', () => {
|
||||
});
|
||||
|
||||
it('allows non-invited users to join public guilds', async () => {
|
||||
await user.update({balance: 1});
|
||||
guild = await user.post('/groups', {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
});
|
||||
let publicGuild = (await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
},
|
||||
})).group;
|
||||
|
||||
let joiningUser = await generateUser();
|
||||
await joiningUser.post(`/groups/${guild._id}/join`);
|
||||
await joiningUser.post(`/groups/${publicGuild._id}/join`);
|
||||
|
||||
await expect(joiningUser.get('/user')).to.eventually.have.property('guilds').to.include(guild._id);
|
||||
await expect(joiningUser.get('/user')).to.eventually.have.property('guilds').and.to.include(publicGuild._id);
|
||||
});
|
||||
|
||||
context('User is invited', () => {
|
||||
beforeEach(async () => {
|
||||
invitedUser = await generateUser({
|
||||
'invitations.guilds': [{ id: guild._id}],
|
||||
});
|
||||
});
|
||||
|
||||
it('allows invited user to join private guilds', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
@@ -92,17 +94,23 @@ describe('POST /group/:groupId/join', () => {
|
||||
let user, invitedUser, party;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
party = await user.post('/groups', {
|
||||
name: 'Test Party',
|
||||
type: 'party',
|
||||
let { group, groupLeader, invitees } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Party',
|
||||
type: 'party',
|
||||
},
|
||||
invites: 1,
|
||||
});
|
||||
|
||||
party = group;
|
||||
user = groupLeader;
|
||||
invitedUser = invitees[0];
|
||||
});
|
||||
|
||||
it('returns error when user is not invited to party', async () => {
|
||||
let joiningUser = await generateUser();
|
||||
let userWithoutInvite = await generateUser();
|
||||
|
||||
await expect(joiningUser.post(`/groups/${party._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
await expect(userWithoutInvite.post(`/groups/${party._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageGroupRequiresInvite'),
|
||||
@@ -110,12 +118,6 @@ describe('POST /group/:groupId/join', () => {
|
||||
});
|
||||
|
||||
context('User is invited', () => {
|
||||
beforeEach(async () => {
|
||||
invitedUser = await generateUser({
|
||||
'invitations.party': { id: party._id, inviter: user._id },
|
||||
});
|
||||
});
|
||||
|
||||
it('allows invited user to join party', async () => {
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
const INVITES_LIMIT = 100;
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('PUT /group', () => {
|
||||
let groupLeader;
|
||||
let leader, nonLeader, groupToUpdate;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
let groupToUpdate;
|
||||
let groupUpdatedName = 'Test Public Guild Updated';
|
||||
|
||||
beforeEach(async () => {
|
||||
groupLeader = await generateUser({balance: 1});
|
||||
groupToUpdate = await groupLeader.post('/groups', {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
groupToUpdate = group;
|
||||
leader = groupLeader;
|
||||
nonLeader = members[0];
|
||||
});
|
||||
|
||||
it('returns an error when a non group leader tries to update', async () => {
|
||||
let memberToAttemptUpdate = await generateUser();
|
||||
|
||||
await groupLeader.post(`/groups/${groupToUpdate._id}/invite`, {
|
||||
uuids: [memberToAttemptUpdate._id],
|
||||
});
|
||||
await memberToAttemptUpdate.post(`/groups/${groupToUpdate._id}/join`);
|
||||
|
||||
await expect(memberToAttemptUpdate.put(`/groups/${groupToUpdate._id}`, {
|
||||
await expect(nonLeader.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
@@ -36,7 +35,7 @@ describe('PUT /group', () => {
|
||||
});
|
||||
|
||||
it('updates a group', async () => {
|
||||
let updatedGroup = await groupLeader.put(`/groups/${groupToUpdate._id}`, {
|
||||
let updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
|
||||
name: groupUpdatedName,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { requester } from '../../../helpers/api-integration.helper';
|
||||
import { requester } from '../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('notFound Middleware', () => {
|
||||
it('returns a 404 error when the resource is not found', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('DELETE /tags/:tagId', () => {
|
||||
let user;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET /tags', () => {
|
||||
let user;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET /tags/:tagId', () => {
|
||||
let user;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /tags', () => {
|
||||
let user;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('PUT /tags/:tagId', () => {
|
||||
let user;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('DELETE /tasks/:id', () => {
|
||||
let user;
|
||||
@@ -14,7 +14,7 @@ describe('DELETE /tasks/:id', () => {
|
||||
let task;
|
||||
|
||||
beforeEach(async () => {
|
||||
task = await user.post('/tasks', {
|
||||
task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
@@ -42,7 +42,7 @@ describe('DELETE /tasks/:id', () => {
|
||||
|
||||
it('cannot delete a task owned by someone else', async () => {
|
||||
let anotherUser = await generateUser();
|
||||
let anotherUsersTask = await anotherUser.post('/tasks', {
|
||||
let anotherUsersTask = await anotherUser.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
import Q from 'q';
|
||||
|
||||
describe('GET /tasks', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('returns all user\'s tasks', async () => {
|
||||
let createdTasks = await Q.all([
|
||||
user.post('/tasks', {text: 'test habit', type: 'habit'}),
|
||||
]);
|
||||
|
||||
let length = createdTasks.length;
|
||||
let tasks = await user.get('/tasks');
|
||||
|
||||
expect(tasks.length).to.equal(length + 1); // + 1 because 1 is a default task
|
||||
});
|
||||
|
||||
it('returns only a type of user\'s tasks if req.query.type is specified', async () => {
|
||||
let task = await user.post('/tasks', {text: 'test habit', type: 'habit'});
|
||||
let tasks = await user.get('/tasks?type=habit');
|
||||
|
||||
expect(tasks.length).to.equal(1);
|
||||
expect(tasks[0]._id).to.equal(task._id);
|
||||
});
|
||||
|
||||
// TODO complete after task scoring is done
|
||||
it('returns completed todos sorted by creation date if req.query.includeCompletedTodos is specified');
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('GET /tasks/:id', () => {
|
||||
@@ -15,7 +15,7 @@ describe('GET /tasks/:id', () => {
|
||||
let task;
|
||||
|
||||
beforeEach(async () => {
|
||||
task = await user.post('/tasks', {
|
||||
task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('GET /tasks/:id', () => {
|
||||
|
||||
it('cannot get a task owned by someone else', async () => {
|
||||
let anotherUser = await generateUser();
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
|
||||
describe('GET /tasks/user', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('returns all user\'s tasks', async () => {
|
||||
let createdTasks = await user.post('/tasks/user', [{text: 'test habit', type: 'habit'}, {text: 'test todo', type: 'todo'}]);
|
||||
let tasks = await user.get('/tasks/user');
|
||||
expect(tasks.length).to.equal(createdTasks.length + 1); // + 1 because 1 is a default task
|
||||
});
|
||||
|
||||
it('returns only a type of user\'s tasks if req.query.type is specified', async () => {
|
||||
let createdTasks = await user.post('/tasks/user', [{text: 'test habit', type: 'habit'}, {text: 'test todo', type: 'todo'}]);
|
||||
let tasks = await user.get('/tasks/user?type=habit');
|
||||
expect(tasks.length).to.equal(1);
|
||||
expect(tasks[0]._id).to.equal(createdTasks[0]._id);
|
||||
});
|
||||
|
||||
// TODO complete after task scoring is done
|
||||
it('returns completed todos sorted by creation date if req.query.includeCompletedTodos is specified');
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/:id/score/:direction', () => {
|
||||
@@ -35,7 +35,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
let todo;
|
||||
|
||||
beforeEach(async () => {
|
||||
todo = await user.post('/tasks', {
|
||||
todo = await user.post('/tasks/user', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
});
|
||||
@@ -134,7 +134,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
let daily;
|
||||
|
||||
beforeEach(async () => {
|
||||
daily = await user.post('/tasks', {
|
||||
daily = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
});
|
||||
@@ -205,24 +205,24 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
let habit, minusHabit, plusHabit, neitherHabit; // eslint-disable-line no-unused-vars
|
||||
|
||||
beforeEach(async () => {
|
||||
habit = await user.post('/tasks', {
|
||||
habit = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
minusHabit = await user.post('/tasks', {
|
||||
minusHabit = await user.post('/tasks/user', {
|
||||
text: 'test min habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
});
|
||||
|
||||
plusHabit = await user.post('/tasks', {
|
||||
plusHabit = await user.post('/tasks/user', {
|
||||
text: 'test plus habit',
|
||||
type: 'habit',
|
||||
down: false,
|
||||
});
|
||||
|
||||
neitherHabit = await user.post('/tasks', {
|
||||
neitherHabit = await user.post('/tasks/user', {
|
||||
text: 'test neither habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
@@ -267,7 +267,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
let reward, updatedUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
reward = await user.post('/tasks', {
|
||||
reward = await user.post('/tasks/user', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
value: 5,
|
||||
|
||||
+43
-39
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /tasks', () => {
|
||||
describe('POST /tasks/user', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
@@ -12,7 +12,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
context('validates params', async () => {
|
||||
it('returns an error if req.body.type is absent', async () => {
|
||||
await expect(user.post('/tasks', {
|
||||
await expect(user.post('/tasks/user', {
|
||||
notType: 'habit',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
@@ -22,7 +22,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('returns an error if req.body.type is not valid', async () => {
|
||||
await expect(user.post('/tasks', {
|
||||
await expect(user.post('/tasks/user', {
|
||||
type: 'habitF',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
@@ -32,7 +32,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('returns an error if one object inside an array is invalid', async () => {
|
||||
await expect(user.post('/tasks', [
|
||||
await expect(user.post('/tasks/user', [
|
||||
{type: 'habitF'},
|
||||
{type: 'habit'},
|
||||
])).to.eventually.be.rejected.and.eql({
|
||||
@@ -43,7 +43,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('returns an error if req.body.text is absent', async () => {
|
||||
await expect(user.post('/tasks', {
|
||||
await expect(user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
@@ -54,7 +54,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('does not update user.tasksOrder.{taskType} when the task is not saved because invalid', async () => {
|
||||
let originalHabitsOrder = (await user.get('/user')).tasksOrder.habits;
|
||||
await expect(user.post('/tasks', {
|
||||
await expect(user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
})).to.eventually.be.rejected.and.eql({ // this block is necessary
|
||||
code: 400,
|
||||
@@ -68,7 +68,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('does not update user.tasksOrder.{taskType} when a task inside an array is not saved because invalid', async () => {
|
||||
let originalHabitsOrder = (await user.get('/user')).tasksOrder.habits;
|
||||
await expect(user.post('/tasks', [
|
||||
await expect(user.post('/tasks/user', [
|
||||
{type: 'habit'}, // Missing text
|
||||
{type: 'habit', text: 'valid'}, // Valid
|
||||
])).to.eventually.be.rejected.and.eql({ // this block is necessary
|
||||
@@ -82,14 +82,18 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('does not save any task sent in an array when 1 is invalid', async () => {
|
||||
let originalTasks = await user.get('/tasks');
|
||||
await expect(user.post('/tasks', [
|
||||
let originalTasks = await user.get('/tasks/user');
|
||||
await expect(user.post('/tasks/user', [
|
||||
{type: 'habit'}, // Missing text
|
||||
{type: 'habit', text: 'valid'}, // Valid
|
||||
])).to.eventually.be.rejected.and.eql({ // this block is necessary
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'habit validation failed',
|
||||
}).then(async () => {
|
||||
let updatedTasks = await user.get('/tasks/user');
|
||||
|
||||
expect(updatedTasks).to.eql(originalTasks);
|
||||
});
|
||||
|
||||
let updatedTasks = await user.get('/tasks');
|
||||
@@ -97,7 +101,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('automatically sets "task.userId" to user\'s uuid', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
@@ -108,7 +112,7 @@ describe('POST /tasks', () => {
|
||||
it(`ignores setting userId, history, createdAt,
|
||||
updatedAt, challenge, completed, streak,
|
||||
dateCompleted fields`, async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
userId: 123,
|
||||
@@ -132,7 +136,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('ignores invalid fields', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
notValid: true,
|
||||
@@ -144,7 +148,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
context('habits', () => {
|
||||
it('creates a habit', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
@@ -162,7 +166,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('updates user.tasksOrder.habits when a new habit is created', async () => {
|
||||
let originalHabitsOrderLen = (await user.get('/user')).tasksOrder.habits.length;
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'an habit',
|
||||
});
|
||||
@@ -174,7 +178,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('updates user.tasksOrder.habits when multiple habits are created', async () => {
|
||||
let originalHabitsOrderLen = (await user.get('/user')).tasksOrder.habits.length;
|
||||
let [task, task2] = await user.post('/tasks', [{
|
||||
let [task, task2] = await user.post('/tasks/user', [{
|
||||
type: 'habit',
|
||||
text: 'an habit',
|
||||
}, {
|
||||
@@ -189,7 +193,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('creates multiple habits', async () => {
|
||||
let [task, task2] = await user.post('/tasks', [{
|
||||
let [task, task2] = await user.post('/tasks/user', [{
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
@@ -219,7 +223,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('defaults to setting up and down to true', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
notes: 1976,
|
||||
@@ -230,7 +234,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('cannot create checklists', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
checklist: [
|
||||
@@ -244,7 +248,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
context('todos', () => {
|
||||
it('creates a todo', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
notes: 1976,
|
||||
@@ -257,7 +261,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('creates multiple todos', async () => {
|
||||
let [task, task2] = await user.post('/tasks', [{
|
||||
let [task, task2] = await user.post('/tasks/user', [{
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
notes: 1976,
|
||||
@@ -280,7 +284,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('updates user.tasksOrder.todos when a new todo is created', async () => {
|
||||
let originalTodosOrderLen = (await user.get('/user')).tasksOrder.todos.length;
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'todo',
|
||||
text: 'a todo',
|
||||
});
|
||||
@@ -292,7 +296,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('updates user.tasksOrder.todos when multiple todos are created', async () => {
|
||||
let originalTodosOrderLen = (await user.get('/user')).tasksOrder.todos.length;
|
||||
let [task, task2] = await user.post('/tasks', [{
|
||||
let [task, task2] = await user.post('/tasks/user', [{
|
||||
type: 'todo',
|
||||
text: 'a todo',
|
||||
}, {
|
||||
@@ -307,7 +311,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('can create checklists', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
checklist: [
|
||||
@@ -328,7 +332,7 @@ describe('POST /tasks', () => {
|
||||
it('creates a daily', async () => {
|
||||
let now = new Date();
|
||||
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
notes: 1976,
|
||||
@@ -347,7 +351,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('creates multiple dailys', async () => {
|
||||
let [task, task2] = await user.post('/tasks', [{
|
||||
let [task, task2] = await user.post('/tasks/user', [{
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
notes: 1976,
|
||||
@@ -370,7 +374,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('updates user.tasksOrder.dailys when a new daily is created', async () => {
|
||||
let originalDailysOrderLen = (await user.get('/user')).tasksOrder.dailys.length;
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'a daily',
|
||||
});
|
||||
@@ -382,7 +386,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('updates user.tasksOrder.dailys when multiple dailys are created', async () => {
|
||||
let originalDailysOrderLen = (await user.get('/user')).tasksOrder.dailys.length;
|
||||
let [task, task2] = await user.post('/tasks', [{
|
||||
let [task, task2] = await user.post('/tasks/user', [{
|
||||
type: 'daily',
|
||||
text: 'a daily',
|
||||
}, {
|
||||
@@ -397,7 +401,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('defaults to a weekly frequency, with every day set', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
});
|
||||
@@ -416,7 +420,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('allows repeat field to be configured', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
repeat: {
|
||||
@@ -440,7 +444,7 @@ describe('POST /tasks', () => {
|
||||
it('defaults startDate to today', async () => {
|
||||
let today = (new Date()).getDay();
|
||||
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
});
|
||||
@@ -449,7 +453,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('can create checklists', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
checklist: [
|
||||
@@ -468,7 +472,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
context('rewards', () => {
|
||||
it('creates a reward', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
notes: 1976,
|
||||
@@ -483,7 +487,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('creates multiple rewards', async () => {
|
||||
let [task, task2] = await user.post('/tasks', [{
|
||||
let [task, task2] = await user.post('/tasks/user', [{
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
notes: 1976,
|
||||
@@ -510,7 +514,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('updates user.tasksOrder.rewards when a new reward is created', async () => {
|
||||
let originalRewardsOrderLen = (await user.get('/user')).tasksOrder.rewards.length;
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'reward',
|
||||
text: 'a reward',
|
||||
});
|
||||
@@ -522,7 +526,7 @@ describe('POST /tasks', () => {
|
||||
|
||||
it('updates user.tasksOrder.dreward when multiple rewards are created', async () => {
|
||||
let originalRewardsOrderLen = (await user.get('/user')).tasksOrder.rewards.length;
|
||||
let [task, task2] = await user.post('/tasks', [{
|
||||
let [task, task2] = await user.post('/tasks/user', [{
|
||||
type: 'reward',
|
||||
text: 'a reward',
|
||||
}, {
|
||||
@@ -537,7 +541,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('defaults to a 0 value', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
});
|
||||
@@ -546,7 +550,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('requires value to be coerced into a number', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
value: '10',
|
||||
@@ -556,7 +560,7 @@ describe('POST /tasks', () => {
|
||||
});
|
||||
|
||||
it('cannot create checklists', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
checklist: [
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('PUT /tasks/:id', () => {
|
||||
@@ -14,7 +14,7 @@ describe('PUT /tasks/:id', () => {
|
||||
let task;
|
||||
|
||||
beforeEach(async () => {
|
||||
task = await user.post('/tasks', {
|
||||
task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
@@ -61,7 +61,7 @@ describe('PUT /tasks/:id', () => {
|
||||
let habit;
|
||||
|
||||
beforeEach(async () => {
|
||||
habit = await user.post('/tasks', {
|
||||
habit = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
notes: 1976,
|
||||
@@ -87,7 +87,7 @@ describe('PUT /tasks/:id', () => {
|
||||
let todo;
|
||||
|
||||
beforeEach(async () => {
|
||||
todo = await user.post('/tasks', {
|
||||
todo = await user.post('/tasks/user', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
notes: 1976,
|
||||
@@ -142,7 +142,7 @@ describe('PUT /tasks/:id', () => {
|
||||
let daily;
|
||||
|
||||
beforeEach(async () => {
|
||||
daily = await user.post('/tasks', {
|
||||
daily = await user.post('/tasks/user', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
notes: 1976,
|
||||
@@ -244,7 +244,7 @@ describe('PUT /tasks/:id', () => {
|
||||
let reward;
|
||||
|
||||
beforeEach(async () => {
|
||||
reward = await user.post('/tasks', {
|
||||
reward = await user.post('/tasks/user', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
notes: 1976,
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration.helper';
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('DELETE /tasks/:taskId/checklist/:itemId', () => {
|
||||
@@ -12,7 +12,7 @@ describe('DELETE /tasks/:taskId/checklist/:itemId', () => {
|
||||
});
|
||||
|
||||
it('deletes a checklist item', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
@@ -26,7 +26,7 @@ describe('DELETE /tasks/:taskId/checklist/:itemId', () => {
|
||||
});
|
||||
|
||||
it('does not work with habits', async () => {
|
||||
let habit = await user.post('/tasks', {
|
||||
let habit = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
@@ -39,7 +39,7 @@ describe('DELETE /tasks/:taskId/checklist/:itemId', () => {
|
||||
});
|
||||
|
||||
it('does not work with rewards', async () => {
|
||||
let reward = await user.post('/tasks', {
|
||||
let reward = await user.post('/tasks/user', {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
@@ -60,7 +60,7 @@ describe('DELETE /tasks/:taskId/checklist/:itemId', () => {
|
||||
});
|
||||
|
||||
it('fails on checklist item not found', async () => {
|
||||
let createdTask = await user.post('/tasks', {
|
||||
let createdTask = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'daily with checklist',
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration.helper';
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/:taskId/checklist/', () => {
|
||||
@@ -12,7 +12,7 @@ describe('POST /tasks/:taskId/checklist/', () => {
|
||||
});
|
||||
|
||||
it('adds a checklist item to a task', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe('POST /tasks/:taskId/checklist/', () => {
|
||||
});
|
||||
|
||||
it('does not add a checklist to habits', async () => {
|
||||
let habit = await user.post('/tasks', {
|
||||
let habit = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
@@ -47,7 +47,7 @@ describe('POST /tasks/:taskId/checklist/', () => {
|
||||
});
|
||||
|
||||
it('does not add a checklist to rewards', async () => {
|
||||
let reward = await user.post('/tasks', {
|
||||
let reward = await user.post('/tasks/user', {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration.helper';
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
|
||||
@@ -12,7 +12,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
|
||||
});
|
||||
|
||||
it('scores a checklist item', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
@@ -29,7 +29,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
|
||||
});
|
||||
|
||||
it('fails on habits', async () => {
|
||||
let habit = await user.post('/tasks', {
|
||||
let habit = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
@@ -44,7 +44,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
|
||||
});
|
||||
|
||||
it('fails on rewards', async () => {
|
||||
let reward = await user.post('/tasks', {
|
||||
let reward = await user.post('/tasks/user', {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
@@ -65,7 +65,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
|
||||
});
|
||||
|
||||
it('fails on checklist item not found', async () => {
|
||||
let createdTask = await user.post('/tasks', {
|
||||
let createdTask = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'daily with checklist',
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration.helper';
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('PUT /tasks/:taskId/checklist/:itemId', () => {
|
||||
@@ -12,7 +12,7 @@ describe('PUT /tasks/:taskId/checklist/:itemId', () => {
|
||||
});
|
||||
|
||||
it('updates a checklist item', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'Daily with checklist',
|
||||
});
|
||||
@@ -35,7 +35,7 @@ describe('PUT /tasks/:taskId/checklist/:itemId', () => {
|
||||
});
|
||||
|
||||
it('fails on habits', async () => {
|
||||
let habit = await user.post('/tasks', {
|
||||
let habit = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'habit with checklist',
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe('PUT /tasks/:taskId/checklist/:itemId', () => {
|
||||
});
|
||||
|
||||
it('fails on rewards', async () => {
|
||||
let reward = await user.post('/tasks', {
|
||||
let reward = await user.post('/tasks/user', {
|
||||
type: 'reward',
|
||||
text: 'reward with checklist',
|
||||
});
|
||||
@@ -69,7 +69,7 @@ describe('PUT /tasks/:taskId/checklist/:itemId', () => {
|
||||
});
|
||||
|
||||
it('fails on checklist item not found', async () => {
|
||||
let createdTask = await user.post('/tasks', {
|
||||
let createdTask = await user.post('/tasks/user', {
|
||||
type: 'daily',
|
||||
text: 'daily with checklist',
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration.helper';
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('DELETE /tasks/:taskId/tags/:tagId', () => {
|
||||
@@ -12,7 +12,7 @@ describe('DELETE /tasks/:taskId/tags/:tagId', () => {
|
||||
});
|
||||
|
||||
it('removes a tag from a task', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
@@ -28,7 +28,7 @@ describe('DELETE /tasks/:taskId/tags/:tagId', () => {
|
||||
});
|
||||
|
||||
it('only deletes existing tags', async () => {
|
||||
let createdTask = await user.post('/tasks', {
|
||||
let createdTask = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration.helper';
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/:taskId/tags/:tagId', () => {
|
||||
@@ -12,7 +12,7 @@ describe('POST /tasks/:taskId/tags/:tagId', () => {
|
||||
});
|
||||
|
||||
it('adds a tag to a task', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
@@ -24,7 +24,7 @@ describe('POST /tasks/:taskId/tags/:tagId', () => {
|
||||
});
|
||||
|
||||
it('does not add a tag to a task twice', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
@@ -41,7 +41,7 @@ describe('POST /tasks/:taskId/tags/:tagId', () => {
|
||||
});
|
||||
|
||||
it('does not add a non existing tag to a task', async () => {
|
||||
let task = await user.post('/tasks', {
|
||||
let task = await user.post('/tasks/user', {
|
||||
type: 'habit',
|
||||
text: 'Task with tag',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('GET /user', () => {
|
||||
let user;
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration.helper';
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateRandomUserName } from 'uuid';
|
||||
import { each } from 'lodash';
|
||||
|
||||
|
||||
@@ -938,124 +938,6 @@ describe('Cron', () => {
|
||||
expect(beforeTasks).to.eql(afterTasks);
|
||||
});
|
||||
|
||||
describe('preening', () => {
|
||||
beforeEach(function () {
|
||||
this.clock = sinon.useFakeTimers(Date.parse('2013-11-20'), 'Date');
|
||||
});
|
||||
afterEach(function () {
|
||||
return this.clock.restore();
|
||||
});
|
||||
|
||||
it('should preen user history', function () {
|
||||
let ref = beforeAfter({
|
||||
daysAgo: 1,
|
||||
});
|
||||
let after = ref.after;
|
||||
|
||||
let history = [
|
||||
{
|
||||
date: '09/01/2012',
|
||||
value: 0,
|
||||
}, {
|
||||
date: '10/01/2012',
|
||||
value: 0,
|
||||
}, {
|
||||
date: '11/01/2012',
|
||||
value: 2,
|
||||
}, {
|
||||
date: '12/01/2012',
|
||||
value: 2,
|
||||
}, {
|
||||
date: '01/01/2013',
|
||||
value: 1,
|
||||
}, {
|
||||
date: '01/15/2013',
|
||||
value: 3,
|
||||
}, {
|
||||
date: '02/01/2013',
|
||||
value: 2,
|
||||
}, {
|
||||
date: '02/15/2013',
|
||||
value: 4,
|
||||
}, {
|
||||
date: '03/01/2013',
|
||||
value: 3,
|
||||
}, {
|
||||
date: '03/15/2013',
|
||||
value: 5,
|
||||
}, {
|
||||
date: '04/01/2013',
|
||||
value: 4,
|
||||
}, {
|
||||
date: '04/15/2013',
|
||||
value: 6,
|
||||
}, {
|
||||
date: '05/01/2013',
|
||||
value: 5,
|
||||
}, {
|
||||
date: '05/15/2013',
|
||||
value: 7,
|
||||
}, {
|
||||
date: '06/01/2013',
|
||||
value: 6,
|
||||
}, {
|
||||
date: '06/15/2013',
|
||||
value: 8,
|
||||
}, {
|
||||
date: '07/01/2013',
|
||||
value: 7,
|
||||
}, {
|
||||
date: '07/15/2013',
|
||||
value: 9,
|
||||
}, {
|
||||
date: '08/01/2013',
|
||||
value: 8,
|
||||
}, {
|
||||
date: '08/15/2013',
|
||||
value: 10,
|
||||
}, {
|
||||
date: '09/01/2013',
|
||||
value: 9,
|
||||
}, {
|
||||
date: '09/15/2013',
|
||||
value: 11,
|
||||
}, {
|
||||
date: '010/01/2013',
|
||||
value: 10,
|
||||
}, {
|
||||
date: '010/15/2013',
|
||||
value: 12,
|
||||
}, {
|
||||
date: '011/01/2013',
|
||||
value: 12,
|
||||
}, {
|
||||
date: '011/02/2013',
|
||||
value: 13,
|
||||
}, {
|
||||
date: '011/03/2013',
|
||||
value: 14,
|
||||
}, {
|
||||
date: '011/04/2013',
|
||||
value: 15,
|
||||
},
|
||||
];
|
||||
|
||||
after.history = {
|
||||
exp: _.cloneDeep(history),
|
||||
todos: _.cloneDeep(history),
|
||||
};
|
||||
after.habits[0].history = _.cloneDeep(history);
|
||||
after.fns.cron();
|
||||
after.history.exp.pop();
|
||||
after.history.todos.pop();
|
||||
_.each([after.history.exp, after.history.todos, after.habits[0].history], function (arr) {
|
||||
expect(_.map(arr, (x) => {
|
||||
return x.value;
|
||||
})).to.eql([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Todos', () => {
|
||||
it('1 day missed', () => {
|
||||
let ref = beforeAfter({
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { preenHistory } from '../../common/script/preening';
|
||||
import moment from 'moment';
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
|
||||
function generateHistory (days) {
|
||||
let history = [];
|
||||
let now = Number(moment().toDate());
|
||||
|
||||
while (days > 0) {
|
||||
history.push({
|
||||
value: days,
|
||||
date: Number(moment(now).subtract(days, 'days').toDate()),
|
||||
});
|
||||
days--;
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
describe('preenHistory', () => {
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
// Replace system clocks so we can get predictable results
|
||||
clock = sinon.useFakeTimers(Number(moment('2013-10-20').zone(0).startOf('day').toDate()), 'Date');
|
||||
});
|
||||
afterEach(() => {
|
||||
return clock.restore();
|
||||
});
|
||||
|
||||
it('does not modify history if all entries are more recent than cutoff (free users)', () => {
|
||||
let h = generateHistory(60);
|
||||
expect(preenHistory(_.cloneDeep(h), false, 0)).to.eql(h);
|
||||
});
|
||||
|
||||
it('does not modify history if all entries are more recent than cutoff (subscribers)', () => {
|
||||
let h = generateHistory(365);
|
||||
expect(preenHistory(_.cloneDeep(h), true, 0)).to.eql(h);
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly entries before cutoff (free users)', () => {
|
||||
let h = generateHistory(81); // Jumps to July
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened.length).to.eql(62); // Keeps 60 days + 2 entries per august and july
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly entries before cutoff (subscribers)', () => {
|
||||
let h = generateHistory(396); // Jumps to September 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), true, 0);
|
||||
expect(preened.length).to.eql(367); // Keeps 365 days + 2 entries per october and september
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly and yearly entries before cutoff (free users)', () => {
|
||||
let h = generateHistory(731); // Jumps to October 21 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened.length).to.eql(73); // Keeps 60 days + 11 montly entries and 2 yearly entry for 2011 and 2012
|
||||
});
|
||||
|
||||
it('does aggregate data in monthly and yearly entries before cutoff (subscribers)', () => {
|
||||
let h = generateHistory(1031); // Jumps to October 21 2012
|
||||
let preened = preenHistory(_.cloneDeep(h), true, 0);
|
||||
expect(preened.length).to.eql(380); // Keeps 365 days + 13 montly entries and 2 yearly entries for 2011 and 2010
|
||||
});
|
||||
|
||||
it('correctly aggregates values', () => {
|
||||
let h = generateHistory(63); // Compress last 3 days
|
||||
let preened = preenHistory(_.cloneDeep(h), false, 0);
|
||||
expect(preened[0].value).to.eql((61 + 62 + 63) / 3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,279 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
|
||||
import {
|
||||
assign,
|
||||
each,
|
||||
isEmpty,
|
||||
set,
|
||||
times,
|
||||
} from 'lodash';
|
||||
import Q from 'q';
|
||||
import { MongoClient as mongo } from 'mongodb';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import superagent from 'superagent';
|
||||
import i18n from '../../common/script/src/i18n';
|
||||
i18n.translations = require('../../website/src/libs/api-v3/i18n').translations;
|
||||
|
||||
const API_TEST_SERVER_PORT = 3003;
|
||||
|
||||
class ApiUser {
|
||||
constructor (options) {
|
||||
assign(this, options);
|
||||
|
||||
this.get = _requestMaker(this, 'get');
|
||||
this.post = _requestMaker(this, 'post');
|
||||
this.put = _requestMaker(this, 'put');
|
||||
this.del = _requestMaker(this, 'del');
|
||||
}
|
||||
|
||||
update (options) {
|
||||
return new Promise((resolve) => {
|
||||
_updateDocument('users', this, options, resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sets up an abject that can make all REST requests
|
||||
// If a user is passed in, the uuid and api token of
|
||||
// the user are used to make the requests
|
||||
export function requester (user = {}, additionalSets) {
|
||||
return {
|
||||
get: _requestMaker(user, 'get', additionalSets),
|
||||
post: _requestMaker(user, 'post', additionalSets),
|
||||
put: _requestMaker(user, 'put', additionalSets),
|
||||
del: _requestMaker(user, 'del', additionalSets),
|
||||
};
|
||||
}
|
||||
|
||||
// Use this to verify error messages returned by the server
|
||||
// That way, if the translated string changes, the test
|
||||
// will not break. NOTE: it checks agains errors with string as well.
|
||||
export function translate (key, variables) {
|
||||
const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.';
|
||||
const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
||||
|
||||
let translatedString = i18n.t(key, variables);
|
||||
|
||||
expect(translatedString).to.not.be.empty;
|
||||
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
|
||||
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
|
||||
|
||||
return translatedString;
|
||||
}
|
||||
|
||||
// Useful for checking things that have been deleted,
|
||||
// but you no longer have access to,
|
||||
// like private parties or users
|
||||
export function checkExistence (collectionName, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
mongo.connect('mongodb://localhost/habitrpg_test', (connectionError, db) => {
|
||||
if (connectionError) return reject(connectionError);
|
||||
let collection = db.collection(collectionName);
|
||||
|
||||
collection.find({_id: id}, {_id: 1}).limit(1).toArray((findError, docs) => {
|
||||
if (findError) return reject(findError);
|
||||
|
||||
let exists = docs.length > 0;
|
||||
|
||||
db.close();
|
||||
resolve(exists);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a new user and returns it
|
||||
// If you need the user to have specific requirements,
|
||||
// such as a balance > 0, just pass in the adjustment
|
||||
// to the update object. If you want to adjust a nested
|
||||
// paramter, such as the number of wolf eggs the user has,
|
||||
// , you can do so by passing in the full path as a string:
|
||||
// { 'items.eggs.Wolf': 10 }
|
||||
export async function generateUser (update = {}) {
|
||||
let username = generateUUID();
|
||||
let password = 'password';
|
||||
let email = `${username}@example.com`;
|
||||
|
||||
let request = _requestMaker({}, 'post');
|
||||
|
||||
let user = await request('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
return Q.promise((resolve) => {
|
||||
_updateDocument('users', user, update, () => {
|
||||
let apiUser = new ApiUser(user);
|
||||
|
||||
resolve(apiUser);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Generates a new group. Requires a user object, which
|
||||
// will will become the groups leader. Takes an update
|
||||
// argument which will update group
|
||||
export function generateGroup (leader, details = {}, update = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
leader.post('/groups', details).then((group) => {
|
||||
_updateDocument('groups', group, update, () => {
|
||||
resolve(group);
|
||||
});
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// This is generate group + the ability to create
|
||||
// real users to populate it. The settings object
|
||||
// takes in:
|
||||
// members: Number - the number of group members to create. Defaults to 0.
|
||||
// inivtes: Number - the number of users to create and invite to the group. Defaults to 0.
|
||||
// groupDetails: Object - how to initialize the group
|
||||
// leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user
|
||||
// can create the group
|
||||
//
|
||||
// Returns an object with
|
||||
// members: an array of user objects that correspond to the members of the group
|
||||
// invitees: an array of user objects that correspond to the invitees of the group
|
||||
// leader: the leader user object
|
||||
// group: the group object
|
||||
export async function createAndPopulateGroup (settings = {}) {
|
||||
let numberOfMembers = settings.members || 0;
|
||||
let numberOfInvites = settings.invites || 0;
|
||||
let groupDetails = settings.groupDetails;
|
||||
let leaderDetails = settings.leaderDetails || { balance: 10 };
|
||||
|
||||
let groupLeader = await generateUser(leaderDetails);
|
||||
let group = await generateGroup(groupLeader, groupDetails);
|
||||
|
||||
let members = await Q.all(
|
||||
times(numberOfMembers, () => {
|
||||
return generateUser();
|
||||
})
|
||||
);
|
||||
|
||||
let groupTypes = {
|
||||
guild: { guilds: [group._id] },
|
||||
party: { 'party._id': group._id },
|
||||
};
|
||||
|
||||
let memberPromises = members.map((member) => {
|
||||
return member.update(groupTypes[group.type]);
|
||||
});
|
||||
|
||||
await Q.all(memberPromises);
|
||||
|
||||
let invitees = await Q.all(
|
||||
times(numberOfInvites, () => {
|
||||
return generateUser();
|
||||
})
|
||||
);
|
||||
|
||||
let invitationPromises = invitees.map((invitee) => {
|
||||
return groupLeader.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [invitee._id],
|
||||
});
|
||||
});
|
||||
|
||||
await Q.all(invitationPromises);
|
||||
|
||||
return {
|
||||
groupLeader,
|
||||
group,
|
||||
members,
|
||||
invitees,
|
||||
};
|
||||
}
|
||||
|
||||
// Specifically helpful for the GET /groups tests,
|
||||
// resets the db to an empty state and creates a tavern document
|
||||
export function resetHabiticaDB () {
|
||||
return new Promise((resolve, reject) => {
|
||||
mongo.connect('mongodb://localhost/habitrpg_test', (err, db) => {
|
||||
if (err) return reject(err);
|
||||
|
||||
db.dropDatabase((dbErr) => {
|
||||
if (dbErr) return reject(dbErr);
|
||||
let groups = db.collection('groups');
|
||||
|
||||
groups.insertOne({
|
||||
_id: 'habitrpg',
|
||||
chat: [],
|
||||
leader: '9',
|
||||
name: 'HabitRPG',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
members: [],
|
||||
}, (insertErr) => {
|
||||
if (insertErr) return reject(insertErr);
|
||||
|
||||
db.close();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _requestMaker (user, method, additionalSets) {
|
||||
return (route, send, query) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = superagent[method](`http://localhost:${API_TEST_SERVER_PORT}/api/v3${route}`)
|
||||
.accept('application/json');
|
||||
|
||||
if (user && user._id && user.apiToken) {
|
||||
request
|
||||
.set('x-api-user', user._id)
|
||||
.set('x-api-key', user.apiToken);
|
||||
}
|
||||
|
||||
if (additionalSets) {
|
||||
request.set(additionalSets);
|
||||
}
|
||||
|
||||
request
|
||||
.query(query)
|
||||
.send(send)
|
||||
.end((err, response) => {
|
||||
if (err) {
|
||||
if (!err.response) return reject(err);
|
||||
|
||||
return reject({
|
||||
code: err.status,
|
||||
error: err.response.body.error,
|
||||
message: err.response.body.message,
|
||||
});
|
||||
}
|
||||
|
||||
resolve(response.body);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function _updateDocument (collectionName, doc, update, cb) {
|
||||
if (isEmpty(update)) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
mongo.connect('mongodb://localhost/habitrpg_test', (connectErr, db) => {
|
||||
if (connectErr) throw new Error(`Error connecting to database when updating ${collectionName} collection: ${connectErr}`);
|
||||
|
||||
let collection = db.collection(collectionName);
|
||||
|
||||
collection.updateOne({ _id: doc._id }, { $set: update }, (updateErr) => {
|
||||
if (updateErr) throw new Error(`Error updating ${collectionName}: ${updateErr}`);
|
||||
_updateLocalDocument(doc, update);
|
||||
db.close();
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _updateLocalDocument (doc, update) {
|
||||
each(update, (value, param) => {
|
||||
set(doc, param, value);
|
||||
});
|
||||
}
|
||||
@@ -117,12 +117,13 @@ api.getGroups = {
|
||||
// If no valid value for type was supplied, return an error
|
||||
if (queries.length === 0) throw new BadRequest(res.t('groupTypesRequired'));
|
||||
|
||||
let results = await Q.all(queries); // TODO we would like not to return a single big array but Q doesn't support the funtionality https://github.com/kriskowal/q/issues/328
|
||||
// TODO we would like not to return a single big array but Q doesn't support the funtionality https://github.com/kriskowal/q/issues/328
|
||||
let results = _.reduce(await Q.all(queries), (previousValue, currentValue) => {
|
||||
if (_.isEmpty(currentValue)) return previousValue; // don't add anything to the results if the query returned null or an empty array
|
||||
return previousValue.concat(Array.isArray(currentValue) ? currentValue : [currentValue]); // otherwise concat the new results to the previousValue
|
||||
}, []);
|
||||
|
||||
res.respond(200, _.reduce(results, (m, v) => {
|
||||
if (_.isEmpty(v)) return m;
|
||||
return m.concat(Array.isArray(v) ? v : [v]);
|
||||
}, []));
|
||||
res.respond(200, results);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { authWithHeaders } from '../../middlewares/api-v3/auth';
|
||||
import cron from '../../middlewares/api-v3/cron';
|
||||
import { sendTaskWebhook } from '../../libs/api-v3/webhook';
|
||||
import * as Tasks from '../../models/task';
|
||||
import { model as Challenge } from '../../models/challenge';
|
||||
import {
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
@@ -10,70 +11,213 @@ import {
|
||||
import shared from '../../../../common';
|
||||
import Q from 'q';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import scoreTask from '../../../../common/script/api-v3/scoreTask';
|
||||
import { preenHistory } from '../../../../common/script/api-v3/preenHistory';
|
||||
import { preenHistory } from '../../../../common/script/api-v3/preening';
|
||||
|
||||
let api = {};
|
||||
|
||||
// challenge must be passed only when a challenge task is being created
|
||||
async function _createTasks (req, res, user, challenge) {
|
||||
let toSave = Array.isArray(req.body) ? req.body : [req.body];
|
||||
|
||||
toSave = toSave.map(taskData => {
|
||||
// Validate that task.type is valid
|
||||
if (!taskData || Tasks.tasksTypes.indexOf(taskData.type) === -1) throw new BadRequest(res.t('invalidTaskType'));
|
||||
|
||||
let taskType = taskData.type;
|
||||
let newTask = new Tasks[taskType](Tasks.Task.sanitizeCreate(taskData));
|
||||
|
||||
if (challenge) {
|
||||
newTask.challenge.id = challenge.id;
|
||||
} else {
|
||||
newTask.userId = user._id;
|
||||
}
|
||||
|
||||
// Validate that the task is valid and throw if it isn't
|
||||
// otherwise since we're saving user/challenge and task in parallel it could save the user/challenge with a tasksOrder that doens't match reality
|
||||
let validationErrors = newTask.validateSync();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
// Otherwise update the user/challenge
|
||||
(challenge || user).tasksOrder[`${taskType}s`].unshift(newTask._id);
|
||||
|
||||
return newTask;
|
||||
}).map(task => task.save({ // If all tasks are valid (this is why it's not in the previous .map()), save everything, withough running validation again
|
||||
validateBeforeSave: false,
|
||||
}));
|
||||
|
||||
toSave.unshift((challenge || user).save());
|
||||
|
||||
let tasks = await Q.all(toSave);
|
||||
tasks.splice(0, 1); // Remove user or challenge
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /tasks Create a new task. Can be passed an object to create a single task or an array of objects to create multiple tasks.
|
||||
* @api {post} /tasks/user Create a new task belonging to the autheticated user. Can be passed an object to create a single task or an array of objects to create multiple tasks.
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName CreateTask
|
||||
* @apiName CreateUserTasks
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiSuccess {Object|Array} task The newly created task(s)
|
||||
*/
|
||||
api.createTask = {
|
||||
api.createUserTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks',
|
||||
url: '/tasks/user',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
let tasksData = Array.isArray(req.body) ? req.body : [req.body];
|
||||
let user = res.locals.user;
|
||||
|
||||
let toSave = tasksData.map(taskData => {
|
||||
// Validate that task.type is valid
|
||||
if (!taskData || Tasks.tasksTypes.indexOf(taskData.type) === -1) throw new BadRequest(res.t('invalidTaskType'));
|
||||
|
||||
let taskType = taskData.type;
|
||||
let newTask = new Tasks[taskType](Tasks.Task.sanitizeCreate(taskData));
|
||||
newTask.userId = user._id;
|
||||
|
||||
// Validate that the task is valid and throw if it isn't
|
||||
// otherwise since we're saving user and task in parallel it could save the user with a tasksOrder that doens't match reality
|
||||
let validationErrors = newTask.validateSync();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
// Otherwise update the user
|
||||
user.tasksOrder[`${taskType}s`].unshift(newTask._id);
|
||||
|
||||
return newTask;
|
||||
});
|
||||
|
||||
// If all tasks are valid, save everything, withough running validation again
|
||||
toSave = toSave.map(task => task.save({
|
||||
validateBeforeSave: false,
|
||||
}));
|
||||
toSave.unshift(user.save());
|
||||
let results = await Q.all(toSave);
|
||||
|
||||
if (results.length === 2) { // Just one task created
|
||||
res.respond(201, results[1]);
|
||||
} else {
|
||||
results.splice(0, 1); // remove the user
|
||||
res.respond(201, results);
|
||||
}
|
||||
let tasks = await _createTasks(req, res, res.locals.user);
|
||||
res.respond(201, tasks.length === 1 ? tasks[0] : tasks);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /tasks Get an user's tasks
|
||||
* @api {post} /tasks/challenge/:challengeId Create a new task belonging to the challenge. Can be passed an object to create a single task or an array of objects to create multiple tasks.
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName CreateChallengeTasks
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} challengeId The id of the challenge the new task(s) will belong to.
|
||||
*
|
||||
* @apiSuccess {Object|Array} task The newly created task(s)
|
||||
*/
|
||||
api.createChallengeTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks/challenge/:challengeId',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let user = res.local.user;
|
||||
let challengeId = req.params.challengeId;
|
||||
|
||||
let challenge = await Challenge.findOne({_id: challengeId}).exec();
|
||||
|
||||
// If the challenge does not exist, or if it exists but user is not the leader -> throw error
|
||||
if (!challenge || user.challenges.indexOf(challengeId) === -1) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
|
||||
let tasks = await _createTasks(req, res, user, challenge);
|
||||
res.respond(201, tasks.length === 1 ? tasks[0] : tasks);
|
||||
|
||||
// If adding tasks to a challenge -> sync users
|
||||
if (challenge) challenge.addTasks(tasks); // TODO catch/log
|
||||
},
|
||||
};
|
||||
|
||||
// challenge must be passed only when a challenge task is being created
|
||||
async function _getTasks (req, res, user, challenge) {
|
||||
let query = challenge ? {'challenge.id': challenge.id, userId: {$exists: false}} : {userId: user._id};
|
||||
let type = req.query.type;
|
||||
|
||||
if (type) {
|
||||
query.type = type;
|
||||
if (type === 'todo') query.completed = false; // Exclude completed todos
|
||||
} else {
|
||||
query.$or = [ // Exclude completed todos
|
||||
{type: 'todo', completed: false},
|
||||
{type: {$in: ['habit', 'daily', 'reward']}},
|
||||
];
|
||||
}
|
||||
|
||||
if (req.query.includeCompletedTodos === 'true' && (!type || type === 'todo')) {
|
||||
if (challenge) throw new BadRequest(res.t('noCompletedTodosChallenge')); // no completed todos for challenges
|
||||
|
||||
let queryCompleted = Tasks.Task.find({
|
||||
type: 'todo',
|
||||
completed: true,
|
||||
}).limit(30).sort({ // TODO add ability to pick more than 30 completed todos
|
||||
dateCompleted: 1,
|
||||
});
|
||||
|
||||
let results = await Q.all([
|
||||
queryCompleted.exec(),
|
||||
Tasks.Task.find(query).exec(),
|
||||
]);
|
||||
|
||||
res.respond(200, results[1].concat(results[0]));
|
||||
} else {
|
||||
let tasks = await Tasks.Task.find(query).exec();
|
||||
res.respond(200, tasks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /tasks/user Get an user's tasks
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetUserTasks
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {string="habit","daily","todo","reward"} type Optional query parameter to return just a type of tasks
|
||||
* @apiParam {boolean} includeCompletedTodos Optional query parameter to include completed todos when "type" is "todo". Only valid whe "tasksOwner" is "user".
|
||||
*
|
||||
* @apiSuccess {Array} tasks An array of task objects
|
||||
*/
|
||||
api.getUserTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/user',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(Tasks.tasksTypes);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
await _getTasks(req, res, res.locals.user);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /tasks/challenge/:challengeId Get a challenge's tasks
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetChallengeTasks
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} challengeId The id of the challenge from which to retrieve the tasks.
|
||||
*
|
||||
* @apiParam {string="habit","daily","todo","reward"} type Optional query parameter to return just a type of tasks
|
||||
*
|
||||
* @apiSuccess {Array} tasks An array of task objects
|
||||
*/
|
||||
api.getChallengeTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/challenge/:challengeId',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(Tasks.tasksTypes);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let user = res.local.user;
|
||||
let challengeId = req.params.challengeId;
|
||||
|
||||
let challenge = await Challenge.findOne({_id: challengeId}).select('leader').exec();
|
||||
|
||||
// If the challenge does not exist, or if it exists but user is not a member, not the leader and not an admin -> throw error
|
||||
if (!challenge || (user.challenges.indexOf(challengeId) === -1 && challenge.leader !== user._id && !user.contributor.admin)) { // eslint-disable-line no-extra-parens
|
||||
throw new NotFound(res.t('challengeNotFound'));
|
||||
}
|
||||
|
||||
await _getTasks(req, res, res.locals.user, challenge);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /tasks/:tasksOwner/:challengeId Get an user's tasks
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetTasks
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {string="user","challenge"} tasksOwner Query parameter to return tasks belonging to a challenge (specifying the "challengeId" parameter) or to the autheticated user.
|
||||
* @apiParam {UUID} challengeId Optional query parameter. If "tasksOwner" is "challenge" then required to specify the challenge id.
|
||||
* @apiParam {string="habit","daily","todo","reward"} type Optional query parameter to return just a type of tasks
|
||||
* @apiParam {boolean} includeCompletedTodos Optional query parameter to include completed todos when "type" is "todo"
|
||||
* @apiParam {boolean} includeCompletedTodos Optional query parameter to include completed todos when "type" is "todo". Only valid whe "tasksOwner" is "user".
|
||||
*
|
||||
* @apiSuccess {Array} tasks An array of task objects
|
||||
*/
|
||||
@@ -82,13 +226,11 @@ api.getTasks = {
|
||||
url: '/tasks',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(Tasks.tasksTypes);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let user = res.locals.user;
|
||||
let query = {userId: user._id};
|
||||
let challengeId = req.query.challengeId;
|
||||
let challenge;
|
||||
|
||||
let query = challenge ? {'challenge.id': challengeId, userId: {$exists: false}} : {userId: user._id};
|
||||
let type = req.query.type;
|
||||
|
||||
if (type) {
|
||||
@@ -102,6 +244,8 @@ api.getTasks = {
|
||||
}
|
||||
|
||||
if (req.query.includeCompletedTodos === 'true' && (!type || type === 'todo')) {
|
||||
if (challengeId) throw new BadRequest(res.t('noCompletedTodosChallenge'));
|
||||
|
||||
let queryCompleted = Tasks.Task.find({
|
||||
type: 'todo',
|
||||
completed: true,
|
||||
@@ -146,10 +290,19 @@ api.getTask = {
|
||||
|
||||
let task = await Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
let challenge = await Challenge.find().selec({_id: task.challenge.id}).select('leader').exec();
|
||||
if (!challenge || (user.challenges.indexOf(task.challenge.id) === -1 && challenge.leader !== user._id && !user.contributor.admin)) { // eslint-disable-line no-extra-parens
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
} else if (task.userId !== user._id) { // If the task is owned by an user make it's the current one
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
|
||||
res.respond(200, task);
|
||||
},
|
||||
};
|
||||
@@ -170,6 +323,7 @@ api.updateTask = {
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
// TODO check that req.body isn't empty
|
||||
@@ -180,10 +334,17 @@ api.updateTask = {
|
||||
|
||||
let task = await Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.find().selec({_id: task.challenge.id}).select('leader').exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
} else if (task.userId !== user._id) { // If the task is owned by an user make it's the current one
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
|
||||
// If checklist is updated -> replace the original one
|
||||
if (req.body.checklist) {
|
||||
@@ -206,6 +367,7 @@ api.updateTask = {
|
||||
|
||||
let savedTask = await task.save();
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask); // TODO catch/log
|
||||
},
|
||||
};
|
||||
|
||||
@@ -313,11 +475,27 @@ api.scoreTask = {
|
||||
}).exec();
|
||||
|
||||
chalTask.value += delta;
|
||||
|
||||
if (chalTask.type === 'habit' || chalTask.type === 'daily') {
|
||||
chalTask.history.push({value: chalTask.value, date: Number(new Date())});
|
||||
// TODO 1. treat challenges as subscribed users for preening 2. it's expensive to do it at every score - how to have it happen once like for cron?
|
||||
chalTask.history = preenHistory(user, chalTask.history);
|
||||
chalTask.markModified('history');
|
||||
// Add only one history entry per day
|
||||
if (moment(chalTask.history[chalTask.history.length - 1].date).isSame(new Date(), 'day')) {
|
||||
chalTask.history[chalTask.history.length - 1] = {
|
||||
date: Number(new Date()),
|
||||
value: chalTask.value,
|
||||
};
|
||||
chalTask.markModified(`history.${chalTask.history.length - 1}`);
|
||||
} else {
|
||||
chalTask.history.push({
|
||||
date: Number(new Date()),
|
||||
value: chalTask.value,
|
||||
});
|
||||
|
||||
// Only preen task history once a day when the task is scored first
|
||||
if (chalTask.history.length > 365) {
|
||||
chalTask.history = preenHistory(chalTask.history, true); // true means the challenge will retain as much entries as a subscribed user
|
||||
chalTask.markModified(`history.${chalTask.history.length - 1}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await chalTask.save();
|
||||
@@ -330,6 +508,7 @@ api.scoreTask = {
|
||||
|
||||
// completed todos cannot be moved, they'll be returned ordered by date of completion
|
||||
// TODO check that it works when a tag is selected or todos are split between dated and due
|
||||
// TODO support challenges?
|
||||
/**
|
||||
* @api {post} /tasks/move/:taskId/to/:position Move a task to a new position
|
||||
* @apiVersion 3.0.0
|
||||
@@ -361,7 +540,7 @@ api.moveTask = {
|
||||
}).exec();
|
||||
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (task.type === 'todo' && task.completed) throw new NotFound(res.t('cantMoveCompletedTodo'));
|
||||
if (task.type === 'todo' && task.completed) throw new BadRequest(res.t('cantMoveCompletedTodo'));
|
||||
let order = user.tasksOrder[`${task.type}s`];
|
||||
let currentIndex = order.indexOf(task._id);
|
||||
|
||||
@@ -397,6 +576,7 @@ api.addChecklistItem = {
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
// TODO check that req.body isn't empty and is an array
|
||||
@@ -406,16 +586,25 @@ api.addChecklistItem = {
|
||||
|
||||
let task = await Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.find().selec({_id: task.challenge.id}).select('leader').exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
} else if (task.userId !== user._id) { // If the task is owned by an user make it's the current one
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
|
||||
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
||||
|
||||
task.checklist.push(Tasks.Task.sanitizeChecklist(req.body));
|
||||
let savedTask = await task.save();
|
||||
|
||||
res.respond(200, savedTask); // TODO what to return
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -478,6 +667,7 @@ api.updateChecklistItem = {
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID();
|
||||
@@ -487,10 +677,17 @@ api.updateChecklistItem = {
|
||||
|
||||
let task = await Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.find().selec({_id: task.challenge.id}).select('leader').exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
} else if (task.userId !== user._id) { // If the task is owned by an user make it's the current one
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
||||
|
||||
let item = _.find(task.checklist, {_id: req.params.itemId});
|
||||
@@ -500,6 +697,7 @@ api.updateChecklistItem = {
|
||||
let savedTask = await task.save();
|
||||
|
||||
res.respond(200, savedTask); // TODO what to return
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -520,6 +718,7 @@ api.removeChecklistItem = {
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID();
|
||||
@@ -529,10 +728,17 @@ api.removeChecklistItem = {
|
||||
|
||||
let task = await Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.find().selec({_id: task.challenge.id}).select('leader').exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
} else if (task.userId !== user._id) { // If the task is owned by an user make it's the current one
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
||||
|
||||
let itemI = _.findIndex(task.checklist, {_id: req.params.itemId});
|
||||
@@ -540,8 +746,9 @@ api.removeChecklistItem = {
|
||||
|
||||
task.checklist.splice(itemI, 1);
|
||||
|
||||
await task.save();
|
||||
let savedTask = await task.save();
|
||||
res.respond(200, {}); // TODO what to return
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -629,22 +836,64 @@ api.removeTagFromTask = {
|
||||
},
|
||||
};
|
||||
|
||||
// Remove a task from user.tasksOrder
|
||||
function _removeTaskTasksOrder (user, taskId) {
|
||||
// Loop through all lists and when the task is found, remove it and return
|
||||
for (let i = 0; i < Tasks.tasksTypes.length; i++) {
|
||||
let list = user.tasksOrder[`${Tasks.tasksTypes[i]}s`];
|
||||
let index = list.indexOf(taskId);
|
||||
// Remove a task from (user|challenge).tasksOrder
|
||||
function _removeTaskTasksOrder (userOrChallenge, taskId, taskType) {
|
||||
let list = userOrChallenge.tasksOrder[`${taskType}s`];
|
||||
let index = list.indexOf(taskId);
|
||||
|
||||
if (index !== -1) {
|
||||
list.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
if (index !== -1) list.splice(index, 1);
|
||||
}
|
||||
|
||||
// TODO this method needs some limitation, like to check if the challenge is really broken?
|
||||
/**
|
||||
* @api {post} /tasks/unlink/:taskId Unlink a challenge task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName UnlinkTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
*
|
||||
* @apiSuccess {object} empty An empty object
|
||||
*/
|
||||
api.unlinkTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/unlink/:taskId',
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('keep', res.t('keepOrRemove')).notEmpty().isIn(['keep', 'remove']);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let user = res.locals.user;
|
||||
let keep = req.query.keep;
|
||||
let taskId = req.params.taskId;
|
||||
|
||||
let task = await Tasks.Task.findOne({
|
||||
_id: taskId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (!task.challenge.id) throw new BadRequest(res.t('cantOnlyUnlinkChalTask'));
|
||||
|
||||
if (keep === 'keep') {
|
||||
task.challenge = {};
|
||||
await task.save();
|
||||
} else { // remove
|
||||
if (task.type !== 'todo' || !task.completed) { // eslint-disable-line no-lonely-if
|
||||
_removeTaskTasksOrder(user, taskId, task.type);
|
||||
await Q.all([user.save(), task.remove()]);
|
||||
} else {
|
||||
await task.remove();
|
||||
}
|
||||
}
|
||||
|
||||
res.respond(200, {}); // TODO what to return
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {delete} /task/:taskId Delete a user task given its id
|
||||
* @apiVersion 3.0.0
|
||||
@@ -661,24 +910,37 @@ api.deleteTask = {
|
||||
middlewares: [authWithHeaders(), cron],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let challenge;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let task = await Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
let taskId = req.params.taskId;
|
||||
let task = await Tasks.Task.findById(taskId).exec();
|
||||
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (task.challenge.id) throw new NotAuthorized(res.t('cantDeleteChallengeTasks'));
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (!task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.find().selec({_id: task.challenge.id}).select('leader').exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
} else if (task.userId !== user._id) { // If the task is owned by an user make it's the current one
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (task.userId && task.challenge.id) {
|
||||
throw new NotAuthorized(res.t('cantDeleteChallengeTasks'));
|
||||
}
|
||||
|
||||
_removeTaskTasksOrder(user, req.params.taskId);
|
||||
await Q.all([user.save(), task.remove()]);
|
||||
if (task.type !== 'todo' || !task.completed) {
|
||||
_removeTaskTasksOrder(challenge || user, taskId, task.type);
|
||||
await Q.all([(challenge || user).save(), task.remove()]);
|
||||
} else {
|
||||
await task.remove();
|
||||
}
|
||||
|
||||
res.respond(200, {});
|
||||
if (challenge) challenge.removeTask(task);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
import cron from '../../../../common/script/api-v3/cron';
|
||||
import common from '../../../../common';
|
||||
import Task from '../../models/task';
|
||||
import Q from 'q';
|
||||
// import Group from '../../models/group';
|
||||
|
||||
// TODO check that it's usef everywhere
|
||||
// TODO check that it's used everywhere
|
||||
export default function cronMiddleware (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
let analytics = res.analytics;
|
||||
@@ -26,7 +27,7 @@ export default function cronMiddleware (req, res, next) {
|
||||
{type: {$in: ['habit', 'daily', 'reward']}},
|
||||
],
|
||||
}).exec()
|
||||
.then((tasks) => {
|
||||
.then(tasks => {
|
||||
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
|
||||
tasks.forEach(task => tasksByType[`${task.type}s`].push(task));
|
||||
|
||||
@@ -34,6 +35,7 @@ export default function cronMiddleware (req, res, next) {
|
||||
cron({user, tasksByType, now, daysMissed, analytics});
|
||||
|
||||
// Clean completed todos - 30 days for free users, 90 for subscribers
|
||||
// Do not delete challenges completed todos TODO unless the task is broken?
|
||||
Task.remove({
|
||||
userId: user._id,
|
||||
type: 'todo',
|
||||
@@ -41,6 +43,7 @@ export default function cronMiddleware (req, res, next) {
|
||||
dateCompleted: {
|
||||
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days'),
|
||||
},
|
||||
'challenge.id': {$exists: false},
|
||||
}).exec(); // TODO catch error or at least log it
|
||||
|
||||
let ranCron = user.isModified();
|
||||
@@ -49,7 +52,15 @@ export default function cronMiddleware (req, res, next) {
|
||||
// if (ranCron) res.locals.wasModified = true; // TODO remove?
|
||||
if (!ranCron) return next();
|
||||
// TODO Group.tavernBoss(user, progress);
|
||||
if (!quest || true /* TODO remove */) return user.save(next);
|
||||
if (!quest || true /* TODO remove */) {
|
||||
// Save user and tasks
|
||||
let toSave = [user.save()];
|
||||
tasks.forEach(task => {
|
||||
if (task.isModified) toSave.push(task.save());
|
||||
});
|
||||
|
||||
return Q.all(toSave).then(() => next()).catch(next);
|
||||
}
|
||||
|
||||
// If user is on a quest, roll for boss & player, or handle collections
|
||||
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
|
||||
|
||||
+109
-59
@@ -4,6 +4,7 @@ import validator from 'validator';
|
||||
import baseModel from '../libs/api-v3/baseModel';
|
||||
import _ from 'lodash';
|
||||
import * as Tasks from './task';
|
||||
import { model as User } from './user';
|
||||
|
||||
let Schema = mongoose.Schema;
|
||||
|
||||
@@ -30,37 +31,18 @@ schema.plugin(baseModel, {
|
||||
noSet: ['_id', 'memberCount', 'challengeCount', 'tasksOrder'],
|
||||
});
|
||||
|
||||
|
||||
// Syncing logic
|
||||
|
||||
// Takes a Task document and return a plain object of attributes that can be synced to the user
|
||||
function _syncableAttrs (task) {
|
||||
let t = task.toObject(); // lodash doesn't seem to like _.omit on EmbeddedDocument
|
||||
let t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
||||
// only sync/compare important attrs
|
||||
let omitAttrs = ['userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO use whitelist instead of blacklist?
|
||||
let omitAttrs = ['userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO what to do with updatedAt?
|
||||
if (t.type !== 'reward') omitAttrs.push('value');
|
||||
return _.omit(t, omitAttrs);
|
||||
}
|
||||
|
||||
// TODO redo
|
||||
// Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
||||
/* function comparableData(obj) {
|
||||
return JSON.stringify(
|
||||
_(obj.habits.concat(obj.dailys).concat(obj.todos).concat(obj.rewards))
|
||||
.sortBy('id') // we don't want to update if they're sort-order is different
|
||||
.transform(function(result, task){
|
||||
result.push(syncableAttrs(task));
|
||||
})
|
||||
.value())
|
||||
}
|
||||
|
||||
ChallengeSchema.methods.isOutdated = function isChallengeOutdated (newData) {
|
||||
return comparableData(this) !== comparableData(newData);
|
||||
}*/
|
||||
|
||||
// Syncs all new tasks, deleted tasks, etc to the user object
|
||||
schema.methods.syncToUser = function syncChallengeToUser (user) {
|
||||
if (!user) throw new Error('User required.');
|
||||
|
||||
// Sync challenge to user, including tasks and tags.
|
||||
// Used when user joins the challenge or to force sync.
|
||||
schema.methods.syncToUser = async function syncChallengeToUser (user) {
|
||||
let challenge = this;
|
||||
challenge.shortName = challenge.shortName || challenge.name;
|
||||
|
||||
@@ -84,8 +66,7 @@ schema.methods.syncToUser = function syncChallengeToUser (user) {
|
||||
});
|
||||
}
|
||||
|
||||
// Sync new tasks and updated tasks
|
||||
return Q.all([
|
||||
let [challengeTasks, userTasks] = await Q.all([
|
||||
// Find original challenge tasks
|
||||
Tasks.Task.find({
|
||||
userId: {$exists: false},
|
||||
@@ -96,43 +77,112 @@ schema.methods.syncToUser = function syncChallengeToUser (user) {
|
||||
userId: user._id,
|
||||
'challenge.id': challenge._id,
|
||||
}).exec(),
|
||||
])
|
||||
.then(results => {
|
||||
let challengeTasks = results[0];
|
||||
let userTasks = results[1];
|
||||
let toSave = []; // An array of things to save
|
||||
]);
|
||||
|
||||
challengeTasks.forEach(chalTask => {
|
||||
let matchingTask = _.find(userTasks, userTask => userTask.challenge.taskId === chalTask._id);
|
||||
let toSave = []; // An array of things to save
|
||||
|
||||
if (!matchingTask) { // If the task is new, create it
|
||||
matchingTask = new Tasks[chalTask.type](Tasks.Task.sanitizeCreate(_syncableAttrs(chalTask)));
|
||||
matchingTask.challenge = {taskId: chalTask._id, id: challenge._id};
|
||||
matchingTask.userId = user._id;
|
||||
user.tasksOrder[`${chalTask.type}s`].push(matchingTask._id);
|
||||
} else {
|
||||
_.merge(matchingTask, _syncableAttrs(chalTask));
|
||||
// Make sure the task is in user.tasksOrder TODO necessary?
|
||||
let orderList = user.tasksOrder[`${chalTask.type}s`];
|
||||
if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id);
|
||||
}
|
||||
challengeTasks.forEach(chalTask => {
|
||||
let matchingTask = _.find(userTasks, userTask => userTask.challenge.taskId === chalTask._id);
|
||||
|
||||
if (!matchingTask.notes) matchingTask.notes = chalTask.notes; // don't override the notes, but provide it if not provided
|
||||
if (matchingTask.tags.indexOf(challenge._id) === -1) matchingTask.tags.push(challenge._id); // add tag if missing
|
||||
toSave.push(matchingTask.save());
|
||||
});
|
||||
if (!matchingTask) { // If the task is new, create it
|
||||
matchingTask = new Tasks[chalTask.type](Tasks.Task.sanitizeCreate(_syncableAttrs(chalTask)));
|
||||
matchingTask.challenge = {taskId: chalTask._id, id: challenge._id};
|
||||
matchingTask.userId = user._id;
|
||||
user.tasksOrder[`${chalTask.type}s`].push(matchingTask._id);
|
||||
} else {
|
||||
_.merge(matchingTask, _syncableAttrs(chalTask));
|
||||
// Make sure the task is in user.tasksOrder TODO necessary?
|
||||
let orderList = user.tasksOrder[`${chalTask.type}s`];
|
||||
if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id);
|
||||
}
|
||||
|
||||
// Flag deleted tasks as "broken"
|
||||
userTasks.forEach(userTask => {
|
||||
if (!_.find(challengeTasks, chalTask => chalTask._id === userTask.challenge.taskId)) {
|
||||
userTask.challenge.broken = 'TASK_DELETED';
|
||||
toSave.push(userTask.save());
|
||||
}
|
||||
});
|
||||
|
||||
toSave.push(user.save());
|
||||
return Q.all(toSave);
|
||||
if (!matchingTask.notes) matchingTask.notes = chalTask.notes; // don't override the notes, but provide it if not provided
|
||||
if (matchingTask.tags.indexOf(challenge._id) === -1) matchingTask.tags.push(challenge._id); // add tag if missing
|
||||
toSave.push(matchingTask.save());
|
||||
});
|
||||
|
||||
// Flag deleted tasks as "broken"
|
||||
userTasks.forEach(userTask => {
|
||||
if (!_.find(challengeTasks, chalTask => chalTask._id === userTask.challenge.taskId)) {
|
||||
userTask.challenge.broken = 'TASK_DELETED';
|
||||
toSave.push(userTask.save());
|
||||
}
|
||||
});
|
||||
|
||||
toSave.push(user.save());
|
||||
return Q.all(toSave);
|
||||
};
|
||||
|
||||
async function _fetchMembersIds (challengeId) {
|
||||
return (await User.find({challenges: {$in: [challengeId]}}).select('_id').lean().exec()).map(member => member._id);
|
||||
}
|
||||
|
||||
// Add a new task to challenge members
|
||||
schema.methods.addTasks = async function challengeAddTasks (tasks) {
|
||||
let challenge = this;
|
||||
let membersIds = await _fetchMembersIds(challenge._id);
|
||||
|
||||
// Sync each user sequentially
|
||||
for (let memberId of membersIds) {
|
||||
let updateTasksOrderQ = {$push: {}};
|
||||
let toSave = [];
|
||||
|
||||
// TODO eslint complaints about ahving a function inside a loop -> make sure it works
|
||||
tasks.forEach(chalTask => { // eslint-disable-line no-loop-func
|
||||
let userTask = new Tasks[chalTask.type](Tasks.Task.sanitizeCreate(_syncableAttrs(chalTask)));
|
||||
userTask.challenge = {taskId: chalTask._id, id: challenge._id};
|
||||
userTask.userId = memberId;
|
||||
|
||||
let tasksOrderList = updateTasksOrderQ.$push[`tasksOrder.${chalTask.type}s`];
|
||||
if (!tasksOrderList) {
|
||||
updateTasksOrderQ.$push[`tasksOrder.${chalTask.type}s`] = {
|
||||
$position: 0, // unshift
|
||||
$each: [userTask._id],
|
||||
};
|
||||
} else {
|
||||
tasksOrderList.$each.unshift(userTask._id);
|
||||
}
|
||||
|
||||
toSave.push(userTask);
|
||||
});
|
||||
|
||||
// Update the user
|
||||
toSave.unshift(User.update({_id: memberId}, updateTasksOrderQ).exec());
|
||||
await Q.all(toSave); // eslint-disable-line babel/no-await-in-loop
|
||||
}
|
||||
};
|
||||
|
||||
// Sync updated task to challenge members
|
||||
schema.methods.updateTask = async function challengeUpdateTask (task) {
|
||||
let challenge = this;
|
||||
|
||||
let updateCmd = {$set: {}};
|
||||
|
||||
_syncableAttrs(task).forEach((value, key) => {
|
||||
updateCmd.$set[key] = value;
|
||||
});
|
||||
|
||||
// TODO reveiw
|
||||
// Updating instead of loading and saving for performances, risks becoming a problem if we introduce more complexity in tasks
|
||||
await Tasks.Task.update({
|
||||
userId: {$exists: true},
|
||||
'challenge.id': challenge.id,
|
||||
'challenge.taskId': task._id,
|
||||
}, updateCmd, {multi: true}).exec();
|
||||
};
|
||||
|
||||
// Remove a task from challenge members
|
||||
schema.methods.removeTask = async function challengeRemoveTask (task) {
|
||||
let challenge = this;
|
||||
|
||||
// Set the task as broken
|
||||
await Tasks.Task.update({
|
||||
userId: {$exists: true},
|
||||
'challenge.id': challenge.id,
|
||||
'challenge.taskId': task._id,
|
||||
}, {
|
||||
$set: {'challenge.broken': 'TASK_DELETED'}, // TODO what about updatedAt?
|
||||
}, {multi: true}).exec();
|
||||
};
|
||||
|
||||
export let model = mongoose.model('Challenge', schema);
|
||||
|
||||
@@ -438,7 +438,7 @@ schema.statics.bossQuest = function bossQuest (user, progress) {
|
||||
|
||||
// Remove user from this group
|
||||
// TODO this is highly inefficient
|
||||
schema.methods.leave = function leaveGroup (user, keep = 'keep-all') {
|
||||
schema.methods.leave = function leaveGroup (user, keep) {
|
||||
let group = this;
|
||||
|
||||
return Q.all([
|
||||
@@ -454,12 +454,12 @@ schema.methods.leave = function leaveGroup (user, keep = 'keep-all') {
|
||||
{_id: {$in: _.pluck(challenges, '_id')}},
|
||||
{$pull: {members: user._id}},
|
||||
{multi: true}
|
||||
).then(() => challenges); // pass `challenges` above to next promise TODO ok to return a non-promise?
|
||||
).then(() => challenges); // pass `challenges` above to next promise
|
||||
}).then(challenges => {
|
||||
return Q.all(challenges.map(chal => {
|
||||
let i = user.challenges.indexOf(chal._id);
|
||||
if (i !== -1) user.challenges.splice(i, 1);
|
||||
return user.unlink({cid: chal._id, keep});
|
||||
return user.unlinkChallengeTasks(chal._id, keep);
|
||||
}));
|
||||
}),
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export let TaskSchema = new Schema({
|
||||
|
||||
challenge: {
|
||||
id: {type: String, ref: 'Challenge', validate: [validator.isUUID, 'Invalid uuid.']}, // When set (and userId not set) it's the original task
|
||||
taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']}, // When not set but challenge.id defined it's the original task
|
||||
taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']}, // When not set but challenge.id defined it's the original task TODO unique index?
|
||||
broken: {type: String, enum: ['CHALLENGE_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED', 'CHALLENGE_CLOSED']},
|
||||
winner: String, // user.profile.name TODO necessary?
|
||||
},
|
||||
|
||||
+25
-31
@@ -651,40 +651,34 @@ schema.methods.isSubscribed = function isSubscribed () {
|
||||
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
|
||||
};
|
||||
|
||||
schema.methods.unlink = function unlink (options, cb) {
|
||||
let cid = options.cid;
|
||||
let keep = options.keep;
|
||||
let tid = options.tid;
|
||||
// Unlink challenges tasks from user
|
||||
schema.methods.unlinkChallengeTasks = async function unlinkChallengeTasks (challengeId, keep) {
|
||||
let user = this;
|
||||
let findQuery = {
|
||||
userId: user._id,
|
||||
'challenge.id': challengeId,
|
||||
};
|
||||
|
||||
if (!cid) {
|
||||
return cb('Could not remove challenge tasks. Please delete them manually.');
|
||||
}
|
||||
|
||||
let self = this;
|
||||
|
||||
if (keep === 'keep') {
|
||||
self.tasks[tid].challenge = {};
|
||||
} else if (keep === 'remove') {
|
||||
self.ops.deleteTask({params: {id: tid}}, () => {});
|
||||
} else if (keep === 'keep-all') {
|
||||
_.each(self.tasks, (t) => {
|
||||
if (t.challenge && t.challenge.id === cid) {
|
||||
t.challenge = {};
|
||||
if (keep === 'keep-all') {
|
||||
await Tasks.Task.update(findQuery, {
|
||||
$set: {challenge: {}}, // TODO what about updatedAt?
|
||||
}, {multi: true}).exec();
|
||||
} else { // keep = 'remove-all'
|
||||
let tasks = Tasks.Task.find(findQuery).select('_id type completed').exec();
|
||||
tasks = tasks.map(task => {
|
||||
// Remove task from user.tasksOrder and delete them
|
||||
if (task.type !== 'todo' || !task.completed) {
|
||||
let list = user.tasksOrder[`${task.type}s`];
|
||||
let index = list.indexOf(task._id);
|
||||
if (index !== -1) list.splice(index, 1);
|
||||
}
|
||||
});
|
||||
} else if (keep === 'remove-all') {
|
||||
_.each(self.tasks, (t) => {
|
||||
if (t.challenge && t.challenge.id === cid) {
|
||||
this.ops.deleteTask({params: {id: tid}}, () => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.markModified('habits');
|
||||
self.markModified('dailys');
|
||||
self.markModified('todos');
|
||||
self.markModified('rewards');
|
||||
self.save(cb);
|
||||
return task.remove();
|
||||
});
|
||||
|
||||
tasks.push(user.save());
|
||||
await Q.all(tasks);
|
||||
}
|
||||
};
|
||||
|
||||
export let model = mongoose.model('User', schema);
|
||||
|
||||
Reference in New Issue
Block a user