mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +01:00
Clone challenges api (#9684)
* Added clone api * Added new clone UI * Fixed challenge clone * Fixed lint and added mongo toObject * Removed clone field, fixed type, fixed challenge task query * Auto selected group * Accounted for group balance when creating challenge * Added check for if user is leader of guild * Added leader existence check * Added fix for leader and prizecost equal to
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
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,
|
||||
@@ -17,9 +20,81 @@ import {
|
||||
import * as Tasks from '../../models/task';
|
||||
import Bluebird from 'bluebird';
|
||||
import csvStringify from '../../libs/csvStringify';
|
||||
import {
|
||||
createTasks,
|
||||
} from '../../libs/taskManager';
|
||||
|
||||
const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge'];
|
||||
|
||||
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.
|
||||
@@ -179,71 +254,10 @@ api.createChallenge = {
|
||||
|
||||
req.checkBody('group', res.t('groupIdRequired')).notEmpty();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
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)
|
||||
const {savedChal, group} = await createChallenge(user, req, res);
|
||||
|
||||
let response = savedChal.toJSON();
|
||||
response.leader = { // the leader is the authenticated user
|
||||
@@ -784,4 +798,70 @@ 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
|
||||
* @apiGroup Challenge
|
||||
*
|
||||
* @apiParam (Path) {UUID} challengeId The _id for the challenge to clone
|
||||
*
|
||||
* @apiSuccess {Object} challenge The cloned challenge
|
||||
*
|
||||
* @apiUse ChallengeNotFound
|
||||
*/
|
||||
api.cloneChallenge = {
|
||||
method: 'POST',
|
||||
url: '/challenges/:challengeId/clone',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const challengeToClone = await Challenge.findOne({_id: req.params.challengeId}).exec();
|
||||
if (!challengeToClone) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
const {savedChal} = await createChallenge(user, req, res);
|
||||
|
||||
const challengeTasks = await Tasks.Task.find({
|
||||
'challenge.id': challengeToClone._id,
|
||||
userId: {$exists: false},
|
||||
}).exec();
|
||||
|
||||
const tasksToClone = challengeTasks.map(task => {
|
||||
let clonedTask = cloneDeep(task.toObject());
|
||||
let omittedTask = cleanUpTask(clonedTask);
|
||||
return omittedTask;
|
||||
});
|
||||
|
||||
const taskRequest = {
|
||||
body: tasksToClone,
|
||||
};
|
||||
|
||||
const clonedTasks = await createTasks(taskRequest, res, {user, challenge: savedChal});
|
||||
|
||||
res.respond(200, {clonedTasks, clonedChallenge: savedChal});
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
|
||||
Reference in New Issue
Block a user