port updateTask, addTask, clearCompleted, taskDefaults, uuid

This commit is contained in:
Matteo Pagliazzi
2016-04-03 21:50:32 +02:00
parent 060e3b1045
commit 382e391fd0
14 changed files with 322 additions and 99 deletions

View File

@@ -121,6 +121,8 @@ import openMysteryItem from './ops/openMysteryItem';
import releasePets from './ops/releasePets'; import releasePets from './ops/releasePets';
import releaseBoth from './ops/releaseBoth'; import releaseBoth from './ops/releaseBoth';
import releaseMounts from './ops/releaseMounts'; import releaseMounts from './ops/releaseMounts';
import updateTask from './ops/updateTask';
import clearCompleted from './ops/clearCompleted';
api.ops = { api.ops = {
scoreTask, scoreTask,
@@ -143,6 +145,8 @@ api.ops = {
releasePets, releasePets,
releaseBoth, releaseBoth,
releaseMounts, releaseMounts,
updateTask,
clearCompleted,
}; };
import handleTwoHanded from './fns/handleTwoHanded'; import handleTwoHanded from './fns/handleTwoHanded';

View File

@@ -1,71 +1,74 @@
import uuid from './uuid'; import { v4 as uuid } from 'uuid';
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment';
/* // Even though Mongoose handles task defaults, we want to make sure defaults are set on the client-side before
Even though Mongoose handles task defaults, we want to make sure defaults are set on the client-side before // sending up to the server for performance
sending up to the server for performance
*/
// TODO revisit // TODO move to client code?
module.exports = function(task) { const tasksTypes = ['habit', 'daily', 'todo', 'reward'];
var defaults, ref, ref1, ref2;
if (task == null) { module.exports = function taskDefaults (task = {}) {
task = {}; if (!task.type || tasksTypes.indexOf(task.type) === -1) {
}
if (!(task.type && ((ref = task.type) === 'habit' || ref === 'daily' || ref === 'todo' || ref === 'reward'))) {
task.type = 'habit'; task.type = 'habit';
} }
defaults = {
id: uuid(), let defaultId = uuid();
text: task.id != null ? task.id : '', let defaults = {
_id: defaultId, // TODO convert all occurencies of id to _id
text: task._id || defaultId,
notes: '', notes: '',
tags: [],
value: task.type === 'reward' ? 10 : 0,
priority: 1, priority: 1,
challenge: {}, challenge: {},
reminders: {},
attribute: 'str', attribute: 'str',
dateCreated: new Date() createdAt: new Date(), // TODO these are going to be overwritten by the server...
updatedAt: new Date(),
}; };
_.defaults(task, defaults); _.defaults(task, defaults);
if (task.type === 'habit' || task.type === 'daily') {
_.defaults(task, {
history: [],
});
}
if (task.type === 'todo' || task.type === 'daily') {
_.defaults(task, {
completed: false,
collapseChecklist: false,
checklist: [],
});
}
if (task.type === 'habit') { if (task.type === 'habit') {
_.defaults(task, { _.defaults(task, {
up: true, up: true,
down: true down: true,
});
}
if ((ref1 = task.type) === 'habit' || ref1 === 'daily') {
_.defaults(task, {
history: []
});
}
if ((ref2 = task.type) === 'daily' || ref2 === 'todo') {
_.defaults(task, {
completed: false
}); });
} }
if (task.type === 'daily') { if (task.type === 'daily') {
_.defaults(task, { _.defaults(task, {
streak: 0, streak: 0,
repeat: { repeat: {
su: true,
m: true, m: true,
t: true, t: true,
w: true, w: true,
th: true, th: true,
f: true, f: true,
s: true s: true,
} su: true,
}, { },
startDate: new Date(), startDate: moment().startOf('day').toDate(),
everyX: 1, everyX: 1,
frequency: 'weekly' frequency: 'weekly',
}); });
} }
task._id = task.id;
if (task.value == null) {
task.value = task.type === 'reward' ? 10 : 0;
}
if (!_.isNumber(task.priority)) {
task.priority = 1;
}
return task; return task;
}; };

View File

@@ -1,9 +1,4 @@
// TODO use node-uuid module import uuid from 'uuid';
module.exports = function() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { // TODO remove this file completely
var r, v; module.exports = uuid.v4;
r = Math.random() * 16 | 0;
v = (c === "x" ? r : r & 0x3 | 0x8);
return v.toString(16);
});
};

View File

