2017-06-28 04:22:20 +00:00
import moment from 'moment' ;
2016-09-03 08:54:55 +00:00
import * as Tasks from '../models/task' ;
import {
BadRequest ,
} from './errors' ;
import Bluebird from 'bluebird' ;
import _ from 'lodash' ;
2017-05-10 13:40:45 +00:00
import shared from '../../common' ;
2016-09-03 08:54:55 +00:00
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 ( ) ;
} ) ;
}
2017-06-28 04:22:20 +00:00
export function setNextDue ( task , user , dueDateOption ) {
2017-05-25 00:49:33 +00:00
if ( task . type !== 'daily' ) return ;
2017-07-08 01:46:54 +00:00
let now = moment ( ) . toDate ( ) ;
2017-06-28 04:22:20 +00:00
let dateTaskIsDue = Date . now ( ) ;
2017-07-08 01:46:54 +00:00
if ( dueDateOption ) {
2017-07-10 20:07:14 +00:00
// @TODO Add required ISO format
2017-07-13 22:11:27 +00:00
dateTaskIsDue = moment ( dueDateOption ) ;
2017-07-10 20:07:14 +00:00
2017-07-08 01:46:54 +00:00
// If not time is supplied. Let's assume we want start of Custom Day Start day.
if ( dateTaskIsDue . hour ( ) === 0 && dateTaskIsDue . minute ( ) === 0 && dateTaskIsDue . second ( ) === 0 && dateTaskIsDue . millisecond ( ) === 0 ) {
2017-07-13 22:11:27 +00:00
dateTaskIsDue . add ( user . preferences . timezoneOffset , 'minutes' ) ;
2017-07-08 01:46:54 +00:00
dateTaskIsDue . add ( user . preferences . dayStart , 'hours' ) ;
}
2017-07-13 22:11:27 +00:00
2017-07-08 01:46:54 +00:00
now = dateTaskIsDue ;
}
2017-06-28 21:25:47 +00:00
2017-05-25 00:49:33 +00:00
let optionsForShouldDo = user . preferences . toObject ( ) ;
2017-07-08 01:46:54 +00:00
optionsForShouldDo . now = now ;
2017-06-28 04:22:20 +00:00
task . isDue = shared . shouldDo ( dateTaskIsDue , task , optionsForShouldDo ) ;
2018-01-12 16:16:51 +00:00
2017-05-25 00:49:33 +00:00
optionsForShouldDo . nextDue = true ;
2017-06-28 04:22:20 +00:00
let nextDue = shared . shouldDo ( dateTaskIsDue , task , optionsForShouldDo ) ;
2017-05-25 00:49:33 +00:00
if ( nextDue && nextDue . length > 0 ) {
task . nextDue = nextDue . map ( ( dueDate ) => {
return dueDate . toISOString ( ) ;
} ) ;
}
}
2016-09-03 08:54:55 +00:00
/ * *
* 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
2016-10-25 12:56:43 +00:00
* @ param options . requiresApproval A boolean stating if the task will require approval
2016-09-03 08:54:55 +00:00
* @ 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 ] ;
2017-11-07 20:19:39 +00:00
let taskOrderToAdd = { } ;
2016-09-03 08:54:55 +00:00
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 ) ) ;
2017-12-11 17:48:50 +00:00
// Attempt to round priority
if ( newTask . priority && ! Number . isNaN ( Number . parseFloat ( newTask . priority ) ) ) {
newTask . priority = Number ( newTask . priority . toFixed ( 1 ) ) ;
}
2016-09-03 08:54:55 +00:00
if ( challenge ) {
newTask . challenge . id = challenge . id ;
} else if ( group ) {
newTask . group . id = group . _id ;
2016-10-25 12:56:43 +00:00
if ( taskData . requiresApproval ) {
2016-10-26 21:01:43 +00:00
newTask . group . approval . required = true ;
2016-10-25 12:56:43 +00:00
}
2016-09-03 08:54:55 +00:00
} else {
newTask . userId = user . _id ;
}
2017-05-25 00:49:33 +00:00
setNextDue ( newTask , user ) ;
2017-05-10 13:40:45 +00:00
2016-09-03 08:54:55 +00:00
// 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
2017-11-07 20:19:39 +00:00
if ( ! taskOrderToAdd [ ` ${ taskType } s ` ] ) taskOrderToAdd [ ` ${ taskType } s ` ] = [ ] ;
taskOrderToAdd [ ` ${ taskType } s ` ] . unshift ( newTask . _id ) ;
2016-09-03 08:54:55 +00:00
return newTask ;
} ) ;
2017-11-07 20:19:39 +00:00
// Push all task ids
let taskOrderUpdateQuery = { $push : { } } ;
for ( let taskType in taskOrderToAdd ) {
taskOrderUpdateQuery . $push [ ` tasksOrder. ${ taskType } ` ] = {
$each : taskOrderToAdd [ taskType ] ,
$position : 0 ,
} ;
}
await owner . update ( taskOrderUpdateQuery ) . exec ( ) ;
2016-09-03 08:54:55 +00:00
// 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 ,
2017-06-28 04:22:20 +00:00
dueDate ,
2016-09-03 08:54:55 +00:00
} = options ;
let query = { userId : user . _id } ;
2017-01-04 15:49:43 +00:00
let limit ;
let sort ;
2016-09-03 08:54:55 +00:00
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
2017-01-04 15:49:43 +00:00
limit = 30 ;
2016-09-03 08:54:55 +00:00
if ( type === '_allCompletedTodos' ) {
limit = 0 ; // no limit
}
2017-01-04 15:49:43 +00:00
query = {
2016-09-03 08:54:55 +00:00
userId : user . _id ,
type : 'todo' ,
completed : true ,
2017-01-04 15:49:43 +00:00
} ;
sort = {
2016-09-03 08:54:55 +00:00
dateCompleted : - 1 ,
2017-01-04 15:49:43 +00:00
} ;
2016-09-03 08:54:55 +00:00
} 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' ] } } ,
] ;
}
2017-01-04 15:49:43 +00:00
let mQuery = Tasks . Task . find ( query ) ;
if ( limit ) mQuery . limit ( limit ) ;
if ( sort ) mQuery . sort ( sort ) ;
let tasks = await mQuery . exec ( ) ;
2016-09-03 08:54:55 +00:00
2017-07-08 01:46:54 +00:00
if ( dueDate ) {
tasks . forEach ( ( task ) => {
setNextDue ( task , user , dueDate ) ;
} ) ;
}
2016-09-03 08:54:55 +00:00
// 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
2017-03-17 21:58:55 +00:00
let omitAttrs = [ '_id' , 'userId' , 'challenge' , 'history' , 'tags' , 'completed' , 'streak' , 'notes' , 'updatedAt' , 'createdAt' , 'group' , 'checklist' , 'attribute' ] ;
2016-09-03 08:54:55 +00:00
if ( t . type !== 'reward' ) omitAttrs . push ( 'value' ) ;
return _ . omit ( t , omitAttrs ) ;
}
2017-01-11 18:16:20 +00:00
/ * *
* Moves a task to a specified position .
*
* @ param order The list of ordered tasks
* @ param taskId The Task . _id of the task to move
* @ param to A integer specifiying the index to move the task to
*
* @ return Empty
* /
export function moveTask ( order , taskId , to ) {
let currentIndex = order . indexOf ( taskId ) ;
// If for some reason the task isn't ordered (should never happen), push it in the new position
// if the task is moved to a non existing position
// or if the task is moved to position -1 (push to bottom)
// -> push task at end of list
if ( ! order [ to ] && to !== - 1 ) {
order . push ( taskId ) ;
return ;
}
if ( currentIndex !== - 1 ) order . splice ( currentIndex , 1 ) ;
if ( to === - 1 ) {
order . push ( taskId ) ;
} else {
order . splice ( to , 0 , taskId ) ;
}
}