port task model to es6 and implement discriminators

This commit is contained in:
Matteo Pagliazzi
2015-11-27 19:06:26 +01:00
parent 9adfd6311f
commit 7d53a4fd54
3 changed files with 112 additions and 160 deletions

View File

@@ -4,6 +4,7 @@ import eslint from 'gulp-eslint';
const SERVER_FILES = [ const SERVER_FILES = [
'./website/src/**/api-v3/**/*.js', './website/src/**/api-v3/**/*.js',
'./website/src/models/user.js', './website/src/models/user.js',
'./website/src/models/task.js',
'./website/src/models/emailUnsubscription.js', './website/src/models/emailUnsubscription.js',
'./website/src/server.js', './website/src/server.js',
]; ];

View File

@@ -1,108 +1,99 @@
// User.js import mongoose from 'mongoose';
// ======= import shared from '../../../common';
// Defines the user data model (schema) for use via the API. import moment from 'moment';
import baseModel from '../libs/api-v3/baseModel';
import _ from 'lodash';
// Dependencies let Schema = mongoose.Schema;
// ------------ let discriminatorOptions = () => {
var mongoose = require("mongoose"); return {discriminatorKey: 'type'}; // the key that distinguishes task types
var Schema = mongoose.Schema;
var shared = require('../../../common');
var _ = require('lodash');
var moment = require('moment');
// Task Schema
// -----------
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`
}
}; };
var HabitSchema = new Schema( // TODO make sure a task can only update the fields belonging to its type
_.defaults({ // We could use discriminators but it looks like when loading from the parent
type: {type:String, 'default': 'habit'}, // Task model the subclasses are not applied - check twice
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 }
);
var collapseChecklist = {type:Boolean, 'default':false}; export let TaskSchema = new Schema({
var checklist = [{ type: {type: String, enum: ['habit', 'todo', 'daily', 'reward'], required: true, default: 'habit'},
completed:{type:Boolean,'default':false},
text: String, text: String,
_id:false, notes: {type: String, default: ''},
id: {type:String,'default':shared.uuid} tags: {type: Schema.Types.Mixed, default: {}}, // TODO dictionary? { "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']},
userId: {type: String, ref: 'User'}, // When null it belongs to a challenge
var DailySchema = new Schema( challenge: {
_.defaults({ id: {type: String, ref: 'Challenge'},
type: {type: String, 'default': 'daily'}, taskId: {type: String, ref: 'Task'}, // When null but challenge.id defined it's the original task
frequency: {type: String, 'default': 'weekly', enum: ['daily', 'weekly']}, broken: String, // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, CHALLENGE_CLOSED TODO enum
everyX: {type: Number, 'default': 1}, // e.g. once every X weeks winner: String, // user.profile.name TODO necessary?
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}
}, },
collapseChecklist:collapseChecklist, }, _.default({
checklist:checklist, minimize: true, // So empty objects are returned
streak: {type: Number, 'default': 0} strict: true,
}, TaskSchema) }, discriminatorOptions()));
, { _id: false, minimize:false }
)
var TodoSchema = new Schema( TaskSchema.plugin(baseModel, {
_.defaults({ noSet: [],
type: {type:String, 'default': 'todo'}, private: [],
completed: {type: Boolean, 'default': false}, timestamps: true,
});
export let Task = mongoose.model('Task', TaskSchema);
// TODO discriminators: it's very important to check that the options and plugins of the parent schema are used in the sub-schemas too
// 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: String,
_id: {type: String, default: shared.uuid},
}],
};
};
export let Habit = Task.discriminator('Habit', new Schema(_.defaults({
up: {type: Boolean, default: true},
down: {type: Boolean, default: true},
}, habitDailySchema())), discriminatorOptions());
export let Daily = Task.discriminator('Daily', 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();
},
},
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())), discriminatorOptions());
export let Todo = Task.discriminator('Todo', new Schema(_.defaults({
dateCompleted: Date, 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 // 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, // TODO change field name
checklist:checklist date: String, // due date for todos
}, TaskSchema) }, dailyTodoSchema())), discriminatorOptions());
, { _id: false, minimize:false }
);
var RewardSchema = new Schema( export let Reward = Task.discriminator('Reward', new Schema({}), discriminatorOptions());
_.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;

View File

@@ -1,10 +1,9 @@
// User schema and model
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import shared from '../../../common'; import shared from '../../../common';
import _ from 'lodash'; import _ from 'lodash';
import validator from 'validator'; import validator from 'validator';
import moment from 'moment'; import moment from 'moment';
import TaskSchemas from './task'; import { model as Task } from './task';
import baseModel from '../libs/api-v3/baseModel'; import baseModel from '../libs/api-v3/baseModel';
// import {model as Challenge} from './challenge'; // import {model as Challenge} from './challenge';
@@ -41,11 +40,9 @@ export let schema = new Schema({
loggedin: {type: Date, default: Date.now}, 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 // 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 // have been updated (http://goo.gl/gQLz41), but we want *every* update
_v: { type: Number, default: 0 }, _v: { type: Number, default: 0 },
achievements: { achievements: {
originalUser: Boolean, originalUser: Boolean,
habitSurveys: Number, habitSurveys: Number,
@@ -455,14 +452,14 @@ export let schema = new Schema({
messages: {type: Schema.Types.Mixed, default: {}}, messages: {type: Schema.Types.Mixed, default: {}},
optOut: {type: Boolean, default: false}, optOut: {type: Boolean, default: false},
}, },
tasksOrder: {
habits: {type: [TaskSchemas.HabitSchema]}, habits: [{type: String, ref: 'Task'}],
dailys: {type: [TaskSchemas.DailySchema]}, dailys: [{type: String, ref: 'Task'}],
todos: {type: [TaskSchemas.TodoSchema]}, todos: [{type: String, ref: 'Task'}],
rewards: {type: [TaskSchemas.RewardSchema]}, completedTodos: [{type: String, ref: 'Task'}],
rewards: [{type: String, ref: 'Task'}],
},
extra: Schema.Types.Mixed, extra: Schema.Types.Mixed,
pushDevices: { pushDevices: {
type: [{ type: [{
regId: {type: String}, regId: {type: String},
@@ -476,10 +473,10 @@ export let schema = new Schema({
}); });
schema.plugin(baseModel, { 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'],
private: ['auth.local.hashed_password', 'auth.local.salt'], private: ['auth.local.hashed_password', 'auth.local.salt'],
toJSONTransform: function toJSON (doc) { toJSONTransform: function toJSON (doc) {
doc.id = doc._id; doc.id = doc._id; // TODO remove?
// FIXME? Is this a reference to `doc.filters` or just disabled code? Remove? // FIXME? Is this a reference to `doc.filters` or just disabled code? Remove?
doc.filters = {}; doc.filters = {};
@@ -489,31 +486,25 @@ 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) { schema.post('init', function postInitUser (doc) {
shared.wrap(doc); shared.wrap(doc);
}); });
function _populateDefaultTasks (user, taskTypes) { function _populateDefaultTasks (user, taskTypes) {
_.each(taskTypes, (taskType) => { _.each(taskTypes, (taskType) => {
user[taskType] = _.map(shared.content.userDefaults[taskType], (task) => { // TODO save in own documents
let newTask = _.cloneDeep(task); user[taskType] = _.map(shared.content.userDefaults[taskType], (taskDefaults) => {
let newTask;
// Render task's text and notes in user's language // Render task's text and notes in user's language
if (taskType === 'tags') { if (taskType === 'tags') {
newTask = _.cloneDeep(taskDefaults);
// tasks automatically get id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here // 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.id = shared.uuid();
newTask.name = newTask.name(user.preferences.language); newTask.name = newTask.name(user.preferences.language);
} else { } else {
newTask = new Task(taskDefaults);
newTask.userId = user._id;
newTask.text = newTask.text(user.preferences.language); newTask.text = newTask.text(user.preferences.language);
if (newTask.notes) { if (newTask.notes) {
newTask.notes = newTask.notes(user.preferences.language); newTask.notes = newTask.notes(user.preferences.language);
@@ -534,27 +525,12 @@ function _populateDefaultTasks (user, taskTypes) {
function _populateDefaultsForNewUser (user) { function _populateDefaultsForNewUser (user) {
let taskTypes; let taskTypes;
let iterableFlags = user.toObject().flags;
if (user.registeredThrough === 'habitica-web') { if (user.registeredThrough === 'habitica-web') {
taskTypes = ['habits', 'dailys', 'todos', 'rewards', 'tags']; taskTypes = ['habits', 'dailys', 'todos', 'rewards', 'tags'];
let tutorialCommonSections = [ _.each(iterableFlags.tutorial.common, (section) => {
'habits',
'dailies',
'todos',
'rewards',
'party',
'pets',
'gems',
'skills',
'classes',
'tavern',
'equipment',
'items',
'inviteParty',
];
_.each(tutorialCommonSections, (section) => {
user.flags.tutorial.common[section] = true; user.flags.tutorial.common[section] = true;
}); });
} else { } else {
@@ -562,23 +538,7 @@ function _populateDefaultsForNewUser (user) {
user.flags.showTour = false; user.flags.showTour = false;
let tourSections = [ _.each(iterableFlags.tour, (section) => {
'showTour',
'intro',
'classes',
'stats',
'tavern',
'party',
'guilds',
'challenges',
'market',
'pets',
'mounts',
'hall',
'equipment',
];
_.each(tourSections, (section) => {
user.flags.tour[section] = -2; user.flags.tour[section] = -2;
}); });
} }
@@ -668,7 +628,7 @@ schema.methods.unlink = function unlink (options, cb) {
if (keep === 'keep') { if (keep === 'keep') {
self.tasks[tid].challenge = {}; self.tasks[tid].challenge = {};
} else if (keep === 'remove') { } else if (keep === 'remove') {
self.deleteTask(tid); self.ops.deleteTask({params: {id: tid}}, () => {});
} else if (keep === 'keep-all') { } else if (keep === 'keep-all') {
_.each(self.tasks, (t) => { _.each(self.tasks, (t) => {
if (t.challenge && t.challenge.id === cid) { if (t.challenge && t.challenge.id === cid) {
@@ -678,7 +638,7 @@ schema.methods.unlink = function unlink (options, cb) {
} else if (keep === 'remove-all') { } else if (keep === 'remove-all') {
_.each(self.tasks, (t) => { _.each(self.tasks, (t) => {
if (t.challenge && t.challenge.id === cid) { if (t.challenge && t.challenge.id === cid) {
self.deleteTask(t.id); this.ops.deleteTask({params: {id: tid}}, () => {});
} }
}); });
} }