From ada185d94e477fb92625d8af9dd93d04aad4e2bb Mon Sep 17 00:00:00 2001 From: osiris Date: Sat, 27 Jun 2020 21:15:33 +0800 Subject: [PATCH 1/9] Fix bug in challenge tags not converted to normal tags after challenge ended/deleted --- website/client/src/components/tasks/user.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/client/src/components/tasks/user.vue b/website/client/src/components/tasks/user.vue index 50531ac4619..7d4b9c92b3f 100644 --- a/website/client/src/components/tasks/user.vue +++ b/website/client/src/components/tasks/user.vue @@ -483,7 +483,7 @@ export default { userTags.forEach(t => { if (t.group) { tagsByType.groups.tags.push(t); - } else if (t.challenge) { + } else if (t.challenge && t.challenge === 'true') { tagsByType.challenges.tags.push(t); } else { tagsByType.user.tags.push(t); From 48f2aafaf54737b6c6d19747f8296f18b498138f Mon Sep 17 00:00:00 2001 From: osiris Date: Sat, 27 Jun 2020 21:16:00 +0800 Subject: [PATCH 2/9] Added test cases to test bug fix --- .../tests/unit/components/tasks/user.spec.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 website/client/tests/unit/components/tasks/user.spec.js diff --git a/website/client/tests/unit/components/tasks/user.spec.js b/website/client/tests/unit/components/tasks/user.spec.js new file mode 100644 index 00000000000..ae9591a80f4 --- /dev/null +++ b/website/client/tests/unit/components/tasks/user.spec.js @@ -0,0 +1,65 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import User from '@/components/tasks/user.vue'; +import Store from '@/libs/store'; + +const localVue = createLocalVue(); +localVue.use(Store); + +describe('Tasks User', () => { + describe('Computed Properties', () => { + it('should render a challenge tag under challenge header in tag filter popup when the challenge is active', () => { + const activeChallengeTag = { + id: '1', + name: 'Challenge1', + challenge: 'true', + }; + const state = { + user: { + data: { + tags: [activeChallengeTag], + }, + }, + }; + const getters = {}; + const store = new Store({ state, getters }); + const wrapper = shallowMount(User, { + store, + localVue, + }); + + const computedTagsByType = wrapper.vm.tagsByType; + + expect(computedTagsByType.challenges.tags.length).to.equal(1); + expect(computedTagsByType.challenges.tags[0].id).to.equal(activeChallengeTag.id); + expect(computedTagsByType.challenges.tags[0].name).to.equal(activeChallengeTag.name); + }); + + it('should render a challenge tag under normal tag header in tag filter popup when the challenge is no longer active', () => { + const inactiveChallengeTag = { + id: '1', + name: 'Challenge1', + challenge: 'false', + }; + const state = { + user: { + data: { + tags: [inactiveChallengeTag], + }, + }, + }; + const getters = {}; + const store = new Store({ state, getters }); + const wrapper = shallowMount(User, { + store, + localVue, + }); + + const computedTagsByType = wrapper.vm.tagsByType; + + expect(computedTagsByType.challenges.tags.length).to.equal(0); + expect(computedTagsByType.user.tags.length).to.equal(1); + expect(computedTagsByType.user.tags[0].id).to.equal(inactiveChallengeTag.id); + expect(computedTagsByType.user.tags[0].name).to.equal(inactiveChallengeTag.name); + }); + }); +}); From d331639bb1be2e4d74f340ad0cafea14ea7cb78a Mon Sep 17 00:00:00 2001 From: osiris Date: Fri, 24 Jul 2020 11:55:47 +0800 Subject: [PATCH 3/9] Set tag.challenge from String to Boolean in tag model schema --- website/client/src/components/tasks/user.vue | 2 +- website/client/tests/unit/components/tasks/user.spec.js | 4 ++-- website/server/models/tag.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/website/client/src/components/tasks/user.vue b/website/client/src/components/tasks/user.vue index f85d346e4b3..058dde76aa7 100644 --- a/website/client/src/components/tasks/user.vue +++ b/website/client/src/components/tasks/user.vue @@ -485,7 +485,7 @@ export default { userTags.forEach(t => { if (t.group) { tagsByType.groups.tags.push(t); - } else if (t.challenge && t.challenge === 'true') { + } else if (t.challenge) { tagsByType.challenges.tags.push(t); } else { tagsByType.user.tags.push(t); diff --git a/website/client/tests/unit/components/tasks/user.spec.js b/website/client/tests/unit/components/tasks/user.spec.js index ae9591a80f4..2bf0097eb6c 100644 --- a/website/client/tests/unit/components/tasks/user.spec.js +++ b/website/client/tests/unit/components/tasks/user.spec.js @@ -11,7 +11,7 @@ describe('Tasks User', () => { const activeChallengeTag = { id: '1', name: 'Challenge1', - challenge: 'true', + challenge: true, }; const state = { user: { @@ -38,7 +38,7 @@ describe('Tasks User', () => { const inactiveChallengeTag = { id: '1', name: 'Challenge1', - challenge: 'false', + challenge: false, }; const state = { user: { diff --git a/website/server/models/tag.js b/website/server/models/tag.js index 94afd95bd80..1ad8dcb2a64 100644 --- a/website/server/models/tag.js +++ b/website/server/models/tag.js @@ -13,7 +13,7 @@ export const schema = new Schema({ required: true, }, name: { $type: String, required: true }, - challenge: { $type: String }, + challenge: { $type: Boolean }, group: { $type: String }, }, { strict: true, From 4efc1e0ace45545b857cfd4f41348bf140f5a2e9 Mon Sep 17 00:00:00 2001 From: osiris Date: Fri, 24 Jul 2020 12:54:30 +0800 Subject: [PATCH 4/9] Update existing test with tag challenge set to boolean instead of string --- .../POST-challenges_challengeId_winner_winnerId.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js b/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js index 3f3b75e7feb..afae509347d 100644 --- a/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js +++ b/test/api/v3/integration/challenges/POST-challenges_challengeId_winner_winnerId.test.js @@ -159,7 +159,7 @@ describe('POST /challenges/:challengeId/winner/:winnerId', () => { expect(testTask.challenge.broken).to.eql('CHALLENGE_CLOSED'); expect(testTask.challenge.winner).to.eql(winningUser.profile.name); - expect(challengeTag.challenge).to.eql('false'); + expect(challengeTag.challenge).to.eql(false); }); }); }); From 8ae9a3a6f6ac5577e18eb91b2332e1aaa0f229b1 Mon Sep 17 00:00:00 2001 From: osiris Date: Fri, 7 Aug 2020 19:04:41 +0800 Subject: [PATCH 5/9] Added migration file for converting tag challenge field from string to bool --- .../users/tag-challenge-field-string2bool.js | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 migrations/users/tag-challenge-field-string2bool.js diff --git a/migrations/users/tag-challenge-field-string2bool.js b/migrations/users/tag-challenge-field-string2bool.js new file mode 100644 index 00000000000..0631e180b7d --- /dev/null +++ b/migrations/users/tag-challenge-field-string2bool.js @@ -0,0 +1,77 @@ +import monk from 'monk'; // eslint-disable-line import/no-extraneous-dependencies +import { concat } from 'lodash'; + +const migrationName = 'tag-challenge-field-string2bool.js'; + +const connectionString = 'mongodb://localhost:27017/habitica-dev?auto_reconnect=true'; // FOR TEST DATABASE + +const dbUsers = monk(connectionString).get('users', { castIds: false }); + +const progressCount = 1000; +let count = 0; + +export default async function processUsers () { + const query = { + migration: { $ne: migrationName }, + tags: { + $elemMatch: { + challenge: { + $exists: true, + $type: "string", + }, + }, + }, + }; + + while (true) { + const users = await dbUsers.find(query, { + sort: { _id: 1 }, + limit: 250, + }); + + if (users.length === 0) { + console.warn('All appropriate users found and modified.'); + console.warn(`\n${count} users processed\n`); + break; + } else { + query._id = { + $gt: users[users.length - 1], + }; + } + + await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop + } +} + +async function updateUser (user) { + count += 1; + + const query = { + _id: user._id + }; + let update = { + $set: { + "tags.$[element].challenge": true + } + }; + let opts = { + multi: true, + arrayFilters: [{ "element.challenge": "true" }] + }; + + await dbUsers.update(query, update, opts); + + update = { + $set: { + "tags.$[element].challenge": false + } + }; + opts = { + multi: true, + arrayFilters: [{ "element.challenge": "false" }] + }; + + dbUsers.update(query, update, opts); + + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); +} From c64b81648f566de9b371dc87bee5278aada8df5d Mon Sep 17 00:00:00 2001 From: osiris Date: Fri, 7 Aug 2020 19:22:53 +0800 Subject: [PATCH 6/9] Implement suggestions from ilnt --- .../users/tag-challenge-field-string2bool.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/migrations/users/tag-challenge-field-string2bool.js b/migrations/users/tag-challenge-field-string2bool.js index 0631e180b7d..67006c8548b 100644 --- a/migrations/users/tag-challenge-field-string2bool.js +++ b/migrations/users/tag-challenge-field-string2bool.js @@ -1,5 +1,4 @@ import monk from 'monk'; // eslint-disable-line import/no-extraneous-dependencies -import { concat } from 'lodash'; const migrationName = 'tag-challenge-field-string2bool.js'; @@ -17,13 +16,14 @@ export default async function processUsers () { $elemMatch: { challenge: { $exists: true, - $type: "string", + $type: 'string', }, }, }, }; - while (true) { + while (true) { // eslint-disable-line no-constant-condition + // eslint-disable-next-line no-await-in-loop const users = await dbUsers.find(query, { sort: { _id: 1 }, limit: 250, @@ -47,28 +47,28 @@ async function updateUser (user) { count += 1; const query = { - _id: user._id + _id: user._id, }; let update = { $set: { - "tags.$[element].challenge": true - } + 'tags.$[element].challenge': true, + }, }; let opts = { multi: true, - arrayFilters: [{ "element.challenge": "true" }] + arrayFilters: [{ 'element.challenge': 'true' }], }; await dbUsers.update(query, update, opts); update = { $set: { - "tags.$[element].challenge": false - } + 'tags.$[element].challenge': false, + }, }; opts = { multi: true, - arrayFilters: [{ "element.challenge": "false" }] + arrayFilters: [{ 'element.challenge': 'false' }], }; dbUsers.update(query, update, opts); From 907615ea8d9936e3c3e8e7e23dd88a714e845692 Mon Sep 17 00:00:00 2001 From: osiris Date: Tue, 18 Aug 2020 12:42:02 +0800 Subject: [PATCH 7/9] Use mongoose instead of Mock in migration --- .../users/tag-challenge-field-string2bool.js | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/migrations/users/tag-challenge-field-string2bool.js b/migrations/users/tag-challenge-field-string2bool.js index 67006c8548b..f212715cd71 100644 --- a/migrations/users/tag-challenge-field-string2bool.js +++ b/migrations/users/tag-challenge-field-string2bool.js @@ -1,17 +1,13 @@ -import monk from 'monk'; // eslint-disable-line import/no-extraneous-dependencies +import { model as User } from '../../website/server/models/user'; -const migrationName = 'tag-challenge-field-string2bool.js'; - -const connectionString = 'mongodb://localhost:27017/habitica-dev?auto_reconnect=true'; // FOR TEST DATABASE - -const dbUsers = monk(connectionString).get('users', { castIds: false }); +const MIGRATION_NAME = 'tag-challenge-field-string2bool'; const progressCount = 1000; let count = 0; export default async function processUsers () { const query = { - migration: { $ne: migrationName }, + migration: { $ne: MIGRATION_NAME }, tags: { $elemMatch: { challenge: { @@ -24,10 +20,12 @@ export default async function processUsers () { while (true) { // eslint-disable-line no-constant-condition // eslint-disable-next-line no-await-in-loop - const users = await dbUsers.find(query, { - sort: { _id: 1 }, - limit: 250, - }); + const users = await User.find(query) + .sort({ _id: 1 }) + .limit(250) + .select({ _id: 1 }) + .lean() + .exec(); if (users.length === 0) { console.warn('All appropriate users found and modified.'); @@ -43,35 +41,36 @@ export default async function processUsers () { } } +/* +db.users.update({ "auth.local.username": "satou2"}, { $set: { "tags.7.challenge": "true" }}) +db.users.updateOne({ + _id: 'bd95ca4c-8db2-4e8d-8492-b83746b90993' +}, { + $set: { + 'tags.$[element].challenge': true, + } +}, { + arrayFilters: [{ 'element.challenge': 'true' }] +}) +*/ async function updateUser (user) { count += 1; const query = { _id: user._id, }; - let update = { + + const update = { $set: { 'tags.$[element].challenge': true, }, }; - let opts = { - multi: true, - arrayFilters: [{ 'element.challenge': 'true' }], - }; - await dbUsers.update(query, update, opts); - - update = { - $set: { - 'tags.$[element].challenge': false, - }, - }; - opts = { - multi: true, - arrayFilters: [{ 'element.challenge': 'false' }], + const opts = { + arrayFilters: [{ 'element.challenge': 'true' }], }; - dbUsers.update(query, update, opts); - if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + + return User.updateOne(query, update, opts).exec(); } From 5c12c0a732b7cbf9beca3046dbfb0da26ec88468 Mon Sep 17 00:00:00 2001 From: osiris Date: Sat, 22 Aug 2020 19:27:02 +0800 Subject: [PATCH 8/9] Change from update to bulkwrite --- .../users/tag-challenge-field-string2bool.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/migrations/users/tag-challenge-field-string2bool.js b/migrations/users/tag-challenge-field-string2bool.js index f212715cd71..3e1e92b0a11 100644 --- a/migrations/users/tag-challenge-field-string2bool.js +++ b/migrations/users/tag-challenge-field-string2bool.js @@ -54,6 +54,7 @@ db.users.updateOne({ }) */ async function updateUser (user) { + console.log('updateUser'); count += 1; const query = { @@ -66,11 +67,17 @@ async function updateUser (user) { }, }; - const opts = { - arrayFilters: [{ 'element.challenge': 'true' }], - }; - if (count % progressCount === 0) console.warn(`${count} ${user._id}`); - return User.updateOne(query, update, opts).exec(); + return User.bulkWrite( + [ + { + updateOne: { + filter: query, + update, + arrayFilters: [{ 'element.challenge': 'true' }], + }, + }, + ], + ); } From 4b46aefd9c5f37fb3361c3b7e132b447acd81a15 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Mon, 7 Sep 2020 16:44:12 +0200 Subject: [PATCH 9/9] update users individually --- .../users/tag-challenge-field-string2bool.js | 60 ++++++++----------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/migrations/users/tag-challenge-field-string2bool.js b/migrations/users/tag-challenge-field-string2bool.js index 3e1e92b0a11..27bb1f3446d 100644 --- a/migrations/users/tag-challenge-field-string2bool.js +++ b/migrations/users/tag-challenge-field-string2bool.js @@ -23,7 +23,7 @@ export default async function processUsers () { const users = await User.find(query) .sort({ _id: 1 }) .limit(250) - .select({ _id: 1 }) + .select({ _id: 1, tags: 1 }) .lean() .exec(); @@ -41,43 +41,33 @@ export default async function processUsers () { } } -/* -db.users.update({ "auth.local.username": "satou2"}, { $set: { "tags.7.challenge": "true" }}) -db.users.updateOne({ - _id: 'bd95ca4c-8db2-4e8d-8492-b83746b90993' -}, { - $set: { - 'tags.$[element].challenge': true, - } -}, { - arrayFilters: [{ 'element.challenge': 'true' }] -}) -*/ async function updateUser (user) { - console.log('updateUser'); count += 1; + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + let requiresUpdate = false; - const query = { - _id: user._id, - }; - - const update = { - $set: { - 'tags.$[element].challenge': true, - }, - }; + if (user && user.tags) { + user.tags.forEach(tag => { + if (tag && typeof tag.challenge === 'string') { + requiresUpdate = true; + if (tag.challenge === 'true') { + tag.challenge = true; + } else if (tag.challenge === 'false') { + tag.challenge = false; + } else { + tag.challenge = null; + } + } + }); + } - if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + if (requiresUpdate) { + const set = { + migration: MIGRATION_NAME, + tags: user.tags, + }; + return User.update({ _id: user._id }, { $set: set }).exec(); + } - return User.bulkWrite( - [ - { - updateOne: { - filter: query, - update, - arrayFilters: [{ 'element.challenge': 'true' }], - }, - }, - ], - ); + return null; }