@@ -1,27 +1,22 @@
import taskDefaults from '../libs/taskDefaults'; import taskDefaults from '../libs/taskDefaults';
import i18n from '../i18n';
module.exports = function(user, req, cb) { // TODO move to client since it's only used there?
var task;
task = taskDefaults(req.body); module.exports = function addTask (user, req = {body: {}}) {
if (user.tasks[task.id] != null) { let task = taskDefaults(req.body);
return typeof cb === "function" ? cb({ user.tasksOrder[`${task.type}s`].unshift(task._id);
code: 409,
message: i18n.t('messageDuplicateTaskID', req.language)
}) : void 0;
}
user[task.type + "s"].unshift(task);
if (user.preferences.newTaskEdit) { if (user.preferences.newTaskEdit) {
task._editing = true; task._editing = true;
} }
if (user.preferences.tagsCollapsed) { if (user.preferences.tagsCollapsed) {
task._tags = true; task._tags = true;
} }
if (!user.preferences.advancedCollapsed) { if (!user.preferences.advancedCollapsed) {
task._advanced = true; task._advanced = true;
} }
if (typeof cb === "function") {
cb(null, task);
}
return task; return task;
}; };

View File

@@ -1,12 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
module.exports = function(user, req, cb) { // TODO move to client since it's only used there?
_.remove(user.todos, function(t) { // TODO rename file to clearCompletedTodos
var ref;
return t.completed && !((ref = t.challenge) != null ? ref.id : void 0); module.exports = function clearCompletedTodos (todos) {
_.remove(todos, todo => {
return todo.completed && (!todo.challenge || !todo.challenge.id || todo.challenge.broken);
}); });
if (typeof user.markModified === "function") {
user.markModified('todos');
}
return typeof cb === "function" ? cb(null, user.todos) : void 0;
}; };

View File

@@ -1,23 +1,26 @@
import i18n from '../i18n';
import _ from 'lodash'; import _ from 'lodash';
module.exports = function(user, req, cb) { // From server pass task.toObject() not the task document directly
var ref, task; module.exports = function updateTask (task, req = {}) {
if (!(task = user.tasks[(ref = req.params) != null ? ref.id : void 0])) { // If reminders are updated -> replace the original ones
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTaskNotFound', req.language)
}) : void 0;
}
_.merge(task, _.omit(req.body, ['checklist', 'reminders', 'id', 'type']));
if (req.body.checklist) {
task.checklist = req.body.checklist;
}
if (req.body.reminders) { if (req.body.reminders) {
task.reminders = req.body.reminders; task.reminders = req.body.reminders;
delete req.body.reminders;
} }
if (typeof task.markModified === "function") {
task.markModified('tags'); // If checklist is updated -> replace the original one
if (req.body.checklist) {
task.checklist = req.body.checklist;
delete req.body.checklist;
} }
return typeof cb === "function" ? cb(null, task) : void 0;
// If tags are updated -> replace the original ones
if (req.body.tags) {
task.tags = req.body.tags;
delete req.body.tags;
}
_.merge(task, _.omit(req.body, ['_id', 'id', 'type']));
return task;
}; };

View File

