mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 07:07:35 +01:00
finish implementing tasks syncing for challenges
This commit is contained in:
@@ -112,12 +112,13 @@ api.getGroups = {
|
||||
// If no valid value for type was supplied, return an error
|
||||
if (queries.length === 0) throw new BadRequest(res.t('groupTypesRequired'));
|
||||
|
||||
let results = await Q.all(queries); // TODO we would like not to return a single big array but Q doesn't support the funtionality https://github.com/kriskowal/q/issues/328
|
||||
// TODO we would like not to return a single big array but Q doesn't support the funtionality https://github.com/kriskowal/q/issues/328
|
||||
let results = _.reduce(await Q.all(queries), (previousValue, currentValue) => {
|
||||
if (_.isEmpty(currentValue)) return previousValue; // don't add anything to the results if the query returned null or an empty array
|
||||
return previousValue.concat(Array.isArray(currentValue) ? currentValue : [currentValue]); // otherwise concat the new results to the previousValue
|
||||
}, []);
|
||||
|
||||
res.respond(200, _.reduce(results, (m, v) => {
|
||||
if (_.isEmpty(v)) return m;
|
||||
return m.concat(Array.isArray(v) ? v : [v]);
|
||||
}, []));
|
||||
res.respond(200, results);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ api.createTask = {
|
||||
req.checkQuery('tasksOwner', res.t('invalidTasksOwner')).isIn(['user', 'challenge']);
|
||||
req.checkQuery('challengeId', res.t('challengeIdRequired')).optional().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let tasksData = Array.isArray(req.body) ? req.body : [req.body];
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -30,15 +30,18 @@ schema.plugin(baseModel, {
|
||||
noSet: ['_id', 'memberCount', 'tasksOrder'],
|
||||
});
|
||||
|
||||
// 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 = ['userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO use whitelist instead of blacklist?
|
||||
let omitAttrs = ['userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO what to do with updatedAt?
|
||||
if (t.type !== 'reward') omitAttrs.push('value');
|
||||
return _.omit(t, omitAttrs);
|
||||
}
|
||||
|
||||
schema.methods.syncToUser = function syncChallengeToUser (user) {
|
||||
// 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;
|
||||
|
||||
@@ -62,12 +65,7 @@ schema.methods.syncToUser = function syncChallengeToUser (user) {
|
||||
});
|
||||
}
|
||||
|
||||
return user.save();
|
||||
|
||||
// Old logic used to sync tasks
|
||||
// TODO might keep it around for when normal syncing doesn't succeed? or for first time syncing?
|
||||
// Sync new tasks and updated tasks
|
||||
/* return Q.all([
|
||||
let [challengeTasks, userTasks] = await Q.all([
|
||||
// Find original challenge tasks
|
||||
Tasks.Task.find({
|
||||
userId: {$exists: false},
|
||||
@@ -78,10 +76,8 @@ schema.methods.syncToUser = function syncChallengeToUser (user) {
|
||||
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 => {
|
||||
@@ -114,50 +110,88 @@ schema.methods.syncToUser = function syncChallengeToUser (user) {
|
||||
|
||||
toSave.push(user.save());
|
||||
return Q.all(toSave);
|
||||
});*/
|
||||
};
|
||||
|
||||
schema.methods.addTasksToMembers = async function addTasksToMembers (tasks) {
|
||||
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);
|
||||
});
|
||||
|
||||
// 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 membersIds = (await User.find({challenges: {$in: [challenge._id]}}).select('_id').exec()).map(member => member._id);
|
||||
let updateCmd = {$set: {}};
|
||||
|
||||
// Add tasks to users sequentially so that we don't kill the server (hopefully);
|
||||
// using a for...of loop allows each op to be run in sequence
|
||||
for (let memberId of membersIds) {
|
||||
let updateQ = {$push: {}};
|
||||
tasks.forEach(chalTask => {
|
||||
|
||||
})
|
||||
await db.post(doc);
|
||||
}
|
||||
|
||||
tasks.forEach(chalTask => {
|
||||
matchingTask = new Tasks[chalTask.type](Tasks.Task.sanitizeCreate(_syncableAttrs(chalTask)));
|
||||
matchingTask.challenge = {taskId: chalTask._id, id: challenge._id};
|
||||
matchingTask.userId = user._id;
|
||||
|
||||
})
|
||||
_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();
|
||||
};
|
||||
|
||||
// Old Syncing logic, kept for reference and maybe will be needed to adapt v2
|
||||
/*
|
||||
// Remove a task from challenge members
|
||||
schema.methods.removeTask = async function challengeRemoveTask (task) {
|
||||
let challenge = this;
|
||||
|
||||
// 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
|
||||
.transform(function(result, task){
|
||||
result.push(syncableAttrs(task));
|
||||
})
|
||||
.value())
|
||||
// Remove the tasks from users' and map each of them to an update query to remove the task from tasksOrder
|
||||
let updateQueries = (await Tasks.Task.findOneAndRemove({
|
||||
userId: {$exists: true},
|
||||
'challenge.id': challenge.id,
|
||||
'challenge.taskId': task._id,
|
||||
}, {
|
||||
fields: {userId: 1, type: 1}, // fetch only what's necessary
|
||||
}).lean().exec())
|
||||
.map(removedTask => {
|
||||
return User.update({_id: removedTask.userId}, {
|
||||
$pull: {[`tasksOrder${removedTask.type}s`]: removedTask._id},
|
||||
});
|
||||
});
|
||||
|
||||
// Execute each update sequentially
|
||||
for (let query of updateQueries) {
|
||||
await query.exec(); // eslint-disable-line babel/no-await-in-loop
|
||||
}
|
||||
|
||||
ChallengeSchema.methods.isOutdated = function isChallengeOutdated (newData) {
|
||||
return comparableData(this) !== comparableData(newData);
|
||||
}*/
|
||||
};
|
||||
|
||||
export let model = mongoose.model('Challenge', schema);
|
||||
|
||||
@@ -28,7 +28,7 @@ export let TaskSchema = new Schema({
|
||||
|
||||
challenge: {
|
||||
id: {type: String, ref: 'Challenge', validate: [validator.isUUID, 'Invalid uuid.']}, // When set (and userId not set) it's the original task
|
||||
taskId: {type: String, ref: 'Task', validate: [validator.isUUID, 'Invalid uuid.']}, // When not set but challenge.id defined it's the original task
|
||||
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?
|
||||
broken: {type: String, enum: ['CHALLENGE_DELETED', 'TASK_DELETED', 'UNSUBSCRIBED', 'CHALLENGE_CLOSED']},
|
||||
winner: String, // user.profile.name TODO necessary?
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user