Merge pull request #6309 from HabitRPG/api-v3-tasks
[API v3] Port tasks and tags
This commit is contained in:
@@ -83,7 +83,7 @@
|
||||
"max-nested-callbacks": [2, 3],
|
||||
"new-cap": 2,
|
||||
"new-parens": 2,
|
||||
"newline-after-var": 2,
|
||||
"newline-after-var": 0,
|
||||
"no-array-constructor": 2,
|
||||
"no-continue": 2,
|
||||
"no-lonely-if": 2,
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -14,5 +14,18 @@
|
||||
"onlyFbSupported": "Only Facebook supported currently.",
|
||||
"cantDetachFb": "Account lacks another authentication method, can't detach Facebook.",
|
||||
"onlySocialAttachLocal": "Local auth can only be added to a social account.",
|
||||
"invalidReqParams": "Invalid request parameters."
|
||||
"invalidReqParams": "Invalid request parameters.",
|
||||
"taskIdRequired": "\"taskId\" must be a valid UUID",
|
||||
"taskNotFound": "Task not found.",
|
||||
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
|
||||
"cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.",
|
||||
"checklistOnlyDailyTodo": "Checklists are supported only on dailies and todos",
|
||||
"checklistItemNotFound": "No checklist item was found with given id.",
|
||||
"itemIdRequired": "\"itemId\" must be a valid UUID.",
|
||||
"tagNotFound": "No tag item was found with given id.",
|
||||
"tagIdRequired": "\"tagId\" must be a valid UUID corresponding to a tag belonging to the user.",
|
||||
"positionRequired": "\"position\" is required and must be a number.",
|
||||
"cantMoveCompletedTodo": "Can't move a completed todo.",
|
||||
"directionUpDown": "\"direction\" is required and must be 'up' or 'down'",
|
||||
"alreadyTagged": "The task is already tagged with give tag."
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import eslint from 'gulp-eslint';
|
||||
const SERVER_FILES = [
|
||||
'./website/src/**/api-v3/**/*.js',
|
||||
'./website/src/models/user.js',
|
||||
'./website/src/models/task.js',
|
||||
'./website/src/models/emailUnsubscription.js',
|
||||
'./website/src/server.js',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
|
||||
describe('DELETE /tasks/:id', () => {
|
||||
let user, api;
|
||||
|
||||
before(() => {
|
||||
return generateUser().then((generatedUser) => {
|
||||
user = generatedUser;
|
||||
api = requester(user);
|
||||
});
|
||||
});
|
||||
|
||||
context('task can be deleted', () => {
|
||||
let task;
|
||||
|
||||
beforeEach(() => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
}).then((createdTask) => {
|
||||
task = createdTask;
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a user\'s task', () => {
|
||||
return api.del('/tasks/' + task._id)
|
||||
.then(() => {
|
||||
return expect(api.get('/tasks/' + task._id)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('task cannot be deleted', () => {
|
||||
it('cannot delete a non-existant task', () => {
|
||||
return expect(api.del('/tasks/550e8400-e29b-41d4-a716-446655440000')).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete a task owned by someone else', () => {
|
||||
return generateUser()
|
||||
.then((user2) => {
|
||||
return requester(user2).post('/tasks', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
})
|
||||
})
|
||||
.then((task2) => {
|
||||
return expect(api.del('/tasks/' + task2._id)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot delete active challenge tasks'); // TODO after challenges are implemented
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
import Q from 'q';
|
||||
|
||||
describe('GET /tasks', () => {
|
||||
let user, api;
|
||||
|
||||
before(() => {
|
||||
return generateUser().then((generatedUser) => {
|
||||
user = generatedUser;
|
||||
api = requester(user);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns all user\'s tasks', () => {
|
||||
let length;
|
||||
return Q.all([
|
||||
api.post('/tasks', {text: 'test habit', type: 'habit'}),
|
||||
])
|
||||
.then((createdTasks) => {
|
||||
length = createdTasks.length;
|
||||
return api.get('/tasks');
|
||||
})
|
||||
.then((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', () => {
|
||||
let habitId;
|
||||
api.post('/tasks', {text: 'test habit', type: 'habit'})
|
||||
.then((task) => {
|
||||
habitId = task._id;
|
||||
return api.get('/tasks?type=habit');
|
||||
})
|
||||
.then((tasks) => {
|
||||
expect(tasks.length).to.equal(1);
|
||||
expect(tasks[0]._id).to.equal(habitId);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO complete after task scoring is done
|
||||
it('returns completed todos sorted by creation date if req.query.includeCompletedTodos is specified')
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
|
||||
describe('GET /tasks/:id', () => {
|
||||
let user, api;
|
||||
|
||||
before(() => {
|
||||
return generateUser().then((generatedUser) => {
|
||||
user = generatedUser;
|
||||
api = requester(user);
|
||||
});
|
||||
});
|
||||
|
||||
context('task can be accessed', () => {
|
||||
let task;
|
||||
|
||||
beforeEach(() => {
|
||||
// generate task
|
||||
// task = generatedTask;
|
||||
});
|
||||
|
||||
it('gets specified task');
|
||||
|
||||
it('can get active challenge task that user does not own'); // Yes?
|
||||
});
|
||||
|
||||
context('task cannot accessed', () => {
|
||||
it('cannot get a non-existant task');
|
||||
|
||||
it('cannot get a task owned by someone else');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
import { v4 as generateRandomUserName } from 'uuid';
|
||||
import { each } from 'lodash';
|
||||
|
||||
describe('POST /tasks', () => {
|
||||
let user, api;
|
||||
|
||||
before(() => {
|
||||
return generateUser().then((generatedUser) => {
|
||||
user = generatedUser;
|
||||
api = requester(user);
|
||||
});
|
||||
});
|
||||
|
||||
context('validates params', () => {
|
||||
it('returns an error if req.body.type is absent', () => {
|
||||
return expect(api.post('/tasks', {
|
||||
notType: 'habit',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.body.type is not valid', () => {
|
||||
return expect(api.post('/tasks', {
|
||||
type: 'habitF',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if req.body.text is absent');
|
||||
|
||||
it('ignores setting userId field');
|
||||
|
||||
it('automatically sets "task.userId" to user\'s uuid', () => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
}).then((task) => {
|
||||
expect(task.userId).to.equal(user._id);
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores setting history field');
|
||||
|
||||
it('ignores setting createdAt field');
|
||||
|
||||
it('ignores setting updatedAt field');
|
||||
|
||||
it('ignores setting challenge field');
|
||||
|
||||
it('ignores setting completed field');
|
||||
|
||||
it('ignores setting streak field');
|
||||
|
||||
it('ignores setting dateCompleted field');
|
||||
|
||||
it('ignores invalid fields');
|
||||
});
|
||||
|
||||
context('habits', () => {
|
||||
it('creates a habit', () => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
}).then((task) => {
|
||||
expect(task.userId).to.equal(user._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('defaults to setting up and down to true');
|
||||
|
||||
it('cannot create checklists');
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
it('creates a todo', () => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
notes: 1976,
|
||||
}).then((task) => {
|
||||
expect(task.userId).to.equal(user._id);
|
||||
expect(task.text).to.eql('test todo');
|
||||
expect(task.notes).to.eql('1976');
|
||||
expect(task.type).to.eql('todo');
|
||||
});
|
||||
});
|
||||
|
||||
it('can create checklists');
|
||||
});
|
||||
|
||||
context('dailys', () => {
|
||||
it('creates a daily', () => {
|
||||
let now = new Date();
|
||||
|
||||
return api.post('/tasks', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
notes: 1976,
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: now,
|
||||
}).then((task) => {
|
||||
expect(task.userId).to.equal(user._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);
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to a weekly frequency, with every day set');
|
||||
|
||||
it('allows repeat field to be configured');
|
||||
|
||||
it('defaults startDate to today');
|
||||
|
||||
it('can create checklists');
|
||||
});
|
||||
|
||||
context('rewards', () => {
|
||||
it('creates a reward', () => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
notes: 1976,
|
||||
value: 10,
|
||||
}).then((task) => {
|
||||
expect(task.userId).to.equal(user._id);
|
||||
expect(task.text).to.eql('test reward');
|
||||
expect(task.notes).to.eql('1976');
|
||||
expect(task.type).to.eql('reward');
|
||||
expect(task.value).to.eql(10);
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults to a 0 value');
|
||||
|
||||
it('requires value to be coerced into a number');
|
||||
|
||||
it('cannot create checklists');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
|
||||
describe('POST /tasks/score/:id/:direction', () => {
|
||||
let user, api;
|
||||
|
||||
before(() => {
|
||||
return generateUser().then((generatedUser) => {
|
||||
user = generatedUser;
|
||||
api = requester(user);
|
||||
});
|
||||
});
|
||||
|
||||
context('all', () => {
|
||||
it('requires a task id');
|
||||
|
||||
it('requires a task direction');
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
let todo;
|
||||
|
||||
beforeEach(() => {
|
||||
// todo = createdTodo
|
||||
});
|
||||
|
||||
it('completes todo when direction is up');
|
||||
|
||||
it('uncompletes todo when direction is down');
|
||||
|
||||
it('scores up todo even if it is already completed'); // Yes?
|
||||
|
||||
it('scores down todo even if it is already uncompleted'); // Yes?
|
||||
|
||||
it('increases user\'s mp when direction is up');
|
||||
|
||||
it('decreases user\'s mp when direction is down');
|
||||
|
||||
it('increases user\'s exp when direction is up');
|
||||
|
||||
it('decreases user\'s exp when direction is down');
|
||||
|
||||
it('increases user\'s gold when direction is up');
|
||||
|
||||
it('decreases user\'s gold when direction is down');
|
||||
});
|
||||
|
||||
context('dailys', () => {
|
||||
let daily;
|
||||
|
||||
beforeEach(() => {
|
||||
// daily = createdDaily
|
||||
});
|
||||
|
||||
it('completes daily when direction is up');
|
||||
|
||||
it('uncompletes daily when direction is down');
|
||||
|
||||
it('scores up daily even if it is already completed'); // Yes?
|
||||
|
||||
it('scores down daily even if it is already uncompleted'); // Yes?
|
||||
|
||||
it('increases user\'s mp when direction is up');
|
||||
|
||||
it('decreases user\'s mp when direction is down');
|
||||
|
||||
it('increases user\'s exp when direction is up');
|
||||
|
||||
it('decreases user\'s exp when direction is down');
|
||||
|
||||
it('increases user\'s gold when direction is up');
|
||||
|
||||
it('decreases user\'s gold when direction is down');
|
||||
});
|
||||
|
||||
context('habits', () => {
|
||||
let habit, minusHabit, plusHabit, neitherHabit;
|
||||
|
||||
beforeEach(() => {
|
||||
// habit = createdHabit
|
||||
// plusHabit = createdPlusHabit
|
||||
// minusHabit = createdMinusHabit
|
||||
// neitherHabit = createdNeitherHabit
|
||||
});
|
||||
|
||||
it('prevents plus only habit from scoring down'); // Yes?
|
||||
|
||||
it('prevents minus only habit from scoring up'); // Yes?
|
||||
|
||||
it('increases user\'s mp when direction is up');
|
||||
|
||||
it('decreases user\'s mp when direction is down');
|
||||
|
||||
it('increases user\'s exp when direction is up');
|
||||
|
||||
it('decreases user\'s exp when direction is down');
|
||||
|
||||
it('increases user\'s gold when direction is up');
|
||||
|
||||
it('decreases user\'s gold when direction is down');
|
||||
});
|
||||
|
||||
context('reward', () => {
|
||||
let reward;
|
||||
|
||||
beforeEach(() => {
|
||||
// reward = createdReward
|
||||
});
|
||||
|
||||
it('purchases reward');
|
||||
|
||||
it('does not change user\'s mp');
|
||||
|
||||
it('does not change user\'s exp');
|
||||
|
||||
it('does not allow a down direction');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration.helper';
|
||||
|
||||
describe('PUT /tasks/:id', () => {
|
||||
let user, api;
|
||||
|
||||
before(() => {
|
||||
return generateUser().then((generatedUser) => {
|
||||
user = generatedUser;
|
||||
api = requester(user);
|
||||
});
|
||||
});
|
||||
|
||||
context('validates params', () => {
|
||||
let task;
|
||||
|
||||
beforeEach(() => {
|
||||
// create sample task
|
||||
// task = createdTask
|
||||
});
|
||||
|
||||
it('ignores setting type field');
|
||||
|
||||
it('ignores setting userId field');
|
||||
|
||||
it('ignores setting history field');
|
||||
|
||||
it('ignores setting createdAt field');
|
||||
|
||||
it('ignores setting updatedAt field');
|
||||
|
||||
it('ignores setting challenge field');
|
||||
|
||||
it('ignores setting value field');
|
||||
|
||||
it('ignores setting completed field');
|
||||
|
||||
it('ignores setting streak field');
|
||||
|
||||
it('ignores setting dateCompleted field');
|
||||
|
||||
it('ignores invalid fields');
|
||||
});
|
||||
|
||||
context('habits', () => {
|
||||
let habit;
|
||||
|
||||
beforeEach(() => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
notes: 1976,
|
||||
}).then((createdHabit) => {
|
||||
habit = createdHabit;
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a habit', () => {
|
||||
return api.put(`/tasks/${habit._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
}).then((task) => {
|
||||
expect(task.text).to.eql('some new text');
|
||||
expect(task.notes).to.eql('some new notes');
|
||||
expect(task.up).to.eql(false);
|
||||
expect(task.down).to.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
let todo;
|
||||
|
||||
beforeEach(() => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
notes: 1976,
|
||||
}).then((createdTodo) => {
|
||||
todo = createdTodo;
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a todo', () => {
|
||||
return api.put(`/tasks/${todo._id}`, {
|
||||
text: 'some new text',
|
||||
notes: 'some new notes',
|
||||
}).then((task) => {
|
||||
expect(task.text).to.eql('some new text');
|
||||
expect(task.notes).to.eql('some new notes');
|
||||
});
|
||||
});
|
||||
|
||||
it('can update checklists'); // Can it?
|
||||
});
|
||||
|
||||
context('dailys', () => {
|
||||
let daily;
|
||||
|
||||
beforeEach(() => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
notes: 1976,
|
||||
}).then((createdDaily) => {
|
||||
daily = createdDaily;
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a daily', () => {
|
||||
let now = new Date();
|
||||
|
||||
return api.put(`/tasks/${daily._id}`, {
|
||||
text: 'some new text',
|
||||
notes: 'some new notes',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
}).then((task) => {
|
||||
expect(task.text).to.eql('some new text');
|
||||
expect(task.notes).to.eql('some new notes');
|
||||
expect(task.frequency).to.eql('daily');
|
||||
expect(task.everyX).to.eql(5);
|
||||
});
|
||||
});
|
||||
|
||||
it('can update checklists'); // Can it?
|
||||
|
||||
it('updates repeat, even if frequency is set to daily');
|
||||
|
||||
it('updates everyX, even if frequency is set to weekly');
|
||||
|
||||
it('defaults startDate to today if none date object is passed in');
|
||||
});
|
||||
|
||||
context('rewards', () => {
|
||||
let reward;
|
||||
|
||||
beforeEach(() => {
|
||||
return api.post('/tasks', {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
notes: 1976,
|
||||
value: 10,
|
||||
}).then((createdReward) => {
|
||||
reward = createdReward;
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a reward', () => {
|
||||
return api.put(`/tasks/${reward._id}`, {
|
||||
text: 'some new text',
|
||||
notes: 'some new notes',
|
||||
value: 10,
|
||||
}).then((task) => {
|
||||
expect(task.text).to.eql('some new text');
|
||||
expect(task.notes).to.eql('some new notes');
|
||||
expect(task.value).to.eql(10);
|
||||
});
|
||||
});
|
||||
|
||||
it('requires value to be coerced into a number');
|
||||
});
|
||||
});
|
||||
@@ -194,10 +194,10 @@ describe('POST /user/auth/local/register', () => {
|
||||
password,
|
||||
confirmPassword: password,
|
||||
}).then((user) => {
|
||||
expect(user.todos).to.not.be.empty;
|
||||
expect(user.dailys).to.be.empty;
|
||||
expect(user.habits).to.be.empty;
|
||||
expect(user.rewards).to.be.empty;
|
||||
expect(user.tasksOrder.todos).to.not.be.empty;
|
||||
expect(user.tasksOrder.dailys).to.be.empty;
|
||||
expect(user.tasksOrder.habits).to.be.empty;
|
||||
expect(user.tasksOrder.rewards).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,10 +245,10 @@ describe('POST /user/auth/local/register', () => {
|
||||
password,
|
||||
confirmPassword: password,
|
||||
}).then((user) => {
|
||||
expect(user.todos).to.not.be.empty;
|
||||
expect(user.dailys).to.be.empty;
|
||||
expect(user.habits).to.not.be.empty;
|
||||
expect(user.rewards).to.not.be.empty;
|
||||
expect(user.tasksOrder.todos).to.not.be.empty;
|
||||
expect(user.tasksOrder.dailys).to.be.empty;
|
||||
expect(user.tasksOrder.habits).to.not.be.empty;
|
||||
expect(user.tasksOrder.rewards).to.not.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -43,6 +43,20 @@ describe('Base model plugin', () => {
|
||||
expect(sanitized.noUpdateForMe).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('accepts an array of additional fields to sanitize at runtime', () => {
|
||||
baseModel(schema, {
|
||||
noSet: ['noUpdateForMe']
|
||||
});
|
||||
|
||||
expect(schema.statics.sanitize).to.exist;
|
||||
let sanitized = schema.statics.sanitize({ok: true, noUpdateForMe: true, usuallySettable: true}, ['usuallySettable']);
|
||||
|
||||
expect(sanitized).to.have.property('ok');
|
||||
expect(sanitized).not.to.have.property('noUpdateForMe');
|
||||
expect(sanitized).not.to.have.property('usuallySettable');
|
||||
});
|
||||
|
||||
|
||||
it('can make fields private', () => {
|
||||
baseModel(schema, {
|
||||
private: ['amPrivate']
|
||||
|
||||
@@ -44,7 +44,6 @@ api.registerLocal = {
|
||||
});
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
let { email, username, password } = req.body;
|
||||
@@ -152,7 +151,6 @@ api.loginLocal = {
|
||||
});
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
req.sanitizeBody('username').trim();
|
||||
@@ -244,7 +242,7 @@ api.loginSocial = {
|
||||
* @apiName UserDeleteSocial
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiSuccess {Boolean=true} success Always true
|
||||
* @apiSuccess {Object} response Empty object
|
||||
*/
|
||||
api.deleteSocial = {
|
||||
method: 'DELETE',
|
||||
@@ -0,0 +1,151 @@
|
||||
import { authWithHeaders } from '../../middlewares/api-v3/auth';
|
||||
import { model as Tag } from '../../models/tag';
|
||||
import {
|
||||
NotFound,
|
||||
} from '../../libs/api-v3/errors';
|
||||
import _ from 'lodash';
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @api {post} /tags Create a new tag
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName CreateTag
|
||||
* @apiGroup Tag
|
||||
*
|
||||
* @apiSuccess {Object} tag The newly created tag
|
||||
*/
|
||||
api.createTag = {
|
||||
method: 'POST',
|
||||
url: '/tags',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
user.tags.push(Tag.sanitize(req.body));
|
||||
|
||||
user.save()
|
||||
.then((savedUser) => {
|
||||
let l = savedUser.tags.length;
|
||||
let tag = savedUser.tags[l - 1];
|
||||
res.respond(201, tag);
|
||||
})
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /tag Get an user's tags
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetTags
|
||||
* @apiGroup Tag
|
||||
*
|
||||
* @apiSuccess {Array} tags An array of tag objects
|
||||
*/
|
||||
api.getTags = {
|
||||
method: 'GET',
|
||||
url: '/tags',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
res.respond(200, user.tags);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /tags/:tagId Get a tag given its id
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetTag
|
||||
* @apiGroup Tag
|
||||
*
|
||||
* @apiParam {UUID} tagId The tag _id
|
||||
*
|
||||
* @apiSuccess {object} tag The tag object
|
||||
*/
|
||||
api.getTag = {
|
||||
method: 'GET',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('tagIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
let tag = user.tags.id(req.params.tagId);
|
||||
if (!tag) return next(new NotFound(res.t('tagNotFound')));
|
||||
res.respond(200, tag);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {put} /tag/:tagId Update a tag
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName UpdateTag
|
||||
* @apiGroup Tag
|
||||
*
|
||||
* @apiParam {UUID} tagId The tag _id
|
||||
*
|
||||
* @apiSuccess {object} tag The updated tag
|
||||
*/
|
||||
api.updateTag = {
|
||||
method: 'PUT',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('tagId', res.t('tagIdRequired')).notEmpty().isUUID();
|
||||
// TODO check that req.body isn't empty
|
||||
|
||||
let tagId = req.params.id;
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
let tag = user.tags.id(tagId);
|
||||
if (!tag) return next(new NotFound(res.t('tagNotFound')));
|
||||
|
||||
_.merge(tag, Tag.sanitize(req.body));
|
||||
|
||||
user.save()
|
||||
.then((savedUser) => res.respond(200, savedUser.tags.id(tagId)))
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {delete} /tag/:tagId Delete a user tag given its id
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName DeleteTag
|
||||
* @apiGroup Tag
|
||||
*
|
||||
* @apiParam {UUID} tagId The tag _id
|
||||
*
|
||||
* @apiSuccess {object} empty An empty object
|
||||
*/
|
||||
api.deleteTag = {
|
||||
method: 'GET',
|
||||
url: '/tags/:tagId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('tagId', res.t('tagIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
let tag = user.tags.id(req.params.tagId);
|
||||
if (!tag) return next(new NotFound(res.t('tagNotFound')));
|
||||
tag.remove();
|
||||
|
||||
user.save()
|
||||
.then(() => res.respond(200, {}))
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
@@ -0,0 +1,590 @@
|
||||
import { authWithHeaders } from '../../middlewares/api-v3/auth';
|
||||
import * as Tasks from '../../models/task';
|
||||
import {
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
} from '../../libs/api-v3/errors';
|
||||
import Q from 'q';
|
||||
import _ from 'lodash';
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @api {post} /tasks Create a new task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName CreateTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiSuccess {Object} task The newly created task
|
||||
*/
|
||||
// TODO should allow to create multiple tasks at once
|
||||
// TODO gives problems when creating tasks concurrently because of how mongoose treats arrays (VersionErrors - treated as 500s)
|
||||
api.createTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
req.checkBody('type', res.t('invalidTaskType')).notEmpty().isIn(Tasks.tasksTypes);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
let user = res.locals.user;
|
||||
let taskType = req.body.type;
|
||||
|
||||
let newTask = new Tasks[taskType](Tasks.Task.sanitizeCreate(req.body));
|
||||
newTask.userId = user._id;
|
||||
|
||||
user.tasksOrder[`${taskType}s`].unshift(newTask._id);
|
||||
|
||||
Q.all([
|
||||
newTask.save(),
|
||||
user.save(),
|
||||
])
|
||||
.then((results) => res.respond(201, results[0]))
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /tasks Get an user's tasks
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetTasks
|
||||
* @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"
|
||||
*
|
||||
* @apiSuccess {Array} tasks An array of task objects
|
||||
*/
|
||||
api.getTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(Tasks.tasksTypes);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
let user = res.locals.user;
|
||||
let query = {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')) {
|
||||
let queryCompleted = Tasks.Task.find({
|
||||
type: 'todo',
|
||||
completed: true,
|
||||
}).limit(30).sort({ // TODO add ability to pick more than 30 completed todos
|
||||
dateCompleted: 1,
|
||||
});
|
||||
|
||||
Q.all([
|
||||
queryCompleted.exec(),
|
||||
Tasks.Task.find(query).exec(),
|
||||
])
|
||||
.then((results) => res.respond(200, results[1].concat(results[0])))
|
||||
.catch(next);
|
||||
} else {
|
||||
Tasks.Task.find(query).exec()
|
||||
.then((tasks) => res.respond(200, tasks))
|
||||
.catch(next);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /task/:taskId Get a task given its id
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
*
|
||||
* @apiSuccess {object} task The task object
|
||||
*/
|
||||
api.getTask = {
|
||||
method: 'GET',
|
||||
url: '/tasks/:taskId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
res.respond(200, task);
|
||||
})
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {put} /task/:taskId Update a task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName UpdateTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
*
|
||||
* @apiSuccess {object} task The updated task
|
||||
*/
|
||||
api.updateTask = {
|
||||
method: 'PUT',
|
||||
url: '/tasks/:taskId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
// TODO check that req.body isn't empty
|
||||
// TODO make sure tags are updated correctly (they aren't set as modified!) maybe use specific routes
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
|
||||
// If checklist is updated -> replace the original one
|
||||
if (req.body.checklist) {
|
||||
delete req.body.checklist;
|
||||
task.checklist = req.body.checklist;
|
||||
}
|
||||
// TODO merge goes deep into objects, it's ok?
|
||||
// TODO also check that array and mixed fields are updated correctly without marking modified
|
||||
_.merge(task, Tasks.Task.sanitizeUpdate(req.body));
|
||||
return task.save();
|
||||
})
|
||||
.then((savedTask) => res.respond(200, savedTask))
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {put} /tasks/score/:taskId/:direction Score a task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName ScoreTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
* @apiParam {string="up","down"} direction The direction for scoring the task
|
||||
*
|
||||
* @apiSuccess {object} empty An empty object
|
||||
*/
|
||||
api.scoreTask = {
|
||||
method: 'POST',
|
||||
url: 'tasks/score/:taskId/:direction',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('direction', res.t('directionUpDown')).notEmpty().isIn(['up', 'down']);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
let user = res.locals.user;
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
})
|
||||
.then(() => res.respond(200, {})) // TODO what to return
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
// 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
|
||||
/**
|
||||
* @api {post} /tasks/move/:taskId/to/:position Move a task to a new position
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName MoveTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
* @apiParam {Number} position Where to move the task (-1 means push to bottom)
|
||||
*
|
||||
* @apiSuccess {object} empty An empty object
|
||||
*/
|
||||
api.moveTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/move/:taskId/to/:position',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
let user = res.locals.user;
|
||||
let to = Number(req.params.position);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (task.type === 'todo' && task.completed) throw new NotFound(res.t('cantMoveCompletedTodo'));
|
||||
let order = user.tasksOrder[`${task.type}s`];
|
||||
let currentIndex = order.indexOf(task._id);
|
||||
|
||||
// If for some reason the task isn't ordered (should never happen)
|
||||
// or if the task is moved to a non existing position
|
||||
// or if the task is moved to postion -1 (push to bottom)
|
||||
// -> push task at end of list
|
||||
if (currentIndex === -1 || !order[to] || to === -1) {
|
||||
order.push(task._id);
|
||||
} else {
|
||||
let taskToMove = order.splice(currentIndex, 1)[0];
|
||||
order.splice(to, 0, taskToMove);
|
||||
}
|
||||
|
||||
return user.save();
|
||||
})
|
||||
.then(() => res.respond(200, {})) // TODO what to return
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /tasks/:taskId/checklist Add an item to a checklist, creating the checklist if it doesn't exist
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AddChecklistItem
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
*
|
||||
* @apiSuccess {object} task The updated task
|
||||
*/
|
||||
api.addChecklistItem = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/checklist',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
// TODO check that req.body isn't empty and is an array
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
||||
|
||||
task.checklist.push(req.body);
|
||||
return task.save();
|
||||
})
|
||||
.then((savedTask) => res.respond(200, savedTask)) // TODO what to return
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /tasks/:taskId/checklist/:itemId/score Score a checklist item
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName ScoreChecklistItem
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
* @apiParam {UUID} itemId The checklist item _id
|
||||
*
|
||||
* @apiSuccess {object} task The updated task
|
||||
*/
|
||||
api.scoreCheckListItem = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/checklist/:itemId/score',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) 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});
|
||||
|
||||
if (!item) throw new NotFound(res.t('checklistItemNotFound'));
|
||||
item.completed = !item.completed;
|
||||
return task.save();
|
||||
})
|
||||
.then((savedTask) => res.respond(200, savedTask)) // TODO what to return
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {put} /tasks/:taskId/checklist/:itemId Update a checklist item
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName UpdateChecklistItem
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
* @apiParam {UUID} itemId The checklist item _id
|
||||
*
|
||||
* @apiSuccess {object} task The updated task
|
||||
*/
|
||||
api.updateChecklistItem = {
|
||||
method: 'PUT',
|
||||
url: '/tasks/:taskId/checklist/:itemId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) 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});
|
||||
if (!item) throw new NotFound(res.t('checklistItemNotFound'));
|
||||
|
||||
delete req.body.id; // Simple sanitization to prevent the ID to be changed
|
||||
_.merge(item, req.body);
|
||||
return task.save();
|
||||
})
|
||||
.then((savedTask) => res.respond(200, savedTask)) // TODO what to return
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {delete} /tasks/:taskId/checklist/:itemId Remove a checklist item
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName RemoveChecklistItem
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
* @apiParam {UUID} itemId The checklist item _id
|
||||
*
|
||||
* @apiSuccess {object} empty An empty object
|
||||
*/
|
||||
api.removeChecklistItem = {
|
||||
method: 'DELETE',
|
||||
url: '/tasks/:taskId/checklist/:itemId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) 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});
|
||||
if (itemI === -1) throw new NotFound(res.t('checklistItemNotFound'));
|
||||
|
||||
task.checklist.splice(itemI, 1);
|
||||
return task.save();
|
||||
})
|
||||
.then(() => res.respond(200, {})) // TODO what to return
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /tasks/:taskId/tags/:tagId Add a tag to a task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName AddTagToTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
* @apiParam {UUID} tagId The tag id
|
||||
*
|
||||
* @apiSuccess {object} task The updated task
|
||||
*/
|
||||
api.addTagToTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/tags',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
let userTags = user.tags.map(tag => tag._id);
|
||||
req.checkParams('tagId', res.t('tagIdRequired')).notEmpty().isUUID().isIn(userTags);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
let tagId = req.params.tagId;
|
||||
|
||||
let alreadyTagged = task.tags.indexOf(tagId) === -1;
|
||||
if (alreadyTagged) throw new BadRequest(res.t('alreadyTagged'));
|
||||
|
||||
task.tags.push(tagId);
|
||||
return task.save();
|
||||
})
|
||||
.then((savedTask) => res.respond(200, savedTask)) // TODO what to return
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {delete} /tasks/:taskId/tags/:tagId Remove a tag
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName RemoveTagFromTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
* @apiParam {UUID} tagId The tag id
|
||||
*
|
||||
* @apiSuccess {object} empty An empty object
|
||||
*/
|
||||
api.removeTagFromTask = {
|
||||
method: 'DELETE',
|
||||
url: '/tasks/:taskId/tags/:tagId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('tagId', res.t('tagIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
|
||||
let tagI = _.findIndex(task.tags, {_id: req.params.tagId});
|
||||
if (tagI === -1) throw new NotFound(res.t('tagNotFound'));
|
||||
|
||||
task.tags.splice(tagI, 1);
|
||||
return task.save();
|
||||
})
|
||||
.then(() => res.respond(200, {})) // TODO what to return
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
// Remove a task from user.tasksOrder
|
||||
function _removeTaskTasksOrder (user, taskId) {
|
||||
// Loop through all lists and when the task is found, remove it and return
|
||||
for (let i = 0; i < Tasks.tasksTypes.length; i++) {
|
||||
let list = user.tasksOrder[`${Tasks.tasksTypes[i]}s`];
|
||||
let index = list.indexOf(taskId);
|
||||
|
||||
if (index !== -1) {
|
||||
list.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {delete} /task/:taskId Delete a user task given its id
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName DeleteTask
|
||||
* @apiGroup Task
|
||||
*
|
||||
* @apiParam {UUID} taskId The task _id
|
||||
*
|
||||
* @apiSuccess {object} empty An empty object
|
||||
*/
|
||||
api.deleteTask = {
|
||||
method: 'DELETE',
|
||||
url: '/tasks/:taskId',
|
||||
middlewares: [authWithHeaders()],
|
||||
handler (req, res, next) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: req.params.taskId,
|
||||
userId: user._id,
|
||||
}).exec()
|
||||
.then((task) => {
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (task.challenge.id) throw new NotAuthorized(res.t('cantDeleteChallengeTasks'));
|
||||
|
||||
_removeTaskTasksOrder(user, req.params.taskId);
|
||||
return Q.all([
|
||||
user.save(),
|
||||
task.remove(),
|
||||
]);
|
||||
})
|
||||
.then(() => res.respond(200, {}))
|
||||
.catch(next);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
@@ -35,8 +35,9 @@ export default function baseModel (schema, options = {}) {
|
||||
let privateFields = ['__v'];
|
||||
|
||||
if (Array.isArray(options.noSet)) noSetFields.push(...options.noSet);
|
||||
schema.statics.sanitize = function sanitize (objToSanitize = {}) {
|
||||
noSetFields.forEach((fieldPath) => {
|
||||
// This method accepts an additional array of fields to be sanitized that can be passed at runtime
|
||||
schema.statics.sanitize = function sanitize (objToSanitize = {}, additionalFields = []) {
|
||||
noSetFields.concat(additionalFields).forEach((fieldPath) => {
|
||||
objectPath.del(objToSanitize, fieldPath);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
// If optional is true, don't error on missing authentication
|
||||
export function authWithHeaders (optional = false) {
|
||||
return function authWithHeadersHandler (req, res, next) {
|
||||
let userId = req.header['x-api-user'];
|
||||
let apiToken = req.header['x-api-key'];
|
||||
let userId = req.header('x-api-user');
|
||||
let apiToken = req.header('x-api-key');
|
||||
|
||||
if (!userId || !apiToken) {
|
||||
if (optional) return next();
|
||||
@@ -30,6 +30,7 @@ export function authWithHeaders (optional = false) {
|
||||
|
||||
res.locals.user = user;
|
||||
// TODO use either session/cookie or headers, not both
|
||||
req.session = req.session || {};
|
||||
req.session.userId = user._id;
|
||||
next();
|
||||
})
|
||||
|
||||
@@ -10,10 +10,10 @@ var ChallengeSchema = new Schema({
|
||||
shortName: String,
|
||||
description: String,
|
||||
official: {type: Boolean,'default':false},
|
||||
habits: [TaskSchemas.HabitSchema],
|
||||
dailys: [TaskSchemas.DailySchema],
|
||||
todos: [TaskSchemas.TodoSchema],
|
||||
rewards: [TaskSchemas.RewardSchema],
|
||||
//habits: [TaskSchemas.HabitSchema],
|
||||
//dailys: [TaskSchemas.DailySchema],
|
||||
//todos: [TaskSchemas.TodoSchema],
|
||||
//rewards: [TaskSchemas.RewardSchema],
|
||||
leader: {type: String, ref: 'User'},
|
||||
group: {type: String, ref: 'Group'},
|
||||
timestamp: {type: Date, 'default': Date.now},
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import mongoose from 'mongoose';
|
||||
import baseModel from '../libs/api-v3/baseModel';
|
||||
|
||||
let Schema = mongoose.Schema;
|
||||
|
||||
export let schema = new Schema({
|
||||
name: {type: String, required: true},
|
||||
challenge: {type: String}, // TODO validate
|
||||
}, {
|
||||
minimize: true, // So empty objects are returned
|
||||
strict: true,
|
||||
});
|
||||
|
||||
schema.plugin(baseModel, {
|
||||
noSet: ['_id', 'challenge'],
|
||||
});
|
||||
|
||||
export let model = mongoose.model('Tag', schema);
|
||||
+107
-97
@@ -1,108 +1,118 @@
|
||||
// User.js
|
||||
// =======
|
||||
// Defines the user data model (schema) for use via the API.
|
||||
import mongoose from 'mongoose';
|
||||
import shared from '../../../common';
|
||||
import validator from 'validator';
|
||||
import moment from 'moment';
|
||||
import baseModel from '../libs/api-v3/baseModel';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Dependencies
|
||||
// ------------
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var shared = require('../../../common');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
let Schema = mongoose.Schema;
|
||||
let discriminatorOptions = {
|
||||
discriminatorKey: 'type', // the key that distinguishes task types
|
||||
};
|
||||
let subDiscriminatorOptions = _.defaults(_.cloneDeep(discriminatorOptions), {_id: false});
|
||||
|
||||
// Task Schema
|
||||
// -----------
|
||||
export let tasksTypes = ['habit', 'daily', 'todo', 'reward'];
|
||||
|
||||
export let TaskSchema = new Schema({
|
||||
type: {type: String, enum: tasksTypes, required: true, default: tasksTypes[0]},
|
||||
text: {type: String, required: true},
|
||||
notes: {type: String, default: ''},
|
||||
tags: [{
|
||||
type: String,
|
||||
validate: [validator.isUUID, 'Invalid uuid.'],
|
||||
}],
|
||||
value: {type: Number, default: 0}, // redness or cost for rewards
|
||||
priority: {type: Number, default: 1, required: true}, // TODO enum?
|
||||
attribute: {type: String, default: 'str', enum: ['str', 'con', 'int', 'per']},
|
||||
userId: {type: String, ref: 'User'}, // When null it belongs to a challenge
|
||||
|
||||
var TaskSchema = {
|
||||
//_id:{type: String,'default': helpers.uuid},
|
||||
id: {type: String,'default': shared.uuid},
|
||||
dateCreated: {type:Date, 'default':Date.now},
|
||||
text: String,
|
||||
notes: {type: String, 'default': ''},
|
||||
tags: {type: Schema.Types.Mixed, 'default': {}}, //{ "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true },
|
||||
value: {type: Number, 'default': 0}, // redness
|
||||
priority: {type: Number, 'default': '1'},
|
||||
attribute: {type: String, 'default': "str", enum: ['str','con','int','per']},
|
||||
challenge: {
|
||||
id: {type: 'String', ref:'Challenge'},
|
||||
broken: String, // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, CHALLENGE_CLOSED
|
||||
winner: String // user.profile.name
|
||||
// group: {type: 'Strign', ref: 'Group'} // if we restore this, rename `id` above to `challenge`
|
||||
}
|
||||
id: {type: String, ref: 'Challenge'},
|
||||
taskId: {type: String, ref: 'Task'}, // When null but challenge.id defined it's the original task
|
||||
broken: String, // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, CHALLENGE_CLOSED TODO enum
|
||||
winner: String, // user.profile.name TODO necessary?
|
||||
},
|
||||
}, _.defaults({
|
||||
minimize: true, // So empty objects are returned
|
||||
strict: true,
|
||||
}, discriminatorOptions));
|
||||
|
||||
TaskSchema.plugin(baseModel, {
|
||||
// TODO checklist fields editable?
|
||||
// TODO value should be settable only for rewards
|
||||
noSet: ['challenge', 'userId', 'completed', 'history', 'streak', 'dateCompleted'],
|
||||
private: [],
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
// A list of additional fields that cannot be set on creation (but can be set on updare)
|
||||
let noCreate = ['completed'];
|
||||
TaskSchema.statics.sanitizeCreate = function sanitizeCreate (createObj) {
|
||||
return Task.sanitize(createObj, noCreate); // eslint-disable-line no-use-before-define
|
||||
};
|
||||
|
||||
var HabitSchema = new Schema(
|
||||
_.defaults({
|
||||
type: {type:String, 'default': 'habit'},
|
||||
history: Array, // [{date:Date, value:Number}], // this causes major performance problems
|
||||
up: {type: Boolean, 'default': true},
|
||||
down: {type: Boolean, 'default': true}
|
||||
}, TaskSchema)
|
||||
, { _id: false, minimize:false }
|
||||
);
|
||||
// A list of additional fields that cannot be updated (but can be set on creation)
|
||||
let noUpdate = ['_id', 'type']; // TODO should prevent changes to checlist.*.id
|
||||
TaskSchema.statics.sanitizeUpdate = function sanitizeUpdate (updateObj) {
|
||||
return Task.sanitize(updateObj, noUpdate); // eslint-disable-line no-use-before-define
|
||||
};
|
||||
|
||||
var collapseChecklist = {type:Boolean, 'default':false};
|
||||
var checklist = [{
|
||||
completed:{type:Boolean,'default':false},
|
||||
text: String,
|
||||
_id:false,
|
||||
id: {type:String,'default':shared.uuid}
|
||||
}];
|
||||
export let Task = mongoose.model('Task', TaskSchema);
|
||||
|
||||
var DailySchema = new Schema(
|
||||
_.defaults({
|
||||
type: {type: String, 'default': 'daily'},
|
||||
frequency: {type: String, 'default': 'weekly', enum: ['daily', 'weekly']},
|
||||
everyX: {type: Number, 'default': 1}, // e.g. once every X weeks
|
||||
startDate: {type: Date, 'default': moment().startOf('day').toDate()},
|
||||
history: Array,
|
||||
completed: {type: Boolean, 'default': false},
|
||||
repeat: { // used only for 'weekly' frequency,
|
||||
m: {type: Boolean, 'default': true},
|
||||
t: {type: Boolean, 'default': true},
|
||||
w: {type: Boolean, 'default': true},
|
||||
th: {type: Boolean, 'default': true},
|
||||
f: {type: Boolean, 'default': true},
|
||||
s: {type: Boolean, 'default': true},
|
||||
su: {type: Boolean, 'default': true}
|
||||
// habits and dailies shared fields
|
||||
let habitDailySchema = () => {
|
||||
return {history: Array}; // [{date:Date, value:Number}], // this causes major performance problems TODO revisit
|
||||
};
|
||||
|
||||
// dailys and todos shared fields
|
||||
let dailyTodoSchema = () => {
|
||||
return {
|
||||
completed: {type: Boolean, default: false},
|
||||
// Checklist fields (dailies and todos)
|
||||
collapseChecklist: {type: Boolean, default: false},
|
||||
checklist: [{
|
||||
completed: {type: Boolean, default: false},
|
||||
text: {type: String, required: true},
|
||||
_id: {type: String, default: shared.uuid, validate: [validator.isUUID, 'Invalid uuid.']},
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
export let HabitSchema = new Schema(_.defaults({
|
||||
up: {type: Boolean, default: true},
|
||||
down: {type: Boolean, default: true},
|
||||
}, habitDailySchema()), subDiscriminatorOptions);
|
||||
export let habit = Task.discriminator('habit', HabitSchema);
|
||||
|
||||
export let DailySchema = new Schema(_.defaults({
|
||||
frequency: {type: String, default: 'weekly', enum: ['daily', 'weekly']},
|
||||
everyX: {type: Number, default: 1}, // e.g. once every X weeks
|
||||
startDate: {
|
||||
type: Date,
|
||||
default () {
|
||||
return moment().startOf('day').toDate();
|
||||
},
|
||||
collapseChecklist:collapseChecklist,
|
||||
checklist:checklist,
|
||||
streak: {type: Number, 'default': 0}
|
||||
}, TaskSchema)
|
||||
, { _id: false, minimize:false }
|
||||
)
|
||||
},
|
||||
repeat: { // used only for 'weekly' frequency,
|
||||
m: {type: Boolean, default: true},
|
||||
t: {type: Boolean, default: true},
|
||||
w: {type: Boolean, default: true},
|
||||
th: {type: Boolean, default: true},
|
||||
f: {type: Boolean, default: true},
|
||||
s: {type: Boolean, default: true},
|
||||
su: {type: Boolean, default: true},
|
||||
},
|
||||
streak: {type: Number, default: 0},
|
||||
}, habitDailySchema(), dailyTodoSchema()), subDiscriminatorOptions);
|
||||
export let daily = Task.discriminator('daily', DailySchema);
|
||||
|
||||
var TodoSchema = new Schema(
|
||||
_.defaults({
|
||||
type: {type:String, 'default': 'todo'},
|
||||
completed: {type: Boolean, 'default': false},
|
||||
dateCompleted: Date,
|
||||
date: String, // due date for todos // FIXME we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to type: Date
|
||||
collapseChecklist:collapseChecklist,
|
||||
checklist:checklist
|
||||
}, TaskSchema)
|
||||
, { _id: false, minimize:false }
|
||||
);
|
||||
export let TodoSchema = new Schema(_.defaults({
|
||||
dateCompleted: Date,
|
||||
// FIXME we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to type: Date
|
||||
// TODO change field name
|
||||
date: String, // due date for todos
|
||||
}, dailyTodoSchema()), subDiscriminatorOptions);
|
||||
export let todo = Task.discriminator('todo', TodoSchema);
|
||||
|
||||
var RewardSchema = new Schema(
|
||||
_.defaults({
|
||||
type: {type:String, 'default': 'reward'}
|
||||
}, TaskSchema)
|
||||
, { _id: false, minimize:false }
|
||||
);
|
||||
|
||||
/**
|
||||
* Workaround for bug when _id & id were out of sync, we can remove this after challenges has been running for a while
|
||||
*/
|
||||
//_.each([HabitSchema, DailySchema, TodoSchema, RewardSchema], function(schema){
|
||||
// schema.post('init', function(doc){
|
||||
// if (!doc.id && doc._id) doc.id = doc._id;
|
||||
// })
|
||||
//})
|
||||
|
||||
module.exports.TaskSchema = TaskSchema;
|
||||
module.exports.HabitSchema = HabitSchema;
|
||||
module.exports.DailySchema = DailySchema;
|
||||
module.exports.TodoSchema = TodoSchema;
|
||||
module.exports.RewardSchema = RewardSchema;
|
||||
export let RewardSchema = new Schema({}, subDiscriminatorOptions);
|
||||
export let reward = Task.discriminator('reward', RewardSchema);
|
||||
|
||||
+72
-98
@@ -1,10 +1,11 @@
|
||||
// User schema and model
|
||||
import mongoose from 'mongoose';
|
||||
import shared from '../../../common';
|
||||
import _ from 'lodash';
|
||||
import validator from 'validator';
|
||||
import moment from 'moment';
|
||||
import TaskSchemas from './task';
|
||||
import * as Tasks from './task';
|
||||
import Q from 'q';
|
||||
import { schema as TagSchema } from './tag';
|
||||
import baseModel from '../libs/api-v3/baseModel';
|
||||
// import {model as Challenge} from './challenge';
|
||||
|
||||
@@ -41,11 +42,9 @@ export let schema = new Schema({
|
||||
loggedin: {type: Date, default: Date.now},
|
||||
},
|
||||
},
|
||||
|
||||
// We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which
|
||||
// have been updated (http://goo.gl/gQLz41), but we want *every* update
|
||||
_v: { type: Number, default: 0 },
|
||||
|
||||
achievements: {
|
||||
originalUser: Boolean,
|
||||
habitSurveys: Number,
|
||||
@@ -440,13 +439,7 @@ export let schema = new Schema({
|
||||
},
|
||||
},
|
||||
|
||||
tags: {type: [{
|
||||
_id: false,
|
||||
id: {type: String, default: shared.uuid},
|
||||
name: String,
|
||||
challenge: String,
|
||||
}]},
|
||||
|
||||
tags: [TagSchema],
|
||||
challenges: [{type: String, ref: 'Challenge'}],
|
||||
|
||||
inbox: {
|
||||
@@ -455,14 +448,14 @@ export let schema = new Schema({
|
||||
messages: {type: Schema.Types.Mixed, default: {}},
|
||||
optOut: {type: Boolean, default: false},
|
||||
},
|
||||
|
||||
habits: {type: [TaskSchemas.HabitSchema]},
|
||||
dailys: {type: [TaskSchemas.DailySchema]},
|
||||
todos: {type: [TaskSchemas.TodoSchema]},
|
||||
rewards: {type: [TaskSchemas.RewardSchema]},
|
||||
|
||||
tasksOrder: {
|
||||
habits: [{type: String, ref: 'Task'}],
|
||||
dailys: [{type: String, ref: 'Task'}],
|
||||
todos: [{type: String, ref: 'Task'}],
|
||||
completedTodos: [{type: String, ref: 'Task'}],
|
||||
rewards: [{type: String, ref: 'Task'}],
|
||||
},
|
||||
extra: Schema.Types.Mixed,
|
||||
|
||||
pushDevices: {
|
||||
type: [{
|
||||
regId: {type: String},
|
||||
@@ -476,11 +469,9 @@ export let schema = new Schema({
|
||||
});
|
||||
|
||||
schema.plugin(baseModel, {
|
||||
noSet: ['_id', 'apikey', 'auth.blocked', 'auth.timestamps', 'lastCron', 'auth.local.hashed_password', 'auth.local.salt'],
|
||||
noSet: ['_id', 'apikey', 'auth.blocked', 'auth.timestamps', 'lastCron', 'auth.local.hashed_password', 'auth.local.salt', 'tasksOrder', 'tags'],
|
||||
private: ['auth.local.hashed_password', 'auth.local.salt'],
|
||||
toJSONTransform: function toJSON (doc) {
|
||||
doc.id = doc._id;
|
||||
|
||||
// FIXME? Is this a reference to `doc.filters` or just disabled code? Remove?
|
||||
doc.filters = {};
|
||||
doc._tmp = this._tmp; // be sure to send down drop notifs
|
||||
@@ -489,101 +480,80 @@ schema.plugin(baseModel, {
|
||||
},
|
||||
});
|
||||
|
||||
schema.methods.deleteTask = function deleteTask (tid) {
|
||||
this.ops.deleteTask({params: {id: tid}}, () => {}); // TODO remove this whole method, since it just proxies, and change all references to this method
|
||||
};
|
||||
|
||||
// schema.virtual('tasks').get(function () {
|
||||
// var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
|
||||
// var tasks = _.object(_.pluck(tasks,'id'), tasks);
|
||||
// return tasks;
|
||||
// });
|
||||
|
||||
schema.post('init', function postInitUser (doc) {
|
||||
shared.wrap(doc);
|
||||
});
|
||||
|
||||
function _populateDefaultTasks (user, taskTypes) {
|
||||
let tagsI = taskTypes.indexOf('tag');
|
||||
|
||||
if (tagsI !== -1) {
|
||||
user.tags = _.map(shared.content.userDefaults.tags, (tag) => {
|
||||
let newTag = _.cloneDeep(tag);
|
||||
|
||||
// tasks automatically get _id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
|
||||
newTag.id = shared.uuid();
|
||||
// Render tag's name in user's language
|
||||
newTag.name = newTag.name(user.preferences.language);
|
||||
return newTag;
|
||||
});
|
||||
}
|
||||
|
||||
let tasksToCreate = [];
|
||||
|
||||
if (tagsI !== -1) {
|
||||
taskTypes = _.clone(taskTypes);
|
||||
taskTypes.splice(tagsI, 1);
|
||||
}
|
||||
|
||||
_.each(taskTypes, (taskType) => {
|
||||
user[taskType] = _.map(shared.content.userDefaults[taskType], (task) => {
|
||||
let newTask = _.cloneDeep(task);
|
||||
let tasksOfType = _.map(shared.content.userDefaults[`${taskType}s`], (taskDefaults) => {
|
||||
let newTask = new Tasks[taskType](taskDefaults);
|
||||
|
||||
// Render task's text and notes in user's language
|
||||
if (taskType === 'tags') {
|
||||
// tasks automatically get id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
|
||||
newTask.id = shared.uuid();
|
||||
newTask.name = newTask.name(user.preferences.language);
|
||||
} else {
|
||||
newTask.text = newTask.text(user.preferences.language);
|
||||
if (newTask.notes) {
|
||||
newTask.notes = newTask.notes(user.preferences.language);
|
||||
}
|
||||
|
||||
if (newTask.checklist) {
|
||||
newTask.checklist = _.map(newTask.checklist, (checklistItem) => {
|
||||
checklistItem.text = checklistItem.text(user.preferences.language);
|
||||
return checklistItem;
|
||||
});
|
||||
}
|
||||
newTask.userId = user._id;
|
||||
newTask.text = taskDefaults.text(user.preferences.language);
|
||||
if (newTask.notes) newTask.notes = taskDefaults.notes(user.preferences.language);
|
||||
if (taskDefaults.checklist) {
|
||||
newTask.checklist = _.map(taskDefaults.checklist, (checklistItem) => {
|
||||
checklistItem.text = checklistItem.text(user.preferences.language);
|
||||
return checklistItem;
|
||||
});
|
||||
}
|
||||
|
||||
return newTask;
|
||||
return newTask.save();
|
||||
});
|
||||
|
||||
tasksToCreate.push(...tasksOfType); // TODO find better way since this creates each task individually
|
||||
});
|
||||
|
||||
return Q.all(tasksToCreate)
|
||||
.then((tasksCreated) => {
|
||||
_.each(tasksCreated, (task) => {
|
||||
user.tasksOrder[`${task.type}s`].push(task._id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _populateDefaultsForNewUser (user) {
|
||||
let taskTypes;
|
||||
let iterableFlags = user.flags.toObject();
|
||||
|
||||
if (user.registeredThrough === 'habitica-web') {
|
||||
taskTypes = ['habits', 'dailys', 'todos', 'rewards', 'tags'];
|
||||
taskTypes = ['habit', 'daily', 'todo', 'reward', 'tag'];
|
||||
|
||||
let tutorialCommonSections = [
|
||||
'habits',
|
||||
'dailies',
|
||||
'todos',
|
||||
'rewards',
|
||||
'party',
|
||||
'pets',
|
||||
'gems',
|
||||
'skills',
|
||||
'classes',
|
||||
'tavern',
|
||||
'equipment',
|
||||
'items',
|
||||
'inviteParty',
|
||||
];
|
||||
|
||||
_.each(tutorialCommonSections, (section) => {
|
||||
_.each(iterableFlags.tutorial.common, (val, section) => {
|
||||
user.flags.tutorial.common[section] = true;
|
||||
});
|
||||
} else {
|
||||
taskTypes = ['todos', 'tags'];
|
||||
|
||||
taskTypes = ['todo', 'tag'];
|
||||
user.flags.showTour = false;
|
||||
|
||||
let tourSections = [
|
||||
'showTour',
|
||||
'intro',
|
||||
'classes',
|
||||
'stats',
|
||||
'tavern',
|
||||
'party',
|
||||
'guilds',
|
||||
'challenges',
|
||||
'market',
|
||||
'pets',
|
||||
'mounts',
|
||||
'hall',
|
||||
'equipment',
|
||||
];
|
||||
|
||||
_.each(tourSections, (section) => {
|
||||
_.each(iterableFlags.tour, (val, section) => {
|
||||
user.flags.tour[section] = -2;
|
||||
});
|
||||
}
|
||||
|
||||
_populateDefaultTasks(user, taskTypes);
|
||||
return _populateDefaultTasks(user, taskTypes);
|
||||
}
|
||||
|
||||
function _setProfileName (user) {
|
||||
@@ -596,13 +566,10 @@ function _setProfileName (user) {
|
||||
return localUsername || facebookUsername || anonymous;
|
||||
}
|
||||
|
||||
schema.pre('save', function postSaveUser (next) {
|
||||
// Populate new users with default content
|
||||
if (this.isNew) {
|
||||
_populateDefaultsForNewUser(this);
|
||||
}
|
||||
schema.pre('save', true, function preSaveUser (next, done) {
|
||||
next();
|
||||
|
||||
// this.markModified('tasks');
|
||||
// TODO remove all unnecessary checks
|
||||
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
|
||||
this.preferences.dayStart = 0;
|
||||
}
|
||||
@@ -651,7 +618,14 @@ schema.pre('save', function postSaveUser (next) {
|
||||
if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0;
|
||||
this._v++;
|
||||
|
||||
next();
|
||||
// Populate new users with default content
|
||||
if (this.isNew) {
|
||||
_populateDefaultsForNewUser(this)
|
||||
.then(() => done())
|
||||
.catch(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
schema.methods.unlink = function unlink (options, cb) {
|
||||
@@ -668,7 +642,7 @@ schema.methods.unlink = function unlink (options, cb) {
|
||||
if (keep === 'keep') {
|
||||
self.tasks[tid].challenge = {};
|
||||
} else if (keep === 'remove') {
|
||||
self.deleteTask(tid);
|
||||
self.ops.deleteTask({params: {id: tid}}, () => {});
|
||||
} else if (keep === 'keep-all') {
|
||||
_.each(self.tasks, (t) => {
|
||||
if (t.challenge && t.challenge.id === cid) {
|
||||
@@ -678,7 +652,7 @@ schema.methods.unlink = function unlink (options, cb) {
|
||||
} else if (keep === 'remove-all') {
|
||||
_.each(self.tasks, (t) => {
|
||||
if (t.challenge && t.challenge.id === cid) {
|
||||
self.deleteTask(t.id);
|
||||
this.ops.deleteTask({params: {id: tid}}, () => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user