Compare commits

..

240 Commits

Author SHA1 Message Date
Sabe Jones 3a0aa4a1dc 4.105.1 2019-07-25 15:07:40 -05:00
Sabe Jones 7ffecf9206 Merge branch 'release' into develop 2019-07-25 15:00:01 -05:00
Sabe Jones ea20bd5070 4.105.0 2019-07-25 14:58:42 -05:00
Sabe Jones c98d7aae37 chore(sprites): compile 2019-07-25 14:58:18 -05:00
Sabe Jones 21d27d223c feat(content): Mystery Items July 2019 2019-07-25 14:58:07 -05:00
Matteo Pagliazzi ea13aece21 fix better tasks color 2019-07-25 21:47:58 +02:00
Sabe Jones b0bb537501 Merge branch 'release' into develop 2019-07-22 16:38:07 -05:00
Sabe Jones 75783345aa 4.104.2 2019-07-22 16:37:48 -05:00
Matteo Pagliazzi 931e2565b6 Use mandrill for password resets and usernames in emails (#11277)
* use mandrill for password resets

* fix email id

* fix username in emails and tests

* fix tests

* fix package-lock
2019-07-22 19:44:01 +02:00
Tressley Cahill 2f6584378d New colors updates (#11261)
* Update $purple-400

* Update colors.scss

Adds new colors and updates existing variables.
2019-07-22 19:00:39 +02:00
Matteo Pagliazzi 519a580b7b fix quests migration 2019-07-17 22:04:25 +02:00
Sabe Jones c99d62228f Merge branch 'release' into develop 2019-07-17 14:20:54 -05:00
Sabe Jones 581a40ffad 4.104.1 2019-07-17 14:20:35 -05:00
Sabe Jones 61c78355bd feat(content): enable Splashy Pals Bundle 2019-07-17 14:20:21 -05:00
Matteo Pagliazzi f82c93a9ec restore default for migration runner and respect unsubscription preferences 2019-07-17 21:07:43 +02:00
Matteo Pagliazzi e8c6340af8 add migration to restore scrolls 2019-07-17 20:48:48 +02:00
Matteo Pagliazzi 1e66cc9193 fix 2019-07-17 15:11:58 +02:00
Matteo Pagliazzi ef6d835391 fix 2019-07-17 14:34:03 +02:00
Matteo Pagliazzi 51f7ce3280 possible fix for script stuck script 2019-07-17 13:34:08 +02:00
Sabe Jones 2b7891e788 WIP(migration): then 2019-07-17 06:05:38 -05:00
Sabe Jones c640a648b9 WIP(migration): refactor group fix script 2019-07-17 10:42:08 +00:00
Sabe Jones 116f0446e5 fix(migrations): don't default to mystery items 2019-07-16 22:55:59 -05:00
Sabe Jones 065e8303df fix(groups): migration to restore disrupted user data 2019-07-16 20:23:05 -05:00
negue 74d3c9366a fix inbox: remove old map-method (#11269) 2019-07-16 22:50:12 +02:00
negue c9a56e8f3e Performance/inbox paging (#11258)
* merge all changes to one commit of PR #11157 / #11226

* rename the new paged messages route

* rename event, move map-method to the Inbox schema, fix lint

* move `mapMessage`-call to `getUserInbox`

* revert schema.method back to a normal one
2019-07-15 11:25:22 +02:00
README Bot d28e1708c5 Add CodeTriage badge to habitrpg/habitrpg (#11205)
Adds a badge showing the number of people helping this repo on CodeTriage.

[![Open Source Helpers](https://www.codetriage.com/habitrpg/habitrpg/badges/users.svg)](https://www.codetriage.com/habitrpg/habitrpg)

## What is CodeTriage?

CodeTriage is an Open Source app that is designed to make contributing to Open Source projects easier. It works by sending subscribers a few open issues in their inbox. If subscribers get busy, there is an algorithm that backs off issue load so they do not get overwhelmed

[Read more about the CodeTriage project](https://www.codetriage.com/what).

## Why am I getting this PR?

Your project was picked by the human, @schneems. They selected it from the projects submitted to https://www.codetriage.com and hand edited the PR. How did your project get added to [CodeTriage](https://www.codetriage.com/what)? Roughly about 3 years ago, [joshsaintjacque](https://github.com/joshsaintjacque) added this project to CodeTriage in order to start contributing. Since then, 71 people have subscribed to help this repo.

## What does adding a badge accomplish?

Adding a badge invites people to help contribute to your project. It also lets developers know that others are invested in the longterm success and maintainability of the project.

You can see an example of a CodeTriage badge on these popular OSS READMEs:

- [![Email clients like GMAIL do not render SVG images](https://www.codetriage.com/rails/rails/badges/users.svg)](https://www.codetriage.com/rails/rails) https://github.com/rails/rails
- [![Email clients like GMAIL do not render SVG images](https://www.codetriage.com/crystal-lang/crystal/badges/users.svg)](https://www.codetriage.com/crystal-lang/crystal) https://github.com/crystal-lang/crystal

## Have a question or comment?

While I am a bot, this PR was manually reviewed and monitored by a human - @schneems. My job is writing commit messages and handling PR logistics.

If you have any questions, you can reply back to this PR and they will be answered by @schneems. If you do not want a badge right now, no worries, close the PR, you will not hear from me again.

Thanks for making your project Open Source! Any feedback is greatly appreciated.
2019-07-14 17:56:42 +02:00
Jack Cogdill e091072367 Enable keyboard shortcuts for autocomplete @username selection in chat (#11235)
* Chat autocomplete mouse hover styling

* Enable keyboard shortcuts for chat autocomplete

- Tab/down to select next
- Shift-tab/up to select previous
- Enter to make selection
- Escape to cancel
2019-07-14 17:55:50 +02:00
Alys 7b1032e898 improve appearance and wording in Hall's Reward User form for admins (#11248)
- adds Username
- changes placeholders to labels (subheadings) for consistency
- lists common and rare contributor titles to assist with keeping them consistent
- converts translated text to hard-coded English for consistency and to minimise work for translators
- removes unused locale strings
2019-07-12 17:14:34 +02:00
kogelnikp 855470ed25 changed order of attributes in equipment views (#11247) 2019-07-12 17:13:07 +02:00
Dominick Triola 631efd1adc Ask for confirmation when buying quests (#11251)
* Trigger confirmation modal for quests that have a currency of gems

https://github.com/HabitRPG/habitica/issues/11244

* Trigger purchase confirmation dialog for gold-purchasable quests

* Refactor to use confirmPurchase method
2019-07-12 17:09:49 +02:00
kogelnikp 2e227909a4 Fixed removing tasks from challenges (#11254)
* fixed missing event handler for removing tasks in challenges

* renamed test

* simplified challengeDetail test
2019-07-12 17:03:30 +02:00
greenkeeper[bot] e5c52625ad Update svg-url-loader to the latest version 🚀 (#11246)
* fix(package): update svg-url-loader to version 3.0.0

* chore(package): update lockfile package-lock.json
2019-07-12 16:35:27 +02:00
Scens 8f9f95fd84 Removed unnecessary "Equipment" sub-heading in Market. (#11237)
* Removed unnecessary sub-heading.

* Corrected the filter in the Market.
Now viewOptions keeps additional info about text to be in filter.
Categories array is no longer passed to filter component.
2019-07-12 16:23:14 +02:00
greenkeeper[bot] ff334d08e2 Update @google-cloud/trace-agent to the latest version 🚀 (#11208)
* fix(package): update @google-cloud/trace-agent to version 4.0.0

* chore(package): update lockfile package-lock.json
2019-07-12 16:12:49 +02:00
Frederik Ar. Mikkelsen 25b443a7c6 Fix task dropdown menu overflow (#11230) 2019-07-12 16:06:03 +02:00
reisturm 6724413ce9 Fixed styling bug on stats profile page. (#11187)
* Fixed styling bug on stats profile page.

* 24 px popover-content-titleonly
2019-07-12 16:02:33 +02:00
Forrest Hatfield 684a615a07 Allow admins to edit challenge tasks - fixes #8149 (#10658)
* Allow admins to edit challenges

* add test for admin creating a task in a challenge they don't own
2019-07-12 15:56:09 +02:00
Tressley Cahill babceaff42 Update "Add Task" button to diamond (#11214)
* Update $purple-400

* Add .diamond-btn

Swapped .rounded-btn for .diamond-btn
2019-07-12 15:36:14 +02:00
citrusella c29e9ddfc6 hes to nudibranchs in questsContent.json (#11211)
test
2019-07-09 16:30:13 +02:00
Jonathan Duncan 0cd0bcf874 Fixed typo. (#11219) 2019-07-09 16:28:12 +02:00
Alys 544a703df5 Minor fixes for apidocs and comments (#11249)
* improve comments for api.getUserChallenges

* clarify members parameter when creating test guilds

I was uncertain about this parameter and read the code to clarify it.
Reading a comment is easier. :)

* add "Custom Day Start time" to api route's title as it's the common name
2019-07-09 16:18:41 +02:00
greenkeeper[bot] 0b32692290 Update chromedriver to the latest version 🚀 (#11215)
* chore(package): update chromedriver to version 75.0.0

* chore(package): update lockfile package-lock.json
2019-07-09 12:16:50 +02:00
Sabe Jones 20fc233ad7 4.104.0 2019-07-02 13:30:46 -05:00
Sabe Jones adbe58a0da chore(sprites): compile 2019-07-02 13:30:41 -05:00
Sabe Jones b1b90d6dba feat(content): backgrounds and Armoire items 2019-07-02 13:30:33 -05:00
Sabe Jones ef29247526 4.103.1 2019-07-01 14:54:03 -05:00
Sabe Jones 1c59e78ea9 chore(sprites): compile 2019-07-01 14:53:50 -05:00
Sabe Jones 18e4e11a95 chore(news): Bailey
and extend a couple of items by 1 day
2019-07-01 14:53:40 -05:00
SabreCat 82f34ed437 v4.103.0 2019-06-25 15:19:00 +00:00
SabreCat 3b268287b1 chore(sprites): compile 2019-06-25 15:14:56 +00:00
SabreCat 0e253fbb06 feat(content): mystery items 2019-06-25 15:13:08 +00:00
Sabe Jones 043696c225 4.102.0 2019-06-20 15:13:55 -05:00
Sabe Jones 69c2b0378f chore(sprites): compile 2019-06-20 15:13:27 -05:00
Sabe Jones fcfc44fd13 feat(content): Watery Potions
also fixes some sprite issues
2019-06-20 15:13:19 -05:00
Sabe Jones 76773d27c6 4.101.1 2019-06-18 23:29:07 -05:00
Sabe Jones b3e81177ec fix(content): correct Mage weapon price, smarten Rebirth banner 2019-06-18 23:29:00 -05:00
Sabe Jones 020726f6f8 4.101.0 2019-06-18 20:05:15 -05:00
Sabe Jones 9f9b0c73c3 fix(event): 30 days hath July? uh, no 2019-06-18 19:49:55 -05:00
Sabe Jones 2c03c114da chore(sprites): compile 2019-06-18 19:20:23 -05:00
Sabe Jones 8e9500fed2 feat(event): Summer Splash 2019 2019-06-18 19:20:14 -05:00
Sabe Jones 377f849f9e Merge branch 'free-rebirth-limit' into release 2019-06-18 16:02:12 -05:00
Sabe Jones 8bbada540d feat(rebirth): add banner to buy modal 2019-06-18 15:24:50 -05:00
Matteo Pagliazzi ee867a9d30 add tests 2019-06-17 21:04:22 +02:00
Matteo Pagliazzi 14a4f42b50 indentation 2019-06-17 20:47:55 +02:00
Matteo Pagliazzi 2cbb0a85a4 limit free rebirth to once every 45 days 2019-06-17 20:37:18 +02:00
Sabe Jones 975e068fe2 4.100.2 2019-06-14 12:35:37 +00:00
Sabe Jones 5a5f1d6895 Revert "Performance: Inbox Paging / loading (#11157)"
This reverts commit 5630e8cc8e.
2019-06-14 12:34:38 +00:00
Sabe Jones 1db0cda6b1 4.100.1 2019-06-13 13:14:32 -05:00
Sabe Jones f987585cf1 fix(quests): content corrections 2019-06-13 13:13:55 -05:00
Sabe Jones 0325ae9636 fix(avatar): style correction 2019-06-13 13:10:06 -05:00
Sabe Jones 4c4fbfe790 fix(backgrounds): put errant BGs on 3x3 grid 2019-06-13 12:22:28 -05:00
Sabe Jones 6b59262e3e Challenge privacy fix (#11222)
* fix(challenges): filter out private content API-side

* fix(challenges): cleaner fix + test
2019-06-13 09:27:47 -05:00
negue 5630e8cc8e Performance: Inbox Paging / loading (#11157)
* load messages per conversation

* only sort ones in ui

*  add contributor to message

* fix correct message layout/message

* mugenScroll on chatMessages

* fix lint, no mugen-scroll, use own scroll handler

* fix height / margin of modal + use button to load more

* fix tests

* user data from inbox

* style "load earlier messages"

*  move mapMessage to the inbox api result / extract sentMessage of members-api-controller

* fix test back

* fix test

* keep last scroll position

* just set the Id of the returned message instead of all other properties

* fix add new messages (buttons were hidden) + load more

* item-mounted debounce to trigger the re-scrolling
2019-06-13 15:18:50 +02:00
Matteo Pagliazzi 5268bbb8a9 fix(subscription): reset lastReminderDate when creating a new subscription 2019-06-12 17:21:08 +02:00
Sabe Jones 34faf2fd50 4.100.0 2019-06-11 15:27:34 -05:00
Sabe Jones 896950dbf0 chore(sprites): compile 2019-06-11 15:26:50 -05:00
Sabe Jones 424c50f2a4 chore(news): Bailey
also direct mod contact form to HTTPS
2019-06-11 15:26:38 -05:00
Sabe Jones f235a64d96 Merge branch 'sabrecat/pet-achievements' into release 2019-06-11 15:12:34 -05:00
Sabe Jones 286abe334e fix(notifications): add missing enum 2019-06-11 14:50:17 -05:00
Sabe Jones 81333a3074 feat(notifications): alert user to achievements 2019-06-11 13:06:00 -05:00
Matteo Pagliazzi 3c31945524 Email subs reminders release (#11216)
* add subscriptions reminders emails

* add field to record the last sub reminder
2019-06-10 22:37:30 +02:00
Sabe Jones 103945f7c5 feat(content): Dolphin Pet Quest
and revise Mind Over Matter achievement
2019-06-10 14:42:38 -05:00
Sabe Jones b9d9a17aca 4.99.1 2019-06-06 15:54:18 -05:00
Sabe Jones a0b6f576d2 Merge branch 'release' into develop 2019-06-04 16:49:17 -05:00
Sabe Jones 30036963b1 4.99.0 2019-06-04 16:48:53 -05:00
Sabe Jones e1fe48bee4 chore(sprites): compile 2019-06-04 16:48:16 -05:00
Sabe Jones 77b19ffe97 feat(content): Armoire items and Backgrounds June 2019 2019-06-04 16:48:05 -05:00
Sabe Jones e9a5e084fc fix(tests): use baseline quest with fewer update calls 2019-06-04 16:09:01 -05:00
Sabe Jones 12250a93f1 feat(basic-auth): allow multiple auth pairs (#11204) 2019-06-04 15:52:25 -05:00
Sabe Jones 7094e75dd8 feat(achievements): new pet-related cheevos 2019-06-04 10:31:25 -05:00
Sabe Jones 74c93955f8 Merge branch 'release' into develop 2019-06-03 15:22:54 -05:00
Sabe Jones e4b2ef6599 4.98.1 2019-06-03 15:22:30 -05:00
Sabe Jones a26c2bce23 chore(date): Bailey and disable potions 2019-06-03 15:22:21 -05:00
Matteo Pagliazzi 6cd00897ed fix(deps): do not update minor changes 2019-06-02 12:19:11 +02:00
greenkeeper[bot] 3c442318d8 Update js2xmlparser to the latest version 🚀 (#11019)
* fix(package): update js2xmlparser to version 4.0.0

* chore(package): update lockfile package-lock.json

* fix package-lock

* fix js2xmlparser usage
2019-06-01 14:45:05 +02:00
greenkeeper[bot] fd3f7e2b0e Update pageres to the latest version 🚀 (#10981)
* fix(package): update pageres to version 5.0.0

* chore(package): update lockfile package-lock.json
2019-06-01 14:40:53 +02:00
Matteo Pagliazzi 6f0bd0b913 fix(package-lock.json): fix missing entry 2019-06-01 13:22:00 +02:00
greenkeeper[bot] 5b7621ceb3 fix(package): update merge-stream to version 2.0.0 (#11190) 2019-06-01 13:19:01 +02:00
greenkeeper[bot] b8b414cae5 Update chromedriver to the latest version 🚀 (#11116)
* chore(package): update chromedriver to version 73.0.0

* chore(package): update lockfile package-lock.json
2019-06-01 09:48:52 +02:00
Sabe Jones 783338f1f0 fix(strings): misformatted variables in pt_BR 2019-05-31 14:56:26 -05:00
HydeHunter2 fb6ca0c73b Add chevron buttons to sub-items in the menu (#11158)
* Add arrow buttons to sub-items

* Fix mistyped in width

* Add 'v-once' to never changing elements(icons)

* Add animation and ability to close tabs

* Fix typo in rotate

* Only 1 opened tab

* Add animation to dropdown elements

* Close menu after clicking tab

* Improved closing the menu after clicking on links

* Remove browser prefixes

* Add 'active' class to some tabs after clicking

* Return ability to open tabs from Desktop

* pin 'currency-tray' at top of menu

* Revert "pin 'currency-tray' at top of menu"

This reverts commit 12d4144a11.

* Fix display of 'Groups' sub-items
2019-05-31 12:51:29 +02:00
greenkeeper[bot] 5792f54aa0 Update validator to the latest version 🚀 (#11188)
* fix(package): update validator to version 11.0.0

* chore(package): update lockfile package-lock.json
2019-05-31 12:41:58 +02:00
greenkeeper[bot] 3c943c8f23 Update gulp-imagemin to the latest version 🚀 (#11195)
* fix(package): update gulp-imagemin to version 6.0.0

* chore(package): update lockfile package-lock.json
2019-05-31 12:28:38 +02:00
garayj 4b2fd60e47 FIX Guild suggestion incorrectly identifies account as new #11159 (#11197)
* Changed the suggested guild string to a translatable string and made changes so that the string only shows within 60 days of user profile creation.

* Removed whitespace.
2019-05-31 12:26:15 +02:00
Alexey Pyltsyn 75281ab638 Remove default color in Instagram icon (#11196) 2019-05-31 12:23:00 +02:00
greenkeeper[bot] d46c9967fb Update axios to the latest version 🚀 (#11198)
* fix(package): update axios to version 0.19.0

* chore(package): update lockfile package-lock.json
2019-05-31 12:19:53 +02:00
Sabe Jones 4132a39b90 4.98.0 2019-05-30 15:14:04 -05:00
Sabe Jones 36cc229dd2 Merge branch 'develop' into release 2019-05-30 15:13:57 -05:00
Sabe Jones ef0c11c2bd chore(sprites): compile 2019-05-30 15:13:35 -05:00
Sabe Jones 3a8312832c feat(content): new freebie glasses
Fixes #11171
2019-05-30 15:13:23 -05:00
Sabe Jones df17753bb6 Merge branch 'release' into develop 2019-05-28 15:48:51 -05:00
Sabe Jones c9b3c646eb 4.97.0 2019-05-28 15:48:07 -05:00
Sabe Jones cd67877ef3 chore(sprites): compile 2019-05-28 15:45:16 -05:00
Sabe Jones 251e1d45af feat(content): May 2019 subscriber set 2019-05-28 15:45:04 -05:00
Matteo Pagliazzi 1a97d69edd Merge pull request #10865 from ianoxley/navbar-a11y-alt-text
Add text alternatives for navbar items
2019-05-26 11:59:04 +02:00
Matteo Pagliazzi 7baa7427a0 Merge pull request #11178 from Alys/mute-ban-block-notification-messages
adjust error messages for muted and banned users, and system flagging error
2019-05-26 11:49:24 +02:00
Matteo Pagliazzi 270078a030 Merge pull request #11170 from HabitRPG/Yutsuten-party-chat-translations
Party Chat Translations (Continuation of #10019)
2019-05-26 11:45:12 +02:00
Alys bb2768071d change "all caps" to "all capital letters" for clarity
I found this question in the Help guild:
'I'm deleting my account and aren't able to find the "cap" to type in
"delete" in order to finish deleting process. Any ideas were that
"cap" is?'
2019-05-26 16:43:42 +10:00
Alys ec75de5a90 add swear words - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc 2019-05-26 16:25:33 +10:00
Alys b9b944ba29 replace similar messages about chat privileges removed with one generic one 2019-05-26 07:59:23 +10:00
Alys fa9553b371 removed mention of Transifex and added link to new Weblate site 2019-05-25 19:57:29 +10:00
Mateus Etto 8d5cbbe9be Docker setup for development (#11165)
* Basic docker setup for development

* Add missing environment variable to client

* Install gulp-cli into docker image for tests
2019-05-24 14:07:21 -05:00
Sabe Jones 3a339a4a09 Merge branch 'release' into develop 2019-05-23 13:41:14 -05:00
Sabe Jones 324b16b8cd 4.96.1 2019-05-23 13:41:00 -05:00
Sabe Jones 8c005aa05a fix(shops): remove outdated Featured Item
and Bailey
2019-05-23 13:40:06 -05:00
Sabe Jones 664cf5a47b Merge branch 'release' into develop 2019-05-21 15:29:40 -05:00
Sabe Jones 71926cff51 4.96.0 2019-05-21 15:29:22 -05:00
Matteo Pagliazzi e971f49c85 fix(test): fix stripe error message output 2019-05-21 15:17:57 -05:00
Sabe Jones 01657f573d feat(quests): create Hatching Potion category 2019-05-21 14:48:44 -05:00
Sabe Jones 95613dcfb8 Merge branch 'sabrecat/potion-quests' into release 2019-05-21 14:28:46 -05:00
Matteo Pagliazzi bcf304984d Task notes now disappear when they are deleted from the main task. Fix #11152 (#11167)
* Task notes now disappear when they are deleted from the main task.

* Html component changed back to Markdown. Markdown logic now accounts for if the value is an empty string.

* if-else statement to be sure that the markdown library doesn't create issues with empty strings.
2019-05-20 11:46:44 +02:00
Chris Pomerville 08d84ba691 hides item count in quest sidebar section for non-participants (#11183) 2019-05-20 11:33:47 +02:00
Matteo Pagliazzi 4007c26801 fix(test): fix stripe error message output 2019-05-20 11:31:20 +02:00
Alys 344c20fd99 improve error notification shown when a player is blocked
This improves the wording in the error notification that a player
sees when they've been blocked.

It also also changes markdown links to plain text because raw
markdown was being shown in some locations (for example when you saw
the error messages on the command line while using an API command).

These changes have been discussed with and approved by beffymaroo
and the other mods.
2019-05-18 21:38:38 +10:00
Alys f5c2c39f6a adjust error messages for muted users and system flagging error 2019-05-18 17:12:24 +10:00
Sabe Jones 0379f341b9 4.95.1 2019-05-16 14:07:57 -05:00
Sabe Jones 8498ab9fe2 chore(news): Bailey 2019-05-16 14:07:39 -05:00
Jose Garay bda3fb5f4d if-else statement to be sure that the markdown library doesn't create issues with empty strings. 2019-05-15 08:10:44 -07:00
Matteo Pagliazzi eff8db0afd fix issue with language being undefined, refactoring auth middleware, new tests 2019-05-15 16:54:55 +02:00
Matteo Pagliazzi 8cce38ede1 Merge branch 'party-chat-translations' of https://github.com/Yutsuten/habitica into Yutsuten-party-chat-translations 2019-05-15 15:20:40 +02:00
Mateus Etto ab0dae8df3 Translate messages only after serialization 2019-05-15 21:26:23 +09:00
Mateus Etto 0824af05b7 Remove unneeded parameter on method call 2019-05-15 19:45:42 +09:00
Mateus Etto 28bf024990 Move translateMessage to libs 2019-05-15 19:07:17 +09:00
Mateus Etto 8c59420d4e Fix quest cancel test 2019-05-15 17:28:05 +09:00
Mateus Etto 8a30ac0607 Add translation support for quest cancel 2019-05-15 16:55:37 +09:00
Mateus Etto e1984762b5 Fix some tests 2019-05-15 16:55:27 +09:00
Mateus Etto 0360326f41 Change translateSystemMessages to be a class method 2019-05-15 15:46:32 +09:00
Mateus Etto f3f215abea Fix typo on schema 2019-05-15 13:50:57 +09:00
Jose Garay feb98a5ac7 Html component changed back to Markdown. Markdown logic now accounts for if the value is an empty string. 2019-05-14 19:06:22 -07:00
Sabe Jones 95de11cf7a Merge branch 'release' into develop 2019-05-14 16:35:29 -05:00
Sabe Jones 5bb336cedc 4.95.0 2019-05-14 16:34:29 -05:00
Sabe Jones 790614a9d4 chore(sprites): compile 2019-05-14 16:33:51 -05:00
Sabe Jones 977968df19 feat(content): Sunshine Potions 2019-05-14 16:33:42 -05:00
HydeHunter2 3df056105c Remove markdown elements from PM (#11150)
* Remove markdown elements from PM

* Replacing limit number of characters to limit number of lines

* Add ellipsis
2019-05-14 22:14:10 +02:00
Sabe Jones 0eb7e10e7f fix(sprites): rebuild including Wolf 2019-05-14 13:22:45 -05:00
Alys 0908fa2a48 change Load Tools icon that mods and staff use (#11169)
The crown icon makes it clearer to us that it's for the special
mod/staff tools. The pencil icon is too similar to normal
edit/compose icons and we were clicking it by mistake, with
accidental double-clicks resulting in users being briefly banned by
mistake since the ban icon is directly "under" the Load Tools icon.

I've checked with the other mods and they feel that the crown will
avoid that problem.
2019-05-14 14:04:28 +02:00
Sabe Jones 71904d106f chore(sprites): compile 2019-05-13 11:02:19 -05:00
Sabe Jones 56040eebaf feat(content): Magic Hatching Potion Quest 2019-05-13 11:02:07 -05:00
Jose Garay 2094a4d4b8 Task notes now disappear when they are deleted from the main task. 2019-05-11 23:47:24 -07:00
Sabe Jones 2048f3ef76 fix(string): correct stat reference 2019-05-10 07:23:20 -05:00
Sabe Jones e9163a1bb2 fix(test): Feathered Friends date range 2019-05-09 14:53:55 -05:00
Sabe Jones 54fd910db2 fix(test): Feathered Friends date range 2019-05-09 14:53:38 -05:00
Sabe Jones 3c0cd7067a 4.94.1 2019-05-09 14:32:23 -05:00
Sabe Jones 5a473eb0e0 Merge branch 'develop' into release 2019-05-09 14:32:14 -05:00
Sabe Jones e6fcdf62ef fix(sprites): dojo resize fix 2019-05-09 14:31:28 -05:00
Sabe Jones 09e748bc92 feat(content): reenable Feathered Friends bundle 2019-05-09 14:27:22 -05:00
Sabe Jones 90532b0763 Merge branch 'develop' into Yutsuten/party-chat-translations 2019-05-08 15:13:53 -05:00
Matteo Pagliazzi 9151690f86 Better group plan and subscription cancellation (#11132)
* wip: better group plan cancellation

* add cancelation confirm modal

* abstract confirm modal for subs

* abstract canceled modal for subs

* working code

* add missing files

* fix text and margins

* fix(cancel modal): share css and add close icon
2019-05-08 21:37:02 +02:00
Sabe Jones c125ac4d93 fix(client): avoid TypeError in store state 2019-05-08 12:18:42 -05:00
Sabe Jones aa61c1fe06 Merge branch 'release' into develop 2019-05-07 15:57:44 -05:00
Sabe Jones c6a7ee3f56 4.94.0 2019-05-07 15:57:14 -05:00
Sabe Jones 411213f381 chore(sprites): compile 2019-05-07 15:57:02 -05:00
Sabe Jones b2dabcaf98 feat(content): Armoire items and backgrounds 5/19 2019-05-07 15:56:51 -05:00
chen 2f3927fcaa column-background should have 100% width (#11142) 2019-05-03 15:57:00 +02:00
negue f84562446d fix loading owned messages (#11147) 2019-05-03 15:53:35 +02:00
Sabe Jones 0a840ca952 4.93.4 2019-05-03 08:47:40 -05:00
HydeHunter2 c0837e3b3c Fix challenge update (#11148)
* Fix challenge update

Fix some problem in challenge update

* Fix test

* Move leader to noUpdate

* Move leader to noUpdate
2019-05-03 15:35:56 +02:00
HydeHunter2 95c1893b0c Fix challenge update (#11148)
* Fix challenge update

Fix some problem in challenge update

* Fix test

* Move leader to noUpdate

* Move leader to noUpdate
2019-05-03 15:35:12 +02:00
Sabe Jones d8ea3bd23a 4.93.3 2019-05-02 14:25:07 -05:00
Sabe Jones 0ce11b82df Merge branch 'develop' into release 2019-05-02 14:25:02 -05:00
Sabe Jones b4c47b4afd chore(sprites): compile 2019-05-02 14:24:39 -05:00
Sabe Jones 4777850601 chore(event): end Flinging 2019-05-02 14:24:29 -05:00
Sabe Jones 9a3e208c9b Merge branch 'release' into develop 2019-04-30 14:17:32 -05:00
Sabe Jones 792d5998b0 4.93.2 2019-04-30 14:17:14 -05:00
Sabe Jones 53515fd3f4 chore(news): Bailey 2019-04-30 14:17:05 -05:00
Sabe Jones 946147a4aa 4.93.1 2019-04-29 13:45:04 -05:00
Sabe Jones 983ea7c6c7 fix(strings): mixed-up armor notes 2019-04-29 13:44:52 -05:00
Sabe Jones 1135ab946e Sabrecat/groups quick wins (#11146)
* WIP(groups): quickish wins

* WIP(groups): two quick wins
1. Don't show task creation button if user is not leader or manager
2. Don't require JS confirm() for approving tasks

* fix(group-plans): allow delete from options button

* fix(group-plans): update tasksOrder when task deleted

* fix(group-tasks): dismiss notification when user takes action

* refactor(tasks): DRY out create button styling

* fix(group-tasks): sync after claiming/unclaiming

* fix(claiming): better sync and notif handling

* fix(tasks): force sync instead of explicitly clearing notif

* fix(tasks): reposition task creation button

* fix(group-tasks): default to single completion

* fix(group-tasks): move completion condition field above approval switch

* fix(group-tasks): todo validation error and approval notif dismissal

* fix(group-tasks): default single completion on client

* fix(group-tasks): move completion condition up more

* fix(group-tasks): maintain client-side user assignment list

* fix(group-tasks): remove approval notifications when task deleted

* fix(group-tasks): send assigned task to top of task list

* fix(group-tasks): remove useless tag filter dropdown

* feat(group-tasks): notify user of assigned task

* fix(group-tasks): don't allow approval of tasks w/ no approval request

* fix(tests): adjust expectations

* fix(group-tasks): more sensible action on assignment notif click
2019-04-29 13:38:28 -05:00
HydeHunter2 5a15c73fca Reload after rebirth (#11125)
* Add restart after rebirth

Page will be reloaded after purchasing "Orb of Rebirth"

* Remove restart after closing achievement

This reload is not needed, as the page now reloads immediately after purchasing "Orb of Rebirth"

* Move rebirth notification to modal

* Delete references to rebirth notification
2019-04-27 19:32:33 +02:00
HydeHunter2 3f99c14a37 Seasonal alert removed for items you own (#11135) 2019-04-27 19:28:35 +02:00
HydeHunter2 9e515d96c3 Remove reward button for non-leader/non-admin (#11136) 2019-04-27 19:26:02 +02:00
HydeHunter2 40e0017b17 Separate tags of different types (#11123)
Challenge, group and user tags are separated
2019-04-27 19:22:09 +02:00
HydeHunter2 251563690e Fix tag text overlapping (#11124)
* Fix word-wrapping in user

* Fix word-wrapping in taskModal
2019-04-27 19:21:05 +02:00
negue 83070e211d Inbox: Add API to list conversations (#11110)
* Add API to list inbox conversations

* fix test + add api doc

* use `.lean()`

* orderBy after the the grouped conversations are loaded

* fix ordering
2019-04-26 18:45:05 +02:00
Matteo Pagliazzi 3be075ad43 fix(tests): Items Utils > castItemVal fix fn call 2019-04-26 00:02:53 +02:00
Matteo Pagliazzi 043e0fb819 fix #9514: await user update client sid 2019-04-25 22:49:58 +02:00
Matteo Pagliazzi 9d473cc92e wip: fix setting (some) items values in the hall of heroes (#11133) 2019-04-25 22:41:43 +02:00
Ian Oxley 6fba71ea2c Add :focus styles to match :hover styles
Add :focus styles to the .habitica-menu-dropdown.
These match the existing :hover styles.
2019-03-28 20:38:17 +00:00
Ian Oxley 5755bfc952 Merge branch 'develop' into navbar-a11y-alt-text 2019-03-28 20:19:04 +00:00
Ian Oxley 08c6e8298c Fix linting errors
Remove parentheses and add trailing comma.
2018-11-24 17:12:12 +00:00
Ian Oxley 5a30b0cf1f Add aria-label to Habitica logo 2018-11-23 22:44:31 +00:00
Ian Oxley cf847cd1d8 Add aria-label to notifaction and user menus
Add aria-label to give a text equivalent for non-visual users.
2018-11-23 22:44:31 +00:00
Ian Oxley 5753d3e648 Improve a11y for the dropdown menu
Add role="button" to make the component report itself as a button.

Add tabindex so the menu toggle can receive keyboard focus.

Add keydown handlers for `<Enter>` and `<Space>` so the dropdown menu
toggle responds to keyboard input.

Set the aria-pressed attribute to true if the menu is open, or false if
it is closed.
2018-11-23 22:44:30 +00:00
Ian Oxley 4718e5e5ea Improve a11y for the sync links
Add `role="link"` so it shows up as a link in VoiceOver.

Add `tabindex="0" so it can receive keyboard focus, and a keyup handler
for the enter key so it will respond to `<Enter>` keypresses.

Add `aria-label="$t('sync')"` to add text for non-visual users.

Add aria-label to mobile sync icon link.
2018-11-23 22:44:06 +00:00
Ian Oxley 9a2dbace30 Add aria-label to Gems and Gold icons
Add `aria-label` attributes to the gems and gold icons in the menu.

Make the gems icon a link, with a href set to `#buy-gems`, which is the element
that contains the gems dialog.
2018-11-23 22:30:43 +00:00
Mateus Etto ef07abfd28 Merge branch 'develop' into party-chat-translations
# Conflicts:
#	test/api/unit/models/group.test.js
#	website/server/controllers/api-v3/user/spells.js
#	website/server/models/chat.js
#	website/server/models/group.js
2018-10-08 18:44:02 +09:00
Mateus Etto b28adc6b42 Merge branch 'develop' into party-chat-translations 2018-08-29 20:07:19 +09:00
Mateus Etto c814eabb29 Merge branch 'develop' into party-chat-translations 2018-07-21 16:06:38 +09:00
Mateus Etto 4dc19a8c60 Merge branch 'develop' into party-chat-translations 2018-07-12 06:28:44 +09:00
Mateus Etto 89f6a9b07e Merge branch 'develop' into party-chat-translations
# Conflicts:
#	test/api/unit/models/group.test.js
2018-06-20 20:24:38 +09:00
Mateus Etto a23926f34f Merge branch 'develop' into party-chat-translations 2018-05-19 10:21:54 +09:00
Mateus Etto 118198b594 Fixed tests that contains random data 2018-05-19 00:26:28 +09:00
Mateus Etto 97e80e2093 Implemented requested changes 2018-05-18 23:08:37 +09:00
Mateus Etto 2588befceb Merge branch 'develop' into party-chat-translations
# Conflicts:
#	website/server/models/group.js
2018-05-06 00:46:47 +09:00
Mateus Etto f09274225a Update chat schema: add 'info' field 2018-04-25 21:28:02 +09:00
Mateus Etto 03b66abe70 Merge branch 'develop' into party-chat-translations
# Conflicts:
#	website/server/controllers/api-v3/quests.js
#	website/server/controllers/api-v3/tasks/groups.js
#	website/server/controllers/api-v3/user/spells.js
#	website/server/models/group.js
2018-04-25 21:15:49 +09:00
Mateus Etto 9d41fb0252 Add tests + some small adjusts 2018-03-25 00:01:20 +09:00
Mateus Etto 012fa2f8ef Improve readability of translateSystemMessages function 2018-03-18 12:09:03 +09:00
Mateus Etto 00d8d9d0cc Merge branch 'develop' into party-chat-translations 2018-03-17 00:08:20 +09:00
Mateus Etto cc4772c75a Update test 2018-03-12 23:48:09 +09:00
Mateus Etto fabaaa6d92 Merge branch 'develop' into party-chat-translations 2018-03-12 22:35:13 +09:00
Mateus Etto 854696728a Fix message when boss don't attack 2018-03-12 21:50:23 +09:00
Mateus Etto 314926cc06 Hard coded messages now using i18n strings 2018-03-12 21:45:41 +09:00
Mateus Etto 68fa834946 spellName -> spell; grammar fix 2018-03-12 21:08:20 +09:00
Mateus Etto 09440d5adf Merge branch 'develop' into party-chat-translations 2018-03-02 19:28:41 +09:00
Mateus Etto 1724bfc553 Merge branch 'develop' into party-chat-translations
# Conflicts:
#	website/server/models/group.js
2018-02-28 19:42:19 +09:00
Mateus Etto 8304f99ecb Added comment explaining the new info object 2018-02-26 20:00:47 +09:00
Mateus Etto d2fcdf4493 Merge branch 'develop' into party-chat-translations
# Conflicts:
#	website/server/controllers/api-v3/user.js
2018-02-26 19:55:32 +09:00
Mateus Etto 649404ac6a Fix travis-ci error 2018-02-19 20:57:31 +09:00
Mateus Etto 774db2564f Moving some strings around 2018-02-19 20:17:03 +09:00
Mateus Etto 521a1e646d Missing message (user claim task) 2018-02-19 20:07:05 +09:00
Mateus Etto 2619ac37d9 Fix mistake on tavern_boss_rage_tired 2018-02-19 00:36:49 +09:00
Mateus Etto 2ce9b319a0 Target username instead of uuid 2018-02-19 00:24:30 +09:00
Mateus Etto 477c23dd67 Save username instead of uuid (no queries necessary anymore) 2018-02-19 00:18:29 +09:00
Mateus Etto 7af71d1457 Fix errors 2018-02-18 19:44:17 +09:00
Mateus Etto 3171550de2 Fix syntax problems found when running tests 2018-02-18 17:14:18 +09:00
Mateus Etto 6477801d3e Translation support for missing sendChat calls 2018-02-18 15:26:27 +09:00
Mateus Etto 14798ced82 Translation support for a lot of messages in party 2018-02-17 22:17:08 +09:00
Mateus Etto 34d37cefcc Boss damage messages translation support 2018-02-17 19:41:15 +09:00
Mateus Etto bd21933cea Quest started translation support 2018-02-17 17:15:14 +09:00
648 changed files with 40991 additions and 36243 deletions
+5
View File
@@ -13,3 +13,8 @@ Habitica uses [Trello](https://trello.com/b/EpoYEYod/habitica) to track feature
# Contributing Code
See [Contributing to Habitica](http://habitica.fandom.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29)
## Issue Triage [![Open Source Helpers](https://www.codetriage.com/habitrpg/habitica/badges/users.svg)](https://www.codetriage.com/habitrpg/habitica)
You can triage issues which may include reproducing bug reports or asking for vital information, such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to habitrpg on CodeTriage](https://www.codetriage.com/habitrpg/habitica).
+5 -18
View File
@@ -1,18 +1,5 @@
FROM node:10
# Install global packages
RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN cp config.json.example config.json
RUN npm install
# Create Build dir
RUN mkdir -p ./website/build
# Start Habitica
EXPOSE 3000
CMD ["npm", "start"]
FROM node:10
WORKDIR /code
COPY package*.json /code/
RUN npm install
RUN npm install -g gulp-cli mocha
+2 -8
View File
@@ -61,18 +61,12 @@
"SESSION_SECRET_IV": "12345678912345678912345678912345",
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
"SITE_HTTP_AUTH_ENABLED": "false",
"SITE_HTTP_AUTH_PASSWORD": "password",
"SITE_HTTP_AUTH_USERNAME": "admin",
"SITE_HTTP_AUTH_PASSWORDS": "password,wordpass,passkey",
"SITE_HTTP_AUTH_USERNAMES": "admin,tester,contributor",
"SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
"SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
"SLACK_URL": "https://hooks.slack.com/services/some-url",
"SMTP_HOST": "example.com",
"SMTP_PASS": "password",
"SMTP_PORT": 587,
"SMTP_SERVICE": "Gmail",
"SMTP_TLS": "true",
"SMTP_USER": "user@example.com",
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
"TEST_DB_URI": "mongodb://localhost/habitrpg_test",
+37 -6
View File
@@ -1,14 +1,45 @@
version: "3"
services:
client:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "run", "client:dev"]
depends_on:
- server
environment:
- NODE_ENV=development
- BASE_URL=http://server:3000
image: habitica
networks:
- habitica
ports:
- "8080:8080"
volumes:
- '.:/usr/src/habitrpg'
- .:/code
- /code/node_modules
server:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
- mongo
environment:
- NODE_ENV=development
- NODE_DB_URI=mongodb://mongo/habitrpg
image: habitica
networks:
- habitica
ports:
- "3000:3000"
volumes:
- '.:/usr/src/habitrpg'
- .:/code
- /code/node_modules
mongo:
image: mongo:3.4
networks:
- habitica
ports:
- "27017:27017"
networks:
habitica:
driver: bridge
+1 -1
View File
@@ -16,7 +16,7 @@ const IMG_DIST_PATH = 'website/client/assets/images/sprites/';
const CSS_DIST_PATH = 'website/client/assets/css/sprites/';
function checkForSpecialTreatment (name) {
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame/;
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame|^eyewear_special_\w+HalfMoon/;
return name.match(regex) || name === 'head_0';
}
@@ -0,0 +1,62 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190530_halfmoon_glasses';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
'items.gear.owned.eyewear_special_blackHalfMoon': true,
'items.gear.owned.eyewear_special_blueHalfMoon': true,
'items.gear.owned.eyewear_special_greenHalfMoon': true,
'items.gear.owned.eyewear_special_pinkHalfMoon': true,
'items.gear.owned.eyewear_special_redHalfMoon': true,
'items.gear.owned.eyewear_special_whiteHalfMoon': true,
'items.gear.owned.eyewear_special_yellowHalfMoon': true,
};
set.migration = MIGRATION_NAME;
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set}).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2019-05-01')},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
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
}
};
@@ -0,0 +1,59 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190618_summer_splash_orcas';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set;
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
set = { migration: MIGRATION_NAME };
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.pets.Orca-Base': 5 };
} else {
set = { migration: MIGRATION_NAME, 'items.mounts.Orca-Base': true };
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2019-05-18')},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
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
}
};
@@ -0,0 +1,84 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190716_groups_fix';
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
let backupUsers;
async function updateUser (user) {
count++;
let set = { migration: MIGRATION_NAME };
let addToSet;
const monkPromise = new Promise((resolve, reject) => {
backupUsers.findOne(
{ _id: user._id },
{ fields: { _id: 1, party: 1, guilds: 1 }},
).then(foundUserInBackup => {
resolve(foundUserInBackup);
}).catch(e => {
reject(e);
})
});
let backupUser = await monkPromise;
if (!backupUser) return;
if (!user.party._id) {
set.party = backupUser.party;
}
addToSet = { guilds: { $each: backupUser.guilds }};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return User.update({ _id: user._id }, { $set: set, $addToSet: addToSet }).exec();
}
module.exports = async function processUsers () {
const query = {
'auth.timestamps.loggedin': {$gt: new Date('2019-07-15')},
};
let backupDb = monk(CONNECTION_STRING);
const backupDbPromise = new Promise((resolve, reject) => {
backupDb.then(() => resolve()).catch((e) => reject(e));
});
await backupDbPromise;
console.log('Connected to backup db');
backupUsers = backupDb.get('users', { castIds: false });
const fields = {
_id: 1,
party: 1,
guilds: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
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
}
};
@@ -0,0 +1,88 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20190717_groups_fix_2';
import monk from 'monk';
import nconf from 'nconf';
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
import { model as User } from '../../../website/server/models/user';
import { sendTxn as sendTxnEmail } from '../../../website/server/libs/email';
import shared from '../../../website/common';
const questScrolls = shared.content.quests;
const progressCount = 1000;
let count = 0;
async function updateGroup (group) {
count++;
if (group && group.quest && group.quest.key && group.quest.leader) {
const quest = questScrolls[group.quest.key];
const leader = await User.findOne({_id: group.quest.leader}).exec();
if (leader && quest) {
await User.update({
_id: leader._id,
migration: {$ne: MIGRATION_NAME},
}, {
$set: {migration: MIGRATION_NAME},
$inc: {
balance: 1,
[`items.quests.${group.quest.key}`]: 1,
},
}).exec();
// unsubscribe from all is already checked by sendTxnEmail
if (leader.preferences && leader.preferences.emailNotifications && leader.preferences.emailNotifications.majorUpdates !== false) {
sendTxnEmail(leader, 'groups-outage');
}
}
}
if (count % progressCount === 0) console.warn(`${count} ${group._id}`);
}
module.exports = async function processUsers () {
const query = {
type: 'party'
};
let backupDb = monk(CONNECTION_STRING);
const backupDbPromise = new Promise((resolve, reject) => {
backupDb.then(() => resolve()).catch((e) => reject(e));
});
await backupDbPromise;
console.log('Connected to backup db');
const backupGroups = backupDb.get('groups', { castIds: false });
while (true) { // eslint-disable-line no-constant-condition
const groupsPromise = new Promise((resolve, reject) => {
backupGroups
.find(query, {
limit: 250,
sort: {_id: 1}
})
.then(foundGroupInBackup => {
resolve(foundGroupInBackup);
}).catch(e => {
reject(e);
});
});
const groups = await groupsPromise;
if (groups.length === 0) {
console.warn('All appropriate groups found and modified.');
console.warn(`\n${count} groups processed\n`);
break;
} else {
query._id = {
$gt: groups[groups.length - 1]._id,
};
}
await Promise.all(groups.map(updateGroup)); // eslint-disable-line no-await-in-loop
}
};
+1 -1
View File
@@ -17,7 +17,7 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./users/mystery-items.js');
const processUsers = require('');
processUsers()
.then(function success () {
process.exit(0);
+2 -2
View File
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
const MIGRATION_NAME = 'mystery_items_201904';
const MYSTERY_ITEMS = ['armor_mystery_201904', 'head_mystery_201904'];
const MIGRATION_NAME = 'mystery_items_201907';
const MYSTERY_ITEMS = ['head_mystery_201907', 'armor_mystery_201907', 'eyewear_mystery_201907'];
import { model as User } from '../../website/server/models/user';
import { model as UserNotification } from '../../website/server/models/userNotification';
+569 -685
View File
File diff suppressed because it is too large Load Diff
+10 -11
View File
@@ -1,10 +1,10 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.93.0",
"version": "4.105.1",
"main": "./website/server/index.js",
"dependencies": {
"@google-cloud/trace-agent": "^3.6.0",
"@google-cloud/trace-agent": "^4.0.0",
"@slack/client": "^3.8.1",
"accepts": "^1.3.5",
"amazon-payments": "^0.2.7",
@@ -14,7 +14,7 @@
"apn": "^2.2.0",
"autoprefixer": "^9.4.0",
"aws-sdk": "^2.432.0",
"axios": "^0.18.0",
"axios": "^0.19.0",
"axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
@@ -48,7 +48,7 @@
"got": "^9.0.0",
"gulp": "^4.0.0",
"gulp-babel": "^7.0.1",
"gulp-imagemin": "^5.0.3",
"gulp-imagemin": "^6.0.0",
"gulp-nodemon": "^2.4.1",
"gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0",
@@ -58,9 +58,9 @@
"in-app-purchase": "^1.11.3",
"intro.js": "^2.9.3",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
"js2xmlparser": "^4.0.0",
"lodash": "^4.17.10",
"merge-stream": "^1.0.0",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.22.1",
"moment-recur": "^1.0.7",
@@ -69,9 +69,8 @@
"nconf": "^0.10.0",
"node-gcm": "^1.0.2",
"node-sass": "^4.9.0",
"nodemailer": "^6.0.0",
"ora": "^3.2.0",
"pageres": "^4.1.1",
"pageres": "^5.1.0",
"passport": "^0.4.0",
"passport-facebook": "^2.0.0",
"passport-google-oauth20": "1.0.0",
@@ -89,7 +88,7 @@
"stripe": "^5.9.0",
"superagent": "^5.0.2",
"svg-inline-loader": "^0.8.0",
"svg-url-loader": "^2.3.2",
"svg-url-loader": "^3.0.0",
"svgo": "^1.2.0",
"svgo-loader": "^2.1.0",
"universal-analytics": "^0.4.17",
@@ -98,7 +97,7 @@
"url-loader": "^1.0.0",
"useragent": "^2.1.9",
"uuid": "^3.0.1",
"validator": "^10.5.0",
"validator": "^11.0.0",
"vinyl-buffer": "^1.0.1",
"vue": "^2.6.10",
"vue-loader": "^14.2.2",
@@ -152,7 +151,7 @@
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chalk": "^2.4.1",
"chromedriver": "^2.40.0",
"chromedriver": "^75.0.0",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.3",
"cross-spawn": "^6.0.5",
+4 -42
View File
@@ -1,9 +1,7 @@
/* eslint-disable global-require */
import got from 'got';
import nconf from 'nconf';
import nodemailer from 'nodemailer';
import requireAgain from 'require-again';
import logger from '../../../../website/server/libs/logger';
import { TAVERN_ID } from '../../../../website/server/models/group';
import { defer } from '../../../helpers/api-unit.helper';
@@ -35,42 +33,6 @@ function getUser () {
describe('emails', () => {
let pathToEmailLib = '../../../../website/server/libs/email';
describe('sendEmail', () => {
let sendMailSpy;
beforeEach(() => {
sendMailSpy = sandbox.stub().returns(defer().promise);
sandbox.stub(nodemailer, 'createTransport').returns({
sendMail: sendMailSpy,
});
});
afterEach(() => {
sandbox.restore();
});
it('can send an email using the default transport', () => {
let attachEmail = requireAgain(pathToEmailLib);
attachEmail.send();
expect(sendMailSpy).to.be.calledOnce;
});
it('logs errors', (done) => {
sandbox.stub(logger, 'error');
let attachEmail = requireAgain(pathToEmailLib);
attachEmail.send();
expect(sendMailSpy).to.be.calledOnce;
defer().reject();
// wait for unhandledRejection event to fire
setTimeout(() => {
expect(logger.error).to.be.calledOnce;
done();
}, 20);
});
});
describe('getUserInfo', () => {
it('returns an empty object if no field request', () => {
let attachEmail = requireAgain(pathToEmailLib);
@@ -84,7 +46,7 @@ describe('emails', () => {
let user = getUser();
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.profile.name);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('email', user.auth.local.email);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
@@ -95,11 +57,11 @@ describe('emails', () => {
let getUserInfo = attachEmail.getUserInfo;
let user = getUser();
delete user.profile.name;
delete user.auth.local;
delete user.auth.local.email;
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.profile.name);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
@@ -114,7 +76,7 @@ describe('emails', () => {
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.profile.name);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).not.to.have.property('email');
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
+46
View File
@@ -2,6 +2,7 @@
import {
validateItemPath,
getDefaultOwnedGear,
castItemVal,
} from '../../../../../website/server/libs/items/utils';
describe('Items Utils', () => {
@@ -64,4 +65,49 @@ describe('Items Utils', () => {
expect(validateItemPath('items.quests.invalid')).to.equal(false);
});
});
describe('castItemVal', () => {
it('returns the item val untouched if not an item path', () => {
expect(castItemVal('notitems.gear.owned.item', 'a string')).to.equal('a string');
});
it('returns the item val untouched if an unsupported path', () => {
expect(castItemVal('items.gear.equipped.weapon', 'a string')).to.equal('a string');
expect(castItemVal('items.currentPet', 'a string')).to.equal('a string');
expect(castItemVal('items.special.snowball', 'a string')).to.equal('a string');
});
it('converts values for pets paths to numbers', () => {
expect(castItemVal('items.pets.Wolf-CottonCandyPink', '5')).to.equal(5);
expect(castItemVal('items.pets.Wolf-Invalid', '5')).to.equal(5);
});
it('converts values for eggs paths to numbers', () => {
expect(castItemVal('items.eggs.LionCub', '5')).to.equal(5);
expect(castItemVal('items.eggs.Armadillo', '5')).to.equal(5);
expect(castItemVal('items.eggs.NotAnArmadillo', '5')).to.equal(5);
});
it('converts values for hatching potions paths to numbers', () => {
expect(castItemVal('items.hatchingPotions.Base', '5')).to.equal(5);
expect(castItemVal('items.hatchingPotions.StarryNight', '5')).to.equal(5);
expect(castItemVal('items.hatchingPotions.Invalid', '5')).to.equal(5);
});
it('converts values for food paths to numbers', () => {
expect(castItemVal('items.food.Cake_Base', '5')).to.equal(5);
expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5);
});
it('converts values for mounts paths to numbers', () => {
expect(castItemVal('items.mounts.Cactus-Base', '5')).to.equal(5);
expect(castItemVal('items.mounts.Aether-Invisible', '5')).to.equal(5);
expect(castItemVal('items.mounts.Aether-Invalid', '5')).to.equal(5);
});
it('converts values for quests paths to numbers', () => {
expect(castItemVal('items.quests.atom3', '5')).to.equal(5);
expect(castItemVal('items.quests.invalid', '5')).to.equal(5);
});
});
});
@@ -16,6 +16,7 @@ describe('payments/index', () => {
beforeEach(async () => {
user = new User();
user.profile.name = 'sender';
user.auth.local.username = 'sender';
await user.save();
group = generateGroup({
+26 -3
View File
@@ -16,7 +16,7 @@ describe('auth middleware', () => {
describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', (done) => {
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
userFieldsToExclude: ['items'],
});
req.headers['x-api-user'] = user._id;
@@ -27,11 +27,34 @@ describe('auth middleware', () => {
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.flags).to.not.exist;
expect(userToJSON.auth.timestamps).to.not.exist;
expect(userToJSON.auth).to.exist;
done();
});
});
it('makes sure some fields are always included', (done) => {
const authWithHeaders = authWithHeadersFactory({
userFieldsToExclude: [
'items', 'auth.timestamps',
'preferences', 'notifications', '_id', 'flags', 'auth', // these are always loaded
],
});
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user.apiToken;
authWithHeaders(req, res, (err) => {
if (err) return done(err);
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.auth.timestamps).to.exist;
expect(userToJSON.auth).to.exist;
expect(userToJSON.notifications).to.exist;
expect(userToJSON.preferences).to.exist;
expect(userToJSON._id).to.exist;
expect(userToJSON.flags).to.exist;
done();
});
+368 -35
View File
@@ -1,7 +1,7 @@
import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import validator from 'validator';
import { sleep } from '../../../helpers/api-unit.helper';
import { sleep, translationCheck } from '../../../helpers/api-unit.helper';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
@@ -271,7 +271,16 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member attacks Wailing Whale for 5.0 damage.` `Wailing Whale attacks party for 7.5 damage.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member attacks Wailing Whale for 5.0 damage. Wailing Whale attacks party for 7.5 damage.`',
info: {
bossDamage: '7.5',
quest: 'whale',
type: 'boss_damage',
user: 'Participating Member',
userDamage: '5.0',
},
});
});
it('applies damage only to participating members of party', async () => {
@@ -344,7 +353,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledTwice;
expect(Group.prototype.sendChat).to.be.calledWith('`You defeated Wailing Whale! Questing party members receive the rewards of victory.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`You defeated Wailing Whale! Questing party members receive the rewards of victory.`',
info: { quest: 'whale', type: 'boss_defeated' },
});
});
it('calls finishQuest when boss has <= 0 hp', async () => {
@@ -387,7 +399,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en'));
expect(Group.prototype.sendChat).to.be.calledWith({
message: quest.boss.rage.effect('en'),
info: { quest: 'trex_undead', type: 'boss_rage' },
});
expect(party.quest.progress.hp).to.eql(383.5);
expect(party.quest.progress.rage).to.eql(0);
});
@@ -437,7 +452,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en'));
expect(Group.prototype.sendChat).to.be.calledWith({
message: quest.boss.rage.effect('en'),
info: { quest: 'lostMasterclasser4', type: 'boss_rage' },
});
expect(party.quest.progress.rage).to.eql(0);
let drainedUser = await User.findById(participatingMember._id);
@@ -488,7 +506,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 5 Bars of Soap.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member found 5 Bars of Soap.`',
info: {
items: { soapBars: 5 },
quest: 'atom1',
type: 'user_found_items',
user: 'Participating Member',
},
});
});
it('sends a chat message if no progress is made', async () => {
@@ -499,7 +525,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 0 Bars of Soap.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member found 0 Bars of Soap.`',
info: {
items: { soapBars: 0 },
quest: 'atom1',
type: 'user_found_items',
user: 'Participating Member',
},
});
});
it('sends a chat message if no progress is made on quest with multiple items', async () => {
@@ -516,9 +550,15 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/);
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Blue Fins/);
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Fire Coral/);
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`Participating Member found 0 Fire Coral, 0 Blue Fins.`',
info: {
items: { blueFins: 0, fireCoral: 0 },
quest: 'dilatoryDistress1',
type: 'user_found_items',
user: 'Participating Member',
},
});
});
it('handles collection quests with multiple items', async () => {
@@ -535,8 +575,14 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/);
expect(Group.prototype.sendChat).to.be.calledWithMatch(/\d* (Tracks|Broken Twigs)/);
expect(Group.prototype.sendChat).to.be.calledWithMatch({
message: sinon.match(/`Participating Member found/).and(sinon.match(/\d* (Tracks|Broken Twigs)/)),
info: {
quest: 'evilsanta2',
type: 'user_found_items',
user: 'Participating Member',
},
});
});
it('sends message about victory', async () => {
@@ -547,7 +593,10 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(Group.prototype.sendChat).to.be.calledTwice;
expect(Group.prototype.sendChat).to.be.calledWith('`All items found! Party has received their rewards.`');
expect(Group.prototype.sendChat).to.be.calledWith({
message: '`All items found! Party has received their rewards.`',
info: { type: 'all_items_found' },
});
});
it('calls finishQuest when all items are found', async () => {
@@ -718,6 +767,258 @@ describe('Group Model', () => {
expect(res.t).to.not.be.called;
});
});
describe('translateSystemMessages', () => {
it('translate quest_start', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_damage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_damage',
user: questLeader.profile.name,
quest: 'basilist',
userDamage: 15.3,
bossDamage: 3.7,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_dont_attack', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_dont_attack',
user: questLeader.profile.name,
quest: 'basilist',
userDamage: 15.3,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_rage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_rage',
quest: 'lostMasterclasser3',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate boss_defeated', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'boss_defeated',
quest: 'lostMasterclasser3',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate user_found_items', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'user_found_items',
user: questLeader.profile.name,
quest: 'lostMasterclasser1',
items: {
ancientTome: 3,
forbiddenTome: 2,
hiddenTome: 1,
},
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate all_items_found', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'all_items_found',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate spell_cast_party', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'spell_cast_party',
user: questLeader.profile.name,
class: 'wizard',
spell: 'earth',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate spell_cast_user', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'spell_cast_user',
user: questLeader.profile.name,
class: 'special',
spell: 'snowball',
target: participatingMember.profile.name,
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate quest_cancel', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_cancel',
user: questLeader.profile.name,
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate quest_abort', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'quest_abort',
user: questLeader.profile.name,
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_quest_completed', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_quest_completed',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_rage_tired', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_rage_tired',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_rage', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_rage',
quest: 'dysheartener',
scene: 'market',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate tavern_boss_desperation', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'tavern_boss_desperation',
quest: 'stressbeast',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
it('translate claim_task', async () => {
questLeader.preferences.language = 'en';
party.chat = [{
info: {
type: 'claim_task',
user: questLeader.profile.name,
task: 'Feed the pet',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
translationCheck(toJSON.chat[0].text);
});
});
describe('toJSONCleanChat', () => {
it('shows messages with 1 flag to non-admins', async () => {
party.chat = [{
flagCount: 1,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
expect(toJSON.chat.length).to.equal(1);
});
it('shows messages with >= 2 flag to admins', async () => {
party.chat = [{
flagCount: 3,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
const admin = new User({'contributor.admin': true});
let toJSON = await Group.toJSONCleanChat(party, admin);
expect(toJSON.chat.length).to.equal(1);
});
it('doesn\'t show flagged messages to non-admins', async () => {
party.chat = [{
flagCount: 3,
info: {
type: 'quest_start',
quest: 'basilist',
},
}];
let toJSON = await Group.toJSONCleanChat(party, questLeader);
expect(toJSON.chat.length).to.equal(0);
});
});
});
context('Instance Methods', () => {
@@ -1007,20 +1308,22 @@ describe('Group Model', () => {
});
it('formats message', () => {
const chatMessage = party.sendChat('a new message', {
_id: 'user-id',
profile: { name: 'user name' },
contributor: {
toObject () {
return 'contributor object';
const chatMessage = party.sendChat({
message: 'a new message', user: {
_id: 'user-id',
profile: { name: 'user name' },
contributor: {
toObject () {
return 'contributor object';
},
},
},
backer: {
toObject () {
return 'backer object';
backer: {
toObject () {
return 'backer object';
},
},
},
});
}}
);
const chat = chatMessage;
@@ -1037,7 +1340,7 @@ describe('Group Model', () => {
});
it('formats message as system if no user is passed in', () => {
const chat = party.sendChat('a system message');
const chat = party.sendChat({message: 'a system message'});
expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true);
@@ -1052,7 +1355,7 @@ describe('Group Model', () => {
});
it('updates users about new messages in party', () => {
party.sendChat('message');
party.sendChat({message: 'message'});
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
@@ -1066,7 +1369,7 @@ describe('Group Model', () => {
type: 'guild',
});
group.sendChat('message');
group.sendChat({message: 'message'});
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
@@ -1076,7 +1379,7 @@ describe('Group Model', () => {
});
it('does not send update to user that sent the message', () => {
party.sendChat('message', {_id: 'user-id', profile: { name: 'user' }});
party.sendChat({message: 'message', user: {_id: 'user-id', profile: { name: 'user' }}});
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
@@ -1088,7 +1391,7 @@ describe('Group Model', () => {
it('skips sending new message notification for guilds with > 5000 members', () => {
party.memberCount = 5001;
party.sendChat('message');
party.sendChat({message: 'message'});
expect(User.update).to.not.be.called;
});
@@ -1096,7 +1399,7 @@ describe('Group Model', () => {
it('skips sending messages to the tavern', () => {
party._id = TAVERN_ID;
party.sendChat('message');
party.sendChat({message: 'message'});
expect(User.update).to.not.be.called;
});
@@ -1431,7 +1734,7 @@ describe('Group Model', () => {
let quest;
beforeEach(() => {
quest = questScrolls.whale;
quest = questScrolls.armadillo;
party.quest.key = quest.key;
party.quest.active = false;
party.quest.leader = questLeader._id;
@@ -1579,6 +1882,36 @@ describe('Group Model', () => {
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
it('gives out other pet-related quest achievements', async () => {
quest = questScrolls.rock;
party.quest.key = quest.key;
questLeader.achievements.quests = {
mayhemMistiflying1: 1,
yarn: 1,
mayhemMistiflying2: 1,
egg: 1,
mayhemMistiflying3: 1,
slime: 2,
};
await questLeader.save();
await party.finishQuest(quest);
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.mindOverMatter).to.eql(true);
expect(updatedParticipatingMember.achievements.mindOverMatter).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.mindOverMatter).to.not.eql(true);
});
it('gives xp and gold', async () => {
await party.finishQuest(quest);
@@ -1715,13 +2048,13 @@ describe('Group Model', () => {
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
expect(questLeader.party.quest.completed).to.eql('whale');
expect(questLeader.party.quest.completed).to.eql('armadillo');
expect(questLeader.party.quest.progress.up).to.eql(10);
expect(questLeader.party.quest.progress.down).to.eql(8);
expect(questLeader.party.quest.progress.collectedItems).to.eql(5);
expect(questLeader.party.quest.RSVPNeeded).to.eql(false);
expect(participatingMember.party.quest.completed).to.eql('whale');
expect(participatingMember.party.quest.completed).to.eql('armadillo');
expect(participatingMember.party.quest.progress.up).to.eql(10);
expect(participatingMember.party.quest.progress.down).to.eql(8);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
@@ -1928,7 +2261,7 @@ describe('Group Model', () => {
await guild.save();
const groupMessage = guild.sendChat('Test message.');
const groupMessage = guild.sendChat({message: 'Test message.'});
await groupMessage.save();
await sleep();
@@ -171,7 +171,7 @@ describe('GET challenges/user', () => {
});
});
it('should return not return challenges in user groups if we send member true param', async () => {
it('should not return challenges in user groups if we send member true param', async () => {
let challenges = await member.get(`/challenges/user?member=${true}`);
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
@@ -214,6 +214,28 @@ describe('GET challenges/user', () => {
let foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
it('should not return challenges user doesn\'t have access to, even with query parameters', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
let privateChallenge = await generateChallenge(groupLeader, group, {categories: [{
name: 'academics',
slug: 'academics',
}]});
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
let challenges = await nonMember.get('/challenges/user?categories=academics&owned=not_owned');
let foundChallenge = _.find(challenges, { _id: privateChallenge._id });
expect(foundChallenge).to.not.exist;
});
});
context('official challenge is present', () => {
@@ -56,11 +56,11 @@ describe('PUT /challenges/:challengeId', () => {
tasksOrder: 'new order',
official: true,
shortName: 'new short name',
leader: member._id,
// applied
name: 'New Challenge Name',
description: 'New challenge description.',
leader: member._id,
});
expect(res.prize).to.equal(0);
@@ -76,12 +76,12 @@ describe('PUT /challenges/:challengeId', () => {
expect(res.shortName).not.to.equal('new short name');
expect(res.leader).to.eql({
_id: member._id,
id: member._id,
profile: {name: member.profile.name},
_id: user._id,
id: user._id,
profile: {name: user.profile.name},
auth: {
local: {
username: member.auth.local.username,
username: user.auth.local.username,
},
},
flags: {
@@ -149,7 +149,7 @@ describe('POST /group', () => {
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotCreatePublicGuildWhenMuted'),
message: t('chatPrivilegesRevoked'),
});
});
});
@@ -100,7 +100,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
message: t('chatPrivilegesRevoked'),
});
});
@@ -262,7 +262,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
message: t('chatPrivilegesRevoked'),
});
});
@@ -436,7 +436,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
message: t('chatPrivilegesRevoked'),
});
});
@@ -526,7 +526,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotInviteWhenMuted'),
message: t('chatPrivilegesRevoked'),
});
});
@@ -6,7 +6,7 @@ describe('GET /inbox/messages', () => {
let user;
let otherUser;
before(async () => {
beforeEach(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
@@ -60,4 +60,10 @@ describe('GET /inbox/messages', () => {
expect(messages.length).to.equal(4);
});
it('returns only the messages of one conversation', async () => {
const messages = await user.get(`/inbox/messages?conversation=${otherUser.id}`);
expect(messages.length).to.equal(3);
});
});
@@ -13,10 +13,10 @@ describe('payments - stripe - #checkout', () => {
});
it('verifies credentials', async () => {
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.eql({
await expect(user.post(endpoint, {id: 123})).to.eventually.be.rejected.and.include({
code: 401,
error: 'Error',
message: 'Invalid API Key provided: ****************************1111',
message: 'Invalid API Key provided: aaaabbbb********************1111',
});
});
@@ -127,7 +127,13 @@ describe('POST /groups/:groupId/quests/abort', () => {
members: {},
});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/);
expect(Group.prototype.sendChat).to.be.calledWithMatch({
message: sinon.match(/aborted the party quest Wail of the Whale.`/),
info: {
quest: 'whale',
type: 'quest_abort',
},
});
stub.restore();
});
@@ -141,7 +141,14 @@ describe('POST /groups/:groupId/quests/cancel', () => {
members: {},
});
expect(Group.prototype.sendChat).to.be.calledOnce;
expect(Group.prototype.sendChat).to.be.calledWithMatch(/cancelled the party quest Wail of the Whale.`/);
expect(Group.prototype.sendChat).to.be.calledWithMatch({
message: sinon.match(/cancelled the party quest Wail of the Whale.`/),
info: {
quest: 'whale',
type: 'quest_cancel',
user: sinon.match.any,
},
});
stub.restore();
});
@@ -54,6 +54,21 @@ describe('POST /tasks/challenge/:challengeId', () => {
expect(tasksOrder.habits).to.include(task.id);
});
it('allows non-leader admin to add tasks to a challenge when not a member', async () => {
const admin = await generateUser({'contributor.admin': true});
let task = await admin.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit from admin',
type: 'habit',
up: false,
down: true,
notes: 1976,
});
let {tasksOrder} = await user.get(`/challenges/${challenge._id}`);
expect(tasksOrder.habits).to.include(task.id);
});
it('returns error when user tries to create task with a alias', async () => {
await expect(user.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
@@ -63,6 +63,38 @@ describe('Groups DELETE /tasks/:id', () => {
});
});
it('removes deleted taskʾs approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await user.put(`/tasks/${task._id}/`, {
requiresApproval: true,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
await member2.del(`/tasks/${task._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(member2.notifications.length).to.equal(1);
});
it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`);
@@ -53,18 +53,29 @@ describe('POST /tasks/:id/approve/:userId', () => {
it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications.length).to.equal(3);
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
@@ -77,18 +88,28 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications.length).to.equal(3);
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[2].type).to.equal('SCORED_TASK');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
@@ -132,6 +153,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
@@ -141,6 +172,17 @@ describe('POST /tasks/:id/approve/:userId', () => {
});
});
it('prevents approving a task if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
it('completes master task when single-completion task is approved', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
@@ -151,6 +193,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
@@ -172,6 +224,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let member2Tasks = await member2.get('/tasks/user');
@@ -193,6 +255,16 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
@@ -214,6 +286,25 @@ describe('POST /tasks/:id/approve/:userId', () => {
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let member2Tasks = await member2.get('/tasks/user');
let member2SyncedTask = find(member2Tasks, findAssignedTask);
await expect(member2.post(`/tasks/${member2SyncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
@@ -51,7 +51,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
});
it('marks as task as needing more work', async () => {
it('marks a task as needing more work', async () => {
const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign/${member._id}`);
@@ -77,7 +77,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct
expect(member.notifications.length).to.equal(initialNotifications + 1);
expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -131,7 +131,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
expect(member.notifications.length).to.equal(initialNotifications + 1);
expect(member.notifications.length).to.equal(initialNotifications + 2);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
@@ -167,6 +167,17 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
@@ -129,6 +129,13 @@ describe('POST /tasks/:id/score/:direction', () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
@@ -113,6 +113,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
expect(syncedTask).to.exist;
});
it('sends a notification to assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await member.sync();
let groupTask = await user.get(`/tasks/group/${guild._id}`);
expect(member.notifications.length).to.equal(1);
expect(member.notifications[0].type).to.equal('GROUP_TASK_ASSIGNED');
expect(member.notifications[0].taskId).to.equal(groupTask._id);
});
it('assigns a task to multiple users', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
@@ -86,6 +86,13 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(syncedTask).to.not.exist;
});
it('removes task assignment notification from unassigned user', async () => {
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(0);
});
it('unassigns a user and only that user from a task', async () => {
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
@@ -44,7 +44,7 @@ describe('DELETE /inbox/messages/:messageId', () => {
});
it('deletes one message', async () => {
const messages = await user.get('/inbox/messages');
const messages = await user.get('/inbox/paged-messages');
expect(messages.length).to.equal(3);
@@ -53,10 +53,10 @@ describe('DELETE /inbox/messages/:messageId', () => {
expect(messages[2].text).to.equal('first');
await user.del(`/inbox/messages/${messages[1]._id}`);
const updatedMessages = await user.get('/inbox/messages');
const updatedMessages = await user.get('/inbox/paged-messages');
expect(updatedMessages.length).to.equal(2);
expect(updatedMessages[0].text).to.equal('third');
expect(updatedMessages[1].text).to.equal('first');
});
});
});
@@ -0,0 +1,91 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
describe('GET /inbox/conversations', () => {
let user;
let otherUser;
let thirdUser;
beforeEach(async () => {
[user, otherUser, thirdUser] = await Promise.all([generateUser(), generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await user.post('/members/send-private-message', {
toUserId: thirdUser.id,
message: 'third',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fifth',
});
});
it('returns the conversations', async () => {
const result = await user.get('/inbox/conversations');
expect(result.length).to.be.equal(3);
expect(result[0].user).to.be.equal(user.profile.name);
expect(result[0].username).to.be.equal(user.auth.local.username);
});
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
const messages = await user.get('/inbox/paged-messages');
expect(messages.length).to.equal(5);
// message to yourself
expect(messages[0].text).to.equal('fifth');
expect(messages[0].sent).to.equal(false);
expect(messages[0].uuid).to.equal(user._id);
expect(messages[1].text).to.equal('fourth');
expect(messages[2].text).to.equal('third');
expect(messages[3].text).to.equal('second');
expect(messages[4].text).to.equal('first');
});
it('returns four messages when using page-query ', async () => {
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
}));
}
await Promise.all(promises);
const messages = await user.get('/inbox/paged-messages?page=1');
expect(messages.length).to.equal(5);
});
it('returns only the messages of one conversation', async () => {
const messages = await user.get(`/inbox/paged-messages?conversation=${otherUser.id}`);
expect(messages.length).to.equal(3);
});
it('returns the correct message format', async () => {
const messages = await otherUser.get(`/inbox/paged-messages?conversation=${user.id}`);
expect(messages[0].toUUID).to.equal(user.id); // from user
expect(messages[1].toUUID).to.not.exist; // only filled if its from the chat partner
expect(messages[2].toUUID).to.equal(user.id); // from user
});
});
@@ -19,10 +19,10 @@ describe('POST /members/flag-private-message/:messageId', () => {
toUserId: receiver._id,
});
let senderMessages = await userToSendMessage.get('/inbox/messages');
let senderMessages = await userToSendMessage.get('/inbox/paged-messages');
let sendersMessageInSendersInbox = _.find(senderMessages, (message) => {
return message.uuid === receiver._id && message.text === messageToSend;
return message.toUUID === receiver._id && message.text === messageToSend;
});
expect(sendersMessageInSendersInbox).to.exist;
@@ -37,7 +37,7 @@ describe('POST /members/flag-private-message/:messageId', () => {
toUserId: receiver._id,
});
let receiversMessages = await receiver.get('/inbox/messages');
let receiversMessages = await receiver.get('/inbox/paged-messages');
let sendersMessageInReceiversInbox = _.find(receiversMessages, (message) => {
return message.uuid === userToSendMessage._id && message.text === messageToSend;
@@ -55,7 +55,7 @@ describe('POST /members/flag-private-message/:messageId', () => {
toUserId: receiver._id,
});
let receiversMessages = await receiver.get('/inbox/messages');
let receiversMessages = await receiver.get('/inbox/paged-messages');
let sendersMessageInReceiversInbox = _.find(receiversMessages, (message) => {
return message.uuid === userToSendMessage._id && message.text === messageToSend;
@@ -83,12 +83,12 @@ context('avatar.vue', () => {
expect(vm.paddingTop).to.equal('28px');
});
it('is 24.5px if user has a pet', () => {
it('is 24px if user has a pet', () => {
vm.member.items = {
currentPet: { name: 'Foo' },
};
expect(vm.paddingTop).to.equal('24.5px');
expect(vm.paddingTop).to.equal('24px');
});
it('is 0px if user has a mount', () => {
@@ -297,4 +297,4 @@ context('avatar.vue', () => {
});
});
});
});
});
@@ -0,0 +1,61 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import ChallengeDetailComponent from 'client/components/challenges/challengeDetail.vue';
import Store from 'client/libs/store';
const localVue = createLocalVue();
localVue.use(Store);
describe('Challenge Detail', () => {
let store;
let wrapper;
beforeEach(() => {
store = new Store({
state: {
user: {
data: {
contributor: {
admin: false,
},
challenges: [],
stats: {
},
flags: {},
preferences: {},
party: {
quest: {
},
},
},
},
},
actions: {
'members:getChallengeMembers': () => {},
'challenges:getChallenge': () => [
{_id: '1', group: { name: '', type: ''}, memberCount: 1, name: '', summary: '', description: '', leader: '', price: 1},
],
'tasks:getChallengeTasks': () => [
{_id: '1', type: 'habit'},
{_id: '2', type: 'daily'},
{_id: '3', type: 'reward'},
{_id: '4', type: 'todo'},
],
},
getters: {
},
});
wrapper = shallowMount(ChallengeDetailComponent, {
store,
localVue,
mocks: {
$t: (string) => string,
},
});
});
it('removes a destroyed task from task list', () => {
let taskToRemove = {_id: '1', type: 'habit'};
wrapper.vm.taskDestroyed(taskToRemove);
expect(wrapper.vm.tasksByType[taskToRemove.type].length).to.eq(0);
});
});
+7
View File
@@ -80,6 +80,13 @@ describe('shared.ops.buy', () => {
headAccessory_special_redHeadband: true,
headAccessory_special_whiteHeadband: true,
headAccessory_special_yellowHeadband: true,
eyewear_special_blackHalfMoon: true,
eyewear_special_blueHalfMoon: true,
eyewear_special_greenHalfMoon: true,
eyewear_special_pinkHalfMoon: true,
eyewear_special_redHalfMoon: true,
eyewear_special_whiteHalfMoon: true,
eyewear_special_yellowHalfMoon: true,
});
});
+7
View File
@@ -75,6 +75,13 @@ describe('shared.ops.buyMarketGear', () => {
headAccessory_special_redHeadband: true,
headAccessory_special_whiteHeadband: true,
headAccessory_special_yellowHeadband: true,
eyewear_special_blackHalfMoon: true,
eyewear_special_blueHalfMoon: true,
eyewear_special_greenHalfMoon: true,
eyewear_special_pinkHalfMoon: true,
eyewear_special_redHalfMoon: true,
eyewear_special_whiteHalfMoon: true,
eyewear_special_yellowHalfMoon: true,
});
expect(analytics.track).to.be.calledOnce;
});
+1 -1
View File
@@ -209,7 +209,7 @@ describe('shared.ops.purchase', () => {
it('purchases quest bundles', () => {
let startingBalance = user.balance;
let clock = sandbox.useFakeTimers(moment('2017-05-20').valueOf());
let clock = sandbox.useFakeTimers(moment('2019-05-20').valueOf());
let type = 'bundles';
let key = 'featheredFriends';
let price = 1.75;
+18
View File
@@ -169,6 +169,24 @@ describe('shared.ops.feed', () => {
expect(user.items.pets['Wolf-Base']).to.equal(7);
});
it('awards All Your Base achievement', () => {
user.items.pets['Wolf-Spooky'] = 5;
user.items.food.Milk = 2;
user.items.mounts = {
'Wolf-Base': true,
'TigerCub-Base': true,
'PandaCub-Base': true,
'LionCub-Base': true,
'Fox-Base': true,
'FlyingPig-Base': true,
'Dragon-Base': true,
'Cactus-Base': true,
'BearCub-Base': true,
};
feed(user, {params: {pet: 'Wolf-Spooky', food: 'Milk'}});
expect(user.achievements.allYourBase).to.eql(true);
});
it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50', () => {
user.items.pets['Wolf-Base'] = 49;
user.items.food.Milk = 2;
+18
View File
@@ -159,6 +159,24 @@ describe('shared.ops.hatch', () => {
expect(user.items.eggs).to.eql({Wolf: 0});
expect(user.items.hatchingPotions).to.eql({Base: 0});
});
it('awards Back to Basics achievement', () => {
user.items.pets = {
'Wolf-Base': 5,
'TigerCub-Base': 5,
'PandaCub-Base': 10,
'LionCub-Base': 5,
'Fox-Base': 5,
'FlyingPig-Base': 5,
'Dragon-Base': 5,
'Cactus-Base': 15,
'BearCub-Base': 5,
};
user.items.eggs = {Wolf: 1};
user.items.hatchingPotions = {Spooky: 1};
hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Spooky'}});
expect(user.achievements.backToBasics).to.eql(true);
});
});
});
});
+11
View File
@@ -49,6 +49,7 @@ describe('shared.ops.rebirth', () => {
let [, message] = rebirth(user);
expect(message).to.equal(i18n.t('rebirthComplete'));
expect(user.flags.lastFreeRebirth).to.exist;
});
it('rebirths a user with not enough gems but more than max level', () => {
@@ -60,6 +61,16 @@ describe('shared.ops.rebirth', () => {
expect(message).to.equal(i18n.t('rebirthComplete'));
});
it('rebirths a user using gems if over max level but rebirthed recently', () => {
user.stats.lvl = MAX_LEVEL + 1;
user.flags.lastFreeRebirth = new Date();
let [, message] = rebirth(user);
expect(message).to.equal(i18n.t('rebirthComplete'));
expect(user.balance).to.equal(0);
});
it('resets user\'s tasks values except for rewards to 0', () => {
tasks[0].value = 1;
tasks[1].value = 1;
@@ -86,7 +86,7 @@ export async function generateGroup (leader, details = {}, update = {}) {
// This is generate group + the ability to create
// real users to populate it. The settings object
// takes in:
// members: Number - the number of group members to create. Defaults to 0.
// members: Number - the number of group members to create. Defaults to 0. Does not include group leader.
// inivtes: Number - the number of users to create and invite to the group. Defaults to 0.
// groupDetails: Object - how to initialize the group
// leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user
@@ -80,7 +80,7 @@ export async function generateGroup (leader, details = {}, update = {}) {
// This is generate group + the ability to create
// real users to populate it. The settings object
// takes in:
// members: Number - the number of group members to create. Defaults to 0.
// members: Number - the number of group members to create. Defaults to 0. Does not include group leader.
// inivtes: Number - the number of users to create and invite to the group. Defaults to 0.
// groupDetails: Object - how to initialize the group
// leaderDetails: Object - defaults for the leader, defaults with a gem balance so the user
+1
View File
@@ -8,6 +8,7 @@ import mongo from './mongo'; // eslint-disable-line
import moment from 'moment';
import i18n from '../../website/common/script/i18n';
import * as Tasks from '../../website/server/models/task';
export { translationCheck } from './translate';
afterEach((done) => {
sandbox.restore();
+6
View File
@@ -16,3 +16,9 @@ export function translate (key, variables, language) {
return translatedString;
}
export function translationCheck (translatedString) {
expect(translatedString).to.not.be.empty;
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
}
+8 -1
View File
@@ -12,6 +12,8 @@ div
banned-account-modal
amazon-payments-modal(v-if='!isStaticPage')
payments-success-modal
sub-cancel-modal-confirm(v-if='isUserLoaded')
sub-canceled-modal(v-if='isUserLoaded')
snackbars
router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else)
@@ -55,6 +57,7 @@ div
display: flex;
flex-direction: column;
min-height: 100vh;
overflow-x: hidden;
}
#loading-screen-inapp {
@@ -87,7 +90,6 @@ div
}
.container-fluid {
overflow-x: hidden;
flex: 1 0 auto;
}
@@ -187,6 +189,8 @@ import notifications from 'client/mixins/notifications';
import { setup as setupPayments } from 'client/libs/payments';
import amazonPaymentsModal from 'client/components/payments/amazonModal';
import paymentsSuccessModal from 'client/components/payments/successModal';
import subCancelModalConfirm from 'client/components/payments/cancelModalConfirm';
import subCanceledModal from 'client/components/payments/canceledModal';
import spellsMixin from 'client/mixins/spells';
import { CONSTANTS, getLocalSetting, removeLocalSetting } from 'client/libs/userlocalManager';
@@ -210,6 +214,8 @@ export default {
amazonPaymentsModal,
bannedAccountModal,
paymentsSuccessModal,
subCancelModalConfirm,
subCanceledModal,
},
data () {
return {
@@ -650,5 +656,6 @@ export default {
<style src="assets/css/sprites/spritesmith-main-22.css"></style>
<style src="assets/css/sprites/spritesmith-main-23.css"></style>
<style src="assets/css/sprites/spritesmith-main-24.css"></style>
<style src="assets/css/sprites/spritesmith-main-25.css"></style>
<style src="assets/css/sprites.css"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
@@ -1,72 +1,78 @@
.promo_april_fools_2019 {
.promo_armoire_backgrounds_201907 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -840px;
background-position: -776px 0px;
width: 423px;
height: 147px;
}
.promo_armoire_backgrounds_201904 {
.promo_glass_watery_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -424px -840px;
background-position: -776px -148px;
width: 423px;
height: 147px;
}
.promo_butterflies {
.promo_mystery_201907 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 676px;
height: 676px;
}
.promo_celestial_rainbow_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -433px -677px;
width: 423px;
height: 147px;
}
.promo_classes_spring2019 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -677px;
width: 432px;
height: 162px;
}
.promo_egg_hunt {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px 0px;
width: 354px;
height: 147px;
}
.promo_mystery_201904 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px -444px;
background-position: -776px -444px;
width: 282px;
height: 144px;
}
.promo_orcas {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -376px -286px;
width: 219px;
height: 147px;
}
.promo_seasonalshop_spring {
.promo_seafoam {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px -592px;
background-position: -313px -473px;
width: 425px;
height: 148px;
}
.promo_seasonal_shop {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -596px -286px;
width: 162px;
height: 138px;
height: 132px;
}
.promo_shiny_seeds {
.promo_splashy_pals_bundle {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px -296px;
width: 351px;
background-position: -776px -296px;
width: 423px;
height: 147px;
}
.promo_spring_avatar_customizations {
.promo_splashy_skins {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1005px -148px;
width: 354px;
height: 147px;
background-position: 0px -286px;
width: 375px;
height: 186px;
}
.customize-option.promo_splashy_skins {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -25px -301px;
width: 60px;
height: 60px;
}
.promo_summer_splash_2019 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -367px 0px;
width: 408px;
height: 186px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1168px -592px;
background-position: -1059px -444px;
width: 96px;
height: 69px;
}
.scene_yesterdailies_repeatables {
.scene_casting_spells {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -677px 0px;
width: 327px;
height: 276px;
background-position: 0px -473px;
width: 312px;
height: 222px;
}
.scene_tools {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 366px;
height: 285px;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 570 KiB

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Some files were not shown because too many files have changed in this diff Show More