misc fixes, port challenge task scoring, delete chal and select winner

This commit is contained in:
Matteo Pagliazzi
2015-12-29 20:57:20 +01:00
parent 61fc490f84
commit 6003dad24b
4 changed files with 150 additions and 9 deletions

View File

@@ -15,7 +15,7 @@
"cantDetachFb": "Account lacks another authentication method, can't detach Facebook.", "cantDetachFb": "Account lacks another authentication method, can't detach Facebook.",
"onlySocialAttachLocal": "Local auth can only be added to a social account.", "onlySocialAttachLocal": "Local auth can only be added to a social account.",
"invalidReqParams": "Invalid request parameters.", "invalidReqParams": "Invalid request parameters.",
"taskIdRequired": "\"taskId\" must be a valid UUID", "taskIdRequired": "\"taskId\" must be a valid UUID.",
"taskNotFound": "Task not found.", "taskNotFound": "Task not found.",
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".", "invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
"cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.", "cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.",
@@ -41,5 +41,10 @@
"inviteMissingEmail": "Missing email address in invite.", "inviteMissingEmail": "Missing email address in invite.",
"onlyGroupLeaderChal": "Only the group leader can create challenges", "onlyGroupLeaderChal": "Only the group leader can create challenges",
"pubChalsMinPrize": "Prize must be at least 1 Gem for public challenges.", "pubChalsMinPrize": "Prize must be at least 1 Gem for public challenges.",
"cantAfford": "You can't afford this prize. Purchase more gems or lower the prize amount." "cantAfford": "You can't afford this prize. Purchase more gems or lower the prize amount.",
"challengeIdRequired": "\"challengeId\" must be a valid UUID.",
"winnerIdRequired": "\"winnerId\" must be a valid UUID.",
"challengeNotFound": "Challenge not found.",
"onlyLeaderDeleteChal": "Only the challenge leader can delete it.",
"winnerNotFound": "Winner with id \"<%= userId %>\" not found or not part of the challenge."
} }

View File

@@ -2,11 +2,15 @@ import { authWithHeaders } from '../../middlewares/api-v3/auth';
import cron from '../../middlewares/api-v3/cron'; import cron from '../../middlewares/api-v3/cron';
import { model as Challenge } from '../../models/challenge'; import { model as Challenge } from '../../models/challenge';
import { model as Group } from '../../models/group'; import { model as Group } from '../../models/group';
import { model as User } from '../../models/user';
import { import {
NotFound, NotFound,
NotAuthorized, NotAuthorized,
} from '../../libs/api-v3/errors'; } from '../../libs/api-v3/errors';
import shared from '../../../../common';
import * as Tasks from '../../models/task'; import * as Tasks from '../../models/task';
import { txnEmail } from '../../libs/api-v3/email';
import pushNotify from '../../libs/api-v3/pushNotifications';
import Q from 'q'; import Q from 'q';
let api = {}; let api = {};
@@ -68,6 +72,8 @@ api.createChallenge = {
} }
} }
group.challengeCount += 1;
let tasks = req.body.tasks || []; // TODO validate let tasks = req.body.tasks || []; // TODO validate
req.body.leader = user._id; req.body.leader = user._id;
req.body.official = user.contributor.admin && req.body.official; req.body.official = user.contributor.admin && req.body.official;
@@ -81,14 +87,11 @@ api.createChallenge = {
return task.save(); return task.save();
}); });
toSave.unshift(challenge, group); toSave.unshift(challenge, group);
return Q.all(toSave); return Q.all(toSave);
}) })
.then(results => { .then(results => {
let savedChal = results[0]; let savedChal = results[0];
user.challenges.push(savedChal._id); // TODO save user only after group created, so that we can account for failed validation. Revisit in other places
return savedChal.syncToUser(user) // (it also saves the user) return savedChal.syncToUser(user) // (it also saves the user)
.then(() => res.respond(201, savedChal)); .then(() => res.respond(201, savedChal));
}) })
@@ -135,4 +138,137 @@ api.getChallenges = {
}, },
}; };
// TODO everything here should be moved to a worker
// actually even for a worker it's probably just to big and will kill mongo
function _closeChal (challenge, broken = {}) {
let winner = broken.winner;
let brokenReason = broken.broken;
let tasks = [
// Delete the challenge
Challenge.remove({_id: challenge._id}).exec(),
// And it's tasks
Tasks.Task.remove({'challenge.id': challenge._id, userId: {$exists: false}}).exec(),
// Set the challenge tag to non-challenge status and remove the challenge from the user's challenges
User.update({
challenges: {$in: [challenge._id]},
'tags._id': challenge._id,
}, {
$set: {'tags.$.challenge': false},
$pull: {challenges: challenge._id},
}, {multi: true}).exec(),
// Break users' tasks
Tasks.Task.update({
'challenge.id': challenge._id,
}, {
$set: {
'challenge.broken': brokenReason,
'challenge.winner': winner && winner.profile.name,
},
}, {multi: true}).exec(),
// Update the challengeCount on the group
Group.update({_id: challenge.group}, {$inc: {challengeCount: -1}}).exec(),
];
// Refund the leader if the challenge is closed and the group not the tavern
if (challenge.group !== 'habitrpg' && brokenReason === 'CHALLENGE_DELETED') {
tasks.push(User.update({_id: challenge.leader}, {$inc: {balance: challenge.prize / 4}}).exec());
}
// Award prize to winner and notify
if (winner) {
winner.achievements.challenges.push(challenge.name);
winner.balance += challenge.prize / 4;
tasks.push(winner.save().then(savedWinner => {
if (savedWinner.preferences.emailNotifications.wonChallenge !== false) {
txnEmail(savedWinner, 'won-challenge', [
{name: 'CHALLENGE_NAME', content: challenge.name},
]);
}
pushNotify.sendNotify(savedWinner, shared.i18n.t('wonChallenge'), challenge.name); // TODO translate
}));
}
return Q.allSettled(tasks); // TODO look if allSettle could be useful somewhere else
// TODO catch and handle
}
/**
* @api {delete} /challenges/:challengeId Delete a challenge
* @apiVersion 3.0.0
* @apiName DeleteChallenge
* @apiGroup Challenge
*
* @apiSuccess {object} empty An empty object
*/
api.deleteChallenge = {
method: 'DELETE',
url: '/challenges/:challengeId',
middlewares: [authWithHeaders(), cron],
handler (req, res, next) {
let user = res.locals.user;
req.checkParams('challenge', res.t('challengeIdRequired')).notEmpty().isUUID();
let validationErrors = req.validationErrors();
if (validationErrors) return next(validationErrors);
Challenge.findOne({_id: req.params.challengeId})
.exec()
.then(challenge => {
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (challenge.leader !== user._id && !user.contributor.admin) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
res.respond(200, {});
// Close channel in background
_closeChal(challenge, {broken: 'CHALLENGE_DELETED'});
})
.catch(next);
},
};
/**
* @api {delete} /challenges/:challengeId Delete a challenge
* @apiVersion 3.0.0
* @apiName DeleteChallenge
* @apiGroup Challenge
*
* @apiSuccess {object} empty An empty object
*/
api.selectChallengeWinner = {
method: 'POST',
url: '/challenges/:challengeId/selectWinner/:winnerId',
middlewares: [authWithHeaders(), cron],
handler (req, res, next) {
let user = res.locals.user;
let challenge;
req.checkParams('challenge', res.t('challengeIdRequired')).notEmpty().isUUID();
req.checkParams('winnerId', res.t('winnerIdRequired')).notEmpty().isUUID();
let validationErrors = req.validationErrors();
if (validationErrors) return next(validationErrors);
Challenge.findOne({_id: req.params.challengeId})
.exec()
.then(challengeFound => {
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
if (challenge.leader !== user._id && !user.contributor.admin) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
challenge = challengeFound;
return User.findOne({_id: req.params.winnerId}).exec();
})
.then(winner => {
if (!winner || winner.challenges.indexOf(challenge._id) === -1) throw new NotFound(res.t('winnerNotFound', {userId: req.parama.winnerId}));
res.respond(200, {});
// Close channel in background
_closeChal(challenge, {broken: 'CHALLENGE_DELETED', winner});
})
.catch(next);
},
};
export default api; export default api;

