2015-11-27 18:06:26 +00:00
import mongoose from 'mongoose' ;
import shared from '../../../common' ;
2015-11-28 12:46:53 +00:00
import validator from 'validator' ;
2015-11-27 18:06:26 +00:00
import moment from 'moment' ;
import baseModel from '../libs/api-v3/baseModel' ;
import _ from 'lodash' ;
2016-03-18 19:00:14 +00:00
import { preenHistory } from '../libs/api-v3/preening' ;
2015-02-10 22:03:59 +00:00
2015-11-27 18:06:26 +00:00
let Schema = mongoose . Schema ;
2015-11-27 20:18:37 +00:00
let discriminatorOptions = {
discriminatorKey : 'type' , // the key that distinguishes task types
2015-11-27 18:06:26 +00:00
} ;
2015-11-27 20:18:37 +00:00
let subDiscriminatorOptions = _ . defaults ( _ . cloneDeep ( discriminatorOptions ) , { _id : false } ) ;
2015-02-10 22:03:59 +00:00
2015-11-30 18:38:53 +00:00
export let tasksTypes = [ 'habit' , 'daily' , 'todo' , 'reward' ] ;
2015-02-10 22:03:59 +00:00
2016-04-03 19:50:32 +00:00
// Important
// When something changes here remember to update the client side model at common/script/libs/taskDefaults
2015-11-27 18:06:26 +00:00
export let TaskSchema = new Schema ( {
2015-11-30 18:38:53 +00:00
type : { type : String , enum : tasksTypes , required : true , default : tasksTypes [ 0 ] } ,
2015-11-28 12:46:53 +00:00
text : { type : String , required : true } ,
2015-11-27 18:06:26 +00:00
notes : { type : String , default : '' } ,
2015-12-03 18:19:53 +00:00
tags : [ {
type : String ,
validate : [ validator . isUUID , 'Invalid uuid.' ] ,
} ] ,
2015-12-29 17:35:34 +00:00
value : { type : Number , default : 0 , required : true } , // redness or cost for rewards Required because it must be settable (for rewards)
2015-12-03 18:19:53 +00:00
priority : { type : Number , default : 1 , required : true } , // TODO enum?
2015-11-27 18:06:26 +00:00
attribute : { type : String , default : 'str' , enum : [ 'str' , 'con' , 'int' , 'per' ] } ,
2015-12-28 09:46:34 +00:00
userId : { type : String , ref : 'User' , validate : [ validator . isUUID , 'Invalid uuid.' ] } , // When not set it belongs to a challenge
2015-11-27 18:06:26 +00:00
2015-02-10 22:03:59 +00:00
challenge : {
2015-12-29 17:35:34 +00:00
id : { type : String , ref : 'Challenge' , validate : [ validator . isUUID , 'Invalid uuid.' ] } , // When set (and userId not set) it's the original task
2016-01-05 19:19:55 +00:00
taskId : { type : String , ref : 'Task' , validate : [ validator . isUUID , 'Invalid uuid.' ] } , // When not set but challenge.id defined it's the original task TODO unique index?
2015-12-28 09:46:34 +00:00
broken : { type : String , enum : [ 'CHALLENGE_DELETED' , 'TASK_DELETED' , 'UNSUBSCRIBED' , 'CHALLENGE_CLOSED' ] } ,
2015-11-27 18:06:26 +00:00
winner : String , // user.profile.name TODO necessary?
} ,
2016-03-10 17:30:09 +00:00
2016-02-18 19:30:02 +00:00
reminders : [ {
2016-04-03 19:50:32 +00:00
_id : { type : String , validate : [ validator . isUUID , 'Invalid uuid.' ] , default : shared . uuid , required : true } ,
2016-03-10 17:30:09 +00:00
startDate : { type : Date , required : true } ,
time : { type : Date , required : true } ,
} ] ,
2015-11-27 20:18:37 +00:00
} , _ . defaults ( {
2015-11-27 18:06:26 +00:00
minimize : true , // So empty objects are returned
strict : true ,
2015-11-27 20:18:37 +00:00
} , discriminatorOptions ) ) ;
2015-02-10 22:03:59 +00:00
2015-11-27 18:06:26 +00:00
TaskSchema . plugin ( baseModel , {
2015-12-06 16:07:29 +00:00
// TODO checklist fields editable?
// TODO value should be settable only for rewards
noSet : [ 'challenge' , 'userId' , 'completed' , 'history' , 'streak' , 'dateCompleted' ] ,
2015-11-27 18:06:26 +00:00
private : [ ] ,
timestamps : true ,
} ) ;
2015-02-10 22:03:59 +00:00
2015-12-03 17:15:22 +00:00
// A list of additional fields that cannot be set on creation (but can be set on updare)
2015-12-06 16:29:14 +00:00
let noCreate = [ 'completed' ] ; // TODO completed should be removed for updates too?
2015-12-03 17:15:22 +00:00
TaskSchema . statics . sanitizeCreate = function sanitizeCreate ( createObj ) {
2016-01-25 16:23:01 +00:00
return this . sanitize ( createObj , noCreate ) ;
2015-12-03 17:15:22 +00:00
} ;
2015-12-16 11:29:03 +00:00
// Sanitize checklist objects (disallowing _id)
TaskSchema . statics . sanitizeChecklist = function sanitizeChecklist ( checklistObj ) {
delete checklistObj . _id ;
return checklistObj ;
} ;
2016-03-10 17:30:09 +00:00
// Sanitize reminder objects (disallowing id)
2016-03-11 08:51:59 +00:00
TaskSchema . statics . sanitizeReminder = function sanitizeReminder ( reminderObj ) {
2016-03-10 17:30:09 +00:00
delete reminderObj . id ;
return reminderObj ;
} ;
2016-03-18 19:00:14 +00:00
TaskSchema . methods . scoreChallengeTask = async function scoreChallengeTask ( delta ) {
let chalTask = this ;
chalTask . value += delta ;
if ( chalTask . type === 'habit' || chalTask . type === 'daily' ) {
// Add only one history entry per day
let lastChallengHistoryIndex = chalTask . history . length - 1 ;
if ( chalTask . history [ lastChallengHistoryIndex ] &&
moment ( chalTask . history [ lastChallengHistoryIndex ] . date ) . isSame ( new Date ( ) , 'day' ) ) {
chalTask . history [ lastChallengHistoryIndex ] = {
date : Number ( new Date ( ) ) ,
value : chalTask . value ,
} ;
chalTask . markModified ( ` history. ${ lastChallengHistoryIndex } ` ) ;
} else {
chalTask . history . push ( {
date : Number ( new Date ( ) ) ,
value : chalTask . value ,
} ) ;
// Only preen task history once a day when the task is scored first
if ( chalTask . history . length > 365 ) {
chalTask . history = preenHistory ( chalTask . history , true ) ; // true means the challenge will retain as much entries as a subscribed user
}
}
}
await chalTask . save ( ) ;
} ;
2016-04-03 17:03:11 +00:00
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the user model)
// These will be removed once API v2 is discontinued
// toJSON for API v2
TaskSchema . methods . toJSONV2 = function toJSONV2 ( ) {
let toJSON = this . toJSON ( ) ;
toJSON . id = toJSON . _id ;
2016-04-06 17:28:43 +00:00
let v3Tags = this . tags ;
toJSON . tags = { } ;
v3Tags . forEach ( tag => {
toJSON . tags [ tag ] = true ;
} ) ;
2016-04-03 17:03:11 +00:00
return toJSON ;
} ;
2016-04-10 17:09:53 +00:00
TaskSchema . statics . fromJSONV2 = function toJSONV2 ( taskObj ) {
taskObj . _id = taskObj . id ;
let v2Tags = taskObj . tags || { } ;
taskObj . tags = [ ] ;
2016-04-10 17:18:46 +00:00
taskObj . tags = _ . map ( v2Tags , ( tag , key ) => key ) ;
2016-04-10 17:09:53 +00:00
return taskObj ;
} ;
2016-04-03 17:03:11 +00:00
// END of API v2 methods
2015-11-30 18:38:53 +00:00
export let Task = mongoose . model ( 'Task' , TaskSchema ) ;
2015-02-10 22:03:59 +00:00
2015-11-27 18:06:26 +00:00
// 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 } ,
2015-11-28 12:46:53 +00:00
text : { type : String , required : true } ,
_id : { type : String , default : shared . uuid , validate : [ validator . isUUID , 'Invalid uuid.' ] } ,
2015-11-27 18:06:26 +00:00
} ] ,
} ;
} ;
2015-02-10 22:03:59 +00:00
2015-11-28 12:46:53 +00:00
export let HabitSchema = new Schema ( _ . defaults ( {
2015-11-27 18:06:26 +00:00
up : { type : Boolean , default : true } ,
down : { type : Boolean , default : true } ,
2015-11-28 12:46:53 +00:00
} , habitDailySchema ( ) ) , subDiscriminatorOptions ) ;
2015-12-02 10:22:53 +00:00
export let habit = Task . discriminator ( 'habit' , HabitSchema ) ;
2015-11-27 18:06:26 +00:00
2015-11-28 12:46:53 +00:00
export let DailySchema = new Schema ( _ . defaults ( {
2015-11-27 18:06:26 +00:00
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 ( ) ;
} ,
} ,
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 } ,
2015-11-28 12:46:53 +00:00
} , habitDailySchema ( ) , dailyTodoSchema ( ) ) , subDiscriminatorOptions ) ;
2015-12-02 10:22:53 +00:00
export let daily = Task . discriminator ( 'daily' , DailySchema ) ;
2015-02-10 22:03:59 +00:00
2015-11-28 12:46:53 +00:00
export let TodoSchema = new Schema ( _ . defaults ( {
2015-11-27 18:06:26 +00:00
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
2015-11-28 12:46:53 +00:00
} , dailyTodoSchema ( ) ) , subDiscriminatorOptions ) ;
2015-12-02 10:22:53 +00:00
export let todo = Task . discriminator ( 'todo' , TodoSchema ) ;
2015-02-10 22:03:59 +00:00
2015-11-28 12:46:53 +00:00
export let RewardSchema = new Schema ( { } , subDiscriminatorOptions ) ;
2015-12-02 10:22:53 +00:00
export let reward = Task . discriminator ( 'reward' , RewardSchema ) ;