From fa99458ca45a2406179dca77b671f6816758b5b9 Mon Sep 17 00:00:00 2001 From: SabreCat Date: Thu, 11 Nov 2021 16:32:30 -0600 Subject: [PATCH] WIP(multiassign): resume shared completion implementation --- .../client/src/components/tasks/taskModal.vue | 60 +++++++++--------- website/client/src/mixins/scoreTask.js | 20 +----- website/common/script/ops/scoreTask.js | 5 -- website/server/controllers/api-v3/tasks.js | 15 ++--- website/server/libs/groupTasks.js | 34 ++++------- website/server/libs/tasks/index.js | 61 ++++++++++--------- website/server/models/task.js | 5 +- 7 files changed, 80 insertions(+), 120 deletions(-) diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue index 391443aef1..4428eeeed6 100644 --- a/website/client/src/components/tasks/taskModal.vue +++ b/website/client/src/components/tasks/taskModal.vue @@ -454,19 +454,19 @@ class="col-10 mb-1" >{{ $t('assignedTo') }} {{ $t('clear') }}
-
@@ -1056,11 +1056,9 @@ import goldIcon from '@/assets/svg/gold.svg'; import chevronIcon from '@/assets/svg/chevron.svg'; import calendarIcon from '@/assets/svg/calendar.svg'; import gripIcon from '@/assets/svg/grip.svg'; -import SelectSingle from './modal-controls/selectSingle'; export default { components: { - SelectSingle, SelectMulti, Datepicker, checklist, @@ -1093,7 +1091,7 @@ export default { members: [], membersNameAndId: [], memberNamesById: {}, - assignedMember: null, + assignedMembers: [], managers: [], showAdvancedOptions: false, attributesStrings: { @@ -1284,10 +1282,9 @@ export default { }); this.memberNamesById[member._id] = member.profile.name; }); - this.assignedMember = null; + this.assignedMembers = []; if (this.task.group?.assignedUsers?.length > 0) { - // eslint-disable-next-line prefer-destructuring - this.assignedMember = this.task.group.assignedUsers[0]; + this.assignedMembers = this.task.group.assignedUsers; } } @@ -1472,13 +1469,12 @@ export default { tasks: [this.task], }); Object.assign(this.task, response); - if (this.assignedMember) { - await this.$store.dispatch('tasks:assignTask', { - taskId: this.task._id, - userId: this.assignedMember, - }); - this.task.group.assignedUsers = [this.assignedMember]; - } + const promises = this.assignedMembers.map(memberId => this.$store.dispatch('tasks:assignTask', { + taskId: this.task._id, + userId: memberId, + })); + Promise.all(promises); + this.task.group.assignedUsers = this.assignedMembers; this.$emit('taskCreated', this.task); } else { this.createTask(this.task); @@ -1505,21 +1501,21 @@ export default { this.$emit('cancel'); }, async toggleAssignment (memberId) { - if (this.purpose === 'edit') { - if (this.assignedMember && this.assignedMember !== memberId) { - await this.$store.dispatch('tasks:unassignTask', { - taskId: this.task._id, - userId: this.assignedMember, - }); - } - if (memberId) { - await this.$store.dispatch('tasks:assignTask', { - taskId: this.task._id, - userId: memberId, - }); - } + if (this.purpose === 'create') { + return; + } + const assignedIndex = this.assignedMembers.indexOf(memberId); + if (assignedIndex === -1) { + 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, + }); } - this.assignedMember = memberId; }, focusInput () { this.$refs.inputToFocus.focus(); diff --git a/website/client/src/mixins/scoreTask.js b/website/client/src/mixins/scoreTask.js index c1622cb395..b650c1c620 100644 --- a/website/client/src/mixins/scoreTask.js +++ b/website/client/src/mixins/scoreTask.js @@ -15,24 +15,8 @@ export default { }), }, methods: { - async beforeTaskScore (task) { - const { user } = this; - if (this.castingSpell) return false; - - if (task.group.approval.required && !task.group.approval.approved) { - task.group.approval.requested = true; - const { data: groupPlans } = await this.$store.dispatch('guilds:getGroupPlans'); - const groupPlan = groupPlans.find(g => g.id === task.group.id); - if (groupPlan) { - const managers = Object.keys(groupPlan.managers); - managers.push(groupPlan.leader); - if (managers.indexOf(user._id) !== -1) { - task.group.approval.approved = true; - } - } - } - - return true; + async beforeTaskScore () { + return (!this.castingSpell); }, playTaskScoreSound (task, direction) { switch (task.type) { // eslint-disable-line default-case diff --git a/website/common/script/ops/scoreTask.js b/website/common/script/ops/scoreTask.js index 51a58f8c51..da258567b7 100644 --- a/website/common/script/ops/scoreTask.js +++ b/website/common/script/ops/scoreTask.js @@ -234,11 +234,6 @@ export default function scoreTask (options = {}, req = {}, analytics) { exp: user.stats.exp, }; - if ( - task.group && task.group.approval && task.group.approval.required - && !task.group.approval.approved && !(task.type === 'todo' && cron) - ) return 0; - // This is for setting one-time temporary flags, // such as streakBonus or itemDropped. Useful for notifying // the API consumer, then cleared afterwards diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 13481c65f4..176c764341 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -775,17 +775,12 @@ api.scoreTask = { const userStats = user.stats.toJSON(); - // group tasks that require a manager's approval - if (taskResponse.requiresApproval === true) { - res.respond(202, { requiresApproval: true }, taskResponse.message); - } else { - const resJsonData = _.assign({ - delta: taskResponse.delta, - _tmp: user._tmp, - }, userStats); + const resJsonData = _.assign({ + delta: taskResponse.delta, + _tmp: user._tmp, + }, userStats); - res.respond(200, resJsonData); - } + res.respond(200, resJsonData); }, }; diff --git a/website/server/libs/groupTasks.js b/website/server/libs/groupTasks.js index 50a5785304..fcd13273c3 100644 --- a/website/server/libs/groupTasks.js +++ b/website/server/libs/groupTasks.js @@ -1,30 +1,18 @@ import * as Tasks from '../models/task'; // eslint-disable-line import/no-cycle -const SHARED_COMPLETION = { - default: 'recurringCompletion', - single: 'singleCompletion', - every: 'allAssignedCompletion', -}; - -async function _deleteUnfinishedTasks (groupMemberTask) { - await Tasks.Task.deleteMany({ - 'group.taskId': groupMemberTask.group.taskId, - $and: [ - { userId: { $exists: true } }, - { userId: { $ne: groupMemberTask.userId } }, - ], - }).exec(); -} - -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(); +async function handleSharedCompletion (teamTask) { + if (teamTask.type === 'reward') return; + const incompleteTask = await Tasks.Task.findOne({ + 'group.taskId': teamTask._id, + userId: { $exists: true }, + completed: false, + }, { _id: 1 }).exec(); + if (!incompleteTask) { + teamTask.completed = true; + teamTask.save(); + } } export { - SHARED_COMPLETION, handleSharedCompletion, }; diff --git a/website/server/libs/tasks/index.js b/website/server/libs/tasks/index.js index db2d9074a4..d122f57677 100644 --- a/website/server/libs/tasks/index.js +++ b/website/server/libs/tasks/index.js @@ -17,7 +17,6 @@ import { NotAuthorized, } from '../errors'; import { - SHARED_COMPLETION, handleSharedCompletion, } from '../groupTasks'; import shared from '../../../common'; @@ -67,7 +66,6 @@ async function createTasks (req, res, options = {}) { if (taskData.requiresApproval) { newTask.group.approval.required = true; } - newTask.group.sharedCompletion = taskData.sharedCompletion || SHARED_COMPLETION.default; newTask.group.managerNotes = taskData.managerNotes || ''; } else { newTask.userId = user._id; @@ -296,22 +294,23 @@ async function handleChallengeTask (task, delta, direction) { } } -async function handleGroupTask (task, delta, direction) { +async function handleTeamTask (task, delta, direction) { if (task.group && task.group.taskId) { // Wrapping everything in a try/catch block because if an error occurs // using `await` it MUST NOT bubble up because the request has already been handled try { - const groupTask = await Tasks.Task.findOne({ + const teamTask = await Tasks.Task.findOne({ _id: task.group.taskId, }).exec(); - if (groupTask) { - await handleSharedCompletion(groupTask, task); - - const groupDelta = groupTask.group.assignedUsers - ? delta / groupTask.group.assignedUsers.length + if (teamTask) { + const groupDelta = teamTask.group.assignedUsers + ? delta / teamTask.group.assignedUsers.length : delta; - await groupTask.scoreChallengeTask(groupDelta, direction); + await teamTask.scoreChallengeTask(groupDelta, direction); + if (task.type === 'daily' || task.type === 'todo') { + await handleSharedCompletion(teamTask); + } } } catch (e) { logger.error(e, 'Error scoring group task'); @@ -371,17 +370,23 @@ async function scoreTask (user, task, direction, req, res) { if (!localTask) throw new NotFound('Task not found.'); } - const wasCompleted = task.completed; + const targetTask = localTask || task; + + const wasCompleted = targetTask.completed; const firstTask = !user.achievements.completedTask; let delta; if (rollbackUser) { - delta = shared.ops.scoreTask({ task, user: rollbackUser, direction }, req, res.analytics); + delta = shared.ops.scoreTask({ + task: targetTask, + user: rollbackUser, + 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: targetTask.text, managerName: user.profile.name }, rollbackUser.preferences.language), task: { - id: task._id, - text: task.text, + id: targetTask._id, + text: targetTask.text, }, group: { id: group._id, @@ -394,51 +399,51 @@ async function scoreTask (user, task, direction, req, res) { }); await rollbackUser.save(); } else { - delta = shared.ops.scoreTask({ task, user, direction }, req, res.analytics); + delta = shared.ops.scoreTask({ task: targetTask, user, direction }, req, res.analytics); } // Drop system (don't run on the client, // as it would only be discarded since ops are sent to the API, not the results) - if (direction === 'up' && !firstTask) shared.fns.randomDrop(user, { task, delta }, req, res.analytics); + if (direction === 'up' && !firstTask) shared.fns.randomDrop(user, { task: targetTask, delta }, req, res.analytics); // 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; let pushTask; - if (task.type === 'todo') { + if (targetTask.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 = localTask ? localTask._id : task._id; + pullTask = targetTask._id; } else if ( wasCompleted - && !task.completed + && !targetTask.completed && user.tasksOrder.todos.indexOf(task._id) === -1 ) { - pushTask = localTask ? localTask._id : task._id; + pushTask = targetTask._id; } } - if (task.completed && task.group.id && !task.userId) { - task.group.completedBy = user._id; + if (targetTask.completed && targetTask.group.id && !targetTask.userId) { + targetTask.group.completedBy = user._id; } setNextDue(task, user); if (localTask) { - localTask.completed = task.completed; - localTask.value = Number(task.value) + Number(delta); + localTask.completed = targetTask.completed; + localTask.value = Number(targetTask.value) + Number(delta); await localTask.save(); } taskScoredWebhook.send(user, { - task, + task: targetTask, direction, delta, user, }); return { - task, + task: targetTask, delta, direction, pullTask, @@ -529,7 +534,7 @@ export async function scoreTasks (user, taskScorings, req, res) { return returnDatas.map(data => { // Handle challenge and group tasks tasks here because the task must have been saved first handleChallengeTask(data.task, data.delta, data.direction); - handleGroupTask(data.task, data.delta, data.direction); + handleTeamTask(data.task, data.delta, data.direction); return { id: data.task._id, delta: data.delta, _tmp: data._tmp }; }); diff --git a/website/server/models/task.js b/website/server/models/task.js index 37bf90e250..cde04dccb3 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -5,7 +5,6 @@ import _ from 'lodash'; import shared from '../../common'; import baseModel from '../libs/baseModel'; import { preenHistory } from '../libs/preening'; -import { SHARED_COMPLETION } from '../libs/groupTasks'; // eslint-disable-line import/no-cycle const { Schema } = mongoose; @@ -142,9 +141,7 @@ export const TaskSchema = new Schema({ requestedDate: { $type: Date }, }, sharedCompletion: { - $type: String, - enum: _.values(SHARED_COMPLETION), - default: SHARED_COMPLETION.single, + $type: String, // legacy data }, managerNotes: { $type: String }, completedBy: { $type: String, ref: 'User', validate: [v => validator.isUUID(v), 'Invalid uuid for group completing user.'] },