mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
port task model to es6 and implement discriminators
This commit is contained in:
@@ -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',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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}}, () => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user