View File

@@ -7,7 +7,6 @@ import {
NotAuthorized, NotAuthorized,
BadRequest, BadRequest,
} from '../../libs/api-v3/errors'; } from '../../libs/api-v3/errors';
import { model as Challenge } from '../../models/challenge';
import shared from '../../../../common'; import shared from '../../../../common';
import Q from 'q'; import Q from 'q';
import _ from 'lodash'; import _ from 'lodash';
@@ -297,11 +296,11 @@ api.scoreTask = {
// TODO test? // TODO test?
if (task.challenge.id && task.challenge.taskId && !task.challenge.broken && task.type !== 'reward') { if (task.challenge.id && task.challenge.taskId && !task.challenge.broken && task.type !== 'reward') {
Tasks.Task.findOne({ Tasks.Task.findOne({
_id: task.challenge.taskId _id: task.challenge.taskId,
}).exec() }).exec()
.then(chalTask => { .then(chalTask => {
chalTask.value += delta; chalTask.value += delta;
if (t.type == 'habit' || t.type == 'daily') { if (chalTask.type === 'habit' || chalTask.type === 'daily') {
chalTask.history.push({value: chalTask.value, date: Number(new Date())}); chalTask.history.push({value: chalTask.value, date: Number(new Date())});
// TODO 1. treat challenges as subscribed users for preening 2. it's expensive to do it at every score - how to have it happen once like for cron? // TODO 1. treat challenges as subscribed users for preening 2. it's expensive to do it at every score - how to have it happen once like for cron?
chalTask.history = preenHistory(user, chalTask.history); chalTask.history = preenHistory(user, chalTask.history);
@@ -310,7 +309,7 @@ api.scoreTask = {
return chalTask.save(); return chalTask.save();
}); });
//.catch(next) TODO what to do here // .catch(next) TODO what to do here
} }
}); });
}) })

View File

@@ -1,3 +1,4 @@
// TODO move to /api-v2
var api = module.exports; var api = module.exports;
var _ = require('lodash'); var _ = require('lodash');
var nconf = require('nconf'); var nconf = require('nconf');