diff --git a/website/server/controllers/api-v3/challenges.js b/website/server/controllers/api-v3/challenges.js index 6bfb5e116b..326a66d5e4 100644 --- a/website/server/controllers/api-v3/challenges.js +++ b/website/server/controllers/api-v3/challenges.js @@ -1,8 +1,6 @@ import { authWithHeaders, authWithSession } from '../../middlewares/auth'; import _ from 'lodash'; import cloneDeep from 'lodash/cloneDeep'; -import omit from 'lodash/omit'; -import uuid from 'uuid'; import { model as Challenge } from '../../models/challenge'; import { model as Group, @@ -24,77 +22,15 @@ import { createTasks, } from '../../libs/taskManager'; -const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge']; +import { + addUserJoinChallengeNotification, + getChallengeGroupResponse, + createChallenge, + cleanUpTask, +} from '../../libs/challenges'; let api = {}; -async function createChallenge (user, req, res) { - let groupId = req.body.group; - let prize = req.body.prize; - - let group = await Group.getGroup({user, groupId, fields: '-chat', mustBeMember: true}); - if (!group) throw new NotFound(res.t('groupNotFound')); - if (!group.isMember(user)) throw new NotAuthorized(res.t('mustBeGroupMember')); - - if (group.leaderOnly && group.leaderOnly.challenges && group.leader !== user._id) { - throw new NotAuthorized(res.t('onlyGroupLeaderChal')); - } - - if (group._id === TAVERN_ID && prize < 1) { - throw new NotAuthorized(res.t('tavChalsMinPrize')); - } - - if (prize > 0) { - let groupBalance = group.balance && group.leader === user._id ? group.balance : 0; - let prizeCost = prize / 4; - - if (prizeCost > user.balance + groupBalance) { - throw new NotAuthorized(res.t('cantAfford')); - } - - if (groupBalance >= prizeCost) { - // Group pays for all of prize - group.balance -= prizeCost; - } else if (groupBalance > 0) { - // User pays remainder of prize cost after group - let remainder = prizeCost - group.balance; - group.balance = 0; - user.balance -= remainder; - } else { - // User pays for all of prize - user.balance -= prizeCost; - } - } - - group.challengeCount += 1; - - if (!req.body.summary) { - req.body.summary = req.body.name; - } - req.body.leader = user._id; - req.body.official = user.contributor.admin && req.body.official ? true : false; - let challenge = new Challenge(Challenge.sanitize(req.body)); - - // First validate challenge so we don't save group if it's invalid (only runs sync validators) - let challengeValidationErrors = challenge.validateSync(); - if (challengeValidationErrors) throw challengeValidationErrors; - - // Add achievement if user's first challenge - if (!user.achievements.joinedChallenge) { - user.achievements.joinedChallenge = true; - user.addNotification('CHALLENGE_JOINED_ACHIEVEMENT'); - } - - let results = await Bluebird.all([challenge.save({ - validateBeforeSave: false, // already validate - }), group.save()]); - let savedChal = results[0]; - - await savedChal.syncToUser(user); // (it also saves the user) - - return {savedChal, group}; -} - /** * @apiDefine ChallengeLeader Challenge Leader * The leader of the challenge can use this route. @@ -264,12 +200,7 @@ api.createChallenge = { _id: user._id, profile: {name: user.profile.name}, }; - response.group = { // we already have the group data - _id: group._id, - name: group.name, - type: group.type, - privacy: group.privacy, - }; + response.group = getChallengeGroupResponse(group); res.analytics.track('challenge create', { uuid: user._id, @@ -320,22 +251,13 @@ api.joinChallenge = { challenge.memberCount += 1; - // Add achievement if user's first challenge - if (!user.achievements.joinedChallenge) { - user.achievements.joinedChallenge = true; - user.addNotification('CHALLENGE_JOINED_ACHIEVEMENT'); - } + addUserJoinChallengeNotification(user); // Add all challenge's tasks to user's tasks and save the challenge let results = await Bluebird.all([challenge.syncToUser(user), challenge.save()]); let response = results[1].toJSON(); - response.group = { // we already have the group data - _id: group._id, - name: group.name, - type: group.type, - privacy: group.privacy, - }; + response.group = getChallengeGroupResponse(group); let chalLeader = await User.findById(response.leader).select(nameFields).exec(); response.leader = chalLeader ? chalLeader.toJSON({minimize: true}) : null; @@ -692,12 +614,7 @@ api.updateChallenge = { let savedChal = await challenge.save(); let response = savedChal.toJSON(); - response.group = { // we already have the group data - _id: group._id, - name: group.name, - type: group.type, - privacy: group.privacy, - }; + response.group = getChallengeGroupResponse(group); let chalLeader = await User.findById(response.leader).select(nameFields).exec(); response.leader = chalLeader ? chalLeader.toJSON({minimize: true}) : null; res.respond(200, response); @@ -798,23 +715,6 @@ api.selectChallengeWinner = { }, }; -function cleanUpTask (task) { - let cleansedTask = omit(task, TASK_KEYS_TO_REMOVE); - - // Copy checklists but reset to uncomplete and assign new id - if (!cleansedTask.checklist) cleansedTask.checklist = []; - cleansedTask.checklist.forEach((item) => { - item.completed = false; - item.id = uuid(); - }); - - if (cleansedTask.type !== 'reward') { - delete cleansedTask.value; - } - - return cleansedTask; -} - /** * @api {post} /api/v3/challenges/:challengeId/clone Clone a challenge * @apiName CloneChallenge diff --git a/website/server/libs/challenges/index.js b/website/server/libs/challenges/index.js new file mode 100644 index 0000000000..84be475b87 --- /dev/null +++ b/website/server/libs/challenges/index.js @@ -0,0 +1,110 @@ +// Currently this holds helpers for challenge api, but we should break this up into submodules as it expands +import omit from 'lodash/omit'; +import uuid from 'uuid'; +import Bluebird from 'bluebird'; +import { model as Challenge } from '../../models/challenge'; +import { + model as Group, + TAVERN_ID, +} from '../../models/group'; +import { + NotFound, + NotAuthorized, +} from '../errors'; + +const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge']; + +export function addUserJoinChallengeNotification (user) { + if (user.achievements.joinedChallenge) return; + user.achievements.joinedChallenge = true; + user.addNotification('CHALLENGE_JOINED_ACHIEVEMENT'); +} + +export function getChallengeGroupResponse (group) { + return { + _id: group._id, + name: group.name, + type: group.type, + privacy: group.privacy, + }; +} + +export async function createChallenge (user, req, res) { + let groupId = req.body.group; + let prize = req.body.prize; + + let group = await Group.getGroup({user, groupId, fields: '-chat', mustBeMember: true}); + if (!group) throw new NotFound(res.t('groupNotFound')); + if (!group.isMember(user)) throw new NotAuthorized(res.t('mustBeGroupMember')); + + if (group.leaderOnly && group.leaderOnly.challenges && group.leader !== user._id) { + throw new NotAuthorized(res.t('onlyGroupLeaderChal')); + } + + if (group._id === TAVERN_ID && prize < 1) { + throw new NotAuthorized(res.t('tavChalsMinPrize')); + } + + if (prize > 0) { + let groupBalance = group.balance && group.leader === user._id ? group.balance : 0; + let prizeCost = prize / 4; + + if (prizeCost > user.balance + groupBalance) { + throw new NotAuthorized(res.t('cantAfford')); + } + + if (groupBalance >= prizeCost) { + // Group pays for all of prize + group.balance -= prizeCost; + } else if (groupBalance > 0) { + // User pays remainder of prize cost after group + let remainder = prizeCost - group.balance; + group.balance = 0; + user.balance -= remainder; + } else { + // User pays for all of prize + user.balance -= prizeCost; + } + } + + group.challengeCount += 1; + + if (!req.body.summary) { + req.body.summary = req.body.name; + } + req.body.leader = user._id; + req.body.official = user.contributor.admin && req.body.official ? true : false; + let challenge = new Challenge(Challenge.sanitize(req.body)); + + // First validate challenge so we don't save group if it's invalid (only runs sync validators) + let challengeValidationErrors = challenge.validateSync(); + if (challengeValidationErrors) throw challengeValidationErrors; + + addUserJoinChallengeNotification(user); + + let results = await Bluebird.all([challenge.save({ + validateBeforeSave: false, // already validate + }), group.save()]); + let savedChal = results[0]; + + await savedChal.syncToUser(user); // (it also saves the user) + + return {savedChal, group}; +} + +export function cleanUpTask (task) { + let cleansedTask = omit(task, TASK_KEYS_TO_REMOVE); + + // Copy checklists but reset to uncomplete and assign new id + if (!cleansedTask.checklist) cleansedTask.checklist = []; + cleansedTask.checklist.forEach((item) => { + item.completed = false; + item.id = uuid(); + }); + + if (cleansedTask.type !== 'reward') { + delete cleansedTask.value; + } + + return cleansedTask; +}