mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
port challenge model
This commit is contained in:
@@ -6,6 +6,7 @@ const SERVER_FILES = [
|
||||
'./website/src/models/user.js',
|
||||
'./website/src/models/task.js',
|
||||
'./website/src/models/group.js',
|
||||
'./website/src/models/challenge.js',
|
||||
'./website/src/models/tag.js',
|
||||
'./website/src/models/emailUnsubscription.js',
|
||||
'./website/src/server.js',
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
var mongoose = require("mongoose");
|
||||
var Schema = mongoose.Schema;
|
||||
var shared = require('../../../common');
|
||||
var _ = require('lodash');
|
||||
var TaskSchemas = require('./task');
|
||||
import mongoose from 'mongoose';
|
||||
import Q from 'q';
|
||||
import validator from 'validator';
|
||||
import baseModel from '../libs/api-v3/baseModel';
|
||||
import _ from 'lodash';
|
||||
import * as Tasks from './task';
|
||||
|
||||
var ChallengeSchema = new Schema({
|
||||
_id: {type: String, 'default': shared.uuid},
|
||||
name: String,
|
||||
shortName: String,
|
||||
let Schema = mongoose.Schema;
|
||||
|
||||
let schema = new Schema({
|
||||
name: {type: String, required: true},
|
||||
shortName: {type: String, required: true}, // TODO what is it?
|
||||
description: String,
|
||||
official: {type: Boolean,'default':false},
|
||||
//habits: [TaskSchemas.HabitSchema],
|
||||
//dailys: [TaskSchemas.DailySchema],
|
||||
//todos: [TaskSchemas.TodoSchema],
|
||||
//rewards: [TaskSchemas.RewardSchema],
|
||||
leader: {type: String, ref: 'User'},
|
||||
group: {type: String, ref: 'Group'},
|
||||
timestamp: {type: Date, 'default': Date.now},
|
||||
members: [{type: String, ref: 'User'}],
|
||||
memberCount: {type: Number, 'default': 0},
|
||||
prize: {type: Number, 'default': 0}
|
||||
official: {type: Boolean, default: false},
|
||||
tasksOrder: {
|
||||
habits: [{type: String, ref: 'Task'}],
|
||||
dailys: [{type: String, ref: 'Task'}],
|
||||
todos: [{type: String, ref: 'Task'}],
|
||||
rewards: [{type: String, ref: 'Task'}],
|
||||
},
|
||||
leader: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
||||
group: {type: String, ref: 'Group', validate: [validator.isUUID, 'Invalid uuid.'], required: true},
|
||||
timestamp: {type: Date, default: Date.now, required: true}, // TODO what is this? use timestamps from plugin?
|
||||
memberCount: {type: Number, default: 0},
|
||||
prize: {type: Number, default: 0, required: true},
|
||||
});
|
||||
|
||||
ChallengeSchema.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;
|
||||
});
|
||||
|
||||
ChallengeSchema.methods.toJSON = function(){
|
||||
var doc = this.toObject();
|
||||
schema.plugin(baseModel, {
|
||||
noSet: ['_id', 'memberCount', 'tasksOrder'],
|
||||
toJSONTransform: function userToJSON (doc) {
|
||||
// TODO fixme
|
||||
// TODO this works?
|
||||
doc._isMember = this._isMember;
|
||||
|
||||
return doc;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// --------------
|
||||
// Syncing logic
|
||||
// --------------
|
||||
|
||||
function syncableAttrs(task) {
|
||||
var t = (task.toObject) ? task.toObject() : task; // lodash doesn't seem to like _.omit on EmbeddedDocument
|
||||
function _syncableAttrs (task) {
|
||||
let t = task.toObject(); // lodash doesn't seem to like _.omit on EmbeddedDocument
|
||||
// only sync/compare important attrs
|
||||
var omitAttrs = 'challenge history tags completed streak notes'.split(' ');
|
||||
if (t.type != 'reward') omitAttrs.push('value');
|
||||
let omitAttrs = ['userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO use whitelist instead of blacklist?
|
||||
if (t.type !== 'reward') omitAttrs.push('value');
|
||||
return _.omit(t, omitAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
||||
*/
|
||||
function comparableData(obj) {
|
||||
// TODO redo
|
||||
// Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
||||
/* function comparableData(obj) {
|
||||
return JSON.stringify(
|
||||
_(obj.habits.concat(obj.dailys).concat(obj.todos).concat(obj.rewards))
|
||||
.sortBy('id') // we don't want to update if they're sort-order is different
|
||||
@@ -59,62 +59,86 @@ function comparableData(obj) {
|
||||
.value())
|
||||
}
|
||||
|
||||
ChallengeSchema.methods.isOutdated = function(newData) {
|
||||
ChallengeSchema.methods.isOutdated = function isChallengeOutdated (newData) {
|
||||
return comparableData(this) !== comparableData(newData);
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Syncs all new tasks, deleted tasks, etc to the user object
|
||||
* @param user
|
||||
* @return nothing, user is modified directly. REMEMBER to save the user!
|
||||
*/
|
||||
ChallengeSchema.methods.syncToUser = function(user, cb) {
|
||||
if (!user) return;
|
||||
var self = this;
|
||||
self.shortName = self.shortName || self.name;
|
||||
// Syncs all new tasks, deleted tasks, etc to the user object
|
||||
schema.methods.syncToUser = function syncChallengeToUser (user) {
|
||||
if (!user) throw new Error('User required.');
|
||||
|
||||
let challenge = this;
|
||||
challenge.shortName = challenge.shortName || challenge.name;
|
||||
|
||||
// Add challenge to user.challenges
|
||||
if (!_.contains(user.challenges, self._id)) {
|
||||
user.challenges.push(self._id);
|
||||
}
|
||||
if (!_.contains(user.challenges, challenge._id)) user.challenges.push(challenge._id);
|
||||
|
||||
// Sync tags
|
||||
var tags = user.tags || [];
|
||||
var i = _.findIndex(tags, {id: self._id})
|
||||
if (~i) {
|
||||
if (tags[i].name !== self.shortName) {
|
||||
let userTags = user.tags;
|
||||
let i = _.findIndex(userTags, {_id: challenge._id});
|
||||
|
||||
if (i !== -1) {
|
||||
if (userTags[i].name !== challenge.shortName) {
|
||||
// update the name - it's been changed since
|
||||
user.tags[i].name = self.shortName;
|
||||
userTags[i].name = challenge.shortName;
|
||||
}
|
||||
} else {
|
||||
user.tags.push({
|
||||
id: self._id,
|
||||
name: self.shortName,
|
||||
challenge: true
|
||||
userTags.push({
|
||||
_id: challenge._id,
|
||||
name: challenge.shortName,
|
||||
challenge: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Sync new tasks and updated tasks
|
||||
_.each(self.tasks, function(task){
|
||||
var list = user[task.type+'s'];
|
||||
var userTask = user.tasks[task.id] || (list.push(syncableAttrs(task)), list[list.length-1]);
|
||||
if (!userTask.notes) userTask.notes = task.notes; // don't override the notes, but provide it if not provided
|
||||
userTask.challenge = {id:self._id};
|
||||
userTask.tags = userTask.tags || {};
|
||||
userTask.tags[self._id] = true;
|
||||
_.merge(userTask, syncableAttrs(task));
|
||||
})
|
||||
return Q.all([
|
||||
// Find original challenge tasks
|
||||
Tasks.Task.find({
|
||||
userId: {$exists: false},
|
||||
'challenge.id': challenge._id,
|
||||
}).exec(),
|
||||
// Find user's tasks linked to this challenge
|
||||
Tasks.Task.find({
|
||||
userId: user._id,
|
||||
'challenge.id': challenge._id,
|
||||
}).exec(),
|
||||
])
|
||||
.then(results => {
|
||||
let challengeTasks = results[0];
|
||||
let userTasks = results[1];
|
||||
let toSave = []; // An array of things to save
|
||||
|
||||
challengeTasks.forEach(chalTask => {
|
||||
let matchingTask = _.find(userTasks, userTask => userTask.challenge.taskId === chalTask._id);
|
||||
|
||||
if (!matchingTask) { // If the task is new, create it
|
||||
matchingTask = new Tasks[chalTask.type](Tasks.Task.sanitizeCreate(_syncableAttrs(chalTask)));
|
||||
matchingTask.challenge = {taskId: chalTask._id, id: challenge._id};
|
||||
matchingTask.userId = user._id;
|
||||
user.tasksOrder[`${chalTask.type}s`].push(matchingTask._id);
|
||||
} else {
|
||||
_.merge(matchingTask, _syncableAttrs(chalTask));
|
||||
// Make sure the task is in user.tasksOrder TODO necessary?
|
||||
let orderList = user.tasksOrder[`${chalTask.type}s`];
|
||||
if (orderList.indexOf(matchingTask._id) === -1 && (matchingTask.type !== 'todo' || !matchingTask.completed)) orderList.push(matchingTask._id);
|
||||
}
|
||||
|
||||
if (!matchingTask.notes) matchingTask.notes = chalTask.notes; // don't override the notes, but provide it if not provided
|
||||
if (matchingTask.tags.indexOf(challenge._id) === -1) matchingTask.tags.push(challenge._id); // add tag if missing
|
||||
toSave.push(matchingTask.save());
|
||||
});
|
||||
|
||||
// Flag deleted tasks as "broken"
|
||||
_.each(user.tasks, function(task){
|
||||
if (task.challenge && task.challenge.id==self._id && !self.tasks[task.id]) {
|
||||
task.challenge.broken = 'TASK_DELETED';
|
||||
userTasks.forEach(userTask => {
|
||||
if (!_.find(challengeTasks, chalTask => chalTask._id === userTask.challenge.taskId)) {
|
||||
userTask.challenge.broken = 'TASK_DELETED';
|
||||
toSave.push(userTask.save());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
user.save(cb);
|
||||
toSave.push(user.save());
|
||||
return Q.all(toSave);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports.schema = ChallengeSchema;
|
||||
module.exports.model = mongoose.model("Challenge", ChallengeSchema);
|
||||
export let model = mongoose.model('Challenge', schema);
|
||||
|
||||
@@ -24,12 +24,12 @@ export let TaskSchema = new Schema({
|
||||
value: {type: Number, default: 0}, // redness or cost for rewards
|
||||
priority: {type: Number, default: 1, required: true}, // TODO enum?
|
||||
attribute: {type: String, default: 'str', enum: ['str', 'con', 'int', 'per']},
|
||||
userId: {type: String, ref: 'User'}, // When null it belongs to a challenge
|
||||
userId: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']}, // When not set it belongs to a challenge
|
||||
|
||||
challenge: {
|
||||
id: {type: String, ref: 'Challenge'},
|
||||
taskId: {type: String, ref: 'Task'}, // When null but challenge.id defined it's the original task
|
||||
broken: String, // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, CHALLENGE_CLOSED TODO enum
|
||||
id: {type: String, ref: 'Challenge', validate: [validator.isUUID, 'Invalid uuid.']},
|
||||
taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']}, // When not set but challenge.id defined it's the original task
|
||||
broken: {type: String, enum: ['CHALLENGE_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED', 'CHALLENGE_CLOSED']},
|
||||
winner: String, // user.profile.name TODO necessary?
|
||||
},
|
||||
}, _.defaults({
|
||||
|
||||
@@ -481,6 +481,7 @@ schema.plugin(baseModel, {
|
||||
private: ['auth.local.hashed_password', 'auth.local.salt'],
|
||||
toJSONTransform: function userToJSON (doc) {
|
||||
// FIXME? Is this a reference to `doc.filters` or just disabled code? Remove?
|
||||
// TODO this works?
|
||||
doc.filters = {};
|
||||
doc._tmp = this._tmp; // be sure to send down drop notifs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user