214 lines
7.7 KiB
JavaScript
214 lines
7.7 KiB
JavaScript
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';
|
|
import { model as User } from './user';
|
|
|
|
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}, // TODO only settable by admin
|
|
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},
|
|
groupId: {type: String, ref: 'Group', validate: [validator.isUUID, 'Invalid uuid.'], required: true}, // TODO no update, no set?
|
|
timestamp: {type: Date, default: Date.now, required: true}, // TODO what is this? use timestamps from plugin? not settable?
|
|
memberCount: {type: Number, default: 0},
|
|
prize: {type: Number, default: 0, min: 0}, // TODO no update?
|
|
});
|
|
|
|
schema.plugin(baseModel, {
|
|
noSet: ['_id', 'memberCount', 'challengeCount', 'tasksOrder'],
|
|
});
|
|
|
|
// Returns true if user has access to the challenge (can join)
|
|
schema.methods.hasAccess = function hasAccessToChallenge (user) {
|
|
let userGroups = user.guilds.slice(0);
|
|
if (user.party._id) userGroups.push(user.party._id);
|
|
userGroups.push('habitrpg'); // tavern challenges
|
|
return this.leader === user._id || userGroups.indexOf(this.groupId) !== -1;
|
|
};
|
|
|
|
// Returns true if user can view the challenge
|
|
// Different from hasAccess because challenges of public guilds can be viewed by everyone
|
|
schema.methods.canView = function canViewChallenge (user, group) {
|
|
if (user.contributor.admin) return true;
|
|
if (group.type === 'guild' && group.privacy === 'public') return true;
|
|
return this.hasAccess(user);
|
|
};
|
|
|
|
// Returns true if user is a member of the challenge
|
|
schema.methods.isMember = function isChallengeMember (user) {
|
|
return user.challenges.indexOf(this._id) !== -1;
|
|
};
|
|
|
|
// Returns true if the user can modify (close, selectWinner, ...) the challenge
|
|
schema.methods.canModify = function canModifyChallenge (user) {
|
|
return user.contributor.admin || this.leader === user._id;
|
|
};
|
|
|
|
// Takes a Task document and return a plain object of attributes that can be synced to the user
|
|
function _syncableAttrs (task) {
|
|
let t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
|
// only sync/compare important attrs
|
|
let omitAttrs = ['_id', 'userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO what to do with updatedAt?
|
|
if (t.type !== 'reward') omitAttrs.push('value');
|
|
return _.omit(t, omitAttrs);
|
|
}
|
|
|
|
// Sync challenge to user, including tasks and tags.
|
|
// Used when user joins the challenge or to force sync.
|
|
schema.methods.syncToUser = async function syncChallengeToUser (user) {
|
|
let challenge = this;
|
|
challenge.shortName = challenge.shortName || challenge.name;
|
|
|
|
// Add challenge to user.challenges
|
|
if (!_.contains(user.challenges, challenge._id)) user.challenges.push(challenge._id);
|
|
|
|
// Sync tags
|
|
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
|
|
userTags[i].name = challenge.shortName;
|
|
}
|
|
} else {
|
|
userTags.push({
|
|
_id: challenge._id,
|
|
name: challenge.shortName,
|
|
challenge: true,
|
|
});
|
|
}
|
|
|
|
let [challengeTasks, userTasks] = await 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(),
|
|
]);
|
|
|
|
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"
|
|
userTasks.forEach(userTask => {
|
|
if (!_.find(challengeTasks, chalTask => chalTask._id === userTask.challenge.taskId)) {
|
|
userTask.challenge.broken = 'TASK_DELETED';
|
|
toSave.push(userTask.save());
|
|
}
|
|
});
|
|
|
|
toSave.push(user.save());
|
|
return Q.all(toSave);
|
|
};
|
|
|
|
async function _fetchMembersIds (challengeId) {
|
|
return (await User.find({challenges: {$in: [challengeId]}}).select('_id').lean().exec()).map(member => member._id);
|
|
}
|
|
|
|
// Add a new task to challenge members
|
|
schema.methods.addTasks = async function challengeAddTasks (tasks) {
|
|
let challenge = this;
|
|
let membersIds = await _fetchMembersIds(challenge._id);
|
|
|
|
// Sync each user sequentially
|
|
for (let memberId of membersIds) {
|
|
let updateTasksOrderQ = {$push: {}};
|
|
let toSave = [];
|
|
|
|
// TODO eslint complaints about ahving a function inside a loop -> make sure it works
|
|
tasks.forEach(chalTask => { // eslint-disable-line no-loop-func
|
|
let userTask = new Tasks[chalTask.type](Tasks.Task.sanitizeCreate(_syncableAttrs(chalTask)));
|
|
userTask.challenge = {taskId: chalTask._id, id: challenge._id};
|
|
userTask.userId = memberId;
|
|
|
|
let tasksOrderList = updateTasksOrderQ.$push[`tasksOrder.${chalTask.type}s`];
|
|
if (!tasksOrderList) {
|
|
updateTasksOrderQ.$push[`tasksOrder.${chalTask.type}s`] = {
|
|
$position: 0, // unshift
|
|
$each: [userTask._id],
|
|
};
|
|
} else {
|
|
tasksOrderList.$each.unshift(userTask._id);
|
|
}
|
|
|
|
toSave.push(userTask.save());
|
|
});
|
|
|
|
// Update the user
|
|
toSave.unshift(User.update({_id: memberId}, updateTasksOrderQ).exec());
|
|
await Q.all(toSave); // eslint-disable-line babel/no-await-in-loop
|
|
}
|
|
};
|
|
|
|
// Sync updated task to challenge members
|
|
schema.methods.updateTask = async function challengeUpdateTask (task) {
|
|
let challenge = this;
|
|
|
|
let updateCmd = {$set: {}};
|
|
|
|
_syncableAttrs(task).forEach((value, key) => {
|
|
updateCmd.$set[key] = value;
|
|
});
|
|
|
|
// TODO reveiw
|
|
// Updating instead of loading and saving for performances, risks becoming a problem if we introduce more complexity in tasks
|
|
await Tasks.Task.update({
|
|
userId: {$exists: true},
|
|
'challenge.id': challenge.id,
|
|
'challenge.taskId': task._id,
|
|
}, updateCmd, {multi: true}).exec();
|
|
};
|
|
|
|
// Remove a task from challenge members
|
|
schema.methods.removeTask = async function challengeRemoveTask (task) {
|
|
let challenge = this;
|
|
|
|
// Set the task as broken
|
|
await Tasks.Task.update({
|
|
userId: {$exists: true},
|
|
'challenge.id': challenge.id,
|
|
'challenge.taskId': task._id,
|
|
}, {
|
|
$set: {'challenge.broken': 'TASK_DELETED'}, // TODO what about updatedAt?
|
|
}, {multi: true}).exec();
|
|
};
|
|
|
|
export let model = mongoose.model('Challenge', schema);
|