From cab4a2a8fa74735d8c390d48ac8fba7c91802d66 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Thu, 4 Feb 2021 14:42:48 -0600 Subject: [PATCH 001/171] WIP(teams): begin simplification --- .../client/src/components/tasks/taskModal.vue | 44 ------------------- website/server/libs/payments/groupPayments.js | 3 ++ website/server/middlewares/cron.js | 38 +++++++++++++--- website/server/models/user/methods.js | 17 ++++++- 4 files changed, 50 insertions(+), 52 deletions(-) diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue index 40dce98644..b85e48bc43 100644 --- a/website/client/src/components/tasks/taskModal.vue +++ b/website/client/src/components/tasks/taskModal.vue @@ -448,20 +448,6 @@ v-if="groupId" class="option group-options mt-3" > -
- - -
-
- - -
= 0) userGroups.splice(index, 1); + await Tasks.Task.remove({ userId: user._id, 'group.id': group._id }).exec(); + const groupPlansQuery = { // type: { $in: ['guild', 'party'] }, // privacy: 'private', diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js index 664d65cc3e..4c86fb2817 100644 --- a/website/server/middlewares/cron.js +++ b/website/server/middlewares/cron.js @@ -73,13 +73,37 @@ async function cronAsync (req, res) { return null; } - const tasks = await Tasks.Task.find({ - userId: user._id, - $or: [ // Exclude completed todos - { type: 'todo', completed: false }, - { type: { $in: ['habit', 'daily', 'reward'] } }, - ], - }).exec(); + const teamsLed = await user.teamsLed(); + let tasksQuery; + + if (teamsLed.length > 0) { + tasksQuery = { + $and: [ + { + $or: [ + { userId: user._id }, + { userId: { $exists: false }, 'group.id': { $in: teamsLed } }, + ], + }, + { + $or: [ + { type: 'todo', completed: false }, + { type: { $in: ['habit', 'daily'] } }, + ], + }, + ], + }; + } else { + tasksQuery = { + userId: user._id, + $or: [ + { type: 'todo', completed: false }, + { type: { $in: ['habit', 'daily'] } }, + ], + }; + } + + const tasks = await Tasks.Task.find(tasksQuery).exec(); const tasksByType = { habits: [], dailys: [], todos: [], rewards: [], diff --git a/website/server/models/user/methods.js b/website/server/models/user/methods.js index c4231fbaa2..897be48dd1 100644 --- a/website/server/models/user/methods.js +++ b/website/server/models/user/methods.js @@ -1,6 +1,6 @@ import moment from 'moment'; import { - defaults, map, flatten, flow, compact, uniq, partialRight, + defaults, map, flatten, flow, compact, uniq, partialRight, remove, } from 'lodash'; import common from '../../../common'; @@ -498,6 +498,21 @@ schema.methods.isMemberOfGroupPlan = async function isMemberOfGroupPlan () { return groups.some(g => g.hasActiveGroupPlan()); }; +schema.methods.teamsLed = async function teamsLed () { + const user = this; + const groups = await getUserGroupData(user); + + remove(groups, group => !group.hasActiveGroupPlan); + remove(groups, group => user._id !== group.leader); + + const groupIds = []; + groups.forEach(group => { + groupIds.push(group._id); + }); + + return groupIds; +}; + schema.methods.isAdmin = function isAdmin () { return Boolean(this.contributor && this.contributor.admin); }; From 5bf4e18ce8bed10e1e1fd5ec01432169e41c12c7 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 5 Feb 2021 18:02:56 -0600 Subject: [PATCH 002/171] WIP(teams): don't damage leader for incomplete team Dailies --- website/common/script/ops/scoreTask.js | 1 + website/server/libs/cron.js | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/website/common/script/ops/scoreTask.js b/website/common/script/ops/scoreTask.js index 5a46d01ada..59645e4b71 100644 --- a/website/common/script/ops/scoreTask.js +++ b/website/common/script/ops/scoreTask.js @@ -104,6 +104,7 @@ function _gainMP (user, val) { // ===== CONSTITUTION ===== // TODO Decreases HP loss from bad habits / missed dailies by 0.5% per point. function _subtractPoints (user, task, stats, delta) { + if (task.group.id && !task.userId) return stats.hp; let conBonus = 1 - statsComputed(user).con / 250; if (conBonus < 0.1) conBonus = 0.1; diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index f08702d761..d44b4a4b4c 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -352,17 +352,18 @@ export async function cron (options = {}) { if (!user.party.quest.progress.down) user.party.quest.progress.down = 0; tasksByType.dailys.forEach(task => { + const isTeamBoardTask = task.group.id && !task.userId; if ( - task.group.assignedDate + !isTeamBoardTask && task.group.assignedDate && moment(task.group.assignedDate).isAfter(user.auth.timestamps.updated) ) return; const { completed } = task; // Deduct points for missed Daily tasks - let EvadeTask = 0; + let evadeTask = 0; let scheduleMisses = daysMissed; if (completed) { - dailyChecked += 1; + if (!isTeamBoardTask) dailyChecked += 1; if (!atLeastOneDailyDue) { // only bother checking until the first thing is found const thatDay = moment(now).subtract({ days: daysMissed }); atLeastOneDailyDue = shouldDo(thatDay.toDate(), task, user.preferences); @@ -377,15 +378,15 @@ export async function cron (options = {}) { if (shouldDo(thatDay.toDate(), task, user.preferences)) { atLeastOneDailyDue = true; scheduleMisses += 1; - if (user.stats.buffs.stealth) { + if (user.stats.buffs.stealth && !isTeamBoardTask) { user.stats.buffs.stealth -= 1; - EvadeTask += 1; + evadeTask += 1; } } if (multiDaysCountAsOneDay) break; } - if (scheduleMisses > EvadeTask) { + if (scheduleMisses > evadeTask) { // The user did not complete this due Daily // (but no penalty if cron is running in safe mode). if (CRON_SAFE_MODE) { @@ -411,7 +412,7 @@ export async function cron (options = {}) { user, task, direction: 'down', - times: multiDaysCountAsOneDay ? 1 : scheduleMisses - EvadeTask, + times: multiDaysCountAsOneDay ? 1 : scheduleMisses - evadeTask, cron: true, }); From 6e39c79cff0cd02632d84f3284c41328dc98470c Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 5 Feb 2021 18:30:22 -0600 Subject: [PATCH 003/171] WIP(teams): simplify task footers --- .../src/components/tasks/approvalFooter.vue | 118 ------------------ 1 file changed, 118 deletions(-) diff --git a/website/client/src/components/tasks/approvalFooter.vue b/website/client/src/components/tasks/approvalFooter.vue index 7a8080667b..c1db06e9f5 100644 --- a/website/client/src/components/tasks/approvalFooter.vue +++ b/website/client/src/components/tasks/approvalFooter.vue @@ -2,58 +2,12 @@
- - -
@@ -132,78 +86,6 @@ export default { ) return true; return false; }, - approvalRequested () { - if ( - (this.task.approvals && this.task.approvals.length === 1) - || (this.task.group && this.task.group.approval && this.task.group.approval.requested) - ) { - return true; - } - return false; - }, - multipleApprovalsRequested () { - if (this.task.approvals && this.task.approvals.length > 1) return true; - return false; - }, - }, - methods: { - async claim () { - let taskId = this.task._id; - // If we are on the user task - if (this.task.userId) { - taskId = this.task.group.taskId; - } - - await this.$store.dispatch('tasks:assignTask', { - taskId, - userId: this.user._id, - }); - this.task.group.assignedUsers.push(this.user._id); - this.sync(); - }, - async unassign () { - if (!window.confirm(this.$t('confirmUnClaim'))) return; // eslint-disable-line no-alert - - let taskId = this.task._id; - // If we are on the user task - if (this.task.userId) { - taskId = this.task.group.taskId; - } - - await this.$store.dispatch('tasks:unassignTask', { - taskId, - userId: this.user._id, - }); - const index = this.task.group.assignedUsers.indexOf(this.user._id); - this.task.group.assignedUsers.splice(index, 1); - - this.sync(); - }, - approve () { - const userIdToApprove = this.task.group.assignedUsers[0]; - this.$store.dispatch('tasks:approve', { - taskId: this.task._id, - userId: userIdToApprove, - }); - this.task.group.assignedUsers.splice(0, 1); - this.task.approvals.splice(0, 1); - - this.sync(); - }, - needsWork () { - if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert - const userIdNeedsMoreWork = this.task.group.assignedUsers[0]; - this.$store.dispatch('tasks:needsWork', { - taskId: this.task._id, - userId: userIdNeedsMoreWork, - }); - this.task.approvals.splice(0, 1); - - this.sync(); - }, - showRequests () { - this.$root.$emit('bv::show::modal', 'approval-modal'); - }, }, }; From 248e1c6fe91a86cb6f274e1b0947e142cc22e46e Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Mon, 8 Feb 2021 17:03:19 -0600 Subject: [PATCH 004/171] WIP(teams): reimplement open tasking --- website/client/src/components/tasks/task.vue | 70 ++++++++++--------- .../src/components/tasks/yesterdailyModal.vue | 4 +- website/client/src/store/getters/tasks.js | 29 ++++---- website/common/script/ops/scoreTask.js | 2 +- website/server/controllers/api-v3/tasks.js | 7 -- .../server/controllers/api-v3/tasks/groups.js | 23 ++---- website/server/models/task.js | 18 +++-- 7 files changed, 76 insertions(+), 77 deletions(-) diff --git a/website/client/src/components/tasks/task.vue b/website/client/src/components/tasks/task.vue index 963c3f85f0..59426dcc1c 100644 --- a/website/client/src/components/tasks/task.vue +++ b/website/client/src/components/tasks/task.vue @@ -3,9 +3,9 @@
@@ -16,8 +16,7 @@ />
@@ -1048,12 +1038,26 @@ export default { }, teamManagerAccess () { if (!this.isGroupTask || !this.group) return true; + if (!this.group.leader && !this.group.managers) return false; return (this.group.leader._id === this.user._id || this.group.managers[this.user._id]); }, displayNotes () { if (this.isGroupTask && !this.isUser) return this.task.group.managerNotes; return this.task.notes; }, + isOpenTask () { + if (!this.isGroupTask) return false; + if (this.task.group.assignedUsers.length > 0) return false; + return true; + }, + showTaskLockIcon () { + if (this.isUser) return false; + if (this.isGroupTask) { + if (this.isOpenTask) return false; + if (this.task.group.assignedUsers.indexOf(this.user._id) !== -1) return false; + } + return true; + }, }, methods: { ...mapActions({ @@ -1121,6 +1125,8 @@ export default { setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0); }, async score (direction) { + if (this.showTaskLockIcon) return; + if (this.task.type === 'habit' && !this.task[direction]) return; if (this.isYesterdaily === true) { await this.beforeTaskScore(this.task); this.task.completed = !this.task.completed; diff --git a/website/client/src/components/tasks/yesterdailyModal.vue b/website/client/src/components/tasks/yesterdailyModal.vue index 0a24fbac8a..1c12e39fb8 100644 --- a/website/client/src/components/tasks/yesterdailyModal.vue +++ b/website/client/src/components/tasks/yesterdailyModal.vue @@ -84,6 +84,7 @@ import moment from 'moment'; import { mapState } from '@/libs/store'; import scoreTask from '@/mixins/scoreTask'; +import sync from '@/mixins/sync'; import Task from './task'; import LoadingSpinner from '../ui/loadingSpinner'; @@ -92,7 +93,7 @@ export default { Task, LoadingSpinner, }, - mixins: [scoreTask], + mixins: [scoreTask, sync], props: { yesterDailies: { type: Array, @@ -180,6 +181,7 @@ export default { this.isLoading = false; this.$root.$emit('bv::hide::modal', 'yesterdaily'); + if (this.$route.fullPath.indexOf('task-information') !== -1) this.sync(); }, }, }; diff --git a/website/client/src/store/getters/tasks.js b/website/client/src/store/getters/tasks.js index 525ffbdb26..a194f385b1 100644 --- a/website/client/src/store/getters/tasks.js +++ b/website/client/src/store/getters/tasks.js @@ -114,15 +114,20 @@ export function canEdit (store) { }; } -function _nonInteractive (task) { - return (task.group && task.group.id && !task.userId) - || (task.challenge && task.challenge.id && !task.userId) - || (task.group && task.group.approval && task.group.approval.requested - && task.type !== 'habit'); +function _nonInteractive (task, userId) { + if (task.userId) return false; + if (task.challenge && task.challenge.id) return true; + if ( + task.group + && task.group.assignedUsers.length > 0 + && task.group.assignedUsers.indexOf(userId) === -1 + ) return true; + return false; } export function getTaskClasses (store) { const userPreferences = store.state.user.data.preferences; + const userId = store.state.user.data._id; // Purpose can be one of the following strings: // Edit Modal: edit-modal-bg, edit-modal-text, edit-modal-icon @@ -171,7 +176,7 @@ export function getTaskClasses (store) { if (type === 'todo' || type === 'daily') { if (task.completed || (!shouldDo(dueDate, task, userPreferences) && type === 'daily')) { return { - bg: _nonInteractive(task) ? 'task-disabled-daily-todo-control-bg-noninteractive' : 'task-disabled-daily-todo-control-bg', + bg: _nonInteractive(task, userId) ? 'task-disabled-daily-todo-control-bg-noninteractive' : 'task-disabled-daily-todo-control-bg', checkbox: 'task-disabled-daily-todo-control-checkbox', inner: 'task-disabled-daily-todo-control-inner', content: 'task-disabled-daily-todo-control-content', @@ -179,28 +184,28 @@ export function getTaskClasses (store) { } return { - bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`, + bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`, checkbox: `task-${color}-control-checkbox`, inner: `task-${color}-control-inner-daily-todo`, icon: `task-${color}-control-icon`, }; } if (type === 'reward') { return { - bg: _nonInteractive(task) ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg', + bg: _nonInteractive(task, userId) ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg', }; } if (type === 'habit') { return { up: task.up ? { - bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`, - inner: _nonInteractive(task) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`, + bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`, + inner: _nonInteractive(task, userId) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`, icon: `task-${color}-control-icon`, } : { bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` }, down: task.down ? { - bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`, - inner: _nonInteractive(task) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`, + bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`, + inner: _nonInteractive(task, userId) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`, icon: `task-${color}-control-icon`, } : { bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` }, diff --git a/website/common/script/ops/scoreTask.js b/website/common/script/ops/scoreTask.js index 59645e4b71..708ce1d5f8 100644 --- a/website/common/script/ops/scoreTask.js +++ b/website/common/script/ops/scoreTask.js @@ -104,7 +104,7 @@ function _gainMP (user, val) { // ===== CONSTITUTION ===== // TODO Decreases HP loss from bad habits / missed dailies by 0.5% per point. function _subtractPoints (user, task, stats, delta) { - if (task.group.id && !task.userId) return stats.hp; + if (task.group.id && !task.userId && task.type === 'daily') return stats.hp; let conBonus = 1 - statsComputed(user).con / 250; if (conBonus < 0.1) conBonus = 0.1; diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 5c20a9f4d7..d6aa1cb958 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -654,13 +654,6 @@ api.updateTask = { // the other of the keys when using .toObject() // see https://github.com/Automattic/mongoose/issues/2749 - task.group.approval.required = false; - if (sanitizedObj.requiresApproval) { - task.group.approval.required = true; - } - if (sanitizedObj.sharedCompletion) { - task.group.sharedCompletion = sanitizedObj.sharedCompletion; - } if (sanitizedObj.managerNotes) { task.group.managerNotes = sanitizedObj.managerNotes; } diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 98da938496..e78735e588 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -208,26 +208,13 @@ api.assignTask = { const group = await Group.getGroup({ user, groupId: task.group.id, fields: groupFields }); if (!group) throw new NotFound(res.t('groupNotFound')); - if (canNotEditTasks(group, user, assignedUserId)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); + if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); const promises = []; const taskText = task.text; const userName = `@${user.auth.local.username}`; - if (user._id === assignedUserId) { - const managerIds = Object.keys(group.managers); - managerIds.push(group.leader); - const managers = await User.find({ _id: managerIds }, 'notifications preferences').exec(); - managers.forEach(manager => { - if (manager._id === user._id) return; - manager.addNotification('GROUP_TASK_CLAIMED', { - message: res.t('taskClaimed', { userName, taskText }, manager.preferences.language), - groupId: group._id, - taskId: task._id, - }); - promises.push(manager.save()); - }); - } else { + if (user._id !== assignedUserId) { assignedUser.addNotification('GROUP_TASK_ASSIGNED', { message: res.t('youHaveBeenAssignedTask', { managerName: userName, taskText }), taskId: task._id, @@ -283,7 +270,7 @@ api.unassignTask = { const group = await Group.getGroup({ user, groupId: task.group.id, fields }); if (!group) throw new NotFound(res.t('groupNotFound')); - if (canNotEditTasks(group, user, assignedUserId)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); + if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); await group.unlinkTask(task, assignedUser); @@ -531,7 +518,7 @@ api.getGroupApprovals = { if (canNotEditTasks(group, user)) { approvals = await Tasks.Task.find({ 'group.id': groupId, - 'group.approval.approved': false, + 'group.approval.approved': { $ne: true }, 'group.approval.requested': true, 'group.assignedUsers': user._id, userId: user._id, @@ -541,7 +528,7 @@ api.getGroupApprovals = { } else { approvals = await Tasks.Task.find({ 'group.id': groupId, - 'group.approval.approved': false, + 'group.approval.approved': { $ne: true }, 'group.approval.requested': true, }, 'userId group text') .populate('userId', 'profile') diff --git a/website/server/models/task.js b/website/server/models/task.js index 604de20ca9..1b40940f4c 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -216,8 +216,6 @@ TaskSchema.statics.findMultipleByIdOrAlias = async function findByIdOrAlias ( if (!userId) throw new Error('User identifier is a required argument'); const query = _.cloneDeep(additionalQueries); - query.userId = userId; - const ids = []; const aliases = []; @@ -229,10 +227,18 @@ TaskSchema.statics.findMultipleByIdOrAlias = async function findByIdOrAlias ( } }); - query.$or = [ - { _id: { $in: ids } }, - { alias: { $in: aliases } }, - ]; + if (ids.length > 0 && aliases.length > 0) { + query.$or = [ + { _id: { $in: ids } }, + { alias: { $in: aliases } }, + ]; + } else if (ids.length > 0) { + query._id = { $in: ids }; + } else if (aliases.length > 0) { + query.alias = { $in: aliases }; + } else { + throw new Error('No identifiers found.'); // Should be covered by the !identifiers check, but.. + } const tasks = await this.find(query).exec(); From 6cddb3bf8267054d179d610896edb4db3622f90c Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Tue, 9 Feb 2021 17:08:43 -0600 Subject: [PATCH 005/171] WIP(teams): more partial fixing --- website/client/src/components/tasks/task.vue | 8 +- website/server/libs/cron.js | 13 --- website/server/libs/groupTasks.js | 35 +------ website/server/libs/tasks/index.js | 97 +++++--------------- 4 files changed, 26 insertions(+), 127 deletions(-) diff --git a/website/client/src/components/tasks/task.vue b/website/client/src/components/tasks/task.vue index 59426dcc1c..becdd85e9f 100644 --- a/website/client/src/components/tasks/task.vue +++ b/website/client/src/components/tasks/task.vue @@ -9,11 +9,6 @@ ]" @click="castEnd($event, task)" > -
{ i.completed = false; }); } } - - if (task.group && task.group.approval && task.group.approval.approved) { - task.group.approval.approved = false; - task.group.approval.dateApproved = null; - task.group.approval.requested = false; - task.group.approval.requestedDate = null; - } }); resetHabitCounters(user, tasksByType, now, daysMissed); @@ -464,12 +457,6 @@ export async function cron (options = {}) { if (task.up === false || task.down === false) { task.value = Math.abs(task.value) < 0.1 ? 0 : task.value /= 2; } - if (task.group && task.group.approval && task.group.approval.approved) { - task.group.approval.approved = false; - task.group.approval.dateApproved = null; - task.group.approval.requested = false; - task.group.approval.requestedDate = null; - } }); // Finished tallying diff --git a/website/server/libs/groupTasks.js b/website/server/libs/groupTasks.js index baad36e57d..f5604d3488 100644 --- a/website/server/libs/groupTasks.js +++ b/website/server/libs/groupTasks.js @@ -6,11 +6,6 @@ const SHARED_COMPLETION = { every: 'allAssignedCompletion', }; -async function _completeMasterTask (masterTask) { - masterTask.completed = true; - await masterTask.save(); -} - async function _deleteUnfinishedTasks (groupMemberTask) { await Tasks.Task.deleteMany({ 'group.taskId': groupMemberTask.group.taskId, @@ -21,33 +16,11 @@ async function _deleteUnfinishedTasks (groupMemberTask) { }).exec(); } -async function _evaluateAllAssignedCompletion (masterTask) { - let completions; - if (masterTask.group.approval && masterTask.group.approval.required) { - completions = await Tasks.Task.countDocuments({ - 'group.taskId': masterTask._id, - 'group.approval.approved': true, - }).exec(); - } else { - completions = await Tasks.Task.countDocuments({ - 'group.taskId': masterTask._id, - completed: true, - }).exec(); - } - if (completions >= masterTask.group.assignedUsers.length) { - await _completeMasterTask(masterTask); - } -} - async function handleSharedCompletion (masterTask, groupMemberTask) { - if (masterTask.type !== 'todo') return; - - if (masterTask.group.sharedCompletion === SHARED_COMPLETION.single) { - await _deleteUnfinishedTasks(groupMemberTask); - await _completeMasterTask(masterTask); - } else if (masterTask.group.sharedCompletion === SHARED_COMPLETION.every) { - await _evaluateAllAssignedCompletion(masterTask); - } + if (masterTask.type === 'reward') return; + if (masterTask.type === 'todo') await _deleteUnfinishedTasks(groupMemberTask); + masterTask.completed = groupMemberTask.completed; + await masterTask.save(); } export { diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index 2e33d1da9a..0c8b8f472a 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -8,7 +8,6 @@ import { } from './utils'; import { model as Challenge } from '../../models/challenge'; import { model as Group } from '../../models/group'; -import { model as User } from '../../models/user'; import * as Tasks from '../../models/task'; import apiError from '../apiError'; import { @@ -336,69 +335,18 @@ async function scoreTask (user, task, direction, req, res) { } } - if (task.group.approval.required && !task.group.approval.approved) { - const fields = requiredGroupFields.concat(' managers'); - const group = await Group.getGroup({ user, groupId: task.group.id, fields }); + let localTask; - const managerIds = Object.keys(group.managers); - managerIds.push(group.leader); - - if (managerIds.indexOf(user._id) !== -1) { - task.group.approval.approved = true; - task.group.approval.requested = true; - task.group.approval.requestedDate = new Date(); - } else { - if (task.group.approval.requested) { - return { - task, - requiresApproval: true, - message: res.t('taskRequiresApproval'), - }; - } - - task.group.approval.requested = true; - task.group.approval.requestedDate = new Date(); - - const managers = await User.find({ _id: managerIds }, 'notifications preferences').exec(); // Use this method so we can get access to notifications - - // @TODO: we can use the User.pushNotification function because - // we need to ensure notifications are translated - const managerPromises = []; - managers.forEach(manager => { - manager.addNotification('GROUP_TASK_APPROVAL', { - message: res.t('userHasRequestedTaskApproval', { - user: user.profile.name, - taskName: task.text, - }, manager.preferences.language), - groupId: group._id, - // user task id, used to match the notification when the task is approved - taskId: task._id, - userId: user._id, - groupTaskId: task.group.taskId, // the original task id - direction, - }); - managerPromises.push(manager.save()); - }); - - managerPromises.push(task.save()); - await Promise.all(managerPromises); - - return { - task, - requiresApproval: true, - message: res.t('taskApprovalHasBeenRequested'), - }; + if (task.group.id && !task.userId && task.group.assignedUsers.length > 0) { + // Task is being scored from team board, and a user copy should exist + if (!task.group.assignedUsers.includes(user._id)) { + throw new BadRequest('Task has not been assigned to this user.'); } - } - if (task.group.approval.required && task.group.approval.approved) { - const notificationIndex = user.notifications.findIndex(notification => notification - && notification.data && notification.data.task - && notification.data.task._id === task._id && notification.type === 'GROUP_TASK_APPROVED'); - - if (notificationIndex !== -1) { - user.notifications.splice(notificationIndex, 1); - } + localTask = await Tasks.Task.findOne( + { userId: user._id, 'group.taskId': task._id }, + ).exec(); + if (!localTask) throw new NotFound('Task not found.'); } const wasCompleted = task.completed; @@ -411,26 +359,30 @@ async function scoreTask (user, task, direction, req, res) { // If a todo was completed or uncompleted move it in or out of the user.tasksOrder.todos list // TODO move to common code? - let pullTask = false; - let pushTask = false; + let pullTask; + let pushTask; if (task.type === 'todo') { if (!wasCompleted && task.completed) { // @TODO: mongoose's push and pull should be atomic and help with // our concurrency issues. If not, we need to use this update $pull and $push - pullTask = true; - // user.tasksOrder.todos.pull(task._id); + pullTask = localTask ? localTask._id : task._id; } else if ( wasCompleted && !task.completed && user.tasksOrder.todos.indexOf(task._id) === -1 ) { - pushTask = true; - // user.tasksOrder.todos.push(task._id); + pushTask = localTask ? localTask._id : task._id; } } setNextDue(task, user); + if (localTask) { + localTask.completed = task.completed; + localTask.value = task.value + delta; + await localTask.save(); + } + taskScoredWebhook.send(user, { task, direction, @@ -514,8 +466,8 @@ export async function scoreTasks (user, taskScorings, req, res) { const pushIDs = []; returnDatas.forEach(returnData => { - if (returnData.pushTask === true) pushIDs.push(returnData.task._id); - if (returnData.pullTask === true) pullIDs.push(returnData.task._id); + if (returnData.pushTask) pushIDs.push(returnData.pushTask); + if (returnData.pullTask) pullIDs.push(returnData.pullTask); }); const moveUpdateObject = {}; @@ -532,13 +484,6 @@ export async function scoreTasks (user, taskScorings, req, res) { handleChallengeTask(data.task, data.delta, data.direction); handleGroupTask(data.task, data.delta, data.direction); - // Handle group tasks that require approval - if (data.requiresApproval === true) { - return { - id: data.task._id, message: data.message, requiresApproval: true, - }; - } - return { id: data.task._id, delta: data.delta, _tmp: data._tmp }; }); } From f4feb09fbc6cdabbf7c881c41b04d734011c6c04 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 3 Mar 2021 17:05:42 -0600 Subject: [PATCH 006/171] fix(cron): actually process cron for assigned tasks --- website/server/middlewares/cron.js | 32 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js index 4c86fb2817..fd9312e25d 100644 --- a/website/server/middlewares/cron.js +++ b/website/server/middlewares/cron.js @@ -82,7 +82,7 @@ async function cronAsync (req, res) { { $or: [ { userId: user._id }, - { userId: { $exists: false }, 'group.id': { $in: teamsLed } }, + { userId: { $exists: false }, 'group.id': { $in: teamsLed }, 'group.assignedUsers': { $size: 0 } }, ], }, { @@ -141,20 +141,36 @@ async function cronAsync (req, res) { // Save user and tasks const toSave = [user.save()]; - tasks.forEach(async task => { + const groupTasks = []; + const groupTasksByType = { + habits: [], dailys: [], todos: [], rewards: [], + }; + for (const task of tasks) { if (task.isModified()) toSave.push(task.save()); if (task.isModified() && task.group && task.group.taskId) { - const groupTask = await Tasks.Task.findOne({ + const groupTask = await Tasks.Task.findOne({ // eslint-disable-line no-await-in-loop _id: task.group.taskId, }).exec(); - if (groupTask) { - let delta = (0.9747 ** task.value) * -1; - if (groupTask.group.assignedUsers) delta /= groupTask.group.assignedUsers.length; - await groupTask.scoreChallengeTask(delta, 'down'); + groupTasks.push(groupTask); + groupTasksByType[`${groupTask.type}s`].push(groupTask); } } - }); + } + if (groupTasks.length > 0) { + await cron({ + user, + tasksByType: groupTasksByType, + now, + daysMissed, + analytics, + timezoneUtcOffsetFromUserPrefs, + headers: req.headers, + }); + groupTasks.forEach(async cronnedGroupTask => { + if (cronnedGroupTask.isModified()) toSave.push(cronnedGroupTask.save()); + }); + } await Promise.all(toSave); await Group.processQuestProgress(user, progress); From 7dbee4caed4404cf56ed437a6325b9947a7dd184 Mon Sep 17 00:00:00 2001 From: negue Date: Wed, 31 Mar 2021 00:44:49 +0200 Subject: [PATCH 007/171] clone selectMulti to selectSingle for assignedMember --- .../tasks/modal-controls/selectMulti.vue | 2 +- .../tasks/modal-controls/selectSingle.vue | 291 ++++++++++++++++++ .../client/src/components/tasks/taskModal.vue | 32 +- 3 files changed, 308 insertions(+), 17 deletions(-) create mode 100644 website/client/src/components/tasks/modal-controls/selectSingle.vue diff --git a/website/client/src/components/tasks/modal-controls/selectMulti.vue b/website/client/src/components/tasks/modal-controls/selectMulti.vue index 5c993196f2..f8781a8d73 100644 --- a/website/client/src/components/tasks/modal-controls/selectMulti.vue +++ b/website/client/src/components/tasks/modal-controls/selectMulti.vue @@ -1,4 +1,4 @@ -multi
t.id !== this.selected); - const searchString = this.search.toLowerCase(); - const filteredItems = availableItems.filter(i => i.name.toLowerCase().includes(searchString)); + const filteredItems = this.allItems.filter(i => i.name.toLowerCase().includes(searchString)); return filteredItems; }, }, watch: { selected () { - this.$emit('changed', this.selected); + this.$emit('changed', this.selectedItem); }, }, created () { @@ -253,14 +256,13 @@ export default { $event.preventDefault(); } }, - closeIfOpen () { - this.closeSelectPopup(); - }, selectItem (item) { - this.$emit('toggle', item.id); - }, - removeItem () { - this.$emit('toggle', null); + if (item.id === this.selectedItem) { + this.$emit('toggle', null); + } else { + this.$emit('toggle', item.id); + } + this.closeSelectPopup(); }, hideCallback ($event) { if (this.preventHide) { diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue index 7fe26c9561..81f96c8716 100644 --- a/website/client/src/components/tasks/taskModal.vue +++ b/website/client/src/components/tasks/taskModal.vue @@ -1489,20 +1489,23 @@ export default { this.$emit('cancel'); }, async toggleAssignment (memberId) { - if (this.purpose === 'create') { - return; - } + if (this.purpose !== 'create') { + if (this.assignedMember === null) { + await this.$store.dispatch('tasks:unassignTask', { + taskId: this.task._id, + userId: memberId, + }); + } else { + await this.$store.dispatch('tasks:unassignTask', { + taskId: this.task._id, + userId: this.assignedMember, + }); - if (this.assignedMember === null) { - await this.$store.dispatch('tasks:unassignTask', { - taskId: this.task._id, - userId: memberId, - }); - } else { - await this.$store.dispatch('tasks:assignTask', { - taskId: this.task._id, - userId: memberId, - }); + await this.$store.dispatch('tasks:assignTask', { + taskId: this.task._id, + userId: memberId, + }); + } } this.assignedMember = memberId; From ae0528e5cd1a379b443df30f0ddd418394ccc963 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Wed, 14 Apr 2021 16:42:56 -0500 Subject: [PATCH 009/171] WIP(teams): fix initially unassigned task, add completedBy data --- website/client/src/components/tasks/taskModal.vue | 12 +++++++----- website/server/libs/groupTasks.js | 1 + website/server/libs/tasks/index.js | 4 ++++ website/server/models/task.js | 1 + 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue index 81f96c8716..b03faeb0f2 100644 --- a/website/client/src/components/tasks/taskModal.vue +++ b/website/client/src/components/tasks/taskModal.vue @@ -1458,11 +1458,13 @@ export default { tasks: [this.task], }); Object.assign(this.task, response); - await this.$store.dispatch('tasks:assignTask', { - taskId: this.task._id, - userId: this.assignedMember, - }); - this.task.group.assignedUsers = [this.assignedMember]; + if (this.assignedMember) { + await this.$store.dispatch('tasks:assignTask', { + taskId: this.task._id, + userId: this.assignedMember, + }); + this.task.group.assignedUsers = [this.assignedMember]; + } this.$emit('taskCreated', this.task); } else { this.createTask(this.task); diff --git a/website/server/libs/groupTasks.js b/website/server/libs/groupTasks.js index f5604d3488..50a5785304 100644 --- a/website/server/libs/groupTasks.js +++ b/website/server/libs/groupTasks.js @@ -20,6 +20,7 @@ async function handleSharedCompletion (masterTask, groupMemberTask) { if (masterTask.type === 'reward') return; if (masterTask.type === 'todo') await _deleteUnfinishedTasks(groupMemberTask); masterTask.completed = groupMemberTask.completed; + masterTask.group.completedBy = groupMemberTask.userId; await masterTask.save(); } diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index 0c8b8f472a..793f4087a1 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -375,6 +375,10 @@ async function scoreTask (user, task, direction, req, res) { } } + if (task.completed && task.group.id && !task.userId) { + task.group.completedBy = user._id; + } + setNextDue(task, user); if (localTask) { diff --git a/website/server/models/task.js b/website/server/models/task.js index 1b40940f4c..37bf90e250 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -147,6 +147,7 @@ export const TaskSchema = new Schema({ default: SHARED_COMPLETION.single, }, managerNotes: { $type: String }, + completedBy: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for group completing user.'] }, }, reminders: [reminderSchema], From a88f97831a133546887a136e1b2c87bfd5680f4b Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Tue, 20 Apr 2021 16:04:29 -0500 Subject: [PATCH 010/171] fix(teams): update single select style and correct create/edit issue --- .../tasks/modal-controls/selectSingle.vue | 78 ++++++++++++++----- .../client/src/components/tasks/taskModal.vue | 13 +--- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/website/client/src/components/tasks/modal-controls/selectSingle.vue b/website/client/src/components/tasks/modal-controls/selectSingle.vue index 9015540a10..3d471d0019 100644 --- a/website/client/src/components/tasks/modal-controls/selectSingle.vue +++ b/website/client/src/components/tasks/modal-controls/selectSingle.vue @@ -4,6 +4,7 @@ ref="dropdown" class="inline-dropdown select-multi" :toggle-class="isOpened ? 'active' : null" + :class="{'margin-adjust': selectedItem}" @show="wasOpened()" @hide="hideCallback($event)" @toggle="openOrClose($event)" @@ -21,9 +22,18 @@ @@ -323,13 +323,14 @@ export default { return this.$t('error'); // task is open, or the other conditions aren't hitting right }, singleAssignLastDone () { - const userId = this.task.group.assignedUsers[0]; - const completion = this.task.group.assignedUsersDetail[userId]; - return completion.completedDate; + const completion = this.task?.group?.assignedUsersDetail[this.user._id]; + if (completion) return completion.completedDate; + return null; }, showGreen () { if (this.assignedUsersCount !== 1) return false; - return this.singleAssignLastDone && moment().diff(this.singleAssignLastDone, 'days') < 1; + return this.singleAssignLastDone + && this.task?.group?.assignedUsersDetail[this.user._id].completed; }, formattedCompletionTime () { if (!this.singleAssignLastDone) return ''; diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue index 2b06f1eb72..65fa9e10b8 100644 --- a/website/client/src/components/tasks/taskModal.vue +++ b/website/client/src/components/tasks/taskModal.vue @@ -592,7 +592,7 @@ @import '~@/assets/scss/colors.scss'; #task-modal { - a { + a:not(.dropdown-item) { font-size: 12px; line-height: 1.33; color: $blue-10; diff --git a/website/client/src/components/tasks/taskSummary.vue b/website/client/src/components/tasks/taskSummary.vue index e88555dbed..1febc771fc 100644 --- a/website/client/src/components/tasks/taskSummary.vue +++ b/website/client/src/components/tasks/taskSummary.vue @@ -75,7 +75,8 @@
@@ -142,6 +143,7 @@ import loginIncentives from './achievements/login-incentives'; import onboardingComplete from './achievements/onboardingComplete'; import verifyUsername from './settings/verifyUsername'; import firstDrops from './achievements/firstDrops'; +import groupPlansUpdate from './group-plans/groupPlansUpdateModal'; const NOTIFICATIONS = { // general notifications @@ -268,6 +270,7 @@ export default { genericAchievement, onboardingComplete, firstDrops, + groupPlansUpdate, }, mixins: [notifications, guide], data () { diff --git a/website/client/src/mixins/guide.js b/website/client/src/mixins/guide.js index 0234196f6b..dfe9314025 100644 --- a/website/client/src/mixins/guide.js +++ b/website/client/src/mixins/guide.js @@ -136,8 +136,8 @@ export default { case 'patrons': return this.goto('hall', 0); case 'items': return this.goto('market', 0); case 'stable': return this.goto('pets', 0); - // @TODO: same page now case 'stable': return this.goto('mounts', 0); case 'equipment': return this.goto('equipment', 0); + case 'groupPlanDetailTaskInformation': return this.alternativeModal('groupPlans', 'group-plans-update'); } return null; @@ -172,6 +172,10 @@ export default { this.markTourComplete(chapter); }); }, + alternativeModal (chapter, modal) { + this.$root.$emit('bv::show::modal', modal); + this.markTourComplete(chapter); + }, markTourComplete (chapter) { const ups = {}; const lastKnownStep = this.user.flags.tour[chapter]; diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 4d15c903c7..dd39eeb687 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -370,5 +370,22 @@ "viewStatus": "Status", "lastCompleted": "Last completed", "you": "You", - "chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later." + "chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later.", + "newGroupsWelcome": "Welcome to the New Shared Task Board!", + "newGroupsWhatsNew": "Check Out What's New:", + "newGroupsBullet01": "Interact with tasks directly from the shared task board", + "newGroupsBullet02": "Anyone can complete an unassigned task", + "newGroupsBullet03": "Shared tasks reset at the same time for everyone for easier collaboration", + "newGroupsBullet04": "Shared Dailies will not cause damage when missed or appear in the Record Yesterday’s Activity prompt", + "newGroupsBullet05": "Shared tasks will degrade in color if left incomplete to help track progress", + "newGroupsBullet06": "The task status view allows you to quickly see which assignee has completed a task", + "newGroupsBullet07": "Toggle the ability to display the shared tasks on your personal task board", + "newGroupsBullet08": "The group leader and managers can quickly add tasks from the top of the task columns", + "newGroupsBullet09": "A shared task can be unchecked to show it still needs work", + "newGroupsBullet10": "Assignment status determines completion condition:", + "newGroupsBullet10a": "Leave a task unassigned if any member can complete it", + "newGroupsBullet10b": "Assign a task to one member so only they can complete it", + "newGroupsBullet10c": "Assign a task to multiple members if they all need to complete it", + "newGroupsVisitFAQ": "Visit the FAQ from the Help dropdown for more guidance.", + "newGroupsEnjoy": "We hope you enjoy the new Group Plans experience!" } diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index f7f4c41c25..3e0b4b84c0 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -236,6 +236,7 @@ export default new Schema({ mounts: { $type: Number, default: -1 }, hall: { $type: Number, default: -1 }, equipment: { $type: Number, default: -1 }, + groupPlans: { $type: Number, default: -1 }, }, tutorial: { common: { From 49a20218a5ba72d8f3329438fc2e4f142ae69a5a Mon Sep 17 00:00:00 2001 From: SabreCat Date: Fri, 19 Aug 2022 14:09:41 -0500 Subject: [PATCH 123/171] fix(onboarding): improve modal launching and clicky behavior --- .../src/components/group-plans/groupPlansUpdateModal.vue | 3 +++ .../client/src/components/group-plans/taskInformation.vue | 3 +++ website/client/src/mixins/guide.js | 6 +----- website/common/locales/en/groups.json | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/website/client/src/components/group-plans/groupPlansUpdateModal.vue b/website/client/src/components/group-plans/groupPlansUpdateModal.vue index 35600137f5..e79c2dd39f 100644 --- a/website/client/src/components/group-plans/groupPlansUpdateModal.vue +++ b/website/client/src/components/group-plans/groupPlansUpdateModal.vue @@ -4,6 +4,8 @@ title="New Shared Task Board" size="lg" hide-footer="hide-footer" + :no-close-on-backdrop="true" + :no-close-on-esc="true" >
Leave a task unassigned if any member can complete it", "newGroupsBullet10b": "Assign a task to one member so only they can complete it", "newGroupsBullet10c": "Assign a task to multiple members if they all need to complete it", - "newGroupsVisitFAQ": "Visit the FAQ from the Help dropdown for more guidance.", + "newGroupsVisitFAQ": "Visit the FAQ from the Help dropdown for more guidance.", "newGroupsEnjoy": "We hope you enjoy the new Group Plans experience!" } From f063b9e81cfb1c336db1edcdfdf9672bd7bf9e8f Mon Sep 17 00:00:00 2001 From: SabreCat Date: Fri, 19 Aug 2022 16:10:18 -0500 Subject: [PATCH 124/171] fix(tests): sanity and common --- test/common/ops/scoreTask.test.js | 12 ------------ .../client/src/components/tasks/approvalFooter.vue | 2 +- website/common/locales/en/groups.json | 2 +- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/test/common/ops/scoreTask.test.js b/test/common/ops/scoreTask.test.js index 7282359aff..bee676a1fd 100644 --- a/test/common/ops/scoreTask.test.js +++ b/test/common/ops/scoreTask.test.js @@ -249,18 +249,6 @@ describe('shared.ops.scoreTask', () => { expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta); }); - it('does not modify stats when task need approval', () => { - todo.group.approval.required = true; - options = { - user: ref.afterUser, task: todo, direction: 'up', times: 5, cron: false, - }; - scoreTask(options); - - expect(ref.afterUser.stats.hp).to.eql(50); - expect(ref.afterUser.stats.exp).to.equal(ref.beforeUser.stats.exp); - expect(ref.afterUser.stats.gp).to.equal(ref.beforeUser.stats.gp); - }); - context('habits', () => { it('up', () => { options = { diff --git a/website/client/src/components/tasks/approvalFooter.vue b/website/client/src/components/tasks/approvalFooter.vue index f2b128f9db..511aea25e6 100644 --- a/website/client/src/components/tasks/approvalFooter.vue +++ b/website/client/src/components/tasks/approvalFooter.vue @@ -303,7 +303,7 @@ export default { return `@${userName} completed at ${moment(completedDate).format('h:mm A')}`; } if (this.userIsAssigned) { - return this.$t('you'); + return this.$t('youEmphasized'); } return `@${userName}`; } diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 0c838b18da..4ceb9cebee 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -369,7 +369,7 @@ "dayStart": "Day start: <%= startTime %>", "viewStatus": "Status", "lastCompleted": "Last completed", - "you": "You", + "youEmphasized": "You", "chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later.", "newGroupsWelcome": "Welcome to the New Shared Task Board!", "newGroupsWhatsNew": "Check Out What's New:", From 595c1313981da8bbda707a9548f0afe22e4da1e5 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Fri, 19 Aug 2022 17:13:48 -0500 Subject: [PATCH 125/171] fix(tests): catch non-array parameter --- website/server/models/group.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/website/server/models/group.js b/website/server/models/group.js index 91b1a5ed33..ff4d43d0be 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -1447,7 +1447,13 @@ schema.methods.unlinkTags = function unlinkTags (user) { schema.methods.syncTask = async function groupSyncTask (taskToSync, users, assigningUser) { const group = this; const toSave = []; - for (const user of users) { + let usersArray; + if (!Array.isArray(users)) { + usersArray = [users]; + } else { + usersArray = users; + } + for (const user of usersArray) { const assignmentData = { assignedDate: new Date(), assignedUsername: user.auth.local.username, From 89c07529eacff3e554ad93a92b23476c326b35a8 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 11:27:08 -0500 Subject: [PATCH 126/171] feat(teams): add FAQ entry Also a few client side fixes --- .../components/header/notificationsDropdown.vue | 8 +------- website/client/src/components/static/faq.vue | 6 +----- website/client/src/components/tasks/task.vue | 4 ++-- website/client/src/components/tasks/taskModal.vue | 2 +- website/common/locales/en/faq.json | 5 ++++- website/server/controllers/api-v3/tasks/groups.js | 15 +++++++++++++++ 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/website/client/src/components/header/notificationsDropdown.vue b/website/client/src/components/header/notificationsDropdown.vue index 4c075820cb..3d83ec3e3e 100644 --- a/website/client/src/components/header/notificationsDropdown.vue +++ b/website/client/src/components/header/notificationsDropdown.vue @@ -129,8 +129,6 @@ import GUILD_INVITATION from './notifications/guildInvitation'; import PARTY_INVITATION from './notifications/partyInvitation'; import CHALLENGE_INVITATION from './notifications/challengeInvitation'; import QUEST_INVITATION from './notifications/questInvitation'; -import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval'; -import GROUP_TASK_APPROVED from './notifications/groupTaskApproved'; import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned'; import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed'; import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints'; @@ -155,8 +153,6 @@ export default { PARTY_INVITATION, CHALLENGE_INVITATION, QUEST_INVITATION, - GROUP_TASK_APPROVAL, - GROUP_TASK_APPROVED, GROUP_TASK_ASSIGNED, GROUP_TASK_CLAIMED, UNALLOCATED_STATS_POINTS, @@ -182,7 +178,7 @@ export default { openStatus: undefined, actionableNotifications: [ 'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION', - 'QUEST_INVITATION', 'GROUP_TASK_APPROVED', + 'QUEST_INVITATION', ], // A list of notifications handled by this component, // listed in the order they should appear in the notifications panel. @@ -196,8 +192,6 @@ export default { 'CHALLENGE_INVITATION', 'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', - 'GROUP_TASK_APPROVAL', - 'GROUP_TASK_APPROVED', 'GROUP_TASK_CLAIMED', 'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED', diff --git a/website/client/src/components/static/faq.vue b/website/client/src/components/static/faq.vue index 8398dffcc0..b9bb3a9d6e 100644 --- a/website/client/src/components/static/faq.vue +++ b/website/client/src/components/static/faq.vue @@ -94,6 +94,7 @@ export default { 'gems', 'bugs-features', 'world-boss', + 'group-plans', ]; const hash = window.location.hash.replace('#', ''); @@ -105,11 +106,6 @@ export default { wikiTechAssistanceEmail: `mailto:${TECH_ASSISTANCE_EMAIL}`, }, visible: hash && headings.includes(hash) ? hash : null, - // @TODO webFaqStillNeedHelp: { - // linkStart: '[', - // linkEnd: '](/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)', - // }, - // "webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the <%= linkStart %>Habitica Help guild<%= linkEnd %>! We're happy to help." }; }, mounted () { diff --git a/website/client/src/components/tasks/task.vue b/website/client/src/components/tasks/task.vue index 6d71d33c30..fdc429eac2 100644 --- a/website/client/src/components/tasks/task.vue +++ b/website/client/src/components/tasks/task.vue @@ -307,7 +307,7 @@ >
@@ -317,7 +317,7 @@ >
diff --git a/website/common/locales/en/faq.json b/website/common/locales/en/faq.json index 46df7df048..ef9583c64b 100644 --- a/website/common/locales/en/faq.json +++ b/website/common/locales/en/faq.json @@ -68,5 +68,8 @@ "iosFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.", "androidFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.", - "webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the [Habitica Help guild](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! We're happy to help." + "webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the [Habitica Help guild](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! We're happy to help.", + + "faqQuestion13": "What is a Group Plan?", + "webFaqAnswer13": "## How do Group Plans work?\n\nA [Group Plan](/group-plans) gives your Party or Guild access to a shared task board that’s similar to your personal task board! It’s a shared Habitica experience where tasks can be created and checked off by anyone in the group.\n\nThere are also features available like member roles, status view, and task assigning that give you a more controlled experience. [Visit our wiki](https://habitica.fandom.com/wiki/Group_Plans) to learn more about our Group Plans’ features!\n\n## Who benefits from a Group Plan?\n\nGroup Plans work best when you have a small team of people who want to collaborate together. We recommend 2-5 members.\n\nGroup Plans are great for families, whether it’s a parent and child or you and a partner. Shared goals, chores, or responsibilities are easy to keep track of on one board.\n\nGroup Plans can also be useful for teams of colleagues that have shared goals, or managers that want to introduce their employees to gamification.\n\n## Quick tips for using Groups\n\n* If you want a member to be able to create and edit tasks, make them a manager\n* Leave tasks unassigned if anyone can complete it and it only needs done once\n* Assign a task to one person to make sure no one else can complete their task\n* Assign a task to multiple people if they all need to complete it\n* Copy tasks to your personal board to not miss anything\n* You get rewarded for the tasks you complete, even multi-assigned\n* Task completion rewards aren’t shared or split between Team members\n* Use task color on the team board to judge the average completion rate of tasks\n* Regularly review the tasks on your Team Board to make sure they are still relevant\n* Missing a Daily won’t damage you or your team, but the task will degrade in color\n\n## How can others in the group create tasks?\n\nOnly the group leader and managers can create tasks. If you’d like a group member to be able to create tasks, then you should promote them to be a manager by going to the Group Information tab, viewing the member list, and clicking the dot icon by their name.\n\n## How does assigning a task work?\n\nGroup Plans give you the unique ability to assign tasks to other group members. Assigning a task is great for delegating. If you assign a task to someone, then other members are prevented from completing it.\n\nYou can also assign a task to multiple people if it needs to be completed by more than one member. For example, if everyone has to brush their teeth, create a task and assign it to each group member. They will all be able to check it off and get their individual rewards for doing so. The main task will show as complete once everyone checks it off.\n\n## How do unassigned tasks work?\n\nUnassigned tasks can be completed by anyone in the group, so leave a task unassigned to allow any member to complete it. For example, taking out the trash. Whoever takes out the trash can check off the unassigned task and it will show as completed for everyone.\n\n## How does the synchronized day reset work?\n\nShared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the group leader’s day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterday’s uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in color to help visualize progress. We don’t want the shared experience to be a negative one!\n\n## How do I use my Group on the mobile apps?\n\nWhile the mobile apps don’t fully support all Group Plans functionality yet, you can still complete shared tasks from the iOS and Android app. On the browser version of Habitica, go to your group’s shared task board and turn on the copy tasks toggle. Now all open and assigned shared tasks will display on your personal task board across all platforms.\n\n## What’s the difference between a Group’s shared tasks and Challenges?\n\nGroup Plan shared task boards are more dynamic than Challenges, in that they can constantly be updated and interacted with. Challenges are great if you have one set of tasks to send out to many people.\n\nGroup Plans are also a paid feature, while Challenges are available free to everyone.\n\nYou cannot assign specific tasks in Challenges, and Challenges do not have a shared day reset. In general, Challenges offer less control and direct interaction." } diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 0fbc03cc98..b9a4d0cb44 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -370,6 +370,21 @@ api.taskNeedsWork = { if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); await scoreTasks(assignedUser, [{ id: task._id, direction: 'down' }], req, res); + assignedUser.addNotification('GROUP_TASK_NEEDS_WORK', { + message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.profile.name }, assignedUser.preferences.language), + task: { + id: task._id, + text: task.text, + }, + group: { + id: group._id, + name: group.name, + }, + manager: { + id: user._id, + name: user.profile.name, + }, + }); await Promise.all([assignedUser.save(), task.save()]); res.respond(200, task); From 631d7111a577a7b79a5ea53ee0ca8eee4f7235b2 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 11:39:53 -0500 Subject: [PATCH 127/171] fix(teams): send @username in notifications --- website/common/locales/en/groups.json | 2 +- website/server/controllers/api-v3/tasks/groups.js | 4 ++-- website/server/libs/tasks/index.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 4ceb9cebee..f777bd766d 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -201,7 +201,7 @@ "yourTaskHasBeenApproved": "Your task <%- taskText %> has been approved.", "thisTaskApproved": "This task was approved", "taskClaimed": "<%- userName %> has claimed the task <%- taskText %>.", - "taskNeedsWork": "<%- taskText %> was unchecked by <%- managerName %>. Your rewards for completing the task were reverted.", + "taskNeedsWork": "<%- taskText %> was unchecked by @<%- managerName %>. Your rewards for completing the task were reverted.", "userHasRequestedTaskApproval": "<%- user %> requests approval for <%- taskName %>", "approve": "Approve", "approveTask": "Approve Task", diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index b9a4d0cb44..d32b1254fb 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -371,7 +371,7 @@ api.taskNeedsWork = { await scoreTasks(assignedUser, [{ id: task._id, direction: 'down' }], req, res); assignedUser.addNotification('GROUP_TASK_NEEDS_WORK', { - message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.profile.name }, assignedUser.preferences.language), + message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.auth.local.username }, assignedUser.preferences.language), task: { id: task._id, text: task.text, @@ -382,7 +382,7 @@ api.taskNeedsWork = { }, manager: { id: user._id, - name: user.profile.name, + name: user.auth.local.username, }, }); await Promise.all([assignedUser.save(), task.save()]); diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index 382c9337e6..f29839dbf7 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -452,7 +452,7 @@ async function scoreTask (user, task, direction, req, res) { direction, }, req, res.analytics); rollbackUser.addNotification('GROUP_TASK_NEEDS_WORK', { - message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.profile.name }, rollbackUser.preferences.language), + message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.auth.local.username }, rollbackUser.preferences.language), task: { id: task._id, text: task.text, @@ -463,7 +463,7 @@ async function scoreTask (user, task, direction, req, res) { }, manager: { id: user._id, - name: user.profile.name, + name: user.auth.local.username, }, }); await rollbackUser.save(); From cccd8c3b1b6f1d4de5e589b08553f0034702834d Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 14:17:06 -0500 Subject: [PATCH 128/171] Revert "fix(tests): catch non-array parameter" This reverts commit 595c1313981da8bbda707a9548f0afe22e4da1e5. --- website/server/models/group.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/website/server/models/group.js b/website/server/models/group.js index ff4d43d0be..91b1a5ed33 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -1447,13 +1447,7 @@ schema.methods.unlinkTags = function unlinkTags (user) { schema.methods.syncTask = async function groupSyncTask (taskToSync, users, assigningUser) { const group = this; const toSave = []; - let usersArray; - if (!Array.isArray(users)) { - usersArray = [users]; - } else { - usersArray = users; - } - for (const user of usersArray) { + for (const user of users) { const assignmentData = { assignedDate: new Date(), assignedUsername: user.auth.local.username, From 35d963a39723cad4af44584190df24f3d7639ae2 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 14:43:21 -0500 Subject: [PATCH 129/171] fix(teams): tweak FAQ and fix sync test --- test/api/unit/models/group_tasks.test.js | 221 ++--------------------- website/common/locales/en/faq.json | 2 +- website/server/models/group.js | 1 + 3 files changed, 13 insertions(+), 211 deletions(-) diff --git a/test/api/unit/models/group_tasks.test.js b/test/api/unit/models/group_tasks.test.js index 1c60fa947b..f4a37e194c 100644 --- a/test/api/unit/models/group_tasks.test.js +++ b/test/api/unit/models/group_tasks.test.js @@ -1,12 +1,10 @@ -import { each, find, findIndex } from 'lodash'; -import { model as Challenge } from '../../../../website/server/models/challenge'; +import { each, findIndex } from 'lodash'; import { model as Group } from '../../../../website/server/models/group'; import { model as User } from '../../../../website/server/models/user'; import * as Tasks from '../../../../website/server/models/task'; describe('Group Task Methods', () => { - let guild; let leader; let challenge; let - task; + let guild; let leader; let task; const tasksToTest = { habit: { text: 'test habit', @@ -31,10 +29,6 @@ describe('Group Task Methods', () => { }, }; - function findLinkedTask (updatedLeadersTask) { - return updatedLeadersTask.group.taskId === task._id; - } - beforeEach(async () => { guild = new Group({ name: 'test party', @@ -47,19 +41,9 @@ describe('Group Task Methods', () => { guild.leader = leader._id; - challenge = new Challenge({ - name: 'Test Challenge', - shortName: 'Test', - leader: leader._id, - group: guild._id, - }); - - leader.challenges = [challenge._id]; - await Promise.all([ guild.save(), leader.save(), - challenge.save(), ]); }); @@ -78,7 +62,15 @@ describe('Group Task Methods', () => { }); it('syncs an assigned task to a user', async () => { - await guild.syncTask(task, leader); + await guild.syncTask(task, [leader], leader); + + const updatedTask = await Tasks.Task.findOne({ _id: task._id }); + expect(updatedTask.group.assignedUsers).to.contain(leader._id); + expect(updatedTask.group.assignedUsersDetail[leader._id]).to.exist; + }); + + it('creates tags for a user when task is synced', async () => { + await guild.syncTask(task, [leader], leader); const updatedLeader = await User.findOne({ _id: leader._id }); const tagIndex = findIndex(updatedLeader.tags, { id: guild._id }); @@ -88,197 +80,6 @@ describe('Group Task Methods', () => { expect(newTag.name).to.equal(guild.name); expect(newTag.group).to.equal(guild._id); }); - - it('create tags for a user when task is synced', async () => { - await guild.syncTask(task, leader); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - expect(task.group.assignedUsers).to.contain(leader._id); - expect(syncedTask).to.exist; - }); - - it('syncs updated info for assigned task to a user', async () => { - await guild.syncTask(task, leader); - const updatedTaskName = 'Update Task name'; - task.text = updatedTaskName; - await guild.syncTask(task, leader); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - expect(task.group.assignedUsers).to.contain(leader._id); - expect(syncedTask).to.exist; - expect(syncedTask.text).to.equal(task.text); - }); - - it('syncs checklist items to an assigned user', async () => { - await guild.syncTask(task, leader); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - if (task.type !== 'daily' && task.type !== 'todo') return; - - expect(syncedTask.checklist.length).to.equal(task.checklist.length); - expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text); - }); - - describe('syncs updated info', async () => { - let newMember; - - beforeEach(async () => { - newMember = new User({ - guilds: [guild._id], - }); - await newMember.save(); - - await guild.syncTask(task, leader); - await guild.syncTask(task, newMember); - }); - - it('syncs updated info for assigned task to all users', async () => { - const updatedTaskName = 'Update Task name'; - task.text = updatedTaskName; - task.group.approval.required = true; - - await guild.updateTask(task); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - const updatedMember = await User.findOne({ _id: newMember._id }); - const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } }); - const syncedMemberTask = find(updatedMemberTasks, findLinkedTask); - - expect(task.group.assignedUsers).to.contain(leader._id); - expect(syncedTask).to.exist; - expect(syncedTask.text).to.equal(task.text); - expect(syncedTask.group.approval.required).to.equal(true); - - expect(task.group.assignedUsers).to.contain(newMember._id); - expect(syncedMemberTask).to.exist; - expect(syncedMemberTask.text).to.equal(task.text); - expect(syncedMemberTask.group.approval.required).to.equal(true); - }); - - it('syncs a new checklist item to all assigned users', async () => { - if (task.type !== 'daily' && task.type !== 'todo') return; - - const newCheckListItem = { - text: 'Checklist Item 1', - completed: false, - }; - - task.checklist.push(newCheckListItem); - - await guild.updateTask(task, { newCheckListItem }); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - const updatedMember = await User.findOne({ _id: newMember._id }); - const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } }); - const syncedMemberTask = find(updatedMemberTasks, findLinkedTask); - - expect(syncedTask.checklist.length).to.equal(task.checklist.length); - expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text); - expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length); - expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text); - }); - - it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => { - if (task.type !== 'daily' && task.type !== 'todo') return; - - const updateCheckListText = 'Updated checklist item'; - if (task.checklist) { - task.checklist[0].text = updateCheckListText; - } - - await guild.updateTask(task, { updateCheckListItems: [task.checklist[0]] }); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - const updatedMember = await User.findOne({ _id: newMember._id }); - const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } }); - const syncedMemberTask = find(updatedMemberTasks, findLinkedTask); - - expect(syncedTask.checklist.length).to.equal(task.checklist.length); - expect(syncedTask.checklist[0].text).to.equal(updateCheckListText); - expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length); - expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText); - }); - - it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => { - if (task.type !== 'daily' && task.type !== 'todo') return; - - await guild.updateTask(task, { removedCheckListItemId: task.checklist[0].id }); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - const updatedMember = await User.findOne({ _id: newMember._id }); - const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } }); - const syncedMemberTask = find(updatedMemberTasks, findLinkedTask); - - expect(syncedTask.checklist.length).to.equal(0); - expect(syncedMemberTask.checklist.length).to.equal(0); - }); - }); - - it('removes assigned tasks when master task is deleted', async () => { - await guild.syncTask(task, leader); - await guild.removeTask(task); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id); - expect(syncedTask).to.not.exist; - }); - - it('unlinks and deletes group tasks for a user when remove-all is specified', async () => { - await guild.syncTask(task, leader); - await guild.unlinkTask(task, leader, 'remove-all'); - - const updatedLeader = await User.findOne({ _id: leader._id }); - const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - expect(task.group.assignedUsers).to.not.contain(leader._id); - expect(syncedTask).to.not.exist; - }); - - it('unlinks and keeps group tasks for a user when keep-all is specified', async () => { - await guild.syncTask(task, leader); - - let updatedLeader = await User.findOne({ _id: leader._id }); - let updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const syncedTask = find(updatedLeadersTasks, findLinkedTask); - - await guild.unlinkTask(task, leader, 'keep-all'); - - updatedLeader = await User.findOne({ _id: leader._id }); - updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } }); - const updatedSyncedTask = find( - updatedLeadersTasks, - updatedLeadersTask => updatedLeadersTask._id === syncedTask._id, - ); - - expect(task.group.assignedUsers).to.not.contain(leader._id); - expect(updatedSyncedTask).to.exist; - expect(updatedSyncedTask.group._id).to.be.undefined; - }); }); }); }); diff --git a/website/common/locales/en/faq.json b/website/common/locales/en/faq.json index ef9583c64b..0008f060b6 100644 --- a/website/common/locales/en/faq.json +++ b/website/common/locales/en/faq.json @@ -71,5 +71,5 @@ "webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the [Habitica Help guild](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! We're happy to help.", "faqQuestion13": "What is a Group Plan?", - "webFaqAnswer13": "## How do Group Plans work?\n\nA [Group Plan](/group-plans) gives your Party or Guild access to a shared task board that’s similar to your personal task board! It’s a shared Habitica experience where tasks can be created and checked off by anyone in the group.\n\nThere are also features available like member roles, status view, and task assigning that give you a more controlled experience. [Visit our wiki](https://habitica.fandom.com/wiki/Group_Plans) to learn more about our Group Plans’ features!\n\n## Who benefits from a Group Plan?\n\nGroup Plans work best when you have a small team of people who want to collaborate together. We recommend 2-5 members.\n\nGroup Plans are great for families, whether it’s a parent and child or you and a partner. Shared goals, chores, or responsibilities are easy to keep track of on one board.\n\nGroup Plans can also be useful for teams of colleagues that have shared goals, or managers that want to introduce their employees to gamification.\n\n## Quick tips for using Groups\n\n* If you want a member to be able to create and edit tasks, make them a manager\n* Leave tasks unassigned if anyone can complete it and it only needs done once\n* Assign a task to one person to make sure no one else can complete their task\n* Assign a task to multiple people if they all need to complete it\n* Copy tasks to your personal board to not miss anything\n* You get rewarded for the tasks you complete, even multi-assigned\n* Task completion rewards aren’t shared or split between Team members\n* Use task color on the team board to judge the average completion rate of tasks\n* Regularly review the tasks on your Team Board to make sure they are still relevant\n* Missing a Daily won’t damage you or your team, but the task will degrade in color\n\n## How can others in the group create tasks?\n\nOnly the group leader and managers can create tasks. If you’d like a group member to be able to create tasks, then you should promote them to be a manager by going to the Group Information tab, viewing the member list, and clicking the dot icon by their name.\n\n## How does assigning a task work?\n\nGroup Plans give you the unique ability to assign tasks to other group members. Assigning a task is great for delegating. If you assign a task to someone, then other members are prevented from completing it.\n\nYou can also assign a task to multiple people if it needs to be completed by more than one member. For example, if everyone has to brush their teeth, create a task and assign it to each group member. They will all be able to check it off and get their individual rewards for doing so. The main task will show as complete once everyone checks it off.\n\n## How do unassigned tasks work?\n\nUnassigned tasks can be completed by anyone in the group, so leave a task unassigned to allow any member to complete it. For example, taking out the trash. Whoever takes out the trash can check off the unassigned task and it will show as completed for everyone.\n\n## How does the synchronized day reset work?\n\nShared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the group leader’s day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterday’s uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in color to help visualize progress. We don’t want the shared experience to be a negative one!\n\n## How do I use my Group on the mobile apps?\n\nWhile the mobile apps don’t fully support all Group Plans functionality yet, you can still complete shared tasks from the iOS and Android app. On the browser version of Habitica, go to your group’s shared task board and turn on the copy tasks toggle. Now all open and assigned shared tasks will display on your personal task board across all platforms.\n\n## What’s the difference between a Group’s shared tasks and Challenges?\n\nGroup Plan shared task boards are more dynamic than Challenges, in that they can constantly be updated and interacted with. Challenges are great if you have one set of tasks to send out to many people.\n\nGroup Plans are also a paid feature, while Challenges are available free to everyone.\n\nYou cannot assign specific tasks in Challenges, and Challenges do not have a shared day reset. In general, Challenges offer less control and direct interaction." + "webFaqAnswer13": "## How do Group Plans work?\n\nA [Group Plan](/group-plans) gives your Party or Guild access to a shared task board that’s similar to your personal task board! It’s a shared Habitica experience where tasks can be created and checked off by anyone in the group.\n\nThere are also features available like member roles, status view, and task assigning that give you a more controlled experience. [Visit our wiki](https://habitica.fandom.com/wiki/Group_Plans) to learn more about our Group Plans’ features!\n\n## Who benefits from a Group Plan?\n\nGroup Plans work best when you have a small team of people who want to collaborate together. We recommend 2-5 members.\n\nGroup Plans are great for families, whether it’s a parent and child or you and a partner. Shared goals, chores, or responsibilities are easy to keep track of on one board.\n\nGroup Plans can also be useful for teams of colleagues that have shared goals, or managers that want to introduce their employees to gamification.\n\n## Quick tips for using Groups\n\nHere are some quick tips to get you started with your new Group. We’ll provide more details in the following sections:\n\n* Make a member a manager to give them the ability to create and edit tasks\n* Leave tasks unassigned if anyone can complete it and it only needs done once\n* Assign a task to one person to make sure no one else can complete their task\n* Assign a task to multiple people if they all need to complete it\n* Toggle the ability to display shared tasks on your personal board to not miss anything\n* You get rewarded for the tasks you complete, even multi-assigned\n* Task completion rewards aren’t shared or split between Team members\n* Use task color on the team board to judge the average completion rate of tasks\n* Regularly review the tasks on your Team Board to make sure they are still relevant\n* Missing a Daily won’t damage you or your team, but the task will degrade in color\n\n## How can others in the group create tasks?\n\nOnly the group leader and managers can create tasks. If you’d like a group member to be able to create tasks, then you should promote them to be a manager by going to the Group Information tab, viewing the member list, and clicking the dot icon by their name.\n\n## How does assigning a task work?\n\nGroup Plans give you the unique ability to assign tasks to other group members. Assigning a task is great for delegating. If you assign a task to someone, then other members are prevented from completing it.\n\nYou can also assign a task to multiple people if it needs to be completed by more than one member. For example, if everyone has to brush their teeth, create a task and assign it to each group member. They will all be able to check it off and get their individual rewards for doing so. The main task will show as complete once everyone checks it off.\n\n## How do unassigned tasks work?\n\nUnassigned tasks can be completed by anyone in the group, so leave a task unassigned to allow any member to complete it. For example, taking out the trash. Whoever takes out the trash can check off the unassigned task and it will show as completed for everyone.\n\n## How does the synchronized day reset work?\n\nShared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the group leader’s day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterday’s uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in color to help visualize progress. We don’t want the shared experience to be a negative one!\n\n## How do I use my Group on the mobile apps?\n\nWhile the mobile apps don’t fully support all Group Plans functionality yet, you can still complete shared tasks from the iOS and Android app. On the browser version of Habitica, go to your group’s shared task board and turn on the copy tasks toggle. Now all open and assigned shared tasks will display on your personal task board across all platforms.\n\n## What’s the difference between a Group’s shared tasks and Challenges?\n\nGroup Plan shared task boards are more dynamic than Challenges, in that they can constantly be updated and interacted with. Challenges are great if you have one set of tasks to send out to many people.\n\nGroup Plans are also a paid feature, while Challenges are available free to everyone.\n\nYou cannot assign specific tasks in Challenges, and Challenges do not have a shared day reset. In general, Challenges offer less control and direct interaction." } diff --git a/website/server/models/group.js b/website/server/models/group.js index 91b1a5ed33..1c770823b6 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -1485,6 +1485,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, users, assig group: group._id, }); } + toSave.push(user.save()); } toSave.push(taskToSync.save()); return Promise.all(toSave); From 149da578fdc4c6fe309dd17091d718a2b1b25d2d Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 16:16:23 -0500 Subject: [PATCH 130/171] fix(teams): fix fix fix Removed testing banner Fixed a JS console error when assigning a user to a previously open task Fixed a potential abuse where user might be able to score someone else's task via API call Fixed an issue where finding tasks by alias could return tasks belonging to other users Fixed an issue that was appending the user's party ID to their list of Guilds Fixed an issue where group tasks were not receiving the default tag needed for filtering them on user's personal list --- test/api/unit/models/task.test.js | 19 +++++-- website/client/src/app.vue | 3 - .../header/banners/teamsTesting.vue | 56 ------------------- .../components/snackbars/notifications.vue | 2 +- .../src/components/tasks/taskSummary.vue | 3 +- website/common/script/ops/scoreTask.js | 7 +++ website/server/controllers/api-v3/tasks.js | 2 +- website/server/libs/tasks/index.js | 4 +- website/server/models/task.js | 4 +- 9 files changed, 28 insertions(+), 72 deletions(-) delete mode 100644 website/client/src/components/header/banners/teamsTesting.vue diff --git a/test/api/unit/models/task.test.js b/test/api/unit/models/task.test.js index 5921a4e2c2..5da3341cbe 100644 --- a/test/api/unit/models/task.test.js +++ b/test/api/unit/models/task.test.js @@ -246,13 +246,23 @@ describe('Task Model', () => { expect(foundTasks[0].text).to.eql(taskWithAlias.text); }); - it('scopes alias lookup to user', async () => { + it('scopes alias lookup to user when querying aliases only', async () => { await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id); + expect(Tasks.Task.find).to.be.calledOnce; + expect(Tasks.Task.find).to.be.calledWithMatch({ + alias: { $in: [taskWithAlias.alias] }, + userId: user._id, + }); + }); + + it('scopes alias lookup to user when querying aliases and IDs', async () => { + await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias, secondTask._id], user._id); + expect(Tasks.Task.find).to.be.calledOnce; expect(Tasks.Task.find).to.be.calledWithMatch({ $or: [ - { _id: { $in: [] } }, + { _id: { $in: [secondTask._id] } }, { alias: { $in: [taskWithAlias.alias] } }, ], userId: user._id, @@ -270,10 +280,7 @@ describe('Task Model', () => { expect(Tasks.Task.find).to.be.calledOnce; expect(Tasks.Task.find).to.be.calledWithMatch({ - $or: [ - { _id: { $in: [] } }, - { alias: { $in: [taskWithAlias.alias] } }, - ], + alias: { $in: [taskWithAlias.alias] }, userId: user._id, foo: 'bar', }); diff --git a/website/client/src/app.vue b/website/client/src/app.vue index 64603fd122..e9afbb2515 100644 --- a/website/client/src/app.vue +++ b/website/client/src/app.vue @@ -42,7 +42,6 @@ -
- -
- - You're previewing new Habitica Groups! - - - Have a question? - -
-
- - - - - diff --git a/website/client/src/components/snackbars/notifications.vue b/website/client/src/components/snackbars/notifications.vue index 9f1705d220..30bd8d73e9 100644 --- a/website/client/src/components/snackbars/notifications.vue +++ b/website/client/src/components/snackbars/notifications.vue @@ -143,7 +143,7 @@ export default { scrollPosToCheck += this.eventPromoBannerHeight ?? 0; } - return scrollPosToCheck + 48; // teams testing banner is always on, for now + return scrollPosToCheck; }, visibleNotificationsWithoutErrors () { return this.visibleNotifications.filter(n => n.type !== 'error'); diff --git a/website/client/src/components/tasks/taskSummary.vue b/website/client/src/components/tasks/taskSummary.vue index a42928968e..109d343c73 100644 --- a/website/client/src/components/tasks/taskSummary.vue +++ b/website/client/src/components/tasks/taskSummary.vue @@ -213,7 +213,8 @@ export default { user: 'user.data', }), assignedUsernames () { - if (!this.task.group || !this.task.group.assignedUsers) return []; + if (!this.task.group || !this.task.group.assignedUsers + || !this.task.group.assignedUsersDetail) return []; const usernames = []; for (const user of this.task.group.assignedUsers) { usernames.push(this.task.group.assignedUsersDetail[user].assignedUsername); diff --git a/website/common/script/ops/scoreTask.js b/website/common/script/ops/scoreTask.js index e85b4509e0..14c0f688d8 100644 --- a/website/common/script/ops/scoreTask.js +++ b/website/common/script/ops/scoreTask.js @@ -4,6 +4,7 @@ import reduce from 'lodash/reduce'; import moment from 'moment'; import max from 'lodash/max'; import { + BadRequest, NotAuthorized, } from '../libs/errors'; import i18n from '../i18n'; @@ -247,6 +248,12 @@ export default function scoreTask (options = {}, req = {}, analytics) { // If they're trying to purchase a too-expensive reward, don't allow them to do that. if (task.value > user.stats.gp && task.type === 'reward') throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language)); + // Thanks to open group tasks, userId is not guaranteed. Don't allow scoring inaccessible tasks + if (task.userId && task.userId !== user._id) { + throw new BadRequest('Cannot score task belonging to another user.'); + } else if (user.guilds.indexOf(task.group.id) === -1 && user.party._id !== task.group.id) { + throw new BadRequest('Cannot score task belonging to another user.'); + } if (task.type === 'habit') { delta += _changeTaskValue(user, task, direction, times, cron); diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 020625de7e..8a665e2da2 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -816,7 +816,7 @@ api.moveTask = { const group = await getGroupFromTaskAndUser(task, user); const challenge = await getChallengeFromTask(task); if (task.group.id && !task.userId) { - if (!group || user.guilds.concat(user.party._id).indexOf(group._id) === -1) { + if (!group || (user.guilds.indexOf(group._id) === -1 && user.party._id !== group._id)) { throw new NotFound(res.t('groupNotFound')); } if (task.group.assignedUsers.length !== 0 diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index f29839dbf7..faf36cd077 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -67,9 +67,7 @@ async function createTasks (req, res, options = {}) { newTask.challenge.id = challenge.id; } else if (group) { newTask.group.id = group._id; - if (taskData.requiresApproval) { - newTask.group.approval.required = true; - } + newTask.tags = [group._id]; newTask.group.managerNotes = taskData.managerNotes || ''; } else { newTask.userId = user._id; diff --git a/website/server/models/task.js b/website/server/models/task.js index e09040a877..f9601cd10c 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -205,7 +205,7 @@ TaskSchema.statics.findByIdOrAlias = async function findByIdOrAlias ( return task; }; -TaskSchema.statics.findMultipleByIdOrAlias = async function findByIdOrAlias ( +TaskSchema.statics.findMultipleByIdOrAlias = async function findMultipleByIdOrAlias ( identifiers, userId, additionalQueries = {}, @@ -226,6 +226,7 @@ TaskSchema.statics.findMultipleByIdOrAlias = async function findByIdOrAlias ( }); if (ids.length > 0 && aliases.length > 0) { + query.userId = userId; query.$or = [ { _id: { $in: ids } }, { alias: { $in: aliases } }, @@ -233,6 +234,7 @@ TaskSchema.statics.findMultipleByIdOrAlias = async function findByIdOrAlias ( } else if (ids.length > 0) { query._id = { $in: ids }; } else if (aliases.length > 0) { + query.userId = userId; query.alias = { $in: aliases }; } else { throw new Error('No identifiers found.'); // Should be covered by the !identifiers check, but.. From 525159836992d1723a4f0f2bec4042bb7d8a361e Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 16:38:51 -0500 Subject: [PATCH 131/171] fix(cron): fix score down breaking during middleware --- website/common/script/ops/scoreTask.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/website/common/script/ops/scoreTask.js b/website/common/script/ops/scoreTask.js index 14c0f688d8..a9a0de139b 100644 --- a/website/common/script/ops/scoreTask.js +++ b/website/common/script/ops/scoreTask.js @@ -246,14 +246,15 @@ export default function scoreTask (options = {}, req = {}, analytics) { if (oldLeveledUp) user._tmp.leveledUp = oldLeveledUp; - // If they're trying to purchase a too-expensive reward, don't allow them to do that. - if (task.value > user.stats.gp && task.type === 'reward') throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language)); // Thanks to open group tasks, userId is not guaranteed. Don't allow scoring inaccessible tasks if (task.userId && task.userId !== user._id) { throw new BadRequest('Cannot score task belonging to another user.'); - } else if (user.guilds.indexOf(task.group.id) === -1 && user.party._id !== task.group.id) { + } else if (task.group.id && user.guilds.indexOf(task.group.id) === -1 + && user.party._id !== task.group.id) { throw new BadRequest('Cannot score task belonging to another user.'); } + // If they're trying to purchase a too-expensive reward, don't allow them to do that. + if (task.value > user.stats.gp && task.type === 'reward') throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language)); if (task.type === 'habit') { delta += _changeTaskValue(user, task, direction, times, cron); From 3ab88bbb3f15854299413c29e0686fcca0fbe405 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 18:49:56 -0500 Subject: [PATCH 132/171] fix(tests): short circuit getter, adjust expectations --- website/client/src/store/getters/tasks.js | 3 ++- .../tests/unit/store/getters/tasks/canEdit.spec.js | 5 ++++- .../unit/store/getters/tasks/getTaskClasses.spec.js | 9 ++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/website/client/src/store/getters/tasks.js b/website/client/src/store/getters/tasks.js index d5d3824247..fbee4bd06e 100644 --- a/website/client/src/store/getters/tasks.js +++ b/website/client/src/store/getters/tasks.js @@ -172,7 +172,8 @@ export function getTaskClasses (store) { if (type === 'todo' || type === 'daily') { if (task.completed || (!shouldDo(dueDate, task, userPreferences) && type === 'daily') - || (task.group.assignedUsersDetail && task.group.assignedUsersDetail[userId] + || (task.group && task.group.assignedUsersDetail + && task.group.assignedUsersDetail[userId] && task.group.assignedUsersDetail[userId].completed) ) { return { diff --git a/website/client/tests/unit/store/getters/tasks/canEdit.spec.js b/website/client/tests/unit/store/getters/tasks/canEdit.spec.js index e736a77493..15962b7030 100644 --- a/website/client/tests/unit/store/getters/tasks/canEdit.spec.js +++ b/website/client/tests/unit/store/getters/tasks/canEdit.spec.js @@ -35,7 +35,10 @@ describe('canEdit getter', () => { }); it('can Edit task in own dashboard', () => { expect(store.getters['tasks:canEdit'](task, 'challenge', true, null, challenge)).to.equal(true); - expect(store.getters['tasks:canEdit'](task, 'group', true, group, null)).to.equal(true); + }); + + it('cannot Edit group task in own dashboard', () => { + expect(store.getters['tasks:canEdit'](task, 'group', true, group, null)).to.equal(false); }); it('can Edit any challenge task if admin', () => { diff --git a/website/client/tests/unit/store/getters/tasks/getTaskClasses.spec.js b/website/client/tests/unit/store/getters/tasks/getTaskClasses.spec.js index 4782eafeac..ad05f73a6c 100644 --- a/website/client/tests/unit/store/getters/tasks/getTaskClasses.spec.js +++ b/website/client/tests/unit/store/getters/tasks/getTaskClasses.spec.js @@ -143,7 +143,14 @@ describe('getTaskClasses getter', () => { }); it('returns noninteractive classes and padlock icons for group board tasks', () => { - const task = { type: 'todo', value: 2, group: { id: 'group-id' } }; + const task = { + type: 'todo', + value: 2, + group: { + id: 'group-id', + assignedUsers: ['not-me'], + }, + }; expect(getTaskClasses(task, 'control')).to.deep.equal({ bg: 'task-good-control-bg-noninteractive', checkbox: 'task-good-control-checkbox', From 02c50b612690cc8070c94608d18faa21ba800c8e Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 20:45:22 -0500 Subject: [PATCH 133/171] WIP(tests): fix various assign requests and needs-work flow --- .../groups/POST-group_remove_manager.test.js | 29 -- .../POST-tasks_clearCompletedTodos.test.js | 5 +- ...asks_taskId_checklist_itemId_score.test.js | 2 +- .../groups/DELETE-group_tasks_id.test.js | 85 +----- .../groups/GET-approvals_group_id.test.js | 78 ------ ...POST-group_tasks_id_approve_userId.test.js | 261 ------------------ ...T-group_tasks_id_needs-work_userId.test.js | 110 ++------ website/server/controllers/api-v3/tasks.js | 9 +- .../server/controllers/api-v3/tasks/groups.js | 1 + 9 files changed, 33 insertions(+), 547 deletions(-) delete mode 100644 test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js delete mode 100644 test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js diff --git a/test/api/v3/integration/groups/POST-group_remove_manager.test.js b/test/api/v3/integration/groups/POST-group_remove_manager.test.js index 894a225d26..cdc37ca79f 100644 --- a/test/api/v3/integration/groups/POST-group_remove_manager.test.js +++ b/test/api/v3/integration/groups/POST-group_remove_manager.test.js @@ -1,4 +1,3 @@ -import { find } from 'lodash'; import { createAndPopulateGroup, translate as t, @@ -11,10 +10,6 @@ describe('POST /group/:groupId/remove-manager', () => { const groupType = 'guild'; let nonManager; - function findAssignedTask (memberTask) { - return memberTask.group.id === groupToUpdate._id; - } - beforeEach(async () => { const { group, groupLeader, members } = await createAndPopulateGroup({ groupDetails: { @@ -63,28 +58,4 @@ describe('POST /group/:groupId/remove-manager', () => { expect(updatedGroup.managers[nonLeader._id]).to.not.exist; }); - - it('removes group approval notifications from a manager that is removed', async () => { - await leader.post(`/groups/${groupToUpdate._id}/add-manager`, { - managerId: nonLeader._id, - }); - const task = await leader.post(`/tasks/group/${groupToUpdate._id}`, { - text: 'test todo', - type: 'todo', - requiresApproval: true, - }); - await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`); - const memberTasks = await nonManager.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - await nonManager.post(`/tasks/${syncedTask._id}/score/up`); - - const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, { - managerId: nonLeader._id, - }); - - await nonLeader.sync(); - - expect(nonLeader.notifications.length).to.equal(1); // user gets mystery items - expect(updatedGroup.managers[nonLeader._id]).to.not.exist; - }); }); diff --git a/test/api/v3/integration/tasks/POST-tasks_clearCompletedTodos.test.js b/test/api/v3/integration/tasks/POST-tasks_clearCompletedTodos.test.js index 92fcb6a13c..ecda9500d5 100644 --- a/test/api/v3/integration/tasks/POST-tasks_clearCompletedTodos.test.js +++ b/test/api/v3/integration/tasks/POST-tasks_clearCompletedTodos.test.js @@ -13,6 +13,9 @@ describe('POST /tasks/clearCompletedTodos', () => { { 'purchased.plan.customerId': 'group-unlimited' }, ); const challenge = await generateChallenge(user, guild); + await user.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); await user.post(`/challenges/${challenge._id}/join`); const initialTodoCount = user.tasksOrder.todos.length; @@ -33,7 +36,7 @@ describe('POST /tasks/clearCompletedTodos', () => { text: 'todo 7', type: 'todo', }); - await user.post(`/tasks/${groupTask._id}/assign/${user._id}`); + await user.post(`/tasks/${groupTask._id}/assign`, [user._id]); const tasks = await user.get('/tasks/user?type=todos'); expect(tasks.length).to.equal(initialTodoCount + 7); diff --git a/test/api/v3/integration/tasks/checklists/POST-tasks_taskId_checklist_itemId_score.test.js b/test/api/v3/integration/tasks/checklists/POST-tasks_taskId_checklist_itemId_score.test.js index 4f772ebaf7..a9aae0685c 100644 --- a/test/api/v3/integration/tasks/checklists/POST-tasks_taskId_checklist_itemId_score.test.js +++ b/test/api/v3/integration/tasks/checklists/POST-tasks_taskId_checklist_itemId_score.test.js @@ -30,7 +30,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => { expect(savedTask.checklist[0].completed).to.equal(true); }); - it('can use a alias to score a checklist item', async () => { + it('can use an alias to score a checklist item', async () => { const task = await user.post('/tasks/user', { type: 'daily', text: 'Daily with checklist', diff --git a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js index b6eb29db11..dda6037419 100644 --- a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js +++ b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js @@ -1,4 +1,3 @@ -import { find } from 'lodash'; import { translate as t, createAndPopulateGroup, @@ -8,10 +7,6 @@ describe('Groups DELETE /tasks/:id', () => { let user; let guild; let member; let member2; let task; - function findAssignedTask (memberTask) { - return memberTask.group.id === guild._id; - } - beforeEach(async () => { const { group, members, groupLeader } = await createAndPopulateGroup({ groupDetails: { @@ -35,8 +30,7 @@ describe('Groups DELETE /tasks/:id', () => { notes: 1976, }); - await user.post(`/tasks/${task._id}/assign/${member._id}`); - await user.post(`/tasks/${task._id}/assign/${member2._id}`); + await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]); }); it('deletes a group task', async () => { @@ -64,81 +58,4 @@ describe('Groups DELETE /tasks/:id', () => { message: t('messageTaskNotFound'), }); }); - - it('removes deleted taskʾs approval pending notifications from managers', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member2._id, - }); - await user.put(`/tasks/${task._id}/`, { - requiresApproval: true, - }); - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - await member.post(`/tasks/${syncedTask._id}/score/up`); - - await user.sync(); - await member2.sync(); - expect(user.notifications.length).to.equal(3); // mystery items - expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL'); - expect(member2.notifications.length).to.equal(3); - expect(member2.notifications[2].type).to.equal('GROUP_TASK_APPROVAL'); - - await member2.del(`/tasks/${task._id}`); - - await user.sync(); - await member2.sync(); - - expect(user.notifications.length).to.equal(2); - expect(member2.notifications.length).to.equal(2); - }); - - it('deletes task from assigned user', async () => { - await user.del(`/tasks/${task._id}`); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - expect(syncedTask).to.not.exist; - }); - - it('deletes task from all assigned users', async () => { - await user.del(`/tasks/${task._id}`); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - const member2Tasks = await member2.get('/tasks/user'); - const member2SyncedTask = find(member2Tasks, findAssignedTask); - - expect(syncedTask).to.not.exist; - expect(member2SyncedTask).to.not.exist; - }); - - it('prevents a user from deleting a task they are assigned to', async () => { - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - await expect(member.del(`/tasks/${syncedTask._id}`)) - .to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('cantDeleteAssignedGroupTasks'), - }); - }); - - it('allows a user to delete a task after leaving a group', async () => { - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - await member.post(`/groups/${guild._id}/leave`); - - await member.del(`/tasks/${syncedTask._id}`); - - await expect(member.get(`/tasks/${syncedTask._id}`)) - .to.eventually.be.rejected.and.eql({ - code: 404, - error: 'NotFound', - message: 'Task not found.', - }); - }); }); diff --git a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js b/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js deleted file mode 100644 index 520a322399..0000000000 --- a/test/api/v3/integration/tasks/groups/GET-approvals_group_id.test.js +++ /dev/null @@ -1,78 +0,0 @@ -import { find } from 'lodash'; -import { - createAndPopulateGroup, -} from '../../../../../helpers/api-integration/v3'; - -describe('GET /approvals/group/:groupId', () => { - let user; let guild; let member; let addlMember; let task; let syncedTask; let - addlSyncedTask; - - function findAssignedTask (memberTask) { - return memberTask.group.id === guild._id; - } - - beforeEach(async () => { - const { group, members, groupLeader } = await createAndPopulateGroup({ - groupDetails: { - name: 'Test Guild', - type: 'guild', - }, - members: 2, - upgradeToGroupPlan: true, - }); - - guild = group; - user = groupLeader; - member = members[0]; // eslint-disable-line prefer-destructuring - addlMember = members[1]; // eslint-disable-line prefer-destructuring - - task = await user.post(`/tasks/group/${guild._id}`, { - text: 'test todo', - type: 'todo', - requiresApproval: true, - }); - - await user.post(`/tasks/${task._id}/assign/${member._id}`); - await user.post(`/tasks/${task._id}/assign/${addlMember._id}`); - - const memberTasks = await member.get('/tasks/user'); - syncedTask = find(memberTasks, findAssignedTask); - - const addlMemberTasks = await addlMember.get('/tasks/user'); - addlSyncedTask = find(addlMemberTasks, findAssignedTask); - - try { - await member.post(`/tasks/${syncedTask._id}/score/up`); - } catch (e) { - // eslint-disable-next-line no-empty - } - - try { - await addlMember.post(`/tasks/${addlSyncedTask._id}/score/up`); - } catch (e) { - // eslint-disable-next-line no-empty - } - }); - - it('provides only user\'s own tasks when user is not the group leader', async () => { - const approvals = await member.get(`/approvals/group/${guild._id}`); - expect(approvals[0]._id).to.equal(syncedTask._id); - expect(approvals[1]).to.not.exist; - }); - - it('allows group leaders to get a list of tasks that need approval', async () => { - const approvals = await user.get(`/approvals/group/${guild._id}`); - expect(approvals[0]._id).to.equal(syncedTask._id); - expect(approvals[1]._id).to.equal(addlSyncedTask._id); - }); - - it('allows managers to get a list of tasks that need approval', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member._id, - }); - - const approvals = await member.get(`/approvals/group/${guild._id}`); - expect(approvals[0]._id).to.equal(syncedTask._id); - expect(approvals[1]._id).to.equal(addlSyncedTask._id); - }); -}); diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js deleted file mode 100644 index 60eeaf4053..0000000000 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js +++ /dev/null @@ -1,261 +0,0 @@ -import { find } from 'lodash'; -import { - createAndPopulateGroup, - translate as t, -} from '../../../../../helpers/api-integration/v3'; - -describe('POST /tasks/:id/approve/:userId', () => { - let user; let guild; let member; let member2; let - task; - - function findAssignedTask (memberTask) { - return memberTask.group.id === guild._id; - } - - beforeEach(async () => { - const { group, members, groupLeader } = await createAndPopulateGroup({ - groupDetails: { - name: 'Test Guild', - type: 'guild', - }, - members: 2, - upgradeToGroupPlan: true, - }); - - guild = group; - user = groupLeader; - member = members[0]; // eslint-disable-line prefer-destructuring - member2 = members[1]; // eslint-disable-line prefer-destructuring - - task = await user.post(`/tasks/group/${guild._id}`, { - text: 'test todo', - type: 'todo', - requiresApproval: true, - }); - }); - - it('errors when user is not assigned', async () => { - await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 404, - error: 'NotFound', - message: t('messageTaskNotFound'), - }); - }); - - it('errors when user is not the group leader', async () => { - await user.post(`/tasks/${task._id}/assign/${member._id}`); - await expect(member.post(`/tasks/${task._id}/approve/${member._id}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('onlyGroupLeaderCanEditTasks'), - }); - }); - - it('approves an assigned user', async () => { - await user.post(`/tasks/${task._id}/assign/${member._id}`); - - let memberTasks = await member.get('/tasks/user'); - let syncedTask = find(memberTasks, findAssignedTask); - - await member.post(`/tasks/${syncedTask._id}/score/up`); - await user.post(`/tasks/${task._id}/approve/${member._id}`); - - await member.sync(); - - expect(member.notifications.length).to.equal(3); - expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED'); - expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); - - memberTasks = await member.get('/tasks/user'); - syncedTask = find(memberTasks, findAssignedTask); - - expect(syncedTask.group.approval.approved).to.be.true; - expect(syncedTask.group.approval.approvingUser).to.equal(user._id); - expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type - }); - - it('allows a manager to approve an assigned user', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member2._id, - }); - - await member2.post(`/tasks/${task._id}/assign/${member._id}`); - - let memberTasks = await member.get('/tasks/user'); - let syncedTask = find(memberTasks, findAssignedTask); - - await member.post(`/tasks/${syncedTask._id}/score/up`); - await member2.post(`/tasks/${task._id}/approve/${member._id}`); - await member.sync(); - - expect(member.notifications.length).to.equal(3); - expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED'); - expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text })); - - memberTasks = await member.get('/tasks/user'); - syncedTask = find(memberTasks, findAssignedTask); - - expect(syncedTask.group.approval.approved).to.be.true; - expect(syncedTask.group.approval.approvingUser).to.equal(member2._id); - expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type - }); - - it('removes approval pending notifications from managers', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member2._id, - }); - - await member2.post(`/tasks/${task._id}/assign/${member._id}`); - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - await member.post(`/tasks/${syncedTask._id}/score/up`); - - await user.sync(); - await member2.sync(); - expect(user.notifications.length).to.equal(3); - expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL'); - expect(member2.notifications.length).to.equal(2); - expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL'); - - await member2.post(`/tasks/${task._id}/approve/${member._id}`); - - await user.sync(); - await member2.sync(); - - expect(user.notifications.length).to.equal(2); - expect(member2.notifications.length).to.equal(1); - }); - - it('prevents double approval on a task', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member2._id, - }); - - await member2.post(`/tasks/${task._id}/assign/${member._id}`); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - await member.post(`/tasks/${syncedTask._id}/score/up`); - await member2.post(`/tasks/${task._id}/approve/${member._id}`); - - await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('canOnlyApproveTaskOnce'), - }); - }); - - it('prevents approving a task if it is not waiting for approval', async () => { - await user.post(`/tasks/${task._id}/assign/${member._id}`); - - await expect(user.post(`/tasks/${task._id}/approve/${member._id}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalWasNotRequested'), - }); - }); - - it('completes master task when single-completion task is approved', async () => { - const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { - text: 'shared completion todo', - type: 'todo', - requiresApproval: true, - sharedCompletion: 'singleCompletion', - }); - - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - await member.post(`/tasks/${syncedTask._id}/score/up`); - await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); - - const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`); - - const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id); - - expect(masterTask.completed).to.equal(true); - }); - - it('deletes other assigned user tasks when single-completion task is approved', async () => { - const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { - text: 'shared completion todo', - type: 'todo', - requiresApproval: true, - sharedCompletion: 'singleCompletion', - }); - - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - await member.post(`/tasks/${syncedTask._id}/score/up`); - await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); - - const member2Tasks = await member2.get('/tasks/user'); - - const syncedTask2 = find( - member2Tasks, - memberTask => memberTask.group.taskId === sharedCompletionTask._id, - ); - - expect(syncedTask2).to.equal(undefined); - }); - - it('does not complete master task when not all user tasks are approved if all assigned must complete', async () => { - const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { - text: 'shared completion todo', - type: 'todo', - requiresApproval: true, - sharedCompletion: 'allAssignedCompletion', - }); - - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - await member.post(`/tasks/${syncedTask._id}/score/up`); - await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); - - const groupTasks = await user.get(`/tasks/group/${guild._id}`); - - const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id); - - expect(masterTask.completed).to.equal(false); - }); - - it('completes master task when all user tasks are approved if all assigned must complete', async () => { - const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { - text: 'shared completion todo', - type: 'todo', - requiresApproval: true, - sharedCompletion: 'allAssignedCompletion', - }); - - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - await member.post(`/tasks/${syncedTask._id}/score/up`); - const member2Tasks = await member2.get('/tasks/user'); - const member2SyncedTask = find(member2Tasks, findAssignedTask); - await member2.post(`/tasks/${member2SyncedTask._id}/score/up`); - - await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`); - await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`); - - const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`); - - const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id); - - expect(masterTask.completed).to.equal(true); - }); -}); diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js index 72542aceba..1b35e1076b 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js @@ -1,4 +1,3 @@ -import { find } from 'lodash'; import { createAndPopulateGroup, translate as t, @@ -8,10 +7,6 @@ describe('POST /tasks/:id/needs-work/:userId', () => { let user; let guild; let member; let member2; let task; - function findAssignedTask (memberTask) { - return memberTask.group.id === guild._id; - } - beforeEach(async () => { const { group, members, groupLeader } = await createAndPopulateGroup({ groupDetails: { @@ -44,7 +39,8 @@ describe('POST /tasks/:id/needs-work/:userId', () => { }); it('errors when user is not the group leader', async () => { - await user.post(`/tasks/${task._id}/assign/${member._id}`); + await user.post(`/tasks/${task._id}/assign`, [member._id]); + await member.post(`/tasks/${task._id}/score/up`); await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`)) .to.eventually.be.rejected.and.to.eql({ code: 401, @@ -54,132 +50,64 @@ describe('POST /tasks/:id/needs-work/:userId', () => { }); it('marks a task as needing more work', async () => { + await member.sync(); const initialNotifications = member.notifications.length; - - await user.post(`/tasks/${task._id}/assign/${member._id}`); - - let memberTasks = await member.get('/tasks/user'); - let syncedTask = find(memberTasks, findAssignedTask); + await user.post(`/tasks/${task._id}/assign`, [member._id]); // score task to require approval - await member.post(`/tasks/${syncedTask._id}/score/up`); + await member.post(`/tasks/${task._id}/score/up`); await user.post(`/tasks/${task._id}/needs-work/${member._id}`); - [memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]); - syncedTask = find(memberTasks, findAssignedTask); - - // Check that the notification approval request has been removed - expect(syncedTask.group.approval.requested).to.equal(false); - expect(syncedTask.group.approval.requestedDate).to.equal(undefined); - // Check that the notification is correct + await member.sync(); expect(member.notifications.length).to.equal(initialNotifications + 3); const notification = member.notifications[member.notifications.length - 1]; expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK'); - const taskText = syncedTask.text; - const managerName = user.profile.name; + const taskText = task.text; + const managerName = user.auth.local.username; expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName })); - expect(notification.data.task.id).to.equal(syncedTask._id); + expect(notification.data.task.id).to.equal(task._id); expect(notification.data.task.text).to.equal(taskText); - expect(notification.data.group.id).to.equal(syncedTask.group.id); + expect(notification.data.group.id).to.equal(task.group.id); expect(notification.data.group.name).to.equal(guild.name); expect(notification.data.manager.id).to.equal(user._id); expect(notification.data.manager.name).to.equal(managerName); - - // Check that the managers' GROUP_TASK_APPROVAL notifications have been removed - await user.sync(); - - expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style - return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL'; - })).to.equal(undefined); }); it('allows a manager to mark a task as needing work', async () => { + await member.sync(); + const initialNotifications = member.notifications.length; await user.post(`/groups/${guild._id}/add-manager`, { managerId: member2._id, }); - await member2.post(`/tasks/${task._id}/assign/${member._id}`); - - let memberTasks = await member.get('/tasks/user'); - let syncedTask = find(memberTasks, findAssignedTask); + await member2.post(`/tasks/${task._id}/assign`, [member._id]); // score task to require approval - await member.post(`/tasks/${syncedTask._id}/score/up`); - - const initialNotifications = member.notifications.length; - + await member.post(`/tasks/${task._id}/score/up`); await member2.post(`/tasks/${task._id}/needs-work/${member._id}`); - [memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]); - syncedTask = find(memberTasks, findAssignedTask); - - // Check that the notification approval request has been removed - expect(syncedTask.group.approval.requested).to.equal(false); - expect(syncedTask.group.approval.requestedDate).to.equal(undefined); - + await member.sync(); expect(member.notifications.length).to.equal(initialNotifications + 3); const notification = member.notifications[member.notifications.length - 1]; expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK'); - const taskText = syncedTask.text; - const managerName = member2.profile.name; + const taskText = task.text; + const managerName = member2.auth.local.username; expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName })); - expect(notification.data.task.id).to.equal(syncedTask._id); + expect(notification.data.task.id).to.equal(task._id); expect(notification.data.task.text).to.equal(taskText); - expect(notification.data.group.id).to.equal(syncedTask.group.id); + expect(notification.data.group.id).to.equal(task.group.id); expect(notification.data.group.name).to.equal(guild.name); expect(notification.data.manager.id).to.equal(member2._id); expect(notification.data.manager.name).to.equal(managerName); - - // Check that the managers' GROUP_TASK_APPROVAL notifications have been removed - await Promise.all([user.sync(), member2.sync()]); - - expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style - return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL'; - })).to.equal(undefined); - - expect(member2.notifications.find(n => { // eslint-disable-line arrow-body-style - return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL'; - })).to.equal(undefined); - }); - - it('prevents marking a task as needing work if it was already approved', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member2._id, - }); - - await member2.post(`/tasks/${task._id}/assign/${member._id}`); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - await member.post(`/tasks/${syncedTask._id}/score/up`); - await member2.post(`/tasks/${task._id}/approve/${member._id}`); - await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('canOnlyApproveTaskOnce'), - }); - }); - - it('prevents marking a task as needing work if it is not waiting for approval', async () => { - await user.post(`/tasks/${task._id}/assign/${member._id}`); - - await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) - .to.eventually.be.rejected.and.to.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskApprovalWasNotRequested'), - }); }); }); diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 8a665e2da2..a386420ad6 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -956,9 +956,14 @@ api.scoreCheckListItem = { if (validationErrors) throw validationErrors; const { taskId } = req.params; - const task = await Tasks.Task.findById(taskId); + const task = await Tasks.Task.findByIdOrAlias(taskId, user._id); - if (!task || (!task.id && !task.group.id)) throw new NotFound(res.t('taskNotFound')); + if (!task || (!task.userId && !task.group.id)) throw new NotFound(res.t('messageTaskNotFound')); + if (task.userId && task.userId !== user._id) { + throw new BadRequest('Cannot score task belonging to another user.'); + } else if (user.guilds.indexOf(task.group.id) === -1 && user.party._id !== task.group.id) { + throw new BadRequest('Cannot score task belonging to another user.'); + } if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo')); const item = _.find(task.checklist, { id: req.params.itemId }); diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index d32b1254fb..073a086612 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -347,6 +347,7 @@ api.taskNeedsWork = { User.findById(assignedUserId).exec(), await Tasks.Task.findOne({ _id: taskId, + 'group.assignedUsers': assignedUserId, }).exec(), ]); From 82abdaa0c45199d5fbb5bbafd9812bf651040094 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 21:47:55 -0500 Subject: [PATCH 134/171] WIP(tests): finish cleaning up v3 integrations --- ...OST-group_tasks_id_score_direction.test.js | 209 ++---------------- ...POST-tasks_group_id_assign_user_id.test.js | 65 +++--- .../POST-tasks_task_id_unassign.test.js | 22 +- .../tasks/groups/PUT-group_task_id.test.js | 91 +------- .../user/POST-user_class_cast_spellId.test.js | 12 +- .../integration/user/POST-user_reset.test.js | 5 +- .../user/POST-user_class_cast_spellId.test.js | 4 +- test/api/v4/user/POST-user_reset.test.js | 5 +- website/server/libs/spells.js | 1 - 9 files changed, 72 insertions(+), 342 deletions(-) diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js index eaa3cf6484..17d653895e 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js @@ -8,10 +8,6 @@ describe('POST /tasks/:id/score/:direction', () => { let user; let guild; let member; let member2; let task; - function findAssignedTask (memberTask) { - return memberTask.group.id === guild._id; - } - beforeEach(async () => { const { group, members, groupLeader } = await createAndPopulateGroup({ groupDetails: { @@ -30,209 +26,50 @@ describe('POST /tasks/:id/score/:direction', () => { task = await user.post(`/tasks/group/${guild._id}`, { text: 'test todo', type: 'todo', - requiresApproval: true, }); - await user.post(`/tasks/${task._id}/assign/${member._id}`); + await user.post(`/tasks/${task._id}/assign`, [member._id]); }); - it('prevents user from scoring a task that needs to be approved', async () => { - await user.update({ - 'preferences.language': 'cs', - }); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - const direction = 'up'; - - const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`); - - expect(response.data.requiresApproval).to.equal(true); - expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); - - const updatedTask = await member.get(`/tasks/${syncedTask._id}`); - - await user.sync(); - - expect(user.notifications.length).to.equal(3); - expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL'); - expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', { - user: member.auth.local.username, - taskName: updatedTask.text, - taskId: updatedTask._id, - direction, - }, 'cs')); // This test only works if we have the notification translated - expect(user.notifications[2].data.groupId).to.equal(guild._id); - - expect(updatedTask.group.approval.requested).to.equal(true); - expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type - }); - - it('sends notifications to all managers', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member2._id, - }); - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - const direction = 'up'; - - await member.post(`/tasks/${syncedTask._id}/score/${direction}`); - const updatedTask = await member.get(`/tasks/${syncedTask._id}`); - await user.sync(); - await member2.sync(); - - expect(user.notifications.length).to.equal(3); - expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL'); - expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', { - user: member.auth.local.username, - taskName: updatedTask.text, - taskId: updatedTask._id, - direction, - })); - expect(user.notifications[2].data.groupId).to.equal(guild._id); - - expect(member2.notifications.length).to.equal(2); - expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL'); - expect(member2.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', { - user: member.auth.local.username, - taskName: updatedTask.text, - taskId: updatedTask._id, - direction, - })); - expect(member2.notifications[1].data.groupId).to.equal(guild._id); - }); - - it('errors when approval has already been requested', async () => { - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - await member.post(`/tasks/${syncedTask._id}/score/up`); - - const response = await member.post(`/tasks/${syncedTask._id}/score/up`); - expect(response.data.requiresApproval).to.equal(true); - expect(response.message).to.equal(t('taskRequiresApproval')); - }); - - it('allows a user to score an approved task', async () => { - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - await member.post(`/tasks/${syncedTask._id}/score/up`); - - await user.post(`/tasks/${task._id}/approve/${member._id}`); - - await member.post(`/tasks/${syncedTask._id}/score/up`); - const updatedTask = await member.get(`/tasks/${syncedTask._id}`); - - expect(updatedTask.completed).to.equal(true); - expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type - }); - - it('completes master task when single-completion task is completed', async () => { - const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { - text: 'shared completion todo', - type: 'todo', - requiresApproval: false, - sharedCompletion: 'singleCompletion', - }); - - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); - const memberTasks = await member.get('/tasks/user'); - - const syncedTask = find( - memberTasks, - memberTask => memberTask.group.taskId === sharedCompletionTask._id, - ); - - await member.post(`/tasks/${syncedTask._id}/score/up`); + it('completes single-assigned task', async () => { + await member.post(`/tasks/${task._id}/score/up`); const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`); - const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id); + const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id); - expect(masterTask.completed).to.equal(true); + expect(sourceTask.completed).to.equal(true); }); - it('deletes other assigned user tasks when single-completion task is completed', async () => { - const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { - text: 'shared completion todo', - type: 'todo', - requiresApproval: false, - sharedCompletion: 'singleCompletion', + it('errors when task has already been completed', async () => { + await member.post(`/tasks/${task._id}/score/up`); + + await expect(member.post(`/tasks/${task._id}/score/up`)).to.be.rejected.and.to.eventually.eql({ + code: 401, + error: 'NotAuthorized', + message: t('sessionOutdated'), }); - - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); - const memberTasks = await member.get('/tasks/user'); - - const syncedTask = find( - memberTasks, - memberTask => memberTask.group.taskId === sharedCompletionTask._id, - ); - - await member.post(`/tasks/${syncedTask._id}/score/up`); - - const member2Tasks = await member2.get('/tasks/user'); - - const syncedTask2 = find( - member2Tasks, - memberTask => memberTask.group.taskId === sharedCompletionTask._id, - ); - - expect(syncedTask2).to.equal(undefined); }); - it('does not complete master task when not all user tasks are completed if all assigned must complete', async () => { - const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { - text: 'shared completion todo', - type: 'todo', - requiresApproval: false, - sharedCompletion: 'allAssignedCompletion', - }); + it('does not complete multi-assigned task when not all assignees have completed', async () => { + await user.post(`/tasks/${task._id}/assign`, [member2._id]); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); - const memberTasks = await member.get('/tasks/user'); - - const syncedTask = find( - memberTasks, - memberTask => memberTask.group.taskId === sharedCompletionTask._id, - ); - - await member.post(`/tasks/${syncedTask._id}/score/up`); + await member.post(`/tasks/${task._id}/score/up`); const groupTasks = await user.get(`/tasks/group/${guild._id}`); - const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id); + const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id); - expect(masterTask.completed).to.equal(false); + expect(sourceTask.completed).to.equal(false); }); - it('completes master task when all user tasks are completed if all assigned must complete', async () => { - const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, { - text: 'shared completion todo', - type: 'todo', - requiresApproval: false, - sharedCompletion: 'allAssignedCompletion', - }); + it('completes multi-assigned task when all assignees have completed', async () => { + await user.post(`/tasks/${task._id}/assign`, [member2._id]); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`); - await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`); - const memberTasks = await member.get('/tasks/user'); - const member2Tasks = await member2.get('/tasks/user'); - const syncedTask = find( - memberTasks, - memberTask => memberTask.group.taskId === sharedCompletionTask._id, - ); - const syncedTask2 = find( - member2Tasks, - memberTask => memberTask.group.taskId === sharedCompletionTask._id, - ); - - await member.post(`/tasks/${syncedTask._id}/score/up`); - await member2.post(`/tasks/${syncedTask2._id}/score/up`); + await member.post(`/tasks/${task._id}/score/up`); + await member2.post(`/tasks/${task._id}/score/up`); const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`); - const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id); + const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id); - expect(masterTask.completed).to.equal(true); + expect(sourceTask.completed).to.equal(true); }); }); diff --git a/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js b/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js index 71b29a1530..2e122e78fc 100644 --- a/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js +++ b/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js @@ -39,7 +39,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => { }); it('returns error when task is not found', async () => { - await expect(user.post(`/tasks/${generateUUID()}/assign/${member._id}`)) + await expect(user.post(`/tasks/${generateUUID()}/assign`, [member._id])) .to.eventually.be.rejected.and.eql({ code: 404, error: 'NotFound', @@ -56,7 +56,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => { notes: 1976, }); - await expect(user.post(`/tasks/${nonGroupTask._id}/assign/${member._id}`)) + await expect(user.post(`/tasks/${nonGroupTask._id}/assign`, [member._id])) .to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', @@ -67,7 +67,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => { it('returns error when user is not a member of the group', async () => { const nonUser = await generateUser(); - await expect(nonUser.post(`/tasks/${task._id}/assign/${member._id}`)) + await expect(nonUser.post(`/tasks/${task._id}/assign`, [member._id])) .to.eventually.be.rejected.and.eql({ code: 404, error: 'NotFound', @@ -76,7 +76,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => { }); it('returns error when non leader tries to create a task', async () => { - await expect(member2.post(`/tasks/${task._id}/assign/${member._id}`)) + await expect(member2.post(`/tasks/${task._id}/assign`, [member._id])) .to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', @@ -84,49 +84,23 @@ describe('POST /tasks/:taskId/assign/:memberId', () => { }); }); - it('allows user to assign themselves (claim)', async () => { - await member.post(`/tasks/${task._id}/assign/${member._id}`); - - const groupTask = await user.get(`/tasks/group/${guild._id}`); - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - expect(groupTask[0].group.assignedUsers).to.contain(member._id); - expect(syncedTask).to.exist; - }); - - it('sends notifications to group leader and managers when a task is claimed', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member2._id, - }); - await member.post(`/tasks/${task._id}/assign/${member._id}`); - await user.sync(); - await member2.sync(); - const groupTask = await user.get(`/tasks/group/${guild._id}`); - - expect(user.notifications.length).to.equal(3); // includes Guild Joined achievement - expect(user.notifications[2].type).to.equal('GROUP_TASK_CLAIMED'); - expect(user.notifications[2].data.taskId).to.equal(groupTask[0]._id); - expect(user.notifications[2].data.groupId).to.equal(guild._id); - expect(member2.notifications.length).to.equal(2); - expect(member2.notifications[1].type).to.equal('GROUP_TASK_CLAIMED'); - expect(member2.notifications[1].data.taskId).to.equal(groupTask[0]._id); - expect(member2.notifications[1].data.groupId).to.equal(guild._id); - }); - it('assigns a task to a user', async () => { - await user.post(`/tasks/${task._id}/assign/${member._id}`); + await user.post(`/tasks/${task._id}/assign`, [member._id]); const groupTask = await user.get(`/tasks/group/${guild._id}`); + await member.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); expect(groupTask[0].group.assignedUsers).to.contain(member._id); + expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist; expect(syncedTask).to.exist; }); it('sends a notification to assigned user', async () => { - await user.post(`/tasks/${task._id}/assign/${member._id}`); + await user.post(`/tasks/${task._id}/assign`, [member._id]); await member.sync(); const groupTask = await user.get(`/tasks/group/${guild._id}`); @@ -137,20 +111,27 @@ describe('POST /tasks/:taskId/assign/:memberId', () => { }); it('assigns a task to multiple users', async () => { - await user.post(`/tasks/${task._id}/assign/${member._id}`); - await user.post(`/tasks/${task._id}/assign/${member2._id}`); + await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]); const groupTask = await user.get(`/tasks/group/${guild._id}`); + await member.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); const memberTasks = await member.get('/tasks/user'); const member1SyncedTask = find(memberTasks, findAssignedTask); + await member2.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); const member2Tasks = await member2.get('/tasks/user'); const member2SyncedTask = find(member2Tasks, findAssignedTask); expect(groupTask[0].group.assignedUsers).to.contain(member._id); - expect(groupTask[0].group.assignedUsers).to.contain(member2._id); + expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist; expect(member1SyncedTask).to.exist; + expect(groupTask[0].group.assignedUsers).to.contain(member2._id); + expect(groupTask[0].group.assignedUsersDetail[member2._id]).to.exist; expect(member2SyncedTask).to.exist; }); @@ -159,13 +140,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => { managerId: member2._id, }); - await member2.post(`/tasks/${task._id}/assign/${member._id}`); + await member2.post(`/tasks/${task._id}/assign`, [member._id]); const groupTask = await member2.get(`/tasks/group/${guild._id}`); + await member.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); const memberTasks = await member.get('/tasks/user'); const syncedTask = find(memberTasks, findAssignedTask); expect(groupTask[0].group.assignedUsers).to.contain(member._id); + expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist; expect(syncedTask).to.exist; }); }); diff --git a/test/api/v3/integration/tasks/groups/POST-tasks_task_id_unassign.test.js b/test/api/v3/integration/tasks/groups/POST-tasks_task_id_unassign.test.js index 83d67246d6..117c9f760f 100644 --- a/test/api/v3/integration/tasks/groups/POST-tasks_task_id_unassign.test.js +++ b/test/api/v3/integration/tasks/groups/POST-tasks_task_id_unassign.test.js @@ -37,7 +37,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => { notes: 1976, }); - await user.post(`/tasks/${task._id}/assign/${member._id}`); + await user.post(`/tasks/${task._id}/assign`, [member._id]); }); it('returns error when task is not found', async () => { @@ -96,7 +96,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => { }); it('unassigns a user and only that user from a task', async () => { - await user.post(`/tasks/${task._id}/assign/${member2._id}`); + await user.post(`/tasks/${task._id}/assign`, [member2._id]); await user.post(`/tasks/${task._id}/unassign/${member._id}`); @@ -105,6 +105,9 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => { const memberTasks = await member.get('/tasks/user'); const member1SyncedTask = find(memberTasks, findAssignedTask); + await member2.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); const member2Tasks = await member2.get('/tasks/user'); const member2SyncedTask = find(member2Tasks, findAssignedTask); @@ -130,20 +133,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => { expect(syncedTask).to.not.exist; }); - it('allows a user to unassign themselves', async () => { - await member.post(`/tasks/${task._id}/unassign/${member._id}`); - - const groupTask = await user.get(`/tasks/group/${guild._id}`); - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - expect(groupTask[0].group.assignedUsers).to.not.contain(member._id); - expect(syncedTask).to.not.exist; - }); - - // @TODO: Which do we want? The user to unassign themselves or not. This test was in - // here, but then we had a request to allow to unaissgn. - xit('returns error when non leader tries to unassign their a task', async () => { + it('returns error when non leader tries to unassign a task', async () => { await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`)) .to.eventually.be.rejected.and.eql({ code: 401, diff --git a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js index abfd5ae629..330983c70c 100644 --- a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js +++ b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js @@ -1,4 +1,3 @@ -import { find } from 'lodash'; import { createAndPopulateGroup, translate as t, } from '../../../../../helpers/api-integration/v3'; @@ -11,10 +10,6 @@ describe('PUT /tasks/:id', () => { let habit; let todo; - function findAssignedTask (memberTask) { - return memberTask.group.id === guild._id; - } - beforeEach(async () => { const { group, members, groupLeader } = await createAndPopulateGroup({ groupDetails: { @@ -44,8 +39,7 @@ describe('PUT /tasks/:id', () => { notes: 1976, }); - await user.post(`/tasks/${habit._id}/assign/${member._id}`); - await user.post(`/tasks/${habit._id}/assign/${member2._id}`); + await user.post(`/tasks/${habit._id}/assign`, [member._id, member2._id]); }); it('updates a group task', async () => { @@ -56,28 +50,6 @@ describe('PUT /tasks/:id', () => { expect(savedHabit.notes).to.eql('some new notes'); }); - it('updates a group task - approval is required', async () => { - // allow to manage - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member._id, - }); - - // change the habit - habit = await member.put(`/tasks/${habit._id}`, { - text: 'new text!', - requiresApproval: true, - }); - - const memberTasks = await member2.get('/tasks/user'); - const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id); - - // score up to trigger approval - const response = await member2.post(`/tasks/${syncedTask._id}/score/up`); - - expect(response.data.requiresApproval).to.equal(true); - expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); - }); - it('member updates a group task value - not allowed', async () => { // change the todo await expect(member.put(`/tasks/${habit._id}`, { @@ -120,7 +92,7 @@ describe('PUT /tasks/:id', () => { ], }); - await user.post(`/tasks/${habit._id}/assign/${member._id}`); + await user.post(`/tasks/${habit._id}/assign`, [member._id]); // change the checklist text habit = await user.put(`/tasks/${habit._id}`, { @@ -137,63 +109,4 @@ describe('PUT /tasks/:id', () => { expect(habit.checklist.length).to.eql(2); }); - - it('updates the linked tasks', async () => { - await user.put(`/tasks/${habit._id}`, { - text: 'some new text', - up: false, - down: false, - notes: 'some new notes', - }); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - expect(syncedTask.text).to.eql('some new text'); - expect(syncedTask.up).to.eql(false); - expect(syncedTask.down).to.eql(false); - }); - - it('updates the linked tasks for all assigned users', async () => { - await user.put(`/tasks/${habit._id}`, { - text: 'some new text', - up: false, - down: false, - notes: 'some new notes', - }); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - const member2Tasks = await member2.get('/tasks/user'); - const member2SyncedTask = find(member2Tasks, findAssignedTask); - - expect(syncedTask.text).to.eql('some new text'); - expect(syncedTask.up).to.eql(false); - expect(syncedTask.down).to.eql(false); - - expect(member2SyncedTask.text).to.eql('some new text'); - expect(member2SyncedTask.up).to.eql(false); - expect(member2SyncedTask.down).to.eql(false); - }); - - it('updates the linked tasks', async () => { - await user.post(`/groups/${guild._id}/add-manager`, { - managerId: member2._id, - }); - - await member2.put(`/tasks/${habit._id}`, { - text: 'some new text', - up: false, - down: false, - notes: 'some new notes', - }); - - const memberTasks = await member.get('/tasks/user'); - const syncedTask = find(memberTasks, findAssignedTask); - - expect(syncedTask.text).to.eql('some new text'); - expect(syncedTask.up).to.eql(false); - expect(syncedTask.down).to.eql(false); - }); }); diff --git a/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js b/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js index df7676b945..18f2c19685 100644 --- a/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js +++ b/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js @@ -145,15 +145,12 @@ describe('POST /user/class/cast/:spellId', () => { text: 'todo group', type: 'todo', }); - await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`); - const memberTasks = await groupLeader.get('/tasks/user'); - const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id); - + await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]); await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 }); await sleep(0.5); await groupLeader.sync(); - await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`)) + await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`)) .to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', @@ -279,7 +276,10 @@ describe('POST /user/class/cast/:spellId', () => { type: 'todo', }); await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 }); - await user.post(`/tasks/${groupTask._id}/assign/${user._id}`); + await user.post(`/tasks/${groupTask._id}/assign`, [user._id]); + await user.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); await user.post('/user/class/cast/brightness'); await user.sync(); diff --git a/test/api/v3/integration/user/POST-user_reset.test.js b/test/api/v3/integration/user/POST-user_reset.test.js index 5d750fde98..a3c00a90a3 100644 --- a/test/api/v3/integration/user/POST-user_reset.test.js +++ b/test/api/v3/integration/user/POST-user_reset.test.js @@ -100,11 +100,14 @@ describe('POST /user/reset', () => { text: 'todo group', type: 'todo', }); - await user.post(`/tasks/${groupTask._id}/assign/${user._id}`); + await user.post(`/tasks/${groupTask._id}/assign`, [user._id]); await user.post('/user/reset'); await user.sync(); + await user.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); const memberTasks = await user.get('/tasks/user'); const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id); diff --git a/test/api/v4/user/POST-user_class_cast_spellId.test.js b/test/api/v4/user/POST-user_class_cast_spellId.test.js index b9382bba3d..75d0cb35fc 100644 --- a/test/api/v4/user/POST-user_class_cast_spellId.test.js +++ b/test/api/v4/user/POST-user_class_cast_spellId.test.js @@ -130,7 +130,7 @@ describe('POST /user/class/cast/:spellId', () => { text: 'todo group', type: 'todo', }); - await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`); + await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]); const memberTasks = await groupLeader.get('/tasks/user'); const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id); @@ -247,7 +247,7 @@ describe('POST /user/class/cast/:spellId', () => { type: 'todo', }); await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 }); - await user.post(`/tasks/${groupTask._id}/assign/${user._id}`); + await user.post(`/tasks/${groupTask._id}/assign`, [user._id]); await user.post('/user/class/cast/brightness'); await user.sync(); diff --git a/test/api/v4/user/POST-user_reset.test.js b/test/api/v4/user/POST-user_reset.test.js index ced9126eb2..c96e30f93a 100644 --- a/test/api/v4/user/POST-user_reset.test.js +++ b/test/api/v4/user/POST-user_reset.test.js @@ -100,11 +100,14 @@ describe('POST /user/reset', () => { text: 'todo group', type: 'todo', }); - await user.post(`/tasks/${groupTask._id}/assign/${user._id}`); + await user.post(`/tasks/${groupTask._id}/assign`, [user._id]); await user.post('/user/reset'); await user.sync(); + await user.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); const memberTasks = await user.get('/tasks/user'); const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id); diff --git a/website/server/libs/spells.js b/website/server/libs/spells.js index 9319205ab4..6132ec5f26 100644 --- a/website/server/libs/spells.js +++ b/website/server/libs/spells.js @@ -22,7 +22,6 @@ async function castTaskSpell (res, req, targetId, user, spell, quantity = 1) { const task = await Tasks.Task.findOne({ _id: targetId, - userId: user._id, }).exec(); if (!task) throw new NotFound(res.t('messageTaskNotFound')); if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast')); From 4c85b933cbf88891fc9bfac8b9c0a716ff573a99 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Mon, 22 Aug 2022 21:59:51 -0500 Subject: [PATCH 135/171] fix(tests): correct one last v3 test and wrap v4 --- test/api/v3/integration/user/PUT-user.test.js | 2 +- test/api/v4/user/POST-user_class_cast_spellId.test.js | 8 ++++---- test/api/v4/user/PUT-user.test.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/api/v3/integration/user/PUT-user.test.js b/test/api/v3/integration/user/PUT-user.test.js index 1a0451318f..ced94b2232 100644 --- a/test/api/v3/integration/user/PUT-user.test.js +++ b/test/api/v3/integration/user/PUT-user.test.js @@ -35,7 +35,7 @@ describe('PUT /user', () => { })).to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', - message: 'mustBeArray', + message: 'Tag list must be an array.', }); }); diff --git a/test/api/v4/user/POST-user_class_cast_spellId.test.js b/test/api/v4/user/POST-user_class_cast_spellId.test.js index 75d0cb35fc..6bfa705d0c 100644 --- a/test/api/v4/user/POST-user_class_cast_spellId.test.js +++ b/test/api/v4/user/POST-user_class_cast_spellId.test.js @@ -131,14 +131,11 @@ describe('POST /user/class/cast/:spellId', () => { type: 'todo', }); await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]); - const memberTasks = await groupLeader.get('/tasks/user'); - const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id); - await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 }); await sleep(0.5); await groupLeader.sync(); - await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`)) + await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`)) .to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', @@ -248,6 +245,9 @@ describe('POST /user/class/cast/:spellId', () => { }); await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 }); await user.post(`/tasks/${groupTask._id}/assign`, [user._id]); + await user.put('/user', { + 'preferences.tasks.mirrorGroupTasks': [guild._id], + }); await user.post('/user/class/cast/brightness'); await user.sync(); diff --git a/test/api/v4/user/PUT-user.test.js b/test/api/v4/user/PUT-user.test.js index a588a6f386..1abf4f8d24 100644 --- a/test/api/v4/user/PUT-user.test.js +++ b/test/api/v4/user/PUT-user.test.js @@ -34,7 +34,7 @@ describe('PUT /user', () => { })).to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', - message: 'mustBeArray', + message: 'Tag list must be an array.', }); }); From da5c3f9602da8dbeab0292677a6f4dd83d7c647a Mon Sep 17 00:00:00 2001 From: SabreCat Date: Tue, 23 Aug 2022 09:36:23 -0500 Subject: [PATCH 136/171] fix(tasks): address not-found error on open uncheck --- website/server/controllers/api-v3/tasks/groups.js | 1 - 1 file changed, 1 deletion(-) diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 073a086612..d32b1254fb 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -347,7 +347,6 @@ api.taskNeedsWork = { User.findById(assignedUserId).exec(), await Tasks.Task.findOne({ _id: taskId, - 'group.assignedUsers': assignedUserId, }).exec(), ]); From 395b8db932ebec42ddaa15edd9e3441aba9d2f35 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Tue, 23 Aug 2022 09:54:44 -0500 Subject: [PATCH 137/171] fix(tasks): fix unassigned error case --- ...T-group_tasks_id_needs-work_userId.test.js | 6 +-- .../server/controllers/api-v3/tasks/groups.js | 37 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js index 1b35e1076b..58392e85e1 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_needs-work_userId.test.js @@ -32,9 +32,9 @@ describe('POST /tasks/:id/needs-work/:userId', () => { it('errors when user is not assigned', async () => { await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`)) .to.eventually.be.rejected.and.to.eql({ - code: 404, - error: 'NotFound', - message: t('messageTaskNotFound'), + code: 400, + error: 'BadRequest', + message: 'Task not completed by this user.', }); }); diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index d32b1254fb..8d0c671421 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -356,10 +356,11 @@ api.taskNeedsWork = { if (['daily', 'todo'].indexOf(task.type) === -1) { throw new BadRequest('Cannot roll back use of Habits or Rewards.'); } + if (!task.group.assignedUsersDetail) task.group.assignedUsersDetail = {}; if ( (task.group.completedBy.userId && task.group.completedBy.userId !== assignedUserId) - || (task.group.assignedUsersDetail && !(task.group.assignedUsersDetail[assignedUserId] - && task.group.assignedUsersDetail[assignedUserId].completed))) { + || !(task.group.assignedUsersDetail[assignedUserId] + && task.group.assignedUsersDetail[assignedUserId].completed)) { throw new BadRequest('Task not completed by this user.'); } @@ -370,21 +371,23 @@ api.taskNeedsWork = { if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); await scoreTasks(assignedUser, [{ id: task._id, direction: 'down' }], req, res); - assignedUser.addNotification('GROUP_TASK_NEEDS_WORK', { - message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.auth.local.username }, assignedUser.preferences.language), - task: { - id: task._id, - text: task.text, - }, - group: { - id: group._id, - name: group.name, - }, - manager: { - id: user._id, - name: user.auth.local.username, - }, - }); + if (assignedUserId !== user._id) { + assignedUser.addNotification('GROUP_TASK_NEEDS_WORK', { + message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.auth.local.username }, assignedUser.preferences.language), + task: { + id: task._id, + text: task.text, + }, + group: { + id: group._id, + name: group.name, + }, + manager: { + id: user._id, + name: user.auth.local.username, + }, + }); + } await Promise.all([assignedUser.save(), task.save()]); res.respond(200, task); From 971891dd6b68dd3ef2ff9759231377394dcac1b4 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Tue, 23 Aug 2022 10:48:21 -0500 Subject: [PATCH 138/171] fix(tasks): no really, address not-found error --- website/server/controllers/api-v3/tasks/groups.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 8d0c671421..963fcdbf91 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -356,11 +356,13 @@ api.taskNeedsWork = { if (['daily', 'todo'].indexOf(task.type) === -1) { throw new BadRequest('Cannot roll back use of Habits or Rewards.'); } - if (!task.group.assignedUsersDetail) task.group.assignedUsersDetail = {}; - if ( - (task.group.completedBy.userId && task.group.completedBy.userId !== assignedUserId) - || !(task.group.assignedUsersDetail[assignedUserId] - && task.group.assignedUsersDetail[assignedUserId].completed)) { + + if (task.group.completedBy.userId) { + if (task.group.completedBy.userId !== assignedUserId) { + throw new BadRequest('Task not completed by this user.'); + } + } else if (!task.group.assignedUsersDetail || !task.group.assignedUsersDetail[assignedUserId] + || !task.group.assignedUsersDetail[assignedUserId].completed) { throw new BadRequest('Task not completed by this user.'); } From aebf13810f1e5c5686f0fcb7452f71e130a74dbe Mon Sep 17 00:00:00 2001 From: SabreCat Date: Tue, 23 Aug 2022 11:33:06 -0500 Subject: [PATCH 139/171] fix(tasks): remove spurious uncheck notification --- website/server/libs/tasks/index.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index faf36cd077..9dd0ef77d9 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -449,21 +449,6 @@ async function scoreTask (user, task, direction, req, res) { user: rollbackUser, direction, }, req, res.analytics); - rollbackUser.addNotification('GROUP_TASK_NEEDS_WORK', { - message: res.t('taskNeedsWork', { taskText: task.text, managerName: user.auth.local.username }, rollbackUser.preferences.language), - task: { - id: task._id, - text: task.text, - }, - group: { - id: group._id, - name: group.name, - }, - manager: { - id: user._id, - name: user.auth.local.username, - }, - }); await rollbackUser.save(); } else { delta = shared.ops.scoreTask({ task, user, direction }, req, res.analytics); From 67ee0b72d33fe26aca89065b6c2e952acc79cbe7 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Tue, 23 Aug 2022 12:40:32 -0500 Subject: [PATCH 140/171] fix(tasks): don't show reset counter control on group tasks --- website/client/src/components/tasks/taskModal.vue | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue index 3fdfdf7b66..6a2d7b0372 100644 --- a/website/client/src/components/tasks/taskModal.vue +++ b/website/client/src/components/tasks/taskModal.vue @@ -386,7 +386,7 @@
@@ -1092,11 +1092,11 @@ export default { isChallengeTask () { return Boolean(this.task.challenge && this.task.challenge.id); }, - onUserPage () { + isUserTask () { return !this.challengeId && !this.groupId; }, challengeAccessRequired () { - return this.onUserPage && this.isChallengeTask; + return this.isUserTask && this.isChallengeTask; }, isOriginalChallengeTask () { const isUserChallenge = Boolean(this.task.userId); @@ -1113,9 +1113,6 @@ export default { const type = this.$t(this.task.type); return this.$t(this.purpose === 'edit' ? 'editATask' : 'createTask', { type }); }, - isUserTask () { - return !this.challengeId && !this.groupId; - }, repeatSuffix () { const { task } = this; From ae3f0641972fcde11b8aa11900e8431fa6d7fe81 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Tue, 23 Aug 2022 12:49:25 -0500 Subject: [PATCH 141/171] 4.241.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f68640825d..021fc37420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.240.0", + "version": "4.241.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b1877e6467..56d14f55a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.240.0", + "version": "4.241.0", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.18.10", From 20854057ad987630644d359cb46e3cabd76d57e9 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Tue, 23 Aug 2022 19:18:01 +0000 Subject: [PATCH 142/171] fix(migration): handle orphaned assignments --- migrations/tasks/team-tasks-v2.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/migrations/tasks/team-tasks-v2.js b/migrations/tasks/team-tasks-v2.js index 2b56a66627..a5c90cab95 100644 --- a/migrations/tasks/team-tasks-v2.js +++ b/migrations/tasks/team-tasks-v2.js @@ -26,7 +26,7 @@ async function updateTeamTasks (team) { const userTask = find(teamUserTasks, task => task.userId === assignedUserId && task.group.taskId === boardTask._id); if (!boardTask.group.assignedUsersDetail) boardTask.group.assignedUsersDetail = {}; - if (userTask) { + if (userTask && assignedUser) { boardTask.group.assignedUsersDetail[assignedUserId] = { assignedDate: userTask.group.assignedDate, assignedUsername: assignedUser.auth.local.username, @@ -34,8 +34,7 @@ async function updateTeamTasks (team) { completed: userTask.completed || false, completedDate: userTask.dateCompleted, }; - toSave.push(Tasks.Task.findByIdAndDelete(userTask._id)); - } else { + } else if (assignedUser) { boardTask.group.assignedUsersDetail[assignedUserId] = { assignedDate: new Date(), assignedUsername: assignedUser.auth.local.username, @@ -43,7 +42,11 @@ async function updateTeamTasks (team) { completed: false, completedDate: null, }; + } else { + const taskIndex = boardTask.group.assignedUsers.indexOf(assignedUserId); + boardTask.group.assignedUsers.splice(taskIndex, 1); } + if (userTask) toSave.push(Tasks.Task.findByIdAndDelete(userTask._id)); } boardTask.markModified('group'); toSave.push(boardTask.save()); From 75c8486b1a3f7bcc06e9d525865648afc8d50176 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Tue, 23 Aug 2022 14:38:05 -0500 Subject: [PATCH 143/171] fix(checklists): allow scoring own items --- website/server/controllers/api-v3/tasks.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index a386420ad6..c937f5efb0 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -961,7 +961,8 @@ api.scoreCheckListItem = { if (!task || (!task.userId && !task.group.id)) throw new NotFound(res.t('messageTaskNotFound')); if (task.userId && task.userId !== user._id) { throw new BadRequest('Cannot score task belonging to another user.'); - } else if (user.guilds.indexOf(task.group.id) === -1 && user.party._id !== task.group.id) { + } else if (task.group.id && user.guilds.indexOf(task.group.id) === -1 + && user.party._id !== task.group.id) { throw new BadRequest('Cannot score task belonging to another user.'); } if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo')); From a2f191089c82bb84cf9820c6414bb773ef4341bc Mon Sep 17 00:00:00 2001 From: SabreCat Date: Tue, 23 Aug 2022 14:38:09 -0500 Subject: [PATCH 144/171] 4.241.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 021fc37420..156312dd24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.241.0", + "version": "4.241.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 56d14f55a8..24d4463760 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.241.0", + "version": "4.241.1", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.18.10", From d5bbc9599c06745446edfeea6dd0ddfef6aa59a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Aug 2022 19:49:41 -0400 Subject: [PATCH 145/171] build(deps): bump core-js from 3.23.5 to 3.24.1 in /website/client (#14154) Bumps [core-js](https://github.com/zloirock/core-js) from 3.23.5 to 3.24.1. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/compare/v3.23.5...v3.24.1) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- website/client/package-lock.json | 6 +++--- website/client/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/website/client/package-lock.json b/website/client/package-lock.json index 1275af51c4..0f25f93dfb 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -15880,9 +15880,9 @@ } }, "core-js": { - "version": "3.23.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.5.tgz", - "integrity": "sha512-7Vh11tujtAZy82da4duVreQysIoO2EvVrur7y6IzZkH1IHPSekuDi8Vuw1+YKjkbfWLRD7Nc9ICQ/sIUDutcyg==" + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz", + "integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==" }, "core-js-compat": { "version": "3.11.0", diff --git a/website/client/package.json b/website/client/package.json index 105b9cc28c..de0c012ae1 100644 --- a/website/client/package.json +++ b/website/client/package.json @@ -32,7 +32,7 @@ "bootstrap": "^4.6.0", "bootstrap-vue": "^2.22.0", "chai": "^4.3.6", - "core-js": "^3.23.5", + "core-js": "^3.24.1", "dompurify": "^2.3.10", "eslint": "^6.8.0", "eslint-config-habitrpg": "^6.2.0", From e6b0c1e48849a8065ebf0042290a0339ca950baa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Aug 2022 19:57:59 -0400 Subject: [PATCH 146/171] build(deps): bump @babel/core from 7.18.10 to 7.18.13 (#14184) Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.18.10 to 7.18.13. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.18.13/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 181 +++++++--------------------------------------- package.json | 2 +- 2 files changed, 27 insertions(+), 156 deletions(-) diff --git a/package-lock.json b/package-lock.json index 021fc37420..0b2090c758 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,20 +27,20 @@ "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==" }, "@babel/core": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", - "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz", + "integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==", "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.10", + "@babel/generator": "^7.18.13", "@babel/helper-compilation-targets": "^7.18.9", "@babel/helper-module-transforms": "^7.18.9", "@babel/helpers": "^7.18.9", - "@babel/parser": "^7.18.10", + "@babel/parser": "^7.18.13", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.18.10", - "@babel/types": "^7.18.10", + "@babel/traverse": "^7.18.13", + "@babel/types": "^7.18.13", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -56,61 +56,16 @@ "@babel/highlight": "^7.18.6" } }, - "@babel/compat-data": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz", - "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==" - }, "@babel/generator": { - "version": "7.18.12", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", - "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz", + "integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==", "requires": { - "@babel/types": "^7.18.10", + "@babel/types": "^7.18.13", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" } }, - "@babel/helper-compilation-targets": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz", - "integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==", - "requires": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.20.2", - "semver": "^6.3.0" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" - }, - "@babel/helper-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", - "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", - "requires": { - "@babel/template": "^7.18.6", - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-module-transforms": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz", - "integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==", - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", - "@babel/template": "^7.18.6", - "@babel/traverse": "^7.18.9", - "@babel/types": "^7.18.9" - } - }, "@babel/helper-validator-identifier": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", @@ -127,9 +82,9 @@ } }, "@babel/parser": { - "version": "7.18.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", - "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==" + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz", + "integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==" }, "@babel/template": { "version": "7.18.10", @@ -142,26 +97,26 @@ } }, "@babel/traverse": { - "version": "7.18.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz", - "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz", + "integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==", "requires": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.10", + "@babel/generator": "^7.18.13", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.18.9", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.11", - "@babel/types": "^7.18.10", + "@babel/parser": "^7.18.13", + "@babel/types": "^7.18.13", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz", - "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz", + "integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==", "requires": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", @@ -646,104 +601,20 @@ "@babel/types": "^7.18.9" }, "dependencies": { - "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/generator": { - "version": "7.18.12", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", - "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", - "requires": { - "@babel/types": "^7.18.10", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" - }, - "@babel/helper-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", - "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", - "requires": { - "@babel/template": "^7.18.6", - "@babel/types": "^7.18.9" - } - }, "@babel/helper-validator-identifier": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==" }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.18.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", - "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==" - }, - "@babel/traverse": { - "version": "7.18.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz", - "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.10", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.11", - "@babel/types": "^7.18.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, "@babel/types": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz", - "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "version": "7.18.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz", + "integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==", "requires": { "@babel/helper-string-parser": "^7.18.10", "@babel/helper-validator-identifier": "^7.18.6", "to-fast-properties": "^2.0.0" } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } } } }, diff --git a/package.json b/package.json index 56d14f55a8..d8349eb211 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "4.241.0", "main": "./website/server/index.js", "dependencies": { - "@babel/core": "^7.18.10", + "@babel/core": "^7.18.13", "@babel/preset-env": "^7.18.10", "@babel/register": "^7.18.9", "@google-cloud/trace-agent": "^5.1.6", From 1aae9638ecf285ee1d9989c745ded4c6c24e2be4 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Wed, 24 Aug 2022 11:05:19 -0500 Subject: [PATCH 147/171] fix(tasks): address regressions from group plan rollout --- .../group-plans/groupPlansUpdateModal.vue | 6 ++---- .../client/src/components/tasks/taskModal.vue | 4 +--- website/server/libs/tasks/index.js | 20 ++++++++++++++----- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/website/client/src/components/group-plans/groupPlansUpdateModal.vue b/website/client/src/components/group-plans/groupPlansUpdateModal.vue index e79c2dd39f..30205a0576 100644 --- a/website/client/src/components/group-plans/groupPlansUpdateModal.vue +++ b/website/client/src/components/group-plans/groupPlansUpdateModal.vue @@ -6,6 +6,7 @@ hide-footer="hide-footer" :no-close-on-backdrop="true" :no-close-on-esc="true" + :centered="true" >
@import '~@/assets/scss/colors.scss'; #group-plans-update { - overflow-y: hidden; - .modal-content { border-top-left-radius: 10px; border-top-right-radius: 10px; @@ -107,8 +106,7 @@ .modal-dialog { max-width: 566px; - margin-top: 50vh; - transform: translateY(-50%); + margin-top: 0px; } .modal-header { diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue index 6a2d7b0372..0dae29e9a5 100644 --- a/website/client/src/components/tasks/taskModal.vue +++ b/website/client/src/components/tasks/taskModal.vue @@ -79,7 +79,6 @@
@@ -363,7 +361,7 @@
diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index 9dd0ef77d9..d32280fd3c 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -156,6 +156,7 @@ async function getTasks (req, res, options = {}) { let limit; let sort; let upgradedGroups = []; + const upgradedGroupIds = []; const owner = group || challenge || user; if (challenge) { @@ -179,15 +180,19 @@ async function getTasks (req, res, options = {}) { ).exec(); } if (upgradedGroups.length > 0) { - const upgradedGroupIds = []; for (const upgradedGroup of upgradedGroups) { upgradedGroupIds.push(upgradedGroup._id); } query = { $or: [ { userId: user._id }, - { 'group.assignedUsers': user._id }, - { 'group.id': { $in: upgradedGroupIds }, 'group.assignedUsers.0': { $exists: false } }, + { + 'group.id': { $in: upgradedGroupIds }, + $or: [ + { 'group.assignedUsers': user._id }, + { 'group.assignedUsers.0': { $exists: false } }, + ], + }, ], }; } else { @@ -214,8 +219,13 @@ async function getTasks (req, res, options = {}) { if (upgradedGroups.length > 0) { query.$or = [ { userId: user._id }, - { 'group.assignedUsers': user._id }, - { 'group.completedBy.userId': user._id }, + { + 'group.id': { $in: upgradedGroupIds }, + $or: [ + { 'group.assignedUsers': user._id }, + { 'group.completedBy.userId': user._id }, + ], + }, ]; } else if (owner._id === user._id) { query.userId = user._id; From bc8b1884b7e9bf125d3f0b489a9b7fa0899c9feb Mon Sep 17 00:00:00 2001 From: SabreCat Date: Wed, 24 Aug 2022 11:05:36 -0500 Subject: [PATCH 148/171] 4.241.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 156312dd24..7c72e5b6b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.241.1", + "version": "4.241.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 24d4463760..036fb92395 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.241.1", + "version": "4.241.2", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.18.10", From a5d9448af1d055d2a6a13f04f7eefe0f19757e48 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Wed, 24 Aug 2022 14:18:07 -0500 Subject: [PATCH 149/171] fix(cron): don't process group tasks during user cron --- website/server/middlewares/cron.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js index ce087ef1f4..7c837c9632 100644 --- a/website/server/middlewares/cron.js +++ b/website/server/middlewares/cron.js @@ -74,19 +74,10 @@ async function cronAsync (req, res) { } const tasks = await Tasks.Task.find({ - $and: [ - { - $or: [ - { userId: user._id }, - { userId: { $exists: false }, 'group.assignedUsers': user._id }, - ], - }, - { - $or: [ // Exclude completed todos - { type: 'todo', completed: false }, - { type: { $in: ['habit', 'daily', 'reward'] } }, - ], - }, + userId: user._id, + $or: [ // Exclude completed todos + { type: 'todo', completed: false }, + { type: { $in: ['habit', 'daily'] } }, ], }).exec(); From 6aabf7b19a3f030685d1c416850d976c6c417cce Mon Sep 17 00:00:00 2001 From: SabreCat Date: Wed, 24 Aug 2022 14:18:20 -0500 Subject: [PATCH 150/171] 4.241.3 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c72e5b6b2..4db3e6d0fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.241.2", + "version": "4.241.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 036fb92395..b4e56c52f7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.241.2", + "version": "4.241.3", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.18.10", From 7129639bbfdac559dc3253593114f61ce9ea4200 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Thu, 25 Aug 2022 09:24:08 -0500 Subject: [PATCH 151/171] fix(misc): correct one groups issue and two others --- website/client/src/store/getters/tasks.js | 3 ++- website/server/libs/spells.js | 1 + website/server/libs/user/index.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/website/client/src/store/getters/tasks.js b/website/client/src/store/getters/tasks.js index fbee4bd06e..859201de84 100644 --- a/website/client/src/store/getters/tasks.js +++ b/website/client/src/store/getters/tasks.js @@ -84,7 +84,8 @@ export function canEdit (store) { const user = store.state.user.data; const userId = user.id || user._id; - const isUserAdmin = user.permissions && user.permissions.challengeAdmin; + const isUserAdmin = user.permissions + && (user.permissions.challengeAdmin || user.permissions.fullAccess); const isUserGroupLeader = group && (group.leader && group.leader._id === userId); const isUserGroupManager = group && (group.managers diff --git a/website/server/libs/spells.js b/website/server/libs/spells.js index 6132ec5f26..4a132f290b 100644 --- a/website/server/libs/spells.js +++ b/website/server/libs/spells.js @@ -21,6 +21,7 @@ async function castTaskSpell (res, req, targetId, user, spell, quantity = 1) { if (!targetId) throw new BadRequest(res.t('targetIdUUID')); const task = await Tasks.Task.findOne({ + userId: user._id, _id: targetId, }).exec(); if (!task) throw new NotFound(res.t('messageTaskNotFound')); diff --git a/website/server/libs/user/index.js b/website/server/libs/user/index.js index 8e19db0ea8..35068ad3f9 100644 --- a/website/server/libs/user/index.js +++ b/website/server/libs/user/index.js @@ -138,7 +138,7 @@ export async function update (req, res, { isV3 = false }) { if (!Array.isArray(groupsToMirror)) { throw new BadRequest('Groups to copy tasks from must be an array.'); } - const memberGroups = user.guilds; + const memberGroups = _.clone(user.guilds); if (user.party._id) memberGroups.push(user.party._id); for (const targetGroup of groupsToMirror) { if (memberGroups.indexOf(targetGroup) === -1) { From 986d38af699c1a9e1ea59030649d90ed8dc366ce Mon Sep 17 00:00:00 2001 From: SabreCat Date: Thu, 25 Aug 2022 09:24:17 -0500 Subject: [PATCH 152/171] 4.241.4 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4db3e6d0fa..f7de511b72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.241.3", + "version": "4.241.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b4e56c52f7..0a86cdcd4c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.241.3", + "version": "4.241.4", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.18.10", From 3284611bbf34cb6f9ad3497d3a904fa20206442d Mon Sep 17 00:00:00 2001 From: SabreCat Date: Thu, 25 Aug 2022 10:22:16 -0500 Subject: [PATCH 153/171] fix(test): adjust expectation for exploit fix --- test/api/v4/user/POST-user_class_cast_spellId.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/api/v4/user/POST-user_class_cast_spellId.test.js b/test/api/v4/user/POST-user_class_cast_spellId.test.js index 6bfa705d0c..8602cb7c2a 100644 --- a/test/api/v4/user/POST-user_class_cast_spellId.test.js +++ b/test/api/v4/user/POST-user_class_cast_spellId.test.js @@ -137,9 +137,9 @@ describe('POST /user/class/cast/:spellId', () => { await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`)) .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('groupTasksNoCast'), + code: 404, + error: 'NotFound', + message: t('messageTaskNotFound'), }); }); From ee0f6fd78f3d236a76bb6e20b9829625821ef5f0 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Thu, 25 Aug 2022 10:35:49 -0500 Subject: [PATCH 154/171] fix(test): same thing for v3 --- .../integration/user/POST-user_class_cast_spellId.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js b/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js index 18f2c19685..eeda226b45 100644 --- a/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js +++ b/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js @@ -152,9 +152,9 @@ describe('POST /user/class/cast/:spellId', () => { await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`)) .to.eventually.be.rejected.and.eql({ - code: 400, - error: 'BadRequest', - message: t('groupTasksNoCast'), + code: 404, + error: 'NotFound', + message: t('messageTaskNotFound'), }); }); From f158852be57917a40111db1473881841b062d2fe Mon Sep 17 00:00:00 2001 From: Jason Mishi Carvalho Date: Sat, 27 Aug 2022 01:52:04 +0530 Subject: [PATCH 155/171] Grey out skill when insufficient mana fixes #13286 (#14100) * disable spell if user doesn't have enough mana * differenciate insufficient mana and disabled spell * linting * reduce opacity, no hover state when insufficient mana * display that lvl insufficient in spell tooltip * change spell text color when spell has no effect change spell-text color to blue-500 when spell has no effect --- .../client/src/components/tasks/spells.vue | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) mode change 100644 => 100755 website/client/src/components/tasks/spells.vue diff --git a/website/client/src/components/tasks/spells.vue b/website/client/src/components/tasks/spells.vue old mode 100644 new mode 100755 index 8e690aee94..bcdcfff581 --- a/website/client/src/components/tasks/spells.vue +++ b/website/client/src/components/tasks/spells.vue @@ -68,7 +68,8 @@
Level {{ skill.lvl }}
-
-
-
-
{{ skill.mana }}
-
-
Date: Sat, 27 Aug 2022 02:15:32 +0530 Subject: [PATCH 156/171] Fix prop change handler for guild challenges. (#14169) Fix props' change handler which is called when guild is changed from notification. Tests: + Guild challenges updated successfully when guild is changed from notification center. Co-authored-by: neel --- .../client/src/components/challenges/groupChallenges.vue | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/website/client/src/components/challenges/groupChallenges.vue b/website/client/src/components/challenges/groupChallenges.vue index 6a19e34700..fc370700c8 100644 --- a/website/client/src/components/challenges/groupChallenges.vue +++ b/website/client/src/components/challenges/groupChallenges.vue @@ -112,10 +112,8 @@ export default { }, }, watch: { - 'group._id': { - async groupId () { - this.loadChallenges(); - }, + 'group._id': function groupId () { + this.loadChallenges(); }, }, mounted () { From 509cb0037486b9e541e83ba93021bd09a8ed6e32 Mon Sep 17 00:00:00 2001 From: Natalie L <78037386+CuriousMagpie@users.noreply.github.com> Date: Fri, 26 Aug 2022 16:46:43 -0400 Subject: [PATCH 157/171] fix(api): add API version (#14177) --- apidoc/apidoc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apidoc/apidoc.json b/apidoc/apidoc.json index 8b2893529d..2b7651614d 100644 --- a/apidoc/apidoc.json +++ b/apidoc/apidoc.json @@ -2,6 +2,7 @@ "name": "Habitica V3 API Documentation", "title": "Habitica", "url": "https://habitica.com", + "version": "3.0.0", "sampleUrl": null, "header": { "title": "Introduction", From 3664a1ebb1df98dbe63bb1d16ee29855c4eefad4 Mon Sep 17 00:00:00 2001 From: Natalie L <78037386+CuriousMagpie@users.noreply.github.com> Date: Fri, 26 Aug 2022 16:51:21 -0400 Subject: [PATCH 158/171] fix(tavern): update Pause Damage Description, and Staff list (#14174) --- .../client/src/components/groups/tavern.vue | 19 ++++++++++++++++--- website/client/src/libs/staffList.js | 5 +++++ website/common/locales/en/npc.json | 7 +++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/website/client/src/components/groups/tavern.vue b/website/client/src/components/groups/tavern.vue index 649f9c6274..2eea992468 100644 --- a/website/client/src/components/groups/tavern.vue +++ b/website/client/src/components/groups/tavern.vue @@ -247,9 +247,6 @@
  • {{ $t('sleepBullet3') }}
  • -
  • - {{ $t('sleepBullet4') }} -