diff --git a/migrations/tasks/tasks-set-everyX.js b/migrations/tasks/tasks-set-everyX.js new file mode 100644 index 0000000000..eb61b2f01d --- /dev/null +++ b/migrations/tasks/tasks-set-everyX.js @@ -0,0 +1,88 @@ +var migrationName = 'tasks-set-everyX'; +var authorName = ''; // in case script author needs to know when their ... +var authorUuid = ''; //... own data is done + +/* + * Iterates over all tasks and sets invalid everyX values (less than 0 or more than 9999 or not an int) field to 0 + */ + +var monk = require('monk'); +var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE +var dbTasks = monk(connectionString).get('tasks', { castIds: false }); + +function processTasks(lastId) { + // specify a query to limit the affected tasks (empty for all tasks): + var query = { + type: "daily", + everyX: { + $not: { + $gte: 0, + $lte: 9999, + $type: "int", + } + }, + }; + + if (lastId) { + query._id = { + $gt: lastId + } + } + + dbTasks.find(query, { + sort: {_id: 1}, + limit: 250, + fields: [], + }) + .then(updateTasks) + .catch(function (err) { + console.log(err); + return exiting(1, 'ERROR! ' + err); + }); +} + +var progressCount = 1000; +var count = 0; + +function updateTasks (tasks) { + if (!tasks || tasks.length === 0) { + console.warn('All appropriate tasks found and modified.'); + displayData(); + return; + } + + var taskPromises = tasks.map(updatetask); + var lasttask = tasks[tasks.length - 1]; + + return Promise.all(taskPromises) + .then(function () { + processTasks(lasttask._id); + }); +} + +function updatetask (task) { + count++; + var set = {'everyX': 0}; + + dbTasks.update({_id: task._id}, {$set:set}); + + if (count % progressCount == 0) console.warn(count + ' ' + task._id); + if (task._id == authorUuid) console.warn(authorName + ' processed'); +} + +function displayData() { + console.warn('\n' + count + ' tasks processed\n'); + return exiting(0); +} + +function exiting(code, msg) { + code = code || 0; // 0 = success + if (code && !msg) { msg = 'ERROR!'; } + if (msg) { + if (code) { console.error(msg); } + else { console.log( msg); } + } + process.exit(code); +} + +module.exports = processTasks; diff --git a/test/api/v3/integration/tasks/POST-tasks_user.test.js b/test/api/v3/integration/tasks/POST-tasks_user.test.js index 729619037b..8b25698d59 100644 --- a/test/api/v3/integration/tasks/POST-tasks_user.test.js +++ b/test/api/v3/integration/tasks/POST-tasks_user.test.js @@ -628,6 +628,43 @@ describe('POST /tasks/user', () => { }); }); + it('returns an error if everyX is a non int', async () => { + await expect(user.post('/tasks/user', { + text: 'test daily', + type: 'daily', + everyX: 2.5, + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'daily validation failed', + }); + }); + + it('returns an error if everyX is negative', async () => { + await expect(user.post('/tasks/user', { + text: 'test daily', + type: 'daily', + everyX: -1, + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'daily validation failed', + }); + }); + + it('returns an error if everyX is above 9999', async () => { + await expect(user.post('/tasks/user', { + text: 'test daily', + type: 'daily', + everyX: 10000, + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: 'daily validation failed', + }); + }); + + it('can create checklists', async () => { let task = await user.post('/tasks/user', { text: 'test daily', diff --git a/test/common/ops/addTask.js b/test/common/ops/addTask.js index d8301a4b35..9d5e1d7a8e 100644 --- a/test/common/ops/addTask.js +++ b/test/common/ops/addTask.js @@ -38,7 +38,7 @@ describe('shared.ops.addTask', () => { expect(habit.counterDown).to.equal(0); }); - it('adds an habtit when type is invalid', () => { + it('adds a habit when type is invalid', () => { let habit = addTask(user, { body: { type: 'invalid', diff --git a/website/client/components/tasks/taskModal.vue b/website/client/components/tasks/taskModal.vue index 218f075c19..3561a47194 100644 --- a/website/client/components/tasks/taskModal.vue +++ b/website/client/components/tasks/taskModal.vue @@ -102,7 +102,7 @@ | {{ $t(frequency) }} .form-group label(v-once) {{ $t('repeatEvery') }} - input(type="number", v-model="task.everyX", min="0", required, :disabled='challengeAccessRequired') + input(type="number", v-model="task.everyX", min="0", max="9999", required, :disabled='challengeAccessRequired') | {{ repeatSuffix }} br template(v-if="task.frequency === 'weekly'") diff --git a/website/server/models/task.js b/website/server/models/task.js index d770481d7c..a99c43664b 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -239,7 +239,14 @@ export let habit = Task.discriminator('habit', HabitSchema); export let DailySchema = new Schema(_.defaults({ frequency: {type: String, default: 'weekly', enum: ['daily', 'weekly', 'monthly', 'yearly']}, - everyX: {type: Number, default: 1}, // e.g. once every X weeks + everyX: { + type: Number, + default: 1, + validate: [ + (val) => val % 1 === 0 && val >= 0 && val <= 9999, + 'Valid everyX values are integers from 0 to 9999', + ], + }, startDate: { type: Date, default () {