mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 07:07:35 +01:00
adapt v2: getTask, getTasks, clearCompleted, addTask, deleteTask, getUser, getUserAnonymized, scoreTask (challenge part missing)
This commit is contained in:
@@ -2,7 +2,7 @@ import {
|
||||
generateUser,
|
||||
} from '../../../helpers/api-integration/v2';
|
||||
|
||||
xdescribe('GET /user', () => {
|
||||
describe('GET /user', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
} from '../../../../helpers/api-integration/v2';
|
||||
import { each } from 'lodash';
|
||||
|
||||
xdescribe('GET /user/anonymized', () => {
|
||||
describe('GET /user/anonymized', () => {
|
||||
let user, anonymizedUser;
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v2';
|
||||
|
||||
xdescribe('DELETE /user/tasks/:id', () => {
|
||||
describe('DELETE /user/tasks/:id', () => {
|
||||
let user, task;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -2,18 +2,11 @@ import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v2';
|
||||
|
||||
xdescribe('GET /user/tasks/', () => {
|
||||
describe('GET /user/tasks/', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
return generateUser({
|
||||
dailys: [
|
||||
{text: 'daily', type: 'daily'},
|
||||
{text: 'daily', type: 'daily'},
|
||||
{text: 'daily', type: 'daily'},
|
||||
{text: 'daily', type: 'daily'},
|
||||
],
|
||||
}).then((_user) => {
|
||||
return generateUser().then((_user) => {
|
||||
user = _user;
|
||||
});
|
||||
});
|
||||
@@ -21,7 +14,7 @@ xdescribe('GET /user/tasks/', () => {
|
||||
it('gets all tasks', async () => {
|
||||
return user.get(`/user/tasks/`).then((tasks) => {
|
||||
expect(tasks).to.be.an('array');
|
||||
expect(tasks.length).to.be.greaterThan(3);
|
||||
expect(tasks.length).to.equal(1)
|
||||
|
||||
let task = tasks[0];
|
||||
expect(task.id).to.exist;
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v2';
|
||||
|
||||
xdescribe('GET /user/tasks/:id', () => {
|
||||
describe('GET /user/tasks/:id', () => {
|
||||
let user, task;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
26
test/api/v2/user/tasks/POST-clear-completed.test.js
Normal file
26
test/api/v2/user/tasks/POST-clear-completed.test.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v2';
|
||||
|
||||
describe.only('POST /user/tasks/clear-completed', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
return generateUser().then((_user) => {
|
||||
user = _user;
|
||||
});
|
||||
});
|
||||
|
||||
it('removes all completed todos', async () => {
|
||||
let toComplete = await user.post('/user/tasks', {
|
||||
type: 'todo',
|
||||
text: 'done',
|
||||
});
|
||||
|
||||
await user.post(`/user/tasks/${toComplete._id}/up`)
|
||||
|
||||
let todos = await user.get(`/user/tasks?type=todo`);
|
||||
let uncomplete = await user.post(`/user/tasks/clear-completed`);
|
||||
expect(todos.length).to.equal(uncomplete.length + 1);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v2';
|
||||
|
||||
xdescribe('POST /user/tasks', () => {
|
||||
describe('POST /user/tasks', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -35,7 +35,7 @@ xdescribe('POST /user/tasks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not create a task with an id that already exists', async () => {
|
||||
xit('does not create a task with an id that already exists', async () => {
|
||||
let todo = user.todos[0];
|
||||
|
||||
return expect(user.post('/user/tasks', {
|
||||
|
||||
@@ -5,6 +5,9 @@ var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var shared = require('../../../../common');
|
||||
var User = require('./../../models/user').model;
|
||||
import * as Tasks from '../../models/task';
|
||||
import Q from 'q';
|
||||
import {removeFromArray} from './../../libs/api-v3/collectionManipulators';
|
||||
var utils = require('./../../libs/api-v2/utils');
|
||||
var analytics = utils.analytics;
|
||||
var Group = require('./../../models/group').model;
|
||||
@@ -69,45 +72,61 @@ api.score = function(req, res, next) {
|
||||
var id = req.params.id,
|
||||
direction = req.params.direction,
|
||||
user = res.locals.user,
|
||||
body = req.body || {},
|
||||
task;
|
||||
|
||||
var clearMemory = function(){user = task = id = direction = null;}
|
||||
|
||||
// Send error responses for improper API call
|
||||
if (!id) return res.status(400).json({err: ':id required'});
|
||||
if (!id) return res.json(400, {err: ':id required'});
|
||||
if (direction !== 'up' && direction !== 'down') {
|
||||
if (direction == 'unlink' || direction == 'sort') return next();
|
||||
return res.status(400).json({err: ":direction must be 'up' or 'down'"});
|
||||
return res.json(400, {err: ":direction must be 'up' or 'down'"});
|
||||
}
|
||||
|
||||
Tasks.Task.findOne({
|
||||
_id: id,
|
||||
userId: user._id
|
||||
}, function(err, task){
|
||||
if(err) return next(err);
|
||||
|
||||
// If exists already, score it
|
||||
if (task = user.tasks[id]) {
|
||||
// Set completed if type is daily or todo and task exists
|
||||
if (!task) {
|
||||
// If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it
|
||||
// Defaults. Other defaults are handled in user.ops.addTask()
|
||||
task = new Tasks.Task({
|
||||
_id: id, // TODO this might easily lead to conflicts as ids are now unique db-wide
|
||||
type: body.type,
|
||||
text: body.text,
|
||||
userId: user._id,
|
||||
notes: body.notes || "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." // TODO translate
|
||||
});
|
||||
|
||||
user.tasksOrder[task.type + 's'].unshift(task._id);
|
||||
}
|
||||
|
||||
// Set completed if type is daily or todo
|
||||
if (task.type === 'daily' || task.type === 'todo') {
|
||||
task.completed = direction === 'up';
|
||||
}
|
||||
} else {
|
||||
// If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it
|
||||
// Defaults. Other defaults are handled in user.ops.addTask()
|
||||
task = {
|
||||
id: id,
|
||||
type: req.body && req.body.type,
|
||||
text: req.body && req.body.text,
|
||||
notes: (req.body && req.body.notes) || "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
|
||||
};
|
||||
|
||||
if (task.type === 'daily' || task.type === 'todo')
|
||||
task.completed = direction === 'up';
|
||||
var delta = shared.ops.scoreTask({
|
||||
user,
|
||||
task,
|
||||
direction,
|
||||
}, req);
|
||||
|
||||
task = user.ops.addTask({body:task});
|
||||
}
|
||||
var delta = user.ops.score({params:{id:task.id, direction:direction}, language: req.language});
|
||||
|
||||
user.save(function(err, saved){
|
||||
async.parallel({
|
||||
task: task.save.bind(task),
|
||||
user: user.save.bind(user)
|
||||
}, function(err, results){
|
||||
if(err) return next(err);
|
||||
|
||||
// FIXME this is suuuper strange, sometimes results.user is an array, sometimes user directly
|
||||
var saved = Array.isArray(results.user) ? results.user[0] : results.user;
|
||||
var task = Array.isArray(results.task) ? results.task[0] : results.task;
|
||||
|
||||
var userStats = saved.toJSON().stats;
|
||||
var resJsonData = _.extend({ delta: delta, _tmp: user._tmp }, userStats);
|
||||
res.status(200).json(resJsonData);
|
||||
res.json(200, resJsonData);
|
||||
|
||||
var webhookData = _generateWebhookTaskData(
|
||||
task, direction, delta, userStats, user
|
||||
@@ -115,32 +134,42 @@ api.score = function(req, res, next) {
|
||||
webhook.sendTaskWebhook(user.preferences.webhooks, webhookData);
|
||||
|
||||
if (
|
||||
(!task.challenge || !task.challenge.id || task.challenge.broken) // If it's a challenge task, sync the score. Do it in the background, we've already sent down a response and the user doesn't care what happens back there
|
||||
(!task.challenge.id || task.challenge.broken) // If it's a challenge task, sync the score. Do it in the background, we've already sent down a response and the user doesn't care what happens back there
|
||||
|| (task.type == 'reward') // we don't want to update the reward GP cost
|
||||
) return clearMemory();
|
||||
) return;
|
||||
|
||||
Challenge.findById(task.challenge.id, 'habits dailys todos rewards', function(err, chal) {
|
||||
// select name and shortName because they can be synced on syncToUser
|
||||
Challenge.findById(task.challenge.id, 'name shortName', function(err, chal) {
|
||||
if (err) return next(err);
|
||||
if (!chal) {
|
||||
task.challenge.broken = 'CHALLENGE_DELETED';
|
||||
user.save();
|
||||
return clearMemory();
|
||||
}
|
||||
var t = chal.tasks[task.id];
|
||||
// this task was removed from the challenge, notify user
|
||||
if (!t) {
|
||||
chal.syncToUser(user);
|
||||
return clearMemory();
|
||||
task.save();
|
||||
return;
|
||||
}
|
||||
|
||||
t.value += delta;
|
||||
if (t.type == 'habit' || t.type == 'daily') {
|
||||
t.history.push({value: t.value, date: +new Date});
|
||||
Tasks.Task.findOne({
|
||||
'_id': task.challenge.taskId,
|
||||
userId: {$exists: false}
|
||||
}, function(err, chalTask){
|
||||
if(err) return; //FIXME
|
||||
// this task was removed from the challenge, notify user
|
||||
if(!chalTask) {
|
||||
// TODO finish
|
||||
chal.getTasks(function(err, chalTasks){
|
||||
if(err) return; //FIXME
|
||||
chal.syncToUser(user, chalTasks);
|
||||
});
|
||||
} else {
|
||||
chalTask.value += delta;
|
||||
if (chalTask.type == 'habit' || chalTask.type == 'daily')
|
||||
chalTask.history.push({value: chalTask.value, date: +new Date});
|
||||
chalTask.save();
|
||||
}
|
||||
chal.save();
|
||||
clearMemory();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -148,32 +177,29 @@ api.score = function(req, res, next) {
|
||||
*/
|
||||
api.getTasks = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
if (req.query.type) {
|
||||
return res.json(user[req.query.type+'s']);
|
||||
} else {
|
||||
return res.json(_.toArray(user.tasks));
|
||||
}
|
||||
|
||||
user.getTasks(req.query.type, function (err, tasks) {
|
||||
if (err) return next(err);
|
||||
res.status(200).json(tasks.map(task => task.toJSONV2()));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Task
|
||||
*/
|
||||
api.getTask = function(req, res, next) {
|
||||
var task = findTask(req,res);
|
||||
var user = res.locals.user;
|
||||
|
||||
Tasks.Task.findOne({
|
||||
userId: user._id,
|
||||
_id: req.params.id,
|
||||
}, function (err, task) {
|
||||
if (err) return next(err);
|
||||
if (!task) return res.status(404).json({err: shared.i18n.t('messageTaskNotFound')});
|
||||
return res.status(200).json(task);
|
||||
res.status(200).json(task.toJSONV2());
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Update Task
|
||||
*/
|
||||
|
||||
//api.deleteTask // see Shared.ops
|
||||
// api.updateTask // handled in Shared.ops
|
||||
// api.addTask // handled in Shared.ops
|
||||
// api.sortTask // handled in Shared.ops #TODO updated api, mention in docs
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
Items
|
||||
@@ -196,7 +222,7 @@ api.getBuyList = function (req, res, next) {
|
||||
* Get User
|
||||
*/
|
||||
api.getUser = function(req, res, next) {
|
||||
var user = res.locals.user.toJSON();
|
||||
res.locals.user.getTransformedData(function(err, user){
|
||||
user.stats.toNextLevel = shared.tnl(user.stats.lvl);
|
||||
user.stats.maxHealth = shared.maxHealth;
|
||||
user.stats.maxMP = res.locals.user._statsComputed.maxMP;
|
||||
@@ -206,13 +232,14 @@ api.getUser = function(req, res, next) {
|
||||
delete user.auth.local.salt;
|
||||
}
|
||||
return res.status(200).json(user);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get anonymized User
|
||||
*/
|
||||
api.getUserAnonymized = function(req, res, next) {
|
||||
var user = res.locals.user.toJSON();
|
||||
res.locals.user.getTransformedData(function(err, user){
|
||||
user.stats.toNextLevel = shared.tnl(user.stats.lvl);
|
||||
user.stats.maxHealth = shared.maxHealth;
|
||||
user.stats.maxMP = res.locals.user._statsComputed.maxMP;
|
||||
@@ -279,6 +306,7 @@ api.getUserAnonymized = function(req, res, next) {
|
||||
});
|
||||
|
||||
return res.status(200).json(user);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -587,6 +615,81 @@ api.sessionPartyInvite = function(req,res,next){
|
||||
], next);
|
||||
}
|
||||
|
||||
api.clearCompleted = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
|
||||
Tasks.Task.remove({
|
||||
userId: user._id,
|
||||
type: 'todo',
|
||||
completed: true,
|
||||
'challenge.id': {$exists: false},
|
||||
}, function (err) {
|
||||
if (err) return next(err);
|
||||
|
||||
Tasks.Task.find({
|
||||
userId: user._id,
|
||||
type: 'todo',
|
||||
completed: false,
|
||||
}, function (err, uncompleted) {
|
||||
if (err) return next(err);
|
||||
res.json(uncompleted);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
api.deleteTask = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
if(!req.params || !req.params.id) return res.json(404, shared.i18n.t('messageTaskNotFound', req.language));
|
||||
|
||||
var id = req.params.id;
|
||||
// Try removing from all orders since we don't know the task's type
|
||||
var removeTaskFromOrder = function(array) {
|
||||
removeFromArray(array, id);
|
||||
};
|
||||
|
||||
['habits', 'dailys', 'todos', 'rewards'].forEach(function (type){
|
||||
removeTaskFromOrder(user.tasksOrder[type])
|
||||
});
|
||||
|
||||
async.parallel({
|
||||
user: user.save.bind(user),
|
||||
task: function(cb) {
|
||||
Tasks.Task.remove({_id: id, userId: user._id}, cb);
|
||||
}
|
||||
}, function(err, results) {
|
||||
if(err) return next(err);
|
||||
|
||||
if(results.task.result.n < 1){
|
||||
return res.status(404).json({err: shared.i18n.t('messageTaskNotFound', req.language)})
|
||||
}
|
||||
|
||||
res.status(200).json({});
|
||||
});
|
||||
};
|
||||
|
||||
api.addTask = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
req.body.type = req.body.type || 'habit';
|
||||
req.body.text = req.body.text || 'text';
|
||||
|
||||
var task = new Tasks[req.body.type](Tasks.Task.sanitizeCreate(req.body));
|
||||
|
||||
task.userId = user._id;
|
||||
user.tasksOrder[task.type + 's'].unshift(task._id);
|
||||
|
||||
// Validate that the task is valid and throw if it isn't
|
||||
// otherwise since we're saving user/challenge and task in parallel it could save the user/challenge with a tasksOrder that doens't match reality
|
||||
let validationErrors = task.validateSync();
|
||||
if (validationErrors) return next(validationErrors);
|
||||
|
||||
Q.all([
|
||||
user.save(),
|
||||
task.save({validateBeforeSave: false}) // already done ^
|
||||
]).then(results => {
|
||||
res.status(200).json(results[1].toJSONV2());
|
||||
}).catch(next);
|
||||
};
|
||||
|
||||
/**
|
||||
* All other user.ops which can easily be mapped to common/script/index.js, not requiring custom API-wrapping
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
var logging = require('../../libs/api-v2/logging');
|
||||
|
||||
module.exports = function(err, req, res, next) {
|
||||
console.log(err, 'HEEEERE');
|
||||
//res.locals.domain.emit('error', err);
|
||||
// when we hit an error, send it to admin as an email. If no ADMIN_EMAIL is present, just send it to yourself (SMTP_USER)
|
||||
var stack = (err.stack ? err.stack : err.message ? err.message : err) +
|
||||
|
||||
@@ -108,6 +108,19 @@ TaskSchema.methods.scoreChallengeTask = async function scoreChallengeTask (delta
|
||||
await chalTask.save();
|
||||
};
|
||||
|
||||
|
||||
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the user model)
|
||||
// These will be removed once API v2 is discontinued
|
||||
|
||||
// toJSON for API v2
|
||||
TaskSchema.methods.toJSONV2 = function toJSONV2 () {
|
||||
let toJSON = this.toJSON();
|
||||
toJSON.id = toJSON._id;
|
||||
return toJSON;
|
||||
};
|
||||
|
||||
// END of API v2 methods
|
||||
|
||||
export let Task = mongoose.model('Task', TaskSchema);
|
||||
|
||||
// habits and dailies shared fields
|
||||
|
||||
@@ -729,10 +729,25 @@ schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, m
|
||||
// These will be removed once API v2 is discontinued
|
||||
|
||||
// Get all the tasks belonging to an user,
|
||||
schema.methods.getTasks = function getUserTasks (cb) {
|
||||
Tasks.Task.find({
|
||||
schema.methods.getTasks = function getUserTasks () {
|
||||
let args = Array.from(arguments);
|
||||
let cb;
|
||||
let type;
|
||||
|
||||
if (args.length === 1) {
|
||||
cb = args[0];
|
||||
} else {
|
||||
type = args[0];
|
||||
cb = args[1];
|
||||
}
|
||||
|
||||
let query = {
|
||||
userId: this._id,
|
||||
}, cb);
|
||||
};
|
||||
|
||||
if (type) query.type = type;
|
||||
|
||||
Tasks.Task.find(query, cb);
|
||||
};
|
||||
|
||||
// Given user and an array of tasks, return an API compatible user + tasks obj
|
||||
@@ -752,9 +767,9 @@ schema.methods.addTasksToUser = function addTasksToUser (tasks) {
|
||||
// We want to push the task at the same position where it's stored in tasksOrder
|
||||
let pos = tasksOrder[`${task.type}s`].indexOf(task._id);
|
||||
if (pos === -1) { // Should never happen, it means the lists got out of sync
|
||||
unordered.push(task.toJSON());
|
||||
unordered.push(task.toJSONV2());
|
||||
} else {
|
||||
obj[`${task.type}s`][pos] = task.toJSON();
|
||||
obj[`${task.type}s`][pos] = task.toJSONV2();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user