Merge branch 'due-dates-in-todos' of https://github.com/CuriousMagpie/habitica into due-dates-in-todos

This commit is contained in:
SabreCat
2023-05-16 14:27:56 -05:00
33 changed files with 908 additions and 719 deletions

1075
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
{ {
"name": "habitica", "name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.", "description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.270.0", "version": "4.270.1",
"main": "./website/server/index.js", "main": "./website/server/index.js",
"dependencies": { "dependencies": {
"@babel/core": "^7.21.4", "@babel/core": "^7.21.8",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.21.5",
"@babel/register": "^7.21.0", "@babel/register": "^7.21.0",
"@google-cloud/trace-agent": "^7.1.2", "@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3", "@parse/node-apn": "^5.1.3",
@@ -42,7 +42,7 @@
"image-size": "^1.0.2", "image-size": "^1.0.2",
"in-app-purchase": "^1.11.3", "in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0", "js2xmlparser": "^5.0.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.0",
"jwks-rsa": "^2.1.5", "jwks-rsa": "^2.1.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
@@ -67,8 +67,8 @@
"remove-markdown": "^0.5.0", "remove-markdown": "^0.5.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"short-uuid": "^4.2.2", "short-uuid": "^4.2.2",
"stripe": "^11.10.0", "stripe": "^12.5.0",
"superagent": "^8.0.6", "superagent": "^8.0.9",
"universal-analytics": "^0.5.3", "universal-analytics": "^0.5.3",
"useragent": "^2.1.9", "useragent": "^2.1.9",
"uuid": "^9.0.0", "uuid": "^9.0.0",

View File

@@ -242,7 +242,7 @@ describe('cron middleware', () => {
sandbox.spy(cronLib, 'recoverCron'); sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'update') sandbox.stub(User, 'updateOne')
.withArgs({ .withArgs({
_id: user._id, _id: user._id,
$or: [ $or: [

View File

@@ -1732,7 +1732,7 @@ describe('Group Model', () => {
}); });
it('updates participting members (not including user)', async () => { it('updates participting members (not including user)', async () => {
sandbox.spy(User, 'update'); sandbox.spy(User, 'updateMany');
await party.startQuest(nonParticipatingMember); await party.startQuest(nonParticipatingMember);
@@ -1740,7 +1740,7 @@ describe('Group Model', () => {
questLeader._id, participatingMember._id, sleepingParticipatingMember._id, questLeader._id, participatingMember._id, sleepingParticipatingMember._id,
]; ];
expect(User.update).to.be.calledWith( expect(User.updateMany).to.be.calledWith(
{ _id: { $in: members } }, { _id: { $in: members } },
{ {
$set: { $set: {
@@ -1753,11 +1753,11 @@ describe('Group Model', () => {
}); });
it('updates non-user quest leader and decrements quest scroll', async () => { it('updates non-user quest leader and decrements quest scroll', async () => {
sandbox.spy(User, 'update'); sandbox.spy(User, 'updateOne');
await party.startQuest(participatingMember); await party.startQuest(participatingMember);
expect(User.update).to.be.calledWith( expect(User.updateOne).to.be.calledWith(
{ _id: questLeader._id }, { _id: questLeader._id },
{ {
$inc: { $inc: {
@@ -1819,29 +1819,29 @@ describe('Group Model', () => {
}; };
it('doesn\'t retry successful operations', async () => { it('doesn\'t retry successful operations', async () => {
sandbox.stub(User, 'update').returns(successfulMock); sandbox.stub(User, 'updateOne').returns(successfulMock);
await party.finishQuest(quest); await party.finishQuest(quest);
expect(User.update).to.be.calledThrice; expect(User.updateOne).to.be.calledThrice;
}); });
it('stops retrying when a successful update has occurred', async () => { it('stops retrying when a successful update has occurred', async () => {
const updateStub = sandbox.stub(User, 'update'); const updateStub = sandbox.stub(User, 'updateOne');
updateStub.onCall(0).returns(failedMock); updateStub.onCall(0).returns(failedMock);
updateStub.returns(successfulMock); updateStub.returns(successfulMock);
await party.finishQuest(quest); await party.finishQuest(quest);
expect(User.update.callCount).to.equal(4); expect(User.updateOne.callCount).to.equal(4);
}); });
it('retries failed updates at most five times per user', async () => { it('retries failed updates at most five times per user', async () => {
sandbox.stub(User, 'update').returns(failedMock); sandbox.stub(User, 'updateOne').returns(failedMock);
await expect(party.finishQuest(quest)).to.eventually.be.rejected; await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(15); // for 3 users expect(User.updateOne.callCount).to.eql(15); // for 3 users
}); });
}); });
@@ -2088,17 +2088,17 @@ describe('Group Model', () => {
context('Party quests', () => { context('Party quests', () => {
it('updates participating members with rewards', async () => { it('updates participating members with rewards', async () => {
sandbox.spy(User, 'update'); sandbox.spy(User, 'updateOne');
await party.finishQuest(quest); await party.finishQuest(quest);
expect(User.update).to.be.calledThrice; expect(User.updateOne).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({ expect(User.updateOne).to.be.calledWithMatch({
_id: questLeader._id, _id: questLeader._id,
}); });
expect(User.update).to.be.calledWithMatch({ expect(User.updateOne).to.be.calledWithMatch({
_id: participatingMember._id, _id: participatingMember._id,
}); });
expect(User.update).to.be.calledWithMatch({ expect(User.updateOne).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id, _id: sleepingParticipatingMember._id,
}); });
}); });
@@ -2173,11 +2173,11 @@ describe('Group Model', () => {
}); });
it('updates all users with rewards', async () => { it('updates all users with rewards', async () => {
sandbox.spy(User, 'update'); sandbox.spy(User, 'updateMany');
await party.finishQuest(tavernQuest); await party.finishQuest(tavernQuest);
expect(User.update).to.be.calledOnce; expect(User.updateMany).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({}); expect(User.updateMany).to.be.calledWithMatch({});
}); });
it('sets quest completed to the world quest key', async () => { it('sets quest completed to the world quest key', async () => {

View File

@@ -202,18 +202,86 @@ describe('POST /user/class/cast/:spellId', () => {
await group.groupLeader.post('/user/class/cast/mpheal'); await group.groupLeader.post('/user/class/cast/mpheal');
promises = []; promises = [];
promises.push(group.groupLeader.sync());
promises.push(group.members[0].sync()); promises.push(group.members[0].sync());
promises.push(group.members[1].sync()); promises.push(group.members[1].sync());
promises.push(group.members[2].sync()); promises.push(group.members[2].sync());
promises.push(group.members[3].sync()); promises.push(group.members[3].sync());
await Promise.all(promises); await Promise.all(promises);
expect(group.groupLeader.stats.mp).to.be.equal(170); // spell caster
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
}); });
const spellList = [
{
className: 'warrior',
spells: [['smash', 'task'], ['defensiveStance'], ['valorousPresence'], ['intimidate']],
},
{
className: 'wizard',
spells: [['fireball', 'task'], ['mpheal'], ['earth'], ['frost']],
},
{
className: 'healer',
spells: [['heal'], ['brightness'], ['protectAura'], ['healAll']],
},
{
className: 'rogue',
spells: [['pickPocket', 'task'], ['backStab', 'task'], ['toolsOfTrade'], ['stealth']],
},
];
spellList.forEach(async habitClass => {
describe(`For a ${habitClass.className}`, async () => {
habitClass.spells.forEach(async spell => {
describe(`Using ${spell[0]}`, async () => {
it('Deducts MP from spell caster', async () => {
const { groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 3,
});
await groupLeader.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await groupLeader.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await groupLeader.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await groupLeader.post(`/user/class/cast/${spell[0]}`);
}
await groupLeader.sync();
expect(groupLeader.stats.mp).to.be.lessThan(200);
});
it('works without a party', async () => {
await user.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await user.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await user.post(`/user/class/cast/${spell[0]}`);
}
await user.sync();
expect(user.stats.mp).to.be.lessThan(200);
});
});
});
});
});
it('cast bulk', async () => { it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({ // eslint-disable-line prefer-const let { group, groupLeader } = await createAndPopulateGroup({ // eslint-disable-line prefer-const
groupDetails: { type: 'party', privacy: 'private' }, groupDetails: { type: 'party', privacy: 'private' },

View File

@@ -16901,9 +16901,9 @@
} }
}, },
"core-js": { "core-js": {
"version": "3.30.1", "version": "3.30.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==" "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg=="
}, },
"core-js-compat": { "core-js-compat": {
"version": "3.11.0", "version": "3.11.0",
@@ -17952,9 +17952,9 @@
} }
}, },
"dompurify": { "dompurify": {
"version": "2.4.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ==" "integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
}, },
"domutils": { "domutils": {
"version": "1.7.0", "version": "1.7.0",
@@ -21071,6 +21071,11 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
}, },
"immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg=="
},
"import-cwd": { "import-cwd": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -21431,9 +21436,9 @@
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==" "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
}, },
"intro.js": { "intro.js": {
"version": "6.0.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-6.0.0.tgz", "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-7.0.1.tgz",
"integrity": "sha512-ZUiR6BoLSvPSlLG0boewnWVgji1fE1gBvP/pyw5pgCKXEDQz1mMeUxarggClPNs71UTq364LwSk9zxz17A9gaQ==" "integrity": "sha512-1oqz6aOz9cGQ3CrtVYhCSo6AkjnXUn302kcIWLaZ3TI4kKssRXDwDSz4VRoGcfC1jN+WfaSJXRBrITz+QVEBzg=="
}, },
"invariant": { "invariant": {
"version": "2.2.4", "version": "2.2.4",
@@ -22013,9 +22018,9 @@
} }
}, },
"jquery": { "jquery": {
"version": "3.6.4", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ==" "integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
}, },
"js-message": { "js-message": {
"version": "1.0.5", "version": "1.0.5",
@@ -27361,17 +27366,19 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sass": { "sass": {
"version": "1.34.0", "version": "1.62.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.34.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
"integrity": "sha512-rHEN0BscqjUYuomUEaqq3BMgsXqQfkcMVR7UhscsAVub0/spUrZGBMxQXFS2kfiDsPLZw5yuU9iJEFNC2x38Qw==", "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
"requires": { "requires": {
"chokidar": ">=3.0.0 <4.0.0" "chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
}, },
"dependencies": { "dependencies": {
"anymatch": { "anymatch": {
"version": "3.1.2", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"requires": { "requires": {
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"picomatch": "^2.0.4" "picomatch": "^2.0.4"
@@ -27391,18 +27398,18 @@
} }
}, },
"chokidar": { "chokidar": {
"version": "3.5.1", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"requires": { "requires": {
"anymatch": "~3.1.1", "anymatch": "~3.1.2",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.3.1", "fsevents": "~2.3.2",
"glob-parent": "~5.1.0", "glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
"normalize-path": "~3.0.0", "normalize-path": "~3.0.0",
"readdirp": "~3.5.0" "readdirp": "~3.6.0"
} }
}, },
"fill-range": { "fill-range": {
@@ -27441,9 +27448,9 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
}, },
"readdirp": { "readdirp": {
"version": "3.5.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"requires": { "requires": {
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
} }
@@ -30265,9 +30272,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
}, },
"uuid": { "uuid": {
"version": "8.3.2", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
}, },
"uuid-browser": { "uuid-browser": {
"version": "3.1.0", "version": "3.1.0",

View File

@@ -32,8 +32,8 @@
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1", "bootstrap-vue": "^2.23.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"core-js": "^3.30.1", "core-js": "^3.30.2",
"dompurify": "^2.4.3", "dompurify": "^3.0.3",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0", "eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0", "eslint-plugin-mocha": "^5.3.0",
@@ -41,12 +41,12 @@
"habitica-markdown": "^3.0.0", "habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0", "hellojs": "^1.20.0",
"inspectpack": "^4.7.1", "inspectpack": "^4.7.1",
"intro.js": "^6.0.0", "intro.js": "^7.0.1",
"jquery": "^3.6.4", "jquery": "^3.7.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"sass": "^1.34.0", "sass": "^1.62.1",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"smartbanner.js": "^1.19.2", "smartbanner.js": "^1.19.2",
"stopword": "^2.0.8", "stopword": "^2.0.8",
@@ -54,7 +54,7 @@
"svg-url-loader": "^7.1.1", "svg-url-loader": "^7.1.1",
"svgo": "^1.3.2", "svgo": "^1.3.2",
"svgo-loader": "^2.2.1", "svgo-loader": "^2.2.1",
"uuid": "^8.3.2", "uuid": "^9.0.0",
"validator": "^13.9.0", "validator": "^13.9.0",
"vue": "^2.7.10", "vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0", "vue-cli-plugin-storybook": "2.1.0",

View File

@@ -144,22 +144,6 @@
>{{ $t('startAdvCollapsed') }}</span> >{{ $t('startAdvCollapsed') }}</span>
</label> </label>
</div> </div>
<div class="checkbox">
<label>
<input
v-model="user.preferences.dailyDueDefaultView"
type="checkbox"
class="mr-2"
@change="set('dailyDueDefaultView')"
>
<span
class="hint"
popover-trigger="mouseenter"
popover-placement="right"
:popover="$t('dailyDueDefaultViewPop')"
>{{ $t('dailyDueDefaultView') }}</span>
</label>
</div>
<div <div
v-if="party.memberCount === 1" v-if="party.memberCount === 1"
class="checkbox" class="checkbox"

View File

@@ -521,7 +521,12 @@ export default {
// Get Category Filter Labels // Get Category Filter Labels
this.typeFilters = getFilterLabels(this.type, this.challenge); this.typeFilters = getFilterLabels(this.type, this.challenge);
// Set default filter for task column // Set default filter for task column
this.activateFilter(this.type);
if (this.challenge) {
this.activateFilter(this.type);
} else {
this.activateFilter(this.type, this.user.preferences.tasks.activeFilter[this.type], true);
}
}, },
mounted () { mounted () {
this.setColumnBackgroundVisibility(); this.setColumnBackgroundVisibility();
@@ -661,7 +666,7 @@ export default {
taskSummary (task) { taskSummary (task) {
this.$emit('taskSummary', task); this.$emit('taskSummary', task);
}, },
activateFilter (type, filter = '') { activateFilter (type, filter = '', skipSave = false) {
// Needs a separate API call as this data may not reside in store // Needs a separate API call as this data may not reside in store
if (type === 'todo' && filter === 'complete2') { if (type === 'todo' && filter === 'complete2') {
if (this.group && this.group._id) { if (this.group && this.group._id) {
@@ -677,14 +682,16 @@ export default {
// as default filter for daily // as default filter for daily
// and set the filter as 'due' only when the component first // and set the filter as 'due' only when the component first
// loads and not on subsequent reloads. // loads and not on subsequent reloads.
if ( if (type === 'daily' && filter === '' && !this.challenge) {
type === 'daily' && filter === '' && !this.challenge
&& this.user.preferences.dailyDueDefaultView
) {
filter = 'due'; // eslint-disable-line no-param-reassign filter = 'due'; // eslint-disable-line no-param-reassign
} }
this.activeFilter = getActiveFilter(type, filter, this.challenge); this.activeFilter = getActiveFilter(type, filter, this.challenge);
if (!skipSave && !this.challenge) {
const propertyToUpdate = `preferences.tasks.activeFilter.${type}`;
this.$store.dispatch('user:set', { [propertyToUpdate]: filter });
}
}, },
setColumnBackgroundVisibility () { setColumnBackgroundVisibility () {
this.$nextTick(() => { this.$nextTick(() => {

View File

@@ -114,7 +114,7 @@ export default {
this.castCancel(); this.castCancel();
// the selected member doesn't have the flags property which sets `cardReceived` // the selected member doesn't have the flags property which sets `cardReceived`
if (spell.pinType !== 'card') { if (spell.pinType !== 'card' && spell.bulk !== true) {
try { try {
spell.cast(this.user, target, {}); spell.cast(this.user, target, {});
} catch (e) { } catch (e) {

View File

@@ -21,6 +21,18 @@ describe('Task Column', () => {
getters: { getters: {
'tasks:getFilteredTaskList': () => [], 'tasks:getFilteredTaskList': () => [],
}, },
state: {
user: {
data: {
preferences: {
tasks: {
activeFilter: {},
},
},
},
},
},
}, },
mocks, mocks,
stubs, stubs,
@@ -76,7 +88,20 @@ describe('Task Column', () => {
'tasks:getFilteredTaskList': () => () => habits, 'tasks:getFilteredTaskList': () => () => habits,
}; };
const store = new Store({ getters }); const store = new Store({
getters,
state: {
user: {
data: {
preferences: {
tasks: {
activeFilter: {},
},
},
},
},
},
});
wrapper = makeWrapper({ store }); wrapper = makeWrapper({ store });
}); });

View File

@@ -5,8 +5,6 @@
"helpWithTranslation": "Would you like to help with the translation of Habitica? Great! Then visit <a href=\"/groups/guild/7732f64c-33ee-4cce-873c-fc28f147a6f7\">the Aspiring Linguists Guild</a>!", "helpWithTranslation": "Would you like to help with the translation of Habitica? Great! Then visit <a href=\"/groups/guild/7732f64c-33ee-4cce-873c-fc28f147a6f7\">the Aspiring Linguists Guild</a>!",
"stickyHeader": "Sticky header", "stickyHeader": "Sticky header",
"newTaskEdit": "Open new tasks in edit mode", "newTaskEdit": "Open new tasks in edit mode",
"dailyDueDefaultView": "Set Dailies default to 'due' tab",
"dailyDueDefaultViewPop": "With this option set, the Dailies tasks will default to 'due' instead of 'all'",
"reverseChatOrder": "Show chat messages in reverse order", "reverseChatOrder": "Show chat messages in reverse order",
"startAdvCollapsed": "Advanced Settings in tasks start collapsed", "startAdvCollapsed": "Advanced Settings in tasks start collapsed",
"startAdvCollapsedPop": "With this option set, Advanced Settings will be hidden when you first open a task for editing.", "startAdvCollapsedPop": "With this option set, Advanced Settings will be hidden when you first open a task for editing.",

View File

@@ -10,11 +10,15 @@ const gemsPromo = {
export const EVENTS = { export const EVENTS = {
noEvent: { noEvent: {
start: '2023-05-01T23:59-04:00', start: '2023-05-31T23:59-04:00',
end: '2023-06-22T08:00-04:00', end: '2023-06-22T08:00-04:00',
season: 'normal', season: 'normal',
npcImageSuffix: '', npcImageSuffix: '',
}, },
potions202305: {
start:'2023-05-16T08:00-04:00',
end:'2023-05-31T23:59-04:00',
},
aprilFools2023: { aprilFools2023: {
start: '2023-04-01T08:00-04:00', start: '2023-04-01T08:00-04:00',
end: '2023-04-02T08:00-04:00', end: '2023-04-02T08:00-04:00',

View File

@@ -88,26 +88,26 @@ const premium = {
value: 2, value: 2,
text: t('hatchingPotionFairy'), text: t('hatchingPotionFairy'),
limited: true, limited: true,
event: EVENTS.potions202105, event: EVENTS.potions202305,
_addlNotes: t('eventAvailabilityReturning', { _addlNotes: t('eventAvailabilityReturning', {
availableDate: t('dateEndMay'), availableDate: t('dateEndMay'),
previousDate: t('mayYYYY', { year: 2020 }), previousDate: t('mayYYYY', { year: 2021 }),
}), }),
canBuy () { canBuy () {
return moment().isBefore(EVENTS.potions202105.end); return moment().isBefore(EVENTS.potions202305.end);
}, },
}, },
Floral: { Floral: {
value: 2, value: 2,
text: t('hatchingPotionFloral'), text: t('hatchingPotionFloral'),
limited: true, limited: true,
event: EVENTS.potions202205, event: EVENTS.potions202305,
_addlNotes: t('eventAvailabilityReturning', { _addlNotes: t('eventAvailabilityReturning', {
availableDate: t('dateEndMay'), availableDate: t('dateEndMay'),
previousDate: t('mayYYYY', { year: 2021 }), previousDate: t('mayYYYY', { year: 2022 }),
}), }),
canBuy () { canBuy () {
return moment().isBefore(EVENTS.potions202205.end); return moment().isBefore(EVENTS.potions202305.end);
}, },
}, },
Aquatic: { Aquatic: {

View File

@@ -5,7 +5,7 @@ import { EVENTS } from './constants';
// path: 'premiumHatchingPotions.Rainbow', // path: 'premiumHatchingPotions.Rainbow',
const featuredItems = { const featuredItems = {
market () { market () {
if (moment().isBetween(EVENTS.spring2023.start, EVENTS.spring2023.end)) { if (moment().isBetween(EVENTS.potions202305.start, EVENTS.potions202305.end)) {
return [ return [
{ {
type: 'armoire', type: 'armoire',
@@ -13,15 +13,15 @@ const featuredItems = {
}, },
{ {
type: 'premiumHatchingPotion', type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.PolkaDot', path: 'premiumHatchingPotions.Fairy',
}, },
{ {
type: 'premiumHatchingPotion', type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.BirchBark', path: 'premiumHatchingPotions.Floral',
}, },
{ {
type: 'premiumHatchingPotion', type: 'hatchingPotions',
path: 'premiumHatchingPotions.Rainbow', path: 'hatchingPotions.Golden',
}, },
]; ];
} }

View File

@@ -77,13 +77,11 @@ spells.wizard = {
lvl: 12, lvl: 12,
target: 'party', target: 'party',
notes: t('spellWizardMPHealNotes'), notes: t('spellWizardMPHealNotes'),
cast (user, target) { bulk: true,
each(target, member => { cast (user, data) {
const bonus = statsComputed(user).int; const bonus = statsComputed(user).int;
if (user._id !== member._id && member.stats.class !== 'wizard') { data.query['stats.class'] = { $ne: 'wizard' };
member.stats.mp += Math.ceil(diminishingReturns(bonus, 25, 125)); data.update = { $inc: { 'stats.mp': Math.ceil(diminishingReturns(bonus, 25, 125)) } };
}
});
}, },
}, },
earth: { // Earthquake earth: { // Earthquake
@@ -92,12 +90,10 @@ spells.wizard = {
lvl: 13, lvl: 13,
target: 'party', target: 'party',
notes: t('spellWizardEarthNotes'), notes: t('spellWizardEarthNotes'),
cast (user, target) { bulk: true,
each(target, member => { cast (user, data) {
const bonus = statsComputed(user).int - user.stats.buffs.int; const bonus = statsComputed(user).int - user.stats.buffs.int;
if (!member.stats.buffs.int) member.stats.buffs.int = 0; data.update = { $inc: { 'stats.buffs.int': Math.ceil(diminishingReturns(bonus, 30, 200)) } };
member.stats.buffs.int += Math.ceil(diminishingReturns(bonus, 30, 200));
});
}, },
}, },
frost: { // Chilling Frost frost: { // Chilling Frost
@@ -147,12 +143,10 @@ spells.warrior = {
lvl: 13, lvl: 13,
target: 'party', target: 'party',
notes: t('spellWarriorValorousPresenceNotes'), notes: t('spellWarriorValorousPresenceNotes'),
cast (user, target) { bulk: true,
each(target, member => { cast (user, data) {
const bonus = statsComputed(user).str - user.stats.buffs.str; const bonus = statsComputed(user).str - user.stats.buffs.str;
if (!member.stats.buffs.str) member.stats.buffs.str = 0; data.update = { $inc: { 'stats.buffs.str': Math.ceil(diminishingReturns(bonus, 20, 200)) } };
member.stats.buffs.str += Math.ceil(diminishingReturns(bonus, 20, 200));
});
}, },
}, },
intimidate: { // Intimidating Gaze intimidate: { // Intimidating Gaze
@@ -161,12 +155,10 @@ spells.warrior = {
lvl: 14, lvl: 14,
target: 'party', target: 'party',
notes: t('spellWarriorIntimidateNotes'), notes: t('spellWarriorIntimidateNotes'),
cast (user, target) { bulk: true,
each(target, member => { cast (user, data) {
const bonus = statsComputed(user).con - user.stats.buffs.con; const bonus = statsComputed(user).con - user.stats.buffs.con;
if (!member.stats.buffs.con) member.stats.buffs.con = 0; data.update = { $inc: { 'stats.buffs.con': Math.ceil(diminishingReturns(bonus, 24, 200)) } };
member.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 24, 200));
});
}, },
}, },
}; };
@@ -203,12 +195,10 @@ spells.rogue = {
lvl: 13, lvl: 13,
target: 'party', target: 'party',
notes: t('spellRogueToolsOfTradeNotes'), notes: t('spellRogueToolsOfTradeNotes'),
cast (user, target) { bulk: true,
each(target, member => { cast (user, data) {
const bonus = statsComputed(user).per - user.stats.buffs.per; const bonus = statsComputed(user).per - user.stats.buffs.per;
if (!member.stats.buffs.per) member.stats.buffs.per = 0; data.update = { $inc: { 'stats.buffs.per': Math.ceil(diminishingReturns(bonus, 100, 50)) } };
member.stats.buffs.per += Math.ceil(diminishingReturns(bonus, 100, 50));
});
}, },
}, },
stealth: { // Stealth stealth: { // Stealth
@@ -257,12 +247,10 @@ spells.healer = {
lvl: 13, lvl: 13,
target: 'party', target: 'party',
notes: t('spellHealerProtectAuraNotes'), notes: t('spellHealerProtectAuraNotes'),
cast (user, target) { bulk: true,
each(target, member => { cast (user, data) {
const bonus = statsComputed(user).con - user.stats.buffs.con; const bonus = statsComputed(user).con - user.stats.buffs.con;
if (!member.stats.buffs.con) member.stats.buffs.con = 0; data.update = { $inc: { 'stats.buffs.con': Math.ceil(diminishingReturns(bonus, 200, 200)) } };
member.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 200, 200));
});
}, },
}, },
healAll: { // Blessing healAll: { // Blessing

View File

@@ -282,6 +282,8 @@ api.postChat = {
analyticsObject.groupName = group.name; analyticsObject.groupName = group.name;
} }
res.analytics.track('group chat', analyticsObject);
if (chatUpdated) { if (chatUpdated) {
res.respond(200, { chat: chatRes.chat }); res.respond(200, { chat: chatRes.chat });
} else { } else {

View File

@@ -93,7 +93,7 @@ api.inviteToQuest = {
user.party.quest.RSVPNeeded = false; user.party.quest.RSVPNeeded = false;
user.party.quest.key = questKey; user.party.quest.key = questKey;
await User.update({ await User.updateMany({
'party._id': group._id, 'party._id': group._id,
_id: { $ne: user._id }, _id: { $ne: user._id },
}, { }, {
@@ -101,7 +101,7 @@ api.inviteToQuest = {
'party.quest.RSVPNeeded': true, 'party.quest.RSVPNeeded': true,
'party.quest.key': questKey, 'party.quest.key': questKey,
}, },
}, { multi: true }).exec(); }).exec();
_.each(members, member => { _.each(members, member => {
group.quest.members[member._id] = null; group.quest.members[member._id] = null;
@@ -409,10 +409,9 @@ api.cancelQuest = {
const [savedGroup] = await Promise.all([ const [savedGroup] = await Promise.all([
group.save(), group.save(),
newChatMessage.save(), newChatMessage.save(),
User.update( User.updateMany(
{ 'party._id': groupId }, { 'party._id': groupId },
Group.cleanQuestParty(), Group.cleanQuestParty(),
{ multi: true },
).exec(), ).exec(),
]); ]);
@@ -467,12 +466,11 @@ api.abortQuest = {
}); });
await newChatMessage.save(); await newChatMessage.save();
const memberUpdates = User.update({ const memberUpdates = User.updateMany({
'party._id': groupId, 'party._id': groupId,
}, Group.cleanQuestParty(), }, Group.cleanQuestParty()).exec();
{ multi: true }).exec();
const questLeaderUpdate = User.update({ const questLeaderUpdate = User.updateOne({
_id: group.quest.leader, _id: group.quest.leader,
}, { }, {
$inc: { $inc: {

View File

@@ -227,7 +227,7 @@ api.deleteTag = {
const tagFound = find(user.tags, tag => tag.id === req.params.tagId); const tagFound = find(user.tags, tag => tag.id === req.params.tagId);
if (!tagFound) throw new NotFound(res.t('tagNotFound')); if (!tagFound) throw new NotFound(res.t('tagNotFound'));
await user.update({ await user.updateOne({
$pull: { tags: { id: tagFound.id } }, $pull: { tags: { id: tagFound.id } },
}).exec(); }).exec();
@@ -237,13 +237,13 @@ api.deleteTag = {
user._v += 1; user._v += 1;
// Remove from all the tasks TODO test // Remove from all the tasks TODO test
await Tasks.Task.update({ await Tasks.Task.updateMany({
userId: user._id, userId: user._id,
}, { }, {
$pull: { $pull: {
tags: tagFound.id, tags: tagFound.id,
}, },
}, { multi: true }).exec(); }).exec();
res.respond(200, {}); res.respond(200, {});
}, },

View File

@@ -840,7 +840,7 @@ api.moveTask = {
// Cannot send $pull and $push on same field in one single op // Cannot send $pull and $push on same field in one single op
const pullQuery = { $pull: {} }; const pullQuery = { $pull: {} };
pullQuery.$pull[`tasksOrder.${task.type}s`] = task.id; pullQuery.$pull[`tasksOrder.${task.type}s`] = task.id;
await owner.update(pullQuery).exec(); await owner.updateOne(pullQuery).exec();
let position = to; let position = to;
if (to === -1) position = order.length - 1; // push to bottom if (to === -1) position = order.length - 1; // push to bottom
@@ -850,7 +850,7 @@ api.moveTask = {
$each: [task._id], $each: [task._id],
$position: position, $position: position,
}; };
await owner.update(updateQuery).exec(); await owner.updateOne(updateQuery).exec();
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook
@@ -1434,7 +1434,7 @@ api.deleteTask = {
const pullQuery = { $pull: {} }; const pullQuery = { $pull: {} };
pullQuery.$pull[`tasksOrder.${task.type}s`] = task._id; pullQuery.$pull[`tasksOrder.${task.type}s`] = task._id;
const taskOrderUpdate = (challenge || user).update(pullQuery).exec(); const taskOrderUpdate = (challenge || user).updateOne(pullQuery).exec();
// Update the user version field manually, // Update the user version field manually,
// it cannot be updated in the pre update hook // it cannot be updated in the pre update hook

View File

@@ -37,7 +37,15 @@ export default function baseModel (schema, options = {}) {
}); });
schema.pre('update', function preUpdateModel () { schema.pre('update', function preUpdateModel () {
this.update({}, { $set: { updatedAt: new Date() } }); this.set({}, { $set: { updatedAt: new Date() } });
});
schema.pre('updateOne', function preUpdateModel () {
this.set({}, { $set: { updatedAt: new Date() } });
});
schema.pre('updateMany', function preUpdateModel () {
this.set({}, { $set: { updatedAt: new Date() } });
}); });
} }

View File

@@ -21,7 +21,7 @@ const apnProvider = APN_ENABLED ? new apn.Provider({
}) : undefined; }) : undefined;
function removePushDevice (user, pushDevice) { function removePushDevice (user, pushDevice) {
return User.update({ _id: user._id }, { return User.updateOne({ _id: user._id }, {
$pull: { pushDevices: { regId: pushDevice.regId } }, $pull: { pushDevices: { regId: pushDevice.regId } },
}).exec().catch(err => { }).exec().catch(err => {
logger.error(err, `Error removing pushDevice ${pushDevice.regId} for user ${user._id}`); logger.error(err, `Error removing pushDevice ${pushDevice.regId} for user ${user._id}`);

View File

@@ -24,7 +24,7 @@ export function readController (router, controller, overrides = []) {
// If an authentication middleware is used run getUserLanguage after it, otherwise before // If an authentication middleware is used run getUserLanguage after it, otherwise before
// for cron instead use it only if an authentication middleware is present // for cron instead use it only if an authentication middleware is present
const authMiddlewareIndex = _.findIndex(middlewares, middleware => { let authMiddlewareIndex = _.findIndex(middlewares, middleware => {
if (middleware.name.indexOf('authWith') === 0) { // authWith{Headers|Session|Url|...} if (middleware.name.indexOf('authWith') === 0) { // authWith{Headers|Session|Url|...}
return true; return true;
} }
@@ -36,6 +36,7 @@ export function readController (router, controller, overrides = []) {
// disable caching for all routes with mandatory or optional authentication // disable caching for all routes with mandatory or optional authentication
if (authMiddlewareIndex !== -1) { if (authMiddlewareIndex !== -1) {
middlewares.unshift(disableCache); middlewares.unshift(disableCache);
authMiddlewareIndex += 1;
} }
if (action.noLanguage !== true) { // unless getting the language is explictly disabled if (action.noLanguage !== true) { // unless getting the language is explictly disabled

View File

@@ -11,7 +11,7 @@ import {
} from '../models/group'; } from '../models/group';
import apiError from './apiError'; import apiError from './apiError';
const partyMembersFields = 'profile.name stats achievements items.special notifications flags pinnedItems'; const partyMembersFields = 'profile.name stats achievements items.special pinnedItems notifications flags';
// Excluding notifications and flags from the list of public fields to return. // Excluding notifications and flags from the list of public fields to return.
const partyMembersPublicFields = 'profile.name stats achievements items.special'; const partyMembersPublicFields = 'profile.name stats achievements items.special';
@@ -74,12 +74,13 @@ async function castSelfSpell (req, user, spell, quantity = 1) {
await user.save(); await user.save();
} }
async function castPartySpell (req, party, partyMembers, user, spell, quantity = 1) { async function getPartyMembers (user, party) {
let partyMembers;
if (!party) { if (!party) {
// Act as solo party // Act as solo party
partyMembers = [user]; // eslint-disable-line no-param-reassign partyMembers = [user];
} else { } else {
partyMembers = await User // eslint-disable-line no-param-reassign partyMembers = await User
.find({ .find({
'party._id': party._id, 'party._id': party._id,
_id: { $ne: user._id }, // add separately _id: { $ne: user._id }, // add separately
@@ -89,22 +90,40 @@ async function castPartySpell (req, party, partyMembers, user, spell, quantity =
partyMembers.unshift(user); partyMembers.unshift(user);
} }
for (let i = 0; i < quantity; i += 1) {
spell.cast(user, partyMembers, req);
}
await Promise.all(partyMembers.map(m => m.save()));
return partyMembers; return partyMembers;
} }
async function castUserSpell (res, req, party, partyMembers, targetId, user, spell, quantity = 1) { async function castPartySpell (req, party, user, spell, quantity = 1) {
let partyMembers;
if (spell.bulk) {
const data = { };
if (party) {
data.query = { 'party._id': party._id };
} else {
data.query = { _id: user._id };
}
spell.cast(user, data);
await User.updateMany(data.query, data.update);
await user.save();
partyMembers = await getPartyMembers(user, party);
} else {
partyMembers = await getPartyMembers(user, party);
for (let i = 0; i < quantity; i += 1) {
spell.cast(user, partyMembers, req);
}
await Promise.all(partyMembers.map(m => m.save()));
}
return partyMembers;
}
async function castUserSpell (res, req, party, targetId, user, spell, quantity = 1) {
let partyMembers;
if (!party && (!targetId || user._id === targetId)) { if (!party && (!targetId || user._id === targetId)) {
partyMembers = user; // eslint-disable-line no-param-reassign partyMembers = user;
} else { } else {
if (!targetId) throw new BadRequest(res.t('targetIdUUID')); if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
if (!party) throw new NotFound(res.t('partyNotFound')); if (!party) throw new NotFound(res.t('partyNotFound'));
partyMembers = await User // eslint-disable-line no-param-reassign partyMembers = await User
.findOne({ _id: targetId, 'party._id': party._id }) .findOne({ _id: targetId, 'party._id': party._id })
.select(partyMembersFields) .select(partyMembersFields)
.exec(); .exec();
@@ -195,10 +214,10 @@ async function castSpell (req, res, { isV3 = false }) {
let partyMembers; let partyMembers;
if (targetType === 'party') { if (targetType === 'party') {
partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity); partyMembers = await castPartySpell(req, party, user, spell, quantity);
} else { } else {
partyMembers = await castUserSpell( partyMembers = await castUserSpell(
res, req, party, partyMembers, res, req, party,
targetId, user, spell, quantity, targetId, user, spell, quantity,
); );
} }

View File

@@ -114,7 +114,7 @@ async function createTasks (req, res, options = {}) {
}; };
} }
await owner.update(taskOrderUpdateQuery).exec(); await owner.updateOne(taskOrderUpdateQuery).exec();
// tasks with aliases need to be validated asynchronously // tasks with aliases need to be validated asynchronously
await validateTaskAlias(toSave, res); await validateTaskAlias(toSave, res);

View File

@@ -121,7 +121,7 @@ async function checkNewInputForProfanity (user, res, newValue) {
export async function update (req, res, { isV3 = false }) { export async function update (req, res, { isV3 = false }) {
const { user } = res.locals; const { user } = res.locals;
const promisesForTagsRemoval = []; let promisesForTagsRemoval = [];
if (req.body['party.seeking'] !== undefined && req.body['party.seeking'] !== null) { if (req.body['party.seeking'] !== undefined && req.body['party.seeking'] !== null) {
user.invitations.party = {}; user.invitations.party = {};
@@ -218,13 +218,13 @@ export async function update (req, res, { isV3 = false }) {
// Remove from all the tasks // Remove from all the tasks
// NOTE each tag to remove requires a query // NOTE each tag to remove requires a query
promisesForTagsRemoval.push(removedTagsIds.map(tagId => Tasks.Task.update({ promisesForTagsRemoval = removedTagsIds.map(tagId => Tasks.Task.updateMany({
userId: user._id, userId: user._id,
}, { }, {
$pull: { $pull: {
tags: tagId, tags: tagId,
}, },
}, { multi: true }).exec())); }).exec());
} else if (key === 'flags.newStuff' && val === false) { } else if (key === 'flags.newStuff' && val === false) {
// flags.newStuff was removed from the user schema and is only returned for compatibility // flags.newStuff was removed from the user schema and is only returned for compatibility
// reasons but we're keeping the ability to set it in API v3 // reasons but we're keeping the ability to set it in API v3

View File

@@ -61,7 +61,7 @@ function sendWebhook (webhook, body, user) {
}; };
} }
return User.update({ return User.updateOne({
_id: user._id, _id: user._id,
'webhooks.id': webhook.id, 'webhooks.id': webhook.id,
}, update).exec(); }, update).exec();

View File

@@ -16,7 +16,7 @@ async function checkForActiveCron (user, now) {
// To avoid double cron we first set _cronSignature // To avoid double cron we first set _cronSignature
// and then check that it's not changed while processing // and then check that it's not changed while processing
const userUpdateResult = await User.update({ const userUpdateResult = await User.updateOne({
_id: user._id, _id: user._id,
$or: [ // Make sure last cron was successful or failed before cronRetryTime $or: [ // Make sure last cron was successful or failed before cronRetryTime
{ _cronSignature: 'NOT_RUNNING' }, { _cronSignature: 'NOT_RUNNING' },
@@ -36,7 +36,7 @@ async function checkForActiveCron (user, now) {
} }
async function updateLastCron (user, now) { async function updateLastCron (user, now) {
await User.update({ await User.updateOne({
_id: user._id, _id: user._id,
}, { }, {
lastCron: now, // setting lastCron now so we don't risk re-running parts of cron if it fails lastCron: now, // setting lastCron now so we don't risk re-running parts of cron if it fails
@@ -44,7 +44,7 @@ async function updateLastCron (user, now) {
} }
async function unlockUser (user) { async function unlockUser (user) {
await User.update({ await User.updateOne({
_id: user._id, _id: user._id,
}, { }, {
_cronSignature: 'NOT_RUNNING', _cronSignature: 'NOT_RUNNING',
@@ -125,7 +125,7 @@ async function cronAsync (req, res) {
await Group.processQuestProgress(user, progress); await Group.processQuestProgress(user, progress);
// Set _cronSignature, lastCron and auth.timestamps.loggedin to signal end of cron // Set _cronSignature, lastCron and auth.timestamps.loggedin to signal end of cron
await User.update({ await User.updateOne({
_id: user._id, _id: user._id,
}, { }, {
$set: { $set: {
@@ -153,7 +153,7 @@ async function cronAsync (req, res) {
// For any other error make sure to reset _cronSignature // For any other error make sure to reset _cronSignature
// so that it doesn't prevent cron from running // so that it doesn't prevent cron from running
// at the next request // at the next request
await User.update({ await User.updateOne({
_id: user._id, _id: user._id,
}, { }, {
_cronSignature: 'NOT_RUNNING', _cronSignature: 'NOT_RUNNING',

View File

@@ -102,7 +102,7 @@ schema.methods.addToUser = async function addChallengeToUser (user) {
// Add challenge to users challenges atomically (with a condition that checks that it // Add challenge to users challenges atomically (with a condition that checks that it
// is not there already) to prevent multiple concurrent requests from passing through // is not there already) to prevent multiple concurrent requests from passing through
// see https://github.com/HabitRPG/habitica/issues/11295 // see https://github.com/HabitRPG/habitica/issues/11295
const result = await User.update( const result = await User.updateOne(
{ {
_id: user._id, _id: user._id,
challenges: { $nin: [this._id] }, challenges: { $nin: [this._id] },
@@ -249,7 +249,7 @@ async function _addTaskFn (challenge, tasks, memberId) {
}, },
}; };
const updateUserParams = { ...updateTasksOrderQ, ...addToChallengeTagSet }; const updateUserParams = { ...updateTasksOrderQ, ...addToChallengeTagSet };
toSave.unshift(User.update({ _id: memberId }, updateUserParams).exec()); toSave.unshift(User.updateOne({ _id: memberId }, updateUserParams).exec());
return Promise.all(toSave); return Promise.all(toSave);
} }
@@ -278,11 +278,11 @@ schema.methods.updateTask = async function challengeUpdateTask (task) {
const taskSchema = Tasks[task.type]; const taskSchema = Tasks[task.type];
// Updating instead of loading and saving for performances, // Updating instead of loading and saving for performances,
// risks becoming a problem if we introduce more complexity in tasks // risks becoming a problem if we introduce more complexity in tasks
await taskSchema.update({ await taskSchema.updateMany({
userId: { $exists: true }, userId: { $exists: true },
'challenge.id': challenge.id, 'challenge.id': challenge.id,
'challenge.taskId': task._id, 'challenge.taskId': task._id,
}, updateCmd, { multi: true }).exec(); }, updateCmd).exec();
}; };
// Remove a task from challenge members // Remove a task from challenge members
@@ -290,13 +290,13 @@ schema.methods.removeTask = async function challengeRemoveTask (task) {
const challenge = this; const challenge = this;
// Set the task as broken // Set the task as broken
await Tasks.Task.update({ await Tasks.Task.updateMany({
userId: { $exists: true }, userId: { $exists: true },
'challenge.id': challenge.id, 'challenge.id': challenge.id,
'challenge.taskId': task._id, 'challenge.taskId': task._id,
}, { }, {
$set: { 'challenge.broken': 'TASK_DELETED' }, $set: { 'challenge.broken': 'TASK_DELETED' },
}, { multi: true }).exec(); }).exec();
}; };
// Unlink challenges tasks (and the challenge itself) from user. TODO rename to 'leave' // Unlink challenges tasks (and the challenge itself) from user. TODO rename to 'leave'
@@ -311,9 +311,9 @@ schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep, sa
this.memberCount -= 1; this.memberCount -= 1;
if (keep === 'keep-all') { if (keep === 'keep-all') {
await Tasks.Task.update(findQuery, { await Tasks.Task.updateMany(findQuery, {
$set: { challenge: {} }, $set: { challenge: {} },
}, { multi: true }).exec(); }).exec();
const promises = [this.save()]; const promises = [this.save()];
@@ -356,11 +356,12 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
// Refund the leader if the challenge is deleted (no winner chosen) // Refund the leader if the challenge is deleted (no winner chosen)
if (brokenReason === 'CHALLENGE_DELETED') { if (brokenReason === 'CHALLENGE_DELETED') {
await User.update({ _id: challenge.leader }, { $inc: { balance: challenge.prize / 4 } }).exec(); await User.updateOne({ _id: challenge.leader }, { $inc: { balance: challenge.prize / 4 } })
.exec();
} }
// Update the challengeCount on the group // Update the challengeCount on the group
await Group.update({ _id: challenge.group }, { $inc: { challengeCount: -1 } }).exec(); await Group.updateOne({ _id: challenge.group }, { $inc: { challengeCount: -1 } }).exec();
// Award prize to winner and notify // Award prize to winner and notify
if (winner) { if (winner) {
@@ -370,7 +371,7 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
// reimburse the leader // reimburse the leader
const winnerCanGetGems = await winner.canGetGems(); const winnerCanGetGems = await winner.canGetGems();
if (!winnerCanGetGems) { if (!winnerCanGetGems) {
await User.update( await User.updateOne(
{ _id: challenge.leader }, { _id: challenge.leader },
{ $inc: { balance: challenge.prize / 4 } }, { $inc: { balance: challenge.prize / 4 } },
).exec(); ).exec();
@@ -408,22 +409,22 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
Tasks.Task.remove({ 'challenge.id': challenge._id, userId: { $exists: false } }).exec(), Tasks.Task.remove({ 'challenge.id': challenge._id, userId: { $exists: false } }).exec(),
// Set the challenge tag to non-challenge status // Set the challenge tag to non-challenge status
// and remove the challenge from the user's challenges // and remove the challenge from the user's challenges
User.update({ User.updateMany({
challenges: challenge._id, challenges: challenge._id,
'tags.id': challenge._id, 'tags.id': challenge._id,
}, { }, {
$set: { 'tags.$.challenge': false }, $set: { 'tags.$.challenge': false },
$pull: { challenges: challenge._id }, $pull: { challenges: challenge._id },
}, { multi: true }).exec(), }).exec(),
// Break users' tasks // Break users' tasks
Tasks.Task.update({ Tasks.Task.updateMany({
'challenge.id': challenge._id, 'challenge.id': challenge._id,
}, { }, {
$set: { $set: {
'challenge.broken': brokenReason, 'challenge.broken': brokenReason,
'challenge.winner': winner && winner.profile.name, 'challenge.winner': winner && winner.profile.name,
}, },
}, { multi: true }).exec(), }).exec(),
]; ];
Promise.all(backgroundTasks); Promise.all(backgroundTasks);

View File

@@ -268,10 +268,13 @@ schema.statics.getGroup = async function getGroup (options = {}) {
if (groupId === user.party._id) { if (groupId === user.party._id) {
// reset party object to default state // reset party object to default state
user.party = {}; user.party = {};
await user.save();
} else { } else {
removeFromArray(user.guilds, groupId); const item = removeFromArray(user.guilds, groupId);
if (item) {
await user.save();
}
} }
await user.save();
} }
return group; return group;
@@ -659,7 +662,7 @@ schema.methods.handleQuestInvitation = async function handleQuestInvitation (use
// to prevent multiple concurrent requests overriding updates // to prevent multiple concurrent requests overriding updates
// see https://github.com/HabitRPG/habitica/issues/11398 // see https://github.com/HabitRPG/habitica/issues/11398
const Group = this.constructor; const Group = this.constructor;
const result = await Group.update( const result = await Group.updateOne(
{ {
_id: this._id, _id: this._id,
[`quest.members.${user._id}`]: { $type: 10 }, // match BSON Type Null (type number 10) [`quest.members.${user._id}`]: { $type: 10 }, // match BSON Type Null (type number 10)
@@ -707,7 +710,7 @@ schema.methods.startQuest = async function startQuest (user) {
// Persist quest.members early to avoid simultaneous handling of accept/reject // Persist quest.members early to avoid simultaneous handling of accept/reject
// while processing the rest of this script // while processing the rest of this script
await this.update({ $set: { 'quest.members': this.quest.members } }).exec(); await this.updateOne({ $set: { 'quest.members': this.quest.members } }).exec();
const nonUserQuestMembers = _.keys(this.quest.members); const nonUserQuestMembers = _.keys(this.quest.members);
removeFromArray(nonUserQuestMembers, user._id); removeFromArray(nonUserQuestMembers, user._id);
@@ -747,7 +750,7 @@ schema.methods.startQuest = async function startQuest (user) {
user.markModified('items.quests'); user.markModified('items.quests');
promises.push(user.save()); promises.push(user.save());
} else { // another user is starting the quest, update the leader separately } else { // another user is starting the quest, update the leader separately
promises.push(User.update({ _id: this.quest.leader }, { promises.push(User.updateOne({ _id: this.quest.leader }, {
$inc: { $inc: {
[`items.quests.${this.quest.key}`]: -1, [`items.quests.${this.quest.key}`]: -1,
}, },
@@ -755,7 +758,7 @@ schema.methods.startQuest = async function startQuest (user) {
} }
// update the remaining users // update the remaining users
promises.push(User.update({ promises.push(User.updateMany({
_id: { $in: nonUserQuestMembers }, _id: { $in: nonUserQuestMembers },
}, { }, {
$set: { $set: {
@@ -763,16 +766,15 @@ schema.methods.startQuest = async function startQuest (user) {
'party.quest.progress.down': 0, 'party.quest.progress.down': 0,
'party.quest.completed': null, 'party.quest.completed': null,
}, },
}, { multi: true }).exec()); }).exec());
await Promise.all(promises); await Promise.all(promises);
// update the users who are not participating // update the users who are not participating
// Do not block updates // Do not block updates
User.update({ User.updateMany({
_id: { $in: nonMembers }, _id: { $in: nonMembers },
}, _cleanQuestParty(), }, _cleanQuestParty()).exec();
{ multi: true }).exec();
const newMessage = this.sendChat({ const newMessage = this.sendChat({
message: `\`${shared.i18n.t('chatQuestStarted', { questName: quest.text('en') }, 'en')}\``, message: `\`${shared.i18n.t('chatQuestStarted', { questName: quest.text('en') }, 'en')}\``,
@@ -903,7 +905,7 @@ function _getUserUpdateForQuestReward (itemToAward, allAwardedItems) {
async function _updateUserWithRetries (userId, updates, numTry = 1, query = {}) { async function _updateUserWithRetries (userId, updates, numTry = 1, query = {}) {
query._id = userId; query._id = userId;
try { try {
return await User.update(query, updates).exec(); return await User.updateOne(query, updates).exec();
} catch (err) { } catch (err) {
if (numTry < MAX_UPDATE_RETRIES) { if (numTry < MAX_UPDATE_RETRIES) {
numTry += 1; // eslint-disable-line no-param-reassign numTry += 1; // eslint-disable-line no-param-reassign
@@ -949,7 +951,7 @@ schema.methods.finishQuest = async function finishQuest (quest) {
this.markModified('quest'); this.markModified('quest');
if (this._id === TAVERN_ID) { if (this._id === TAVERN_ID) {
return User.update({}, updates, { multi: true }).exec(); return User.updateMany({}, updates).exec();
} }
const promises = participants.map(userId => { const promises = participants.map(userId => {
@@ -1389,10 +1391,10 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
const userUpdate = { $pull: { 'preferences.tasks.mirrorGroupTasks': group._id } }; const userUpdate = { $pull: { 'preferences.tasks.mirrorGroupTasks': group._id } };
if (group.type === 'guild') { if (group.type === 'guild') {
userUpdate.$pull.guilds = group._id; userUpdate.$pull.guilds = group._id;
promises.push(User.update({ _id: user._id }, userUpdate).exec()); promises.push(User.updateOne({ _id: user._id }, userUpdate).exec());
} else { } else {
userUpdate.$set = { party: {} }; userUpdate.$set = { party: {} };
promises.push(User.update({ _id: user._id }, userUpdate).exec()); promises.push(User.updateOne({ _id: user._id }, userUpdate).exec());
update.$unset = { [`quest.members.${user._id}`]: 1 }; update.$unset = { [`quest.members.${user._id}`]: 1 };
} }
@@ -1508,7 +1510,7 @@ schema.methods.unlinkTask = async function groupUnlinkTask (
const promises = [unlinkingTask.save()]; const promises = [unlinkingTask.save()];
if (keep === 'keep-all') { if (keep === 'keep-all') {
await Tasks.Task.update(findQuery, { await Tasks.Task.updateOne(findQuery, {
$set: { group: {} }, $set: { group: {} },
}).exec(); }).exec();

View File

@@ -392,6 +392,13 @@ schema.pre('update', function preUpdateUser () {
this.update({}, { $inc: { _v: 1 } }); this.update({}, { $inc: { _v: 1 } });
}); });
schema.pre('updateOne', function preUpdateUser () {
this.updateOne({}, { $inc: { _v: 1 } });
});
schema.pre('updateMany', function preUpdateUser () {
this.updateMany({}, { $inc: { _v: 1 } });
});
schema.post('save', function postSaveUser () { schema.post('save', function postSaveUser () {
// Send a webhook notification when the user has leveled up // Send a webhook notification when the user has leveled up
if (this._tmp && this._tmp.leveledUp && this._tmp.leveledUp.length > 0) { if (this._tmp && this._tmp.leveledUp && this._tmp.leveledUp.length > 0) {

View File

@@ -225,10 +225,9 @@ schema.statics.pushNotification = async function pushNotification (
throw validationResult; throw validationResult;
} }
await this.update( await this.updateMany(
query, query,
{ $push: { notifications: newNotification.toObject() } }, { $push: { notifications: newNotification.toObject() } },
{ multi: true },
).exec(); ).exec();
}; };
@@ -274,13 +273,12 @@ schema.statics.addAchievementUpdate = async function addAchievementUpdate (query
const validationResult = newNotification.validateSync(); const validationResult = newNotification.validateSync();
if (validationResult) throw validationResult; if (validationResult) throw validationResult;
await this.update( await this.updateMany(
query, query,
{ {
$push: { notifications: newNotification.toObject() }, $push: { notifications: newNotification.toObject() },
$set: { [`achievements.${achievement}`]: true }, $set: { [`achievements.${achievement}`]: true },
}, },
{ multi: true },
).exec(); ).exec();
}; };

View File

@@ -534,6 +534,7 @@ export default new Schema({
stickyHeader: { $type: Boolean, default: true }, stickyHeader: { $type: Boolean, default: true },
disableClasses: { $type: Boolean, default: false }, disableClasses: { $type: Boolean, default: false },
newTaskEdit: { $type: Boolean, default: false }, newTaskEdit: { $type: Boolean, default: false },
// not used anymore, now the current filter is saved in preferences.activeFilter
dailyDueDefaultView: { $type: Boolean, default: false }, dailyDueDefaultView: { $type: Boolean, default: false },
advancedCollapsed: { $type: Boolean, default: false }, advancedCollapsed: { $type: Boolean, default: false },
toolbarCollapsed: { $type: Boolean, default: false }, toolbarCollapsed: { $type: Boolean, default: false },
@@ -594,6 +595,12 @@ export default new Schema({
mirrorGroupTasks: [ mirrorGroupTasks: [
{ $type: String, validate: [v => validator.isUUID(v), 'Invalid group UUID.'], ref: 'Group' }, { $type: String, validate: [v => validator.isUUID(v), 'Invalid group UUID.'], ref: 'Group' },
], ],
activeFilter: {
habit: { $type: String, default: 'all' },
daily: { $type: String, default: 'all' },
todo: { $type: String, default: 'remaining' },
reward: { $type: String, default: 'all' },
},
}, },
improvementCategories: { improvementCategories: {
$type: Array, $type: Array,