Merge branch 'api-v3-challenges-tasks' into api-v3-groups

This commit is contained in:
Matteo Pagliazzi
2016-01-14 18:27:22 +01:00
19 changed files with 492 additions and 238 deletions
+4
View File
@@ -47,6 +47,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.",
@@ -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-v3-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');
});
@@ -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');
});
@@ -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,
@@ -3,7 +3,7 @@ import {
translate as t,
} 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: [
@@ -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,
@@ -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',
});
@@ -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',
});
@@ -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',
});
@@ -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',
});
@@ -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',
});
@@ -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',
});
+6 -5
View File
@@ -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);
},
};
+265 -65
View File
@@ -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,
@@ -16,65 +17,207 @@ 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
*/
@@ -83,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) {
@@ -103,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,
@@ -147,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);
},
};
@@ -171,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
@@ -181,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) {
@@ -207,6 +367,7 @@ api.updateTask = {
let savedTask = await task.save();
res.respond(200, savedTask);
if (challenge) challenge.updateTask(savedTask); // TODO catch/log
},
};
@@ -347,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
@@ -414,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
@@ -423,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);
},
};
@@ -495,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();
@@ -504,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});
@@ -517,6 +697,7 @@ api.updateChecklistItem = {
let savedTask = await task.save();
res.respond(200, savedTask); // TODO what to return
if (challenge) challenge.updateTask(savedTask);
},
};
@@ -537,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();
@@ -546,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});
@@ -557,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);
},
};
@@ -646,11 +836,11 @@ api.removeTagFromTask = {
},
};
// Remove a task from user.tasksOrder
function _removeTaskTasksOrder (user, taskId) {
// Remove a task from (user|challenge).tasksOrder
function _removeTaskTasksOrder (userOrChallenge, 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 list = userOrChallenge.tasksOrder[`${Tasks.tasksTypes[i]}s`];
let index = list.indexOf(taskId);
if (index !== -1) {
@@ -678,6 +868,7 @@ api.deleteTask = {
middlewares: [authWithHeaders(), cron],
async handler (req, res) {
let user = res.locals.user;
let challenge;
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
@@ -686,16 +877,25 @@ api.deleteTask = {
let task = await Tasks.Task.findOne({
_id: req.params.taskId,
userId: user._id,
}).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);
_removeTaskTasksOrder(challenge || user, req.params.taskId);
await Q.all([user.save(), task.remove()]);
res.respond(200, {});
if (challenge) challenge.removeTask(task);
},
};
+2
View File
@@ -35,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',
@@ -42,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();
+109 -59
View File
@@ -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?
}).lean().exec();
};
export let model = mongoose.model('Challenge', schema);
+1 -1
View File
@@ -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?
},