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.'] },