diff --git a/website/client/src/components/group-plans/taskInformation.vue b/website/client/src/components/group-plans/taskInformation.vue index ef428ce4a9..f58f9396fc 100644 --- a/website/client/src/components/group-plans/taskInformation.vue +++ b/website/client/src/components/group-plans/taskInformation.vue @@ -39,7 +39,7 @@ id="taskMirrorToggle" class="mr-3 mb-1 ml-auto" :label="'Copy tasks'" - :checked="user.preferences.tasks.mirrorGroupTasks" + :checked="user.preferences.tasks.mirrorGroupTasks.indexOf(group._id) !== -1" :hover-text="'Add assigned and open tasks to your personal task board'" @change="changeMirrorPreference" /> @@ -431,12 +431,16 @@ export default { eventCategory: 'behavior', hitType: 'event', mirror: newVal, + group: this.group._id, }, { trackOnClient: true }); - Analytics.updateUser({ - mirrorTasks: newVal, - }); + const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || []; + if (newVal) { // we're turning copy ON for this group + groupsToMirror.push(this.group._id); + } else { // we're turning copy OFF for this group + groupsToMirror.splice(groupsToMirror.indexOf(this.group._id), 1); + } this.$store.dispatch('user:set', { - 'preferences.tasks.mirrorGroupTasks': newVal, + 'preferences.tasks.mirrorGroupTasks': groupsToMirror, }); }, }, diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 09b3e21971..020625de7e 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -385,7 +385,7 @@ api.getUserTasks = { url: '/tasks/user', middlewares: [authWithHeaders({ // Some fields (including _id, preferences) are always loaded (see middlewares/auth) - userFieldsToInclude: ['guilds', 'party', 'tasksOrder'], + userFieldsToInclude: ['tasksOrder'], })], async handler (req, res) { const types = Tasks.tasksTypes.map(type => `${type}s`); diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index a74303344b..382c9337e6 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -165,10 +165,11 @@ async function getTasks (req, res, options = {}) { } else if (group) { query = { 'group.id': group._id }; } else { - if (user.preferences.tasks.mirrorGroupTasks) { + const groupsToMirror = user.preferences.tasks.mirrorGroupTasks; + if (groupsToMirror && groupsToMirror.length > 0) { upgradedGroups = await Group.find( { - _id: { $in: user.guilds.concat(user.party._id) }, + _id: { $in: groupsToMirror }, 'purchased.plan.customerId': { $exists: true }, $or: [ { 'purchased.plan.dateTerminated': { $exists: false } }, diff --git a/website/server/libs/user/index.js b/website/server/libs/user/index.js index f2aa73df69..8e19db0ea8 100644 --- a/website/server/libs/user/index.js +++ b/website/server/libs/user/index.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import common from '../../../common'; import * as Tasks from '../../models/task'; +import { model as Groups } from '../../models/group'; import { BadRequest, NotAuthorized, @@ -132,6 +133,34 @@ export async function update (req, res, { isV3 = false }) { await checkNewInputForProfanity(user, res, newBlurb); } + if (req.body['preferences.tasks.mirrorGroupTasks'] !== undefined) { + const groupsToMirror = req.body['preferences.tasks.mirrorGroupTasks']; + if (!Array.isArray(groupsToMirror)) { + throw new BadRequest('Groups to copy tasks from must be an array.'); + } + const memberGroups = user.guilds; + if (user.party._id) memberGroups.push(user.party._id); + for (const targetGroup of groupsToMirror) { + if (memberGroups.indexOf(targetGroup) === -1) { + throw new BadRequest(`User not a member of group ${targetGroup}.`); + } + } + + const matchingGroupsCount = await Groups.countDocuments({ + _id: { $in: groupsToMirror }, + 'purchased.plan.customerId': { $exists: true }, + $or: [ + { 'purchased.plan.dateTerminated': { $exists: false } }, + { 'purchased.plan.dateTerminated': null }, + { 'purchased.plan.dateTerminated': { $gt: new Date() } }, + ], + }).exec(); + + if (matchingGroupsCount !== groupsToMirror.length) { + throw new BadRequest('Groups to copy tasks from must have subscriptions.'); + } + } + _.each(req.body, (val, key) => { const purchasable = requiresPurchase[key]; @@ -140,7 +169,7 @@ export async function update (req, res, { isV3 = false }) { } if (key === 'tags') { - if (!Array.isArray(val)) throw new BadRequest('mustBeArray'); + if (!Array.isArray(val)) throw new BadRequest('Tag list must be an array.'); const removedTagsIds = []; diff --git a/website/server/models/group.js b/website/server/models/group.js index 80d80ced14..91b1a5ed33 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -1386,10 +1386,13 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC const promises = user.isModified() ? [user.save()] : []; // remove the group from the user's groups + const userUpdate = { $pull: { 'preferences.tasks.mirrorGroupTasks': group._id } }; if (group.type === 'guild') { - promises.push(User.update({ _id: user._id }, { $pull: { guilds: group._id } }).exec()); + userUpdate.$pull.guilds = group._id; + promises.push(User.update({ _id: user._id }, userUpdate).exec()); } else { - promises.push(User.update({ _id: user._id }, { $set: { party: {} } }).exec()); + userUpdate.$set = { party: {} }; + promises.push(User.update({ _id: user._id }, userUpdate).exec()); update.$unset = { [`quest.members.${user._id}`]: 1 }; } diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index 1dd5690978..2af49e89f7 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -582,7 +582,9 @@ export default new Schema({ tasks: { groupByChallenge: { $type: Boolean, default: false }, // @TODO remove? not used confirmScoreNotes: { $type: Boolean, default: false }, // @TODO remove? not used - mirrorGroupTasks: { $type: Boolean, default: false }, + mirrorGroupTasks: [ + { $type: String, validate: [v => validator.isUUID(v), 'Invalid group UUID.'], ref: 'Group' }, + ], }, improvementCategories: { $type: Array,