@@ -90,7 +90,8 @@
"validator": "~4.2.1", "validator": "~4.2.1",
"vinyl-buffer": "^1.0.0", "vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0", "vinyl-source-stream": "^1.1.0",
"winston": "^2.1.0" "winston": "^2.1.0",
"uuid": "^2.0.1"
}, },
"private": true, "private": true,
"engines": { "engines": {
@@ -153,7 +154,6 @@
"sinon": "^1.17.2", "sinon": "^1.17.2",
"sinon-chai": "^2.8.0", "sinon-chai": "^2.8.0",
"superagent-defaults": "^0.1.13", "superagent-defaults": "^0.1.13",
"uuid": "^2.0.1",
"vinyl-source-stream": "^1.0.0", "vinyl-source-stream": "^1.0.0",
"vinyl-transform": "^1.0.0", "vinyl-transform": "^1.0.0",
"xml2js": "^0.4.16" "xml2js": "^0.4.16"

View File

@@ -12,10 +12,8 @@ const COMMON_FILES = [
'!./common/script/content/index.js', '!./common/script/content/index.js',
'!./common/script/ops/addPushDevice.js', '!./common/script/ops/addPushDevice.js',
'!./common/script/ops/addTag.js', '!./common/script/ops/addTag.js',
'!./common/script/ops/addTask.js',
'!./common/script/ops/addWebhook.js', '!./common/script/ops/addWebhook.js',
'!./common/script/ops/blockUser.js', '!./common/script/ops/blockUser.js',
'!./common/script/ops/clearCompleted.js',
'!./common/script/ops/clearPMs.js', '!./common/script/ops/clearPMs.js',
'!./common/script/ops/deletePM.js', '!./common/script/ops/deletePM.js',
'!./common/script/ops/deleteTag.js', '!./common/script/ops/deleteTag.js',
@@ -36,7 +34,6 @@ const COMMON_FILES = [
'!./common/script/ops/unlock.js', '!./common/script/ops/unlock.js',
'!./common/script/ops/update.js', '!./common/script/ops/update.js',
'!./common/script/ops/updateTag.js', '!./common/script/ops/updateTag.js',
'!./common/script/ops/updateTask.js',
'!./common/script/ops/updateWebhook.js', '!./common/script/ops/updateWebhook.js',
'!./common/script/fns/crit.js', '!./common/script/fns/crit.js',
'!./common/script/fns/cron.js', '!./common/script/fns/cron.js',
@@ -63,8 +60,6 @@ const COMMON_FILES = [
'!./common/script/libs/silver.js', '!./common/script/libs/silver.js',
'!./common/script/libs/splitWhitespace.js', '!./common/script/libs/splitWhitespace.js',
'!./common/script/libs/taskClasses.js', '!./common/script/libs/taskClasses.js',
'!./common/script/libs/taskDefaults.js',
'!./common/script/libs/uuid.js',
'!./common/script/public/**/*.js', '!./common/script/public/**/*.js',
]; ];
const TEST_FILES = [ const TEST_FILES = [

135
test/common/ops/addTask.js Normal file
View File

@@ -0,0 +1,135 @@
import addTask from '../../../common/script/ops/addTask';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shared.ops.addTask', () => {
let user;
beforeEach(() => {
user = generateUser();
});
it('adds an habit', () => {
let habit = addTask(user, {
body: {
type: 'habit',
text: 'habit',
down: false,
},
});
expect(user.tasksOrder.habits).to.eql([
habit._id,
]);
expect(habit._id).to.be.a('string');
expect(habit.text).to.equal('habit');
expect(habit.type).to.equal('habit');
expect(habit.up).to.equal(true);
expect(habit.down).to.equal(false);
expect(habit.history).to.eql([]);
expect(habit.checklist).to.not.exists;
});
it('adds an habtit when type is invalid', () => {
let habit = addTask(user, {
body: {
type: 'invalid',
text: 'habit',
down: false,
},
});
expect(user.tasksOrder.habits).to.eql([
habit._id,
]);
expect(habit._id).to.be.a('string');
expect(habit.text).to.equal('habit');
expect(habit.type).to.equal('habit');
expect(habit.up).to.equal(true);
expect(habit.down).to.equal(false);
expect(habit.history).to.eql([]);
expect(habit.checklist).to.not.exists;
});
it('adds a daily', () => {
let daily = addTask(user, {
body: {
type: 'daily',
text: 'daily',
},
});
expect(user.tasksOrder.dailys).to.eql([
daily._id,
]);
expect(daily._id).to.be.a('string');
expect(daily.type).to.equal('daily');
expect(daily.text).to.equal('daily');
expect(daily.history).to.eql([]);
expect(daily.checklist).to.eql([]);
expect(daily.completed).to.be.false;
expect(daily.up).to.not.exists;
});
it('adds a todo', () => {
let todo = addTask(user, {
body: {
type: 'todo',
text: 'todo',
},
});
expect(user.tasksOrder.todos).to.eql([
todo._id,
]);
expect(todo._id).to.be.a('string');
expect(todo.type).to.equal('todo');
expect(todo.text).to.equal('todo');
expect(todo.checklist).to.eql([]);
expect(todo.completed).to.be.false;
expect(todo.up).to.not.exists;
});
it('adds a reward', () => {
let reward = addTask(user, {
body: {
type: 'reward',
text: 'reward',
},
});
expect(user.tasksOrder.rewards).to.eql([
reward._id,
]);
expect(reward._id).to.be.a('string');
expect(reward.type).to.equal('reward');
expect(reward.text).to.equal('reward');
expect(reward.value).to.equal(10);
expect(reward.up).to.not.exists;
});
context('respects preferences', () => {
it('true', () => {
user.preferences.newTaskEdit = true;
user.preferences.tagsCollapsed = true;
user.preferences.advancedCollapsed = false;
let task = addTask(user);
expect(task._editing).to.be.true;
expect(task._tags).to.be.true;
expect(task._advanced).to.be.true;
});
it('false', () => {
user.preferences.newTaskEdit = false;
user.preferences.tagsCollapsed = false;
user.preferences.advancedCollapsed = true;
let task = addTask(user);
expect(task._editing).to.not.exists;
expect(task._tags).to.not.exists;
expect(task._advanced).to.not.exists;
});
});
});

View File

@@ -0,0 +1,37 @@
import clearCompleted from '../../../common/script/ops/clearCompleted';
import {
generateTodo,
} from '../../helpers/common.helper';
describe('shared.ops.clearCompleted', () => {
it('clear completed todos', () => {
let todos = [
generateTodo({text: 'todo'}),
generateTodo({
text: 'done',
completed: true,
}),
generateTodo({
text: 'done chellenge broken',
completed: true,
challenge: {
id: 123,
broken: 'TASK_DELETED',
},
}),
generateTodo({
text: 'done chellenge not broken',
completed: true,
challenge: {
id: 123,
},
}),
];
clearCompleted(todos);
expect(todos.length).to.equal(2);
expect(todos[0].text).to.equal('todo');
expect(todos[1].text).to.equal('done chellenge not broken');
});
});

View File

@@ -0,0 +1,53 @@
import updateTask from '../../../common/script/ops/updateTask';
import {
generateHabit,
} from '../../helpers/common.helper';
describe('shared.ops.updateTask', () => {
it('updates a task', () => {
let now = new Date();
let habit = generateHabit({
tags: [
'123',
'456',
],
reminders: [{
_id: '123',
startDate: now,
time: now,
}],
});
let res = updateTask(habit, {
body: {
text: 'updated',
id: '123',
_id: '123',
type: 'todo',
tags: ['678'],
checklist: [{
completed: false,
text: 'item',
_id: '123',
}],
},
});
expect(res.id).to.not.equal('123');
expect(res._id).to.not.equal('123');
expect(res.type).to.equal('habit');
expect(res.text).to.equal('updated');
expect(res.checklist).to.eql([{
completed: false,
text: 'item',
_id: '123',
}]);
expect(res.reminders).to.eql([{
_id: '123',
startDate: now,
time: now,
}]);
expect(res.tags).to.eql(['678']);
});
});

View File

@@ -324,7 +324,7 @@ api.updateTask = {
// TODO we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances? // TODO we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances?
// TODO regarding comment above, make sure other models with nested fields are using this trick too // TODO regarding comment above, make sure other models with nested fields are using this trick too
_.assign(task, _.merge(task.toObject(), Tasks.Task.sanitizeUpdate(req.body))); _.assign(task, common.ops.updateTask(task.toObject(), req));
// TODO console.log(task.modifiedPaths(), task.toObject().repeat === tep) // TODO console.log(task.modifiedPaths(), task.toObject().repeat === tep)
// repeat is always among modifiedPaths because mongoose changes the other of the keys when using .toObject() // repeat is always among modifiedPaths because mongoose changes the other of the keys when using .toObject()
// see https://github.com/Automattic/mongoose/issues/2749 // see https://github.com/Automattic/mongoose/issues/2749
@@ -836,12 +836,15 @@ api.clearCompletedTodos = {
let user = res.locals.user; let user = res.locals.user;
// Clear completed todos // Clear completed todos
// Do not delete challenges completed todos TODO unless the task is broken? // Do not delete challenges completed todos unless the task is broken
await Tasks.Task.remove({ await Tasks.Task.remove({
userId: user._id, userId: user._id,
type: 'todo', type: 'todo',
completed: true, completed: true,
'challenge.id': {$exists: false}, $or: [
{'challenge.id': {$exists: false}},
{'challenge.broken': {$exists: true}},
],
}).exec(); }).exec();
res.respond(200, {}); res.respond(200, {});

View File

@@ -1,4 +1,4 @@
import { uuid } from '../../../../common'; import { v4 as uuid } from 'uuid';
import validator from 'validator'; import validator from 'validator';
import objectPath from 'object-path'; // TODO use lodash's unset once v4 is out import objectPath from 'object-path'; // TODO use lodash's unset once v4 is out
import _ from 'lodash'; import _ from 'lodash';

View File

@@ -14,6 +14,8 @@ let subDiscriminatorOptions = _.defaults(_.cloneDeep(discriminatorOptions), {_id
export let tasksTypes = ['habit', 'daily', 'todo', 'reward']; export let tasksTypes = ['habit', 'daily', 'todo', 'reward'];
// Important
// When something changes here remember to update the client side model at common/script/libs/taskDefaults
export let TaskSchema = new Schema({ export let TaskSchema = new Schema({
type: {type: String, enum: tasksTypes, required: true, default: tasksTypes[0]}, type: {type: String, enum: tasksTypes, required: true, default: tasksTypes[0]},
text: {type: String, required: true}, text: {type: String, required: true},
@@ -35,7 +37,7 @@ export let TaskSchema = new Schema({
}, },
reminders: [{ reminders: [{
id: {type: String, validate: [validator.isUUID, 'Invalid uuid.'], default: shared.uuid, required: true}, _id: {type: String, validate: [validator.isUUID, 'Invalid uuid.'], default: shared.uuid, required: true},
startDate: {type: Date, required: true}, startDate: {type: Date, required: true},
time: {type: Date, required: true}, time: {type: Date, required: true},
}], }],