diff --git a/scripts/team-cron.js b/scripts/team-cron.js new file mode 100644 index 0000000000..1903009691 --- /dev/null +++ b/scripts/team-cron.js @@ -0,0 +1,80 @@ +import forEach from 'lodash/forEach'; +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'; +import { daysSince, shouldDo } from '../website/common/script/cron'; + +const TASK_VALUE_CHANGE_FACTOR = 0.9747; +const MIN_TASK_VALUE = -47.27; + +async function updateTeamTasks (team) { + const toSave = []; + const teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec(); + + if ( + !team.cron || !team.cron.lastProcessed + || daysSince(team.cron.lastProcessed, teamLeader.preferences) > 0 + ) { + const tasks = await Tasks.Task.find({ + 'group.id': team._id, + 'group.assignedUsers': [], + userId: { $exists: false }, + $or: [ + { type: 'todo', completed: false }, + { type: { $in: ['habit', 'daily'] } }, + ], + }).exec(); + + const tasksByType = { + habits: [], dailys: [], todos: [], rewards: [], + }; + forEach(tasks, task => tasksByType[`${task.type}s`].push(task)); + + forEach(tasksByType.habits, habit => { + if (!(habit.up && habit.down) && habit.value !== 0) { + habit.value *= 0.5; + if (Math.abs(habit.value) < 0.1) habit.value = 0; + toSave.push(habit.save()); + } + }); + forEach(tasksByType.todos, todo => { + if (!todo.completed) { + const delta = TASK_VALUE_CHANGE_FACTOR ** todo.value; + todo.value -= delta; + if (todo.value < MIN_TASK_VALUE) todo.value = MIN_TASK_VALUE; + toSave.push(todo.save()); + } + }); + forEach(tasksByType.dailys, daily => { + if (daily.completed) { + daily.completed = false; + } else if (shouldDo(team.cron.lastProcessed, daily, teamLeader.preferences)) { + const delta = TASK_VALUE_CHANGE_FACTOR ** daily.value; + daily.value -= delta; + if (daily.value < MIN_TASK_VALUE) daily.value = MIN_TASK_VALUE; + } + daily.isDue = shouldDo(new Date(), daily, teamLeader.preferences); + if (daily.isModified()) toSave.push(daily.save()); + }); + + if (!team.cron) team.cron = {}; + team.cron.lastProcessed = new Date(); + toSave.push(team.save()); + } + + return Promise.all(toSave); +} + +export default async function processTeamsCron () { + const activeTeams = await Group.find({ + 'purchased.plan.customerId': { $exists: true }, + $or: [ + { 'purchased.plan.dateTerminated': { $exists: false } }, + { 'purchased.plan.dateTerminated': null }, + { 'purchased.plan.dateTerminated': { $gt: new Date() } }, + ], + }).exec(); + + const cronPromises = activeTeams.map(updateTeamTasks); + return Promise.all(cronPromises); +} diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js index fd9312e25d..664d65cc3e 100644 --- a/website/server/middlewares/cron.js +++ b/website/server/middlewares/cron.js @@ -73,37 +73,13 @@ async function cronAsync (req, res) { return null; } - const teamsLed = await user.teamsLed(); - let tasksQuery; - - if (teamsLed.length > 0) { - tasksQuery = { - $and: [ - { - $or: [ - { userId: user._id }, - { userId: { $exists: false }, 'group.id': { $in: teamsLed }, 'group.assignedUsers': { $size: 0 } }, - ], - }, - { - $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 tasks = await Tasks.Task.find({ + userId: user._id, + $or: [ // Exclude completed todos + { type: 'todo', completed: false }, + { type: { $in: ['habit', 'daily', 'reward'] } }, + ], + }).exec(); const tasksByType = { habits: [], dailys: [], todos: [], rewards: [], @@ -141,36 +117,20 @@ async function cronAsync (req, res) { // Save user and tasks const toSave = [user.save()]; - const groupTasks = []; - const groupTasksByType = { - habits: [], dailys: [], todos: [], rewards: [], - }; - for (const task of tasks) { + tasks.forEach(async task => { if (task.isModified()) toSave.push(task.save()); if (task.isModified() && task.group && task.group.taskId) { - const groupTask = await Tasks.Task.findOne({ // eslint-disable-line no-await-in-loop + const groupTask = await Tasks.Task.findOne({ _id: task.group.taskId, }).exec(); + if (groupTask) { - groupTasks.push(groupTask); - groupTasksByType[`${groupTask.type}s`].push(groupTask); + let delta = (0.9747 ** task.value) * -1; + if (groupTask.group.assignedUsers) delta /= groupTask.group.assignedUsers.length; + await groupTask.scoreChallengeTask(delta, 'down'); } } - } - 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); diff --git a/website/server/models/group.js b/website/server/models/group.js index e8d98627fe..d93b0689da 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -144,6 +144,9 @@ export const schema = new Schema({ slug: { $type: String }, name: { $type: String }, }], + cron: { + lastProcessed: { $type: Date }, + }, }, { strict: true, minimize: false, // So empty objects are returned