mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
Groups assign tasks (#7887)
* Added initial code for creating and reading group tasks * Separated group task routes. Separated shared task functions * Added taskOrder to group * Minor style fixes * Fixed lint issues * Added unit tests for task manager * Updated task helper functions * Fixed history test * Fixed group task query * Removed extra var * Updated with new file structure * Updated noset values * Removed unecessary undefineds, fixed comments, Added apiignore * Separated group task routes. Separated shared task functions * Added unit tests for task manager * Added initial groups assign route and tests * Added sync assigned task to user * Added unassign route and unlink method * Added remove and unlink group task * Updated linking and unlinking. Add test for updating task info * Added delete group task and tests * Added sync on task update and tests * Added multiple users assignment * Updated unassign for multiple users * Added test for delete task with multiple assigend users * Added update task for multiple assigned users * Fixed issue with get tasks * Abstracted syncable attributes and add tests * Fixed merge conflicts * Fixed style issues, limited group query fields, and added await * Fixed group fields needed. Removed api v2 code * Fixed style issues * Moved group field under group sub document. Updated tests. Fixed other broken tests * Renamed linkedTaskId and fixed broken alias tests * Added debug middleware to new routes * Fixed debug middleware import * Added additional user id check for original group tasks * Updated challenge task check to look for challenge id * Added checklist sync fix
This commit is contained in:
committed by
Matteo Pagliazzi
parent
173b3f3f84
commit
836cee2531
@@ -191,5 +191,7 @@
|
||||
"uuidsMustBeAnArray": "User ID invites must be an array.",
|
||||
"emailsMustBeAnArray": "Email address invites must be an array.",
|
||||
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
|
||||
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!"
|
||||
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
||||
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
|
||||
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned"
|
||||
}
|
||||
|
||||
@@ -39,12 +39,13 @@ describe('GET /export/history.csv', () => {
|
||||
|
||||
let res = await user.get('/export/history.csv');
|
||||
let splitRes = res.split('\n');
|
||||
|
||||
expect(splitRes[0]).to.equal('Task Name,Task ID,Task Type,Date,Value');
|
||||
expect(splitRes[1]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[0].value}`);
|
||||
expect(splitRes[2]).to.equal(`habit 1,${tasks[0]._id},habit,${moment(tasks[0].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[0].history[1].value}`);
|
||||
expect(splitRes[3]).to.equal(`daily 1,${tasks[1]._id},daily,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`);
|
||||
expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`);
|
||||
expect(splitRes[5]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`);
|
||||
expect(splitRes[3]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[0].value}`);
|
||||
expect(splitRes[4]).to.equal(`habit 2,${tasks[2]._id},habit,${moment(tasks[2].history[1].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[2].history[1].value}`);
|
||||
expect(splitRes[5]).to.equal(`daily 1,${tasks[1]._id},daily,${moment(tasks[1].history[0].date).format('YYYY-MM-DD HH:mm:ss')},${tasks[1].history[0].value}`);
|
||||
expect(splitRes[6]).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
translate as t,
|
||||
createAndPopulateGroup,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('DELETE /tasks/:id', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let {group, members, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
|
||||
});
|
||||
|
||||
it('deletes a group task', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
await expect(user.get(`/tasks/${task._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unlinks assigned user', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
});
|
||||
|
||||
it('unlinks all assigned users', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
let member2Tasks = await member2.get('/tasks/user');
|
||||
let member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { each } from 'lodash';
|
||||
|
||||
describe('GET /tasks/group/:groupId', () => {
|
||||
let user, group, task, groupWithTask;
|
||||
let tasks = [];
|
||||
let tasksToTest = {
|
||||
habit: {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
},
|
||||
todo: {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
},
|
||||
daily: {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
},
|
||||
reward: {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
},
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
group = await generateGroup(user);
|
||||
});
|
||||
|
||||
it('returns error when group is not found', async () => {
|
||||
let dummyId = generateUUID();
|
||||
|
||||
await expect(user.get(`/tasks/group/${dummyId}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
each(tasksToTest, (taskValue, taskType) => {
|
||||
context(`${taskType}`, () => {
|
||||
before(async () => {
|
||||
task = await user.post(`/tasks/group/${group._id}`, taskValue);
|
||||
tasks.push(task);
|
||||
groupWithTask = await user.get(`/groups/${group._id}`);
|
||||
});
|
||||
|
||||
it('gets group tasks', async () => {
|
||||
let getTask = await user.get(`/tasks/group/${groupWithTask._id}`);
|
||||
expect(getTask).to.eql(tasks);
|
||||
});
|
||||
|
||||
it('gets group tasks filtered by type', async () => {
|
||||
let groupTasks = await user.get(`/tasks/group/${groupWithTask._id}?type=${task.type}s`);
|
||||
expect(groupTasks).to.eql([task]);
|
||||
});
|
||||
|
||||
it('cannot get a task owned by someone else', async () => {
|
||||
let anotherUser = await generateUser();
|
||||
|
||||
await expect(anotherUser.get(`/tasks/group/${groupWithTask._id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
119
test/api/v3/integration/tasks/groups/POST-tasks_group_id.test.js
Normal file
119
test/api/v3/integration/tasks/groups/POST-tasks_group_id.test.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/group/:groupid', () => {
|
||||
let user, guild;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({balance: 1});
|
||||
guild = await generateGroup(user, {type: 'guild'});
|
||||
});
|
||||
|
||||
it('returns error when group is not found', async () => {
|
||||
await expect(user.post(`/tasks/group/${generateUUID()}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when user is not a member of the group', async () => {
|
||||
let userWithoutChallenge = await generateUser();
|
||||
|
||||
await expect(userWithoutChallenge.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when non leader tries to create a task', async () => {
|
||||
let userThatIsNotLeaderOfGroup = await generateUser({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
await expect(userThatIsNotLeaderOfGroup.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a habit', async () => {
|
||||
let task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(groupTask[0].group.id).to.equal(guild._id);
|
||||
expect(task.text).to.eql('test habit');
|
||||
expect(task.notes).to.eql('1976');
|
||||
expect(task.type).to.eql('habit');
|
||||
expect(task.up).to.eql(false);
|
||||
expect(task.down).to.eql(true);
|
||||
});
|
||||
|
||||
it('creates a todo', async () => {
|
||||
let task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(groupTask[0].group.id).to.equal(guild._id);
|
||||
expect(task.text).to.eql('test todo');
|
||||
expect(task.notes).to.eql('1976');
|
||||
expect(task.type).to.eql('todo');
|
||||
});
|
||||
|
||||
it('creates a daily', async () => {
|
||||
let now = new Date();
|
||||
let task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
notes: 1976,
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: now,
|
||||
});
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(groupTask[0].group.id).to.equal(guild._id);
|
||||
expect(task.text).to.eql('test daily');
|
||||
expect(task.notes).to.eql('1976');
|
||||
expect(task.type).to.eql('daily');
|
||||
expect(task.frequency).to.eql('daily');
|
||||
expect(task.everyX).to.eql(5);
|
||||
expect(new Date(task.startDate)).to.eql(now);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:taskId', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let {group, members, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when task is not found', async () => {
|
||||
await expect(user.post(`/tasks/${generateUUID()}/assign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when task is not a group task', async () => {
|
||||
let nonGroupTask = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${nonGroupTask._id}/assign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupTasksCanBeAssigned'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when user is not a member of the group', async () => {
|
||||
let nonUser = await generateUser();
|
||||
|
||||
await expect(nonUser.post(`/tasks/${task._id}/assign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when non leader tries to create a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/assign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('assigns a task to a user', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('assigns a task to multiple users', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let member1SyncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
let member2Tasks = await member2.get('/tasks/user');
|
||||
let member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
|
||||
expect(member1SyncedTask).to.exist;
|
||||
expect(member2SyncedTask).to.exist;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let {group, members, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
});
|
||||
|
||||
it('returns error when task is not found', async () => {
|
||||
await expect(user.post(`/tasks/${generateUUID()}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when task is not a group task', async () => {
|
||||
let nonGroupTask = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await expect(user.post(`/tasks/${nonGroupTask._id}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupTasksCanBeAssigned'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when user is not a member of the group', async () => {
|
||||
let nonUser = await generateUser();
|
||||
|
||||
await expect(nonUser.post(`/tasks/${task._id}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when non leader tries to create a task', async () => {
|
||||
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlyGroupLeaderCanEditTasks'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unassigns a user from a task', async () => {
|
||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('unassigns a user and only that user from a task', async () => {
|
||||
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
|
||||
|
||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let member1SyncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
let member2Tasks = await member2.get('/tasks/user');
|
||||
let member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||
expect(member1SyncedTask).to.not.exist;
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
|
||||
expect(member2SyncedTask).to.exist;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('PUT /tasks/:id', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let {group, members, groupLeader} = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
|
||||
});
|
||||
|
||||
it('updates a group task', async () => {
|
||||
let savedHabit = await user.put(`/tasks/${task._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
});
|
||||
|
||||
expect(savedHabit.text).to.eql('some new text');
|
||||
expect(savedHabit.notes).to.eql('some new notes');
|
||||
expect(savedHabit.up).to.eql(false);
|
||||
expect(savedHabit.down).to.eql(false);
|
||||
});
|
||||
|
||||
it('updates the linked tasks', async () => {
|
||||
await user.put(`/tasks/${task._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
});
|
||||
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.text).to.eql('some new text');
|
||||
expect(syncedTask.up).to.eql(false);
|
||||
expect(syncedTask.down).to.eql(false);
|
||||
});
|
||||
|
||||
it('updates the linked tasks for all assigned users', async () => {
|
||||
await user.put(`/tasks/${task._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
});
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
let member2Tasks = await member2.get('/tasks/user');
|
||||
let member2SyncedTask = find(member2Tasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.text).to.eql('some new text');
|
||||
expect(syncedTask.up).to.eql(false);
|
||||
expect(syncedTask.down).to.eql(false);
|
||||
|
||||
expect(member2SyncedTask.text).to.eql('some new text');
|
||||
expect(member2SyncedTask.up).to.eql(false);
|
||||
expect(member2SyncedTask.down).to.eql(false);
|
||||
});
|
||||
});
|
||||
172
test/api/v3/unit/libs/taskManager.js
Normal file
172
test/api/v3/unit/libs/taskManager.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
createTasks,
|
||||
getTasks,
|
||||
syncableAttrs,
|
||||
} from '../../../../../website/server/libs/taskManager';
|
||||
import i18n from '../../../../../common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
generateChallenge,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
|
||||
describe('taskManager', () => {
|
||||
let user, group, challenge;
|
||||
let testHabit = {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
};
|
||||
let req = {};
|
||||
let res = {};
|
||||
|
||||
beforeEach(() => {
|
||||
req = {};
|
||||
res = {};
|
||||
user = generateUser();
|
||||
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
leader: user._id,
|
||||
});
|
||||
|
||||
challenge = generateChallenge({
|
||||
name: 'test challenge',
|
||||
shortName: 'testc',
|
||||
group: group._id,
|
||||
leader: user._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates user tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
let newTasks = await createTasks(req, res, {user});
|
||||
let newTask = newTasks[0];
|
||||
|
||||
expect(newTask.text).to.equal(testHabit.text);
|
||||
expect(newTask.type).to.equal(testHabit.type);
|
||||
expect(newTask.up).to.equal(testHabit.up);
|
||||
expect(newTask.down).to.equal(testHabit.down);
|
||||
expect(newTask.createdAt).isNotEmtpy;
|
||||
});
|
||||
|
||||
it('gets user tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
await createTasks(req, res, {user});
|
||||
|
||||
req.body = {};
|
||||
req.query = {
|
||||
type: 'habits',
|
||||
};
|
||||
|
||||
let tasks = await getTasks(req, res, {user});
|
||||
let task = tasks[0];
|
||||
|
||||
expect(task.text).to.equal(testHabit.text);
|
||||
expect(task.type).to.equal(testHabit.type);
|
||||
expect(task.up).to.equal(testHabit.up);
|
||||
expect(task.down).to.equal(testHabit.down);
|
||||
expect(task.createdAt).isNotEmtpy;
|
||||
});
|
||||
|
||||
it('creates group tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
let newTasks = await createTasks(req, res, {user, group});
|
||||
let newTask = newTasks[0];
|
||||
|
||||
expect(newTask.text).to.equal(testHabit.text);
|
||||
expect(newTask.type).to.equal(testHabit.type);
|
||||
expect(newTask.up).to.equal(testHabit.up);
|
||||
expect(newTask.down).to.equal(testHabit.down);
|
||||
expect(newTask.createdAt).isNotEmtpy;
|
||||
expect(newTask.group.id).to.equal(group._id);
|
||||
});
|
||||
|
||||
it('gets group tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
await createTasks(req, res, {user, group});
|
||||
|
||||
req.body = {};
|
||||
req.query = {
|
||||
type: 'habits',
|
||||
};
|
||||
|
||||
let tasks = await getTasks(req, res, {user, group});
|
||||
let task = tasks[0];
|
||||
|
||||
expect(task.text).to.equal(testHabit.text);
|
||||
expect(task.type).to.equal(testHabit.type);
|
||||
expect(task.up).to.equal(testHabit.up);
|
||||
expect(task.down).to.equal(testHabit.down);
|
||||
expect(task.createdAt).isNotEmtpy;
|
||||
expect(task.group.id).to.equal(group._id);
|
||||
});
|
||||
|
||||
it('creates challenge tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
let newTasks = await createTasks(req, res, {user, challenge});
|
||||
let newTask = newTasks[0];
|
||||
|
||||
expect(newTask.text).to.equal(testHabit.text);
|
||||
expect(newTask.type).to.equal(testHabit.type);
|
||||
expect(newTask.up).to.equal(testHabit.up);
|
||||
expect(newTask.down).to.equal(testHabit.down);
|
||||
expect(newTask.createdAt).isNotEmtpy;
|
||||
expect(newTask.challenge.id).to.equal(challenge._id);
|
||||
});
|
||||
|
||||
it('gets challenge tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
await createTasks(req, res, {user, challenge});
|
||||
|
||||
req.body = {};
|
||||
req.query = {
|
||||
type: 'habits',
|
||||
};
|
||||
|
||||
let tasks = await getTasks(req, res, {user, challenge});
|
||||
let task = tasks[0];
|
||||
|
||||
expect(task.text).to.equal(testHabit.text);
|
||||
expect(task.type).to.equal(testHabit.type);
|
||||
expect(task.up).to.equal(testHabit.up);
|
||||
expect(task.down).to.equal(testHabit.down);
|
||||
expect(task.createdAt).isNotEmtpy;
|
||||
expect(task.challenge.id).to.equal(challenge._id);
|
||||
});
|
||||
|
||||
it('returns syncable attibutes', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
let tasks = await createTasks(req, res, {user, challenge});
|
||||
|
||||
let syncableTask = syncableAttrs(tasks[0]);
|
||||
|
||||
expect(syncableTask._id).to.not.exist;
|
||||
expect(syncableTask.userId).to.not.exist;
|
||||
expect(syncableTask.challenge).to.not.exist;
|
||||
expect(syncableTask.history).to.not.exist;
|
||||
expect(syncableTask.tags).to.not.exist;
|
||||
expect(syncableTask.completed).to.not.exist;
|
||||
expect(syncableTask.streak).to.not.exist;
|
||||
expect(syncableTask.notes).to.not.exist;
|
||||
expect(syncableTask.updatedAt).to.not.exist;
|
||||
});
|
||||
});
|
||||
174
test/api/v3/unit/models/group_tasks.test.js
Normal file
174
test/api/v3/unit/models/group_tasks.test.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import { model as Challenge } from '../../../../../website/server/models/challenge';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../../website/server/models/task';
|
||||
import { each, find } from 'lodash';
|
||||
|
||||
describe('Group Task Methods', () => {
|
||||
let guild, leader, challenge, task;
|
||||
let tasksToTest = {
|
||||
habit: {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
},
|
||||
todo: {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
},
|
||||
daily: {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
},
|
||||
reward: {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
},
|
||||
};
|
||||
|
||||
function findLinkedTask (updatedLeadersTask) {
|
||||
return updatedLeadersTask.group.taskId === task._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
guild = new Group({
|
||||
name: 'test party',
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
leader = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
guild.leader = leader._id;
|
||||
|
||||
challenge = new Challenge({
|
||||
name: 'Test Challenge',
|
||||
shortName: 'Test',
|
||||
leader: leader._id,
|
||||
group: guild._id,
|
||||
});
|
||||
|
||||
leader.challenges = [challenge._id];
|
||||
|
||||
await Promise.all([
|
||||
guild.save(),
|
||||
leader.save(),
|
||||
challenge.save(),
|
||||
]);
|
||||
});
|
||||
|
||||
each(tasksToTest, (taskValue, taskType) => {
|
||||
context(`${taskType}`, () => {
|
||||
beforeEach(async() => {
|
||||
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
|
||||
task.group.id = guild._id;
|
||||
await task.save();
|
||||
});
|
||||
|
||||
it('syncs an assigned task to a user', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('syncs updated info for assigned task to a user', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
let updatedTaskName = 'Update Task name';
|
||||
task.text = updatedTaskName;
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.text).to.equal(task.text);
|
||||
});
|
||||
|
||||
it('syncs updated info for assigned task to all users', async () => {
|
||||
let newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.syncTask(task, newMember);
|
||||
|
||||
let updatedTaskName = 'Update Task name';
|
||||
task.text = updatedTaskName;
|
||||
|
||||
await guild.updateTask(task);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
let updatedMember = await User.findOne({_id: newMember._id});
|
||||
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.text).to.equal(task.text);
|
||||
|
||||
expect(task.group.assignedUsers).to.contain(newMember._id);
|
||||
expect(syncedMemberTask).to.exist;
|
||||
expect(syncedMemberTask.text).to.equal(task.text);
|
||||
});
|
||||
|
||||
it('removes an assigned task and unlinks assignees', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.removeTask(task);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||
});
|
||||
|
||||
it('unlinks and deletes group tasks for a user when remove-all is specified', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
await guild.unlinkTask(task, leader, 'remove-all');
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
expect(task.group.assignedUsers).to.not.contain(leader._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
|
||||
it('unlinks and keeps group tasks for a user when keep-all is specified', async () => {
|
||||
await guild.syncTask(task, leader);
|
||||
|
||||
let updatedLeader = await User.findOne({_id: leader._id});
|
||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
|
||||
await guild.unlinkTask(task, leader, 'keep-all');
|
||||
|
||||
updatedLeader = await User.findOne({_id: leader._id});
|
||||
updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let updatedSyncedTask = find(updatedLeadersTasks, function findUpdatedLinkedTask (updatedLeadersTask) {
|
||||
return updatedLeadersTask._id === syncedTask._id;
|
||||
});
|
||||
|
||||
expect(task.group.assignedUsers).to.not.contain(leader._id);
|
||||
expect(updatedSyncedTask).to.exist;
|
||||
expect(updatedSyncedTask.group._id).to.be.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import mongoose from 'mongoose';
|
||||
import { defaultsDeep as defaults } from 'lodash';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
import { model as Group } from '../../website/server/models/group';
|
||||
import { model as Challenge } from '../../website/server/models/challenge';
|
||||
import mongo from './mongo'; // eslint-disable-line
|
||||
import moment from 'moment';
|
||||
import i18n from '../../common/script/i18n';
|
||||
@@ -20,7 +21,11 @@ export function generateUser (options = {}) {
|
||||
}
|
||||
|
||||
export function generateGroup (options = {}) {
|
||||
return new Group(options).toObject();
|
||||
return new Group(options);
|
||||
}
|
||||
|
||||
export function generateChallenge (options = {}) {
|
||||
return new Challenge(options);
|
||||
}
|
||||
|
||||
export function generateRes (options = {}) {
|
||||
|
||||
@@ -9,70 +9,17 @@ import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
} from '../../libs/errors';
|
||||
import {
|
||||
createTasks,
|
||||
getTasks,
|
||||
} from '../../libs/taskManager';
|
||||
import common from '../../../../common';
|
||||
import Bluebird from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import logger from '../../libs/logger';
|
||||
|
||||
let api = {};
|
||||
|
||||
async function _validateTaskAlias (tasks, res) {
|
||||
let tasksWithAliases = tasks.filter(task => task.alias);
|
||||
let aliases = tasksWithAliases.map(task => task.alias);
|
||||
|
||||
// Compares the short names in tasks against
|
||||
// a Set, where values cannot repeat. If the
|
||||
// lengths are different, some name was duplicated
|
||||
if (aliases.length !== [...new Set(aliases)].length) {
|
||||
throw new BadRequest(res.t('taskAliasAlreadyUsed'));
|
||||
}
|
||||
|
||||
await Bluebird.map(tasksWithAliases, (task) => {
|
||||
return task.validate();
|
||||
});
|
||||
}
|
||||
|
||||
// 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.sanitize(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;
|
||||
});
|
||||
|
||||
// tasks with aliases need to be validated asyncronously
|
||||
await _validateTaskAlias(toSave, res);
|
||||
|
||||
toSave = toSave.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 Bluebird.all(toSave);
|
||||
tasks.splice(0, 1); // Remove user or challenge
|
||||
return tasks;
|
||||
}
|
||||
let requiredGroupFields = '_id leader tasksOrder name';
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/tasks/user Create a new task belonging to the user
|
||||
@@ -88,7 +35,8 @@ api.createUserTasks = {
|
||||
url: '/tasks/user',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let tasks = await _createTasks(req, res, res.locals.user);
|
||||
let user = res.locals.user;
|
||||
let tasks = await createTasks(req, res, {user});
|
||||
res.respond(201, tasks.length === 1 ? tasks[0] : tasks);
|
||||
},
|
||||
};
|
||||
@@ -123,75 +71,15 @@ api.createChallengeTasks = {
|
||||
if (!challenge) 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);
|
||||
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);
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
// 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) {
|
||||
if (type === 'todos') {
|
||||
query.completed = false; // Exclude completed todos
|
||||
query.type = 'todo';
|
||||
} else if (type === 'completedTodos' || type === '_allCompletedTodos') { // _allCompletedTodos is currently in BETA and is likely to be removed in future
|
||||
let limit = 30;
|
||||
|
||||
if (type === '_allCompletedTodos') {
|
||||
limit = 0; // no limit
|
||||
}
|
||||
query = Tasks.Task.find({
|
||||
userId: user._id,
|
||||
type: 'todo',
|
||||
completed: true,
|
||||
}).limit(limit).sort({
|
||||
dateCompleted: -1,
|
||||
});
|
||||
} else {
|
||||
query.type = type.slice(0, -1); // removing the final "s"
|
||||
}
|
||||
} else {
|
||||
query.$or = [ // Exclude completed todos
|
||||
{type: 'todo', completed: false},
|
||||
{type: {$in: ['habit', 'daily', 'reward']}},
|
||||
];
|
||||
}
|
||||
|
||||
let tasks = await Tasks.Task.find(query).exec();
|
||||
|
||||
// Order tasks based on tasksOrder
|
||||
if (type && type !== 'completedTodos' && type !== '_allCompletedTodos') {
|
||||
let order = (challenge || user).tasksOrder[type];
|
||||
let orderedTasks = new Array(tasks.length);
|
||||
let unorderedTasks = []; // what we want to add later
|
||||
|
||||
tasks.forEach((task, index) => {
|
||||
let taskId = task._id;
|
||||
let i = order[index] === taskId ? index : order.indexOf(taskId);
|
||||
if (i === -1) {
|
||||
unorderedTasks.push(task);
|
||||
} else {
|
||||
orderedTasks[i] = task;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove empty values from the array and add any unordered task
|
||||
orderedTasks = _.compact(orderedTasks).concat(unorderedTasks);
|
||||
res.respond(200, orderedTasks);
|
||||
} else {
|
||||
res.respond(200, tasks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/tasks/user Get a user's tasks
|
||||
* @apiVersion 3.0.0
|
||||
@@ -214,7 +102,10 @@ api.getUserTasks = {
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
return await _getTasks(req, res, res.locals.user);
|
||||
let user = res.locals.user;
|
||||
|
||||
let tasks = await getTasks(req, res, {user});
|
||||
return res.respond(200, tasks);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -249,7 +140,8 @@ api.getChallengeTasks = {
|
||||
let group = await Group.getGroup({user, groupId: challenge.group, fields: '_id type privacy', optionalMembership: true});
|
||||
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
return await _getTasks(req, res, res.locals.user, challenge);
|
||||
let tasks = await getTasks(req, res, {user, challenge});
|
||||
return res.respond(200, tasks);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -274,7 +166,7 @@ api.getTask = {
|
||||
|
||||
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
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
let challenge = await Challenge.find({_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'));
|
||||
@@ -312,10 +204,16 @@ api.updateTask = {
|
||||
|
||||
let taskId = req.params.taskId;
|
||||
let task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
let group;
|
||||
|
||||
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
|
||||
} else if (task.group.id && !task.userId) {
|
||||
// @TODO: Abstract this access snippet
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
@@ -341,10 +239,13 @@ api.updateTask = {
|
||||
// see https://github.com/Automattic/mongoose/issues/2749
|
||||
|
||||
let savedTask = await task.save();
|
||||
|
||||
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||
await group.updateTask(savedTask);
|
||||
}
|
||||
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -461,8 +362,6 @@ api.scoreTask = {
|
||||
direction
|
||||
});
|
||||
*/
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -548,7 +447,7 @@ api.addChecklistItem = {
|
||||
|
||||
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
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
@@ -563,8 +462,6 @@ api.addChecklistItem = {
|
||||
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -638,7 +535,7 @@ api.updateChecklistItem = {
|
||||
|
||||
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
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
@@ -655,8 +552,6 @@ api.updateChecklistItem = {
|
||||
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -690,7 +585,7 @@ api.removeChecklistItem = {
|
||||
|
||||
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
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
@@ -705,8 +600,6 @@ api.removeChecklistItem = {
|
||||
let savedTask = await task.save();
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -952,7 +845,13 @@ api.deleteTask = {
|
||||
|
||||
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
|
||||
} else if (task.group.id && !task.userId) {
|
||||
// @TODO: Abstract this access snippet
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
await group.removeTask(task);
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (challenge.leader !== user._id) throw new NotAuthorized(res.t('onlyChalLeaderEditTasks'));
|
||||
@@ -971,8 +870,6 @@ api.deleteTask = {
|
||||
|
||||
res.respond(200, {});
|
||||
if (challenge) challenge.removeTask(task);
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
185
website/server/controllers/api-v3/tasks/groups.js
Normal file
185
website/server/controllers/api-v3/tasks/groups.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import ensureDevelpmentMode from '../../../middlewares/ensureDevelpmentMode';
|
||||
import * as Tasks from '../../../models/task';
|
||||
import { model as Group } from '../../../models/group';
|
||||
import { model as User } from '../../../models/user';
|
||||
import {
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
} from '../../../libs/errors';
|
||||
import {
|
||||
createTasks,
|
||||
getTasks,
|
||||
} from '../../../libs/taskManager';
|
||||
|
||||
let requiredGroupFields = '_id leader tasksOrder name';
|
||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/tasks/group/:groupId Create a new task belonging to a group
|
||||
* @apiDescription Can be passed an object to create a single task or an array of objects to create multiple tasks.
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName CreateGroupTasks
|
||||
* @apiGroup Task
|
||||
* @apiIgnore
|
||||
*
|
||||
* @apiParam {UUID} groupId The id of the group the new task(s) will belong to
|
||||
*
|
||||
* @apiSuccess data An object if a single task was created, otherwise an array of tasks
|
||||
*/
|
||||
api.createGroupTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks/group/:groupId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let user = res.locals.user;
|
||||
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: requiredGroupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
let tasks = await createTasks(req, res, {user, group});
|
||||
|
||||
res.respond(201, tasks.length === 1 ? tasks[0] : tasks);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/tasks/group/:groupId Get a group's tasks
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetGroupTasks
|
||||
* @apiGroup Task
|
||||
* @apiIgnore
|
||||
*
|
||||
* @apiParam {UUID} groupId The id of the group from which to retrieve the tasks
|
||||
* @apiParam {string="habits","dailys","todos","rewards"} type Optional query parameter to return just a type of tasks
|
||||
*
|
||||
* @apiSuccess {Array} data An array of tasks
|
||||
*/
|
||||
api.getGroupTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/group/:groupId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let user = res.locals.user;
|
||||
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: requiredGroupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
let tasks = await getTasks(req, res, {user, group});
|
||||
res.respond(200, tasks);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/tasks/:taskId/assign/:assignedUserId Assign a group task to a user
|
||||
* @apiDescription Assigns a user to a group task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AssignTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The id of the task that will be assigned
|
||||
* @apiParam {UUID} userId The id of the user that will be assigned to the task
|
||||
*
|
||||
* @apiSuccess data An object if a single task was created, otherwise an array of tasks
|
||||
*/
|
||||
api.assignTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/assign/:assignedUserId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let user = res.locals.user;
|
||||
let assignedUserId = req.params.assignedUserId;
|
||||
let assignedUser = await User.findById(assignedUserId);
|
||||
|
||||
let taskId = req.params.taskId;
|
||||
let task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
|
||||
if (!task.group.id) {
|
||||
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
|
||||
}
|
||||
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
await group.syncTask(task, assignedUser);
|
||||
|
||||
res.respond(200, task);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/tasks/:taskId/unassign/:assignedUserId Unassign a user from a task
|
||||
* @apiDescription Unassigns a user to from a group task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName UnassignTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The id of the task that will be assigned
|
||||
* @apiParam {UUID} userId The id of the user that will be assigned to the task
|
||||
*
|
||||
* @apiSuccess data An object if a single task was created, otherwise an array of tasks
|
||||
*/
|
||||
api.unassignTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/unassign/:assignedUserId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let user = res.locals.user;
|
||||
let assignedUserId = req.params.assignedUserId;
|
||||
let assignedUser = await User.findById(assignedUserId);
|
||||
|
||||
let taskId = req.params.taskId;
|
||||
let task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
|
||||
if (!task.group.id) {
|
||||
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
|
||||
}
|
||||
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
await group.unlinkTask(task, assignedUser);
|
||||
|
||||
res.respond(200, task);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
177
website/server/libs/taskManager.js
Normal file
177
website/server/libs/taskManager.js
Normal file
@@ -0,0 +1,177 @@
|
||||
import * as Tasks from '../models/task';
|
||||
import {
|
||||
BadRequest,
|
||||
} from './errors';
|
||||
import Bluebird from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
|
||||
async function _validateTaskAlias (tasks, res) {
|
||||
let tasksWithAliases = tasks.filter(task => task.alias);
|
||||
let aliases = tasksWithAliases.map(task => task.alias);
|
||||
|
||||
// Compares the short names in tasks against
|
||||
// a Set, where values cannot repeat. If the
|
||||
// lengths are different, some name was duplicated
|
||||
if (aliases.length !== [...new Set(aliases)].length) {
|
||||
throw new BadRequest(res.t('taskAliasAlreadyUsed'));
|
||||
}
|
||||
|
||||
await Bluebird.map(tasksWithAliases, (task) => {
|
||||
return task.validate();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates tasks for a user, challenge or group.
|
||||
*
|
||||
* @param req The Express req variable
|
||||
* @param res The Express res variable
|
||||
* @param options
|
||||
* @param options.user The user that these tasks belong to
|
||||
* @param options.challenge The challenge that these tasks belong to
|
||||
* @param options.group The group that these tasks belong to
|
||||
* @return The created tasks
|
||||
*/
|
||||
export async function createTasks (req, res, options = {}) {
|
||||
let {
|
||||
user,
|
||||
challenge,
|
||||
group,
|
||||
} = options;
|
||||
|
||||
let owner = group || challenge || user;
|
||||
|
||||
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.sanitize(taskData));
|
||||
|
||||
if (challenge) {
|
||||
newTask.challenge.id = challenge.id;
|
||||
} else if (group) {
|
||||
newTask.group.id = group._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/group and task in parallel it could save the user/challenge/group with a tasksOrder that doens't match reality
|
||||
let validationErrors = newTask.validateSync();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
// Otherwise update the user/challenge/group
|
||||
owner.tasksOrder[`${taskType}s`].unshift(newTask._id);
|
||||
|
||||
return newTask;
|
||||
});
|
||||
|
||||
// tasks with aliases need to be validated asyncronously
|
||||
await _validateTaskAlias(toSave, res);
|
||||
|
||||
toSave = toSave.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(owner.save());
|
||||
|
||||
let tasks = await Bluebird.all(toSave);
|
||||
tasks.splice(0, 1); // Remove user, challenge, or group promise
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tasks for a user, challenge or group.
|
||||
*
|
||||
* @param req The Express req variable
|
||||
* @param res The Express res variable
|
||||
* @param options
|
||||
* @param options.user The user that these tasks belong to
|
||||
* @param options.challenge The challenge that these tasks belong to
|
||||
* @param options.group The group that these tasks belong to
|
||||
* @return The tasks found
|
||||
*/
|
||||
export async function getTasks (req, res, options = {}) {
|
||||
let {
|
||||
user,
|
||||
challenge,
|
||||
group,
|
||||
} = options;
|
||||
|
||||
let query = {userId: user._id};
|
||||
let owner = group || challenge || user;
|
||||
|
||||
if (challenge) {
|
||||
query = {'challenge.id': challenge.id, userId: {$exists: false}};
|
||||
} else if (group) {
|
||||
query = {'group.id': group._id, userId: {$exists: false}};
|
||||
}
|
||||
|
||||
let type = req.query.type;
|
||||
|
||||
if (type) {
|
||||
if (type === 'todos') {
|
||||
query.completed = false; // Exclude completed todos
|
||||
query.type = 'todo';
|
||||
} else if (type === 'completedTodos' || type === '_allCompletedTodos') { // _allCompletedTodos is currently in BETA and is likely to be removed in future
|
||||
let limit = 30;
|
||||
|
||||
if (type === '_allCompletedTodos') {
|
||||
limit = 0; // no limit
|
||||
}
|
||||
query = Tasks.Task.find({
|
||||
userId: user._id,
|
||||
type: 'todo',
|
||||
completed: true,
|
||||
}).limit(limit).sort({
|
||||
dateCompleted: -1,
|
||||
});
|
||||
} else {
|
||||
query.type = type.slice(0, -1); // removing the final "s"
|
||||
}
|
||||
} else {
|
||||
query.$or = [ // Exclude completed todos
|
||||
{type: 'todo', completed: false},
|
||||
{type: {$in: ['habit', 'daily', 'reward']}},
|
||||
];
|
||||
}
|
||||
|
||||
let tasks = await Tasks.Task.find(query).exec();
|
||||
|
||||
// Order tasks based on tasksOrder
|
||||
if (type && type !== 'completedTodos' && type !== '_allCompletedTodos') {
|
||||
let order = owner.tasksOrder[type];
|
||||
let orderedTasks = new Array(tasks.length);
|
||||
let unorderedTasks = []; // what we want to add later
|
||||
|
||||
tasks.forEach((task, index) => {
|
||||
let taskId = task._id;
|
||||
let i = order[index] === taskId ? index : order.indexOf(taskId);
|
||||
if (i === -1) {
|
||||
unorderedTasks.push(task);
|
||||
} else {
|
||||
orderedTasks[i] = task;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove empty values from the array and add any unordered task
|
||||
orderedTasks = _.compact(orderedTasks).concat(unorderedTasks);
|
||||
return orderedTasks;
|
||||
} else {
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
|
||||
// Takes a Task document and return a plain object of attributes that can be synced to the user
|
||||
|
||||
export function syncableAttrs (task) {
|
||||
let t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
||||
// only sync/compare important attrs
|
||||
let omitAttrs = ['_id', 'userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes', 'updatedAt', 'group', 'checklist'];
|
||||
if (t.type !== 'reward') omitAttrs.push('value');
|
||||
return _.omit(t, omitAttrs);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import shared from '../../../common';
|
||||
import { sendTxn as txnEmail } from '../libs/email';
|
||||
import sendPushNotification from '../libs/pushNotifications';
|
||||
import cwait from 'cwait';
|
||||
import { syncableAttrs } from '../libs/taskManager';
|
||||
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
@@ -71,15 +72,6 @@ schema.methods.canView = function canViewChallenge (user, group) {
|
||||
return this.hasAccess(user, group);
|
||||
};
|
||||
|
||||
// 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 Document
|
||||
// only sync/compare important attrs
|
||||
let omitAttrs = ['_id', 'userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes', 'updatedAt', 'checklist'];
|
||||
if (t.type !== 'reward') omitAttrs.push('value');
|
||||
return _.omit(t, omitAttrs);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -125,12 +117,12 @@ schema.methods.syncToUser = async function syncChallengeToUser (user) {
|
||||
let matchingTask = _.find(userTasks, userTask => userTask.challenge.taskId === chalTask._id);
|
||||
|
||||
if (!matchingTask) { // If the task is new, create it
|
||||
matchingTask = new Tasks[chalTask.type](Tasks.Task.sanitize(_syncableAttrs(chalTask)));
|
||||
matchingTask = new Tasks[chalTask.type](Tasks.Task.sanitize(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));
|
||||
_.merge(matchingTask, syncableAttrs(chalTask));
|
||||
// Make sure the task is in user.tasksOrder
|
||||
let orderList = user.tasksOrder[`${chalTask.type}s`];
|
||||
if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id);
|
||||
@@ -162,7 +154,7 @@ async function _addTaskFn (challenge, tasks, memberId) {
|
||||
let toSave = [];
|
||||
|
||||
tasks.forEach(chalTask => {
|
||||
let userTask = new Tasks[chalTask.type](Tasks.Task.sanitize(_syncableAttrs(chalTask)));
|
||||
let userTask = new Tasks[chalTask.type](Tasks.Task.sanitize(syncableAttrs(chalTask)));
|
||||
userTask.challenge = {taskId: chalTask._id, id: challenge._id};
|
||||
userTask.userId = memberId;
|
||||
|
||||
@@ -204,9 +196,9 @@ schema.methods.updateTask = async function challengeUpdateTask (task) {
|
||||
|
||||
let updateCmd = {$set: {}};
|
||||
|
||||
let syncableAttrs = _syncableAttrs(task);
|
||||
for (let key in syncableAttrs) {
|
||||
updateCmd.$set[key] = syncableAttrs[key];
|
||||
let syncableTask = syncableAttrs(task);
|
||||
for (let key in syncableTask) {
|
||||
updateCmd.$set[key] = syncableTask[key];
|
||||
}
|
||||
|
||||
let taskSchema = Tasks[task.type];
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import shared from '../../../common';
|
||||
import _ from 'lodash';
|
||||
import { model as Challenge} from './challenge';
|
||||
import * as Tasks from './task';
|
||||
import validator from 'validator';
|
||||
import { removeFromArray } from '../libs/collectionManipulators';
|
||||
import {
|
||||
@@ -18,6 +19,9 @@ import Bluebird from 'bluebird';
|
||||
import nconf from 'nconf';
|
||||
import sendPushNotification from '../libs/pushNotifications';
|
||||
import pusher from '../libs/pusher';
|
||||
import {
|
||||
syncableAttrs,
|
||||
} from '../libs/taskManager';
|
||||
|
||||
const questScrolls = shared.content.quests;
|
||||
const Schema = mongoose.Schema;
|
||||
@@ -79,13 +83,19 @@ export let schema = new Schema({
|
||||
return {};
|
||||
}},
|
||||
},
|
||||
tasksOrder: {
|
||||
habits: [{type: String, ref: 'Task'}],
|
||||
dailys: [{type: String, ref: 'Task'}],
|
||||
todos: [{type: String, ref: 'Task'}],
|
||||
rewards: [{type: String, ref: 'Task'}],
|
||||
},
|
||||
}, {
|
||||
strict: true,
|
||||
minimize: false, // So empty objects are returned
|
||||
});
|
||||
|
||||
schema.plugin(baseModel, {
|
||||
noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount'],
|
||||
noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder'],
|
||||
});
|
||||
|
||||
// A list of additional fields that cannot be updated (but can be set on creation)
|
||||
@@ -760,6 +770,117 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
||||
return await Bluebird.all(promises);
|
||||
};
|
||||
|
||||
schema.methods.updateTask = async function updateTask (taskToSync) {
|
||||
let group = this;
|
||||
|
||||
let updateCmd = {$set: {}};
|
||||
|
||||
let syncableAttributes = syncableAttrs(taskToSync);
|
||||
for (let key in syncableAttributes) {
|
||||
updateCmd.$set[key] = syncableAttributes[key];
|
||||
}
|
||||
|
||||
let taskSchema = Tasks[taskToSync.type];
|
||||
// Updating instead of loading and saving for performances, risks becoming a problem if we introduce more complexity in tasks
|
||||
await taskSchema.update({
|
||||
userId: {$exists: true},
|
||||
'group.id': group.id,
|
||||
'group.taskId': taskToSync._id,
|
||||
}, updateCmd, {multi: true}).exec();
|
||||
};
|
||||
|
||||
schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
|
||||
let group = this;
|
||||
let toSave = [];
|
||||
|
||||
if (taskToSync.group.assignedUsers.indexOf(user._id) === -1) {
|
||||
taskToSync.group.assignedUsers.push(user._id);
|
||||
}
|
||||
|
||||
// Sync tags
|
||||
let userTags = user.tags;
|
||||
let i = _.findIndex(userTags, {id: group._id});
|
||||
|
||||
if (i !== -1) {
|
||||
if (userTags[i].name !== group.name) {
|
||||
// update the name - it's been changed since
|
||||
userTags[i].name = group.name;
|
||||
}
|
||||
} else {
|
||||
userTags.push({
|
||||
id: group._id,
|
||||
name: group.name,
|
||||
});
|
||||
}
|
||||
|
||||
let findQuery = {
|
||||
'group.taskId': taskToSync._id,
|
||||
userId: user._id,
|
||||
'group.id': group._id,
|
||||
};
|
||||
|
||||
let matchingTask = await Tasks.Task.findOne(findQuery).exec();
|
||||
|
||||
if (!matchingTask) { // If the task is new, create it
|
||||
matchingTask = new Tasks[taskToSync.type](Tasks.Task.sanitize(syncableAttrs(taskToSync)));
|
||||
matchingTask.group.id = taskToSync.group.id;
|
||||
matchingTask.userId = user._id;
|
||||
matchingTask.group.taskId = taskToSync._id;
|
||||
user.tasksOrder[`${taskToSync.type}s`].push(matchingTask._id);
|
||||
} else {
|
||||
_.merge(matchingTask, syncableAttrs(taskToSync));
|
||||
// Make sure the task is in user.tasksOrder
|
||||
let orderList = user.tasksOrder[`${taskToSync.type}s`];
|
||||
if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id);
|
||||
}
|
||||
|
||||
if (!matchingTask.notes) matchingTask.notes = taskToSync.notes; // don't override the notes, but provide it if not provided
|
||||
if (matchingTask.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id); // add tag if missing
|
||||
|
||||
toSave.push(matchingTask.save(), taskToSync.save(), user.save());
|
||||
return Bluebird.all(toSave);
|
||||
};
|
||||
|
||||
schema.methods.unlinkTask = async function groupUnlinkTask (unlinkingTask, user, keep) {
|
||||
let findQuery = {
|
||||
'group.taskId': unlinkingTask._id,
|
||||
userId: user._id,
|
||||
};
|
||||
|
||||
let assignedUserIndex = unlinkingTask.group.assignedUsers.indexOf(user._id);
|
||||
unlinkingTask.group.assignedUsers.splice(assignedUserIndex, 1);
|
||||
|
||||
if (keep === 'keep-all') {
|
||||
await Tasks.Task.update(findQuery, {
|
||||
$set: {group: {}},
|
||||
}).exec();
|
||||
|
||||
await user.save();
|
||||
} else { // keep = 'remove-all'
|
||||
let task = await Tasks.Task.findOne(findQuery).select('_id type completed').exec();
|
||||
// Remove task from user.tasksOrder and delete them
|
||||
if (task.type !== 'todo' || !task.completed) {
|
||||
removeFromArray(user.tasksOrder[`${task.type}s`], task._id);
|
||||
user.markModified('tasksOrder');
|
||||
}
|
||||
|
||||
return Bluebird.all([task.remove(), user.save(), unlinkingTask.save()]);
|
||||
}
|
||||
};
|
||||
|
||||
schema.methods.removeTask = async function groupRemoveTask (task) {
|
||||
let group = this;
|
||||
|
||||
// Set the task as broken
|
||||
await Tasks.Task.update({
|
||||
userId: {$exists: true},
|
||||
'group.id': group.id,
|
||||
'group.taskId': task._id,
|
||||
}, {
|
||||
$set: {'group.broken': 'TASK_DELETED'},
|
||||
}, {multi: true}).exec();
|
||||
};
|
||||
|
||||
export let model = mongoose.model('Group', schema);
|
||||
|
||||
// initialize tavern if !exists (fresh installs)
|
||||
|
||||
@@ -64,6 +64,13 @@ export let TaskSchema = new Schema({
|
||||
winner: String, // user.profile.name of the winner
|
||||
},
|
||||
|
||||
group: {
|
||||
id: {type: String, ref: 'Group', validate: [validator.isUUID, 'Invalid uuid.']},
|
||||
broken: {type: String, enum: ['GROUP_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED']},
|
||||
assignedUsers: [{type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}],
|
||||
taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']},
|
||||
},
|
||||
|
||||
reminders: [{
|
||||
_id: false,
|
||||
id: {type: String, validate: [validator.isUUID, 'Invalid uuid.'], default: shared.uuid, required: true},
|
||||
@@ -76,7 +83,7 @@ export let TaskSchema = new Schema({
|
||||
}, discriminatorOptions));
|
||||
|
||||
TaskSchema.plugin(baseModel, {
|
||||
noSet: ['challenge', 'userId', 'completed', 'history', 'dateCompleted', '_legacyId'],
|
||||
noSet: ['challenge', 'userId', 'completed', 'history', 'dateCompleted', '_legacyId', 'group'],
|
||||
sanitizeTransform (taskObj) {
|
||||
if (taskObj.type && taskObj.type !== 'reward') { // value should be settable directly only for rewards
|
||||
delete taskObj.value;
|
||||
|
||||
Reference in New Issue
Block a user