From 581271e9303166d4c27ee72d73778ae3d0c51dd4 Mon Sep 17 00:00:00 2001 From: Natalie <78037386+CuriousMagpie@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:24:56 -0400 Subject: [PATCH 1/2] Reporting challenges (#14756) * initial commit * update logic to display flagged challenges properly to users and admins * add report button to pages 'My Challenges' and 'Discover Challenges' * allow mods to view flagged messages on challengeDetail view * update showing flagged challenges for group challenges * update showing flagged challenges for a specific challenge * disallow closing a flagged challenge * update notes to reflect apiParams properly * fix css spacing * update challenge en locales * fix spacing * update title of closeChallengeModal * let user know flagged challenges cannot be cloned * fix linting errors * ensure flagged challenges cannot be declared with a winner and cloned via API * define a non user challenge properly * fix logic to check for a nonParticipant and nonLeader user when grabbing flagged challenges * fix linting of max character of 100 / line * remove reporting on 'my challenges' and 'discover challenges' * WIP(challenges): disable clone button and add notes to new functions * WIP(challenges): smol changes * WIP(challenges): clone button only disabled for admin and flagged user; other users can still clone but the flag goes along with the clone * WIP(challenges): stop flags carrying over on cloned challenges * WIP(challenges): typo fixing, undoing a smol change * fix(challenges): improved query logic for flags * WIP(challenges): more smol changes * fix(challenges): refactor queries * fix(challenges): correct My Challenges tab logic * WIP(challenges): fix clone button state * WIP(challenges): really fixed clone button & clear flags from clones * WIP(challenge): implement new design for reporting modal * WIP(challenge): making things pretty * WIP(challenge): conquering the close button * WIP(challenge): fixin some spacing * WIP(challenge): smol fix * WIP(challenge): making sure the button is actually disabled * WIP(challenge): fix blockquote css * fix(tests): no private guilds * fix(lint): curlies etc * fix(test): moderator permission * fix(lint): sure man whatever * fix(lint): bad vim no tabby * fix(test): permissions not contrib lol * fix(challenges): add icon and fix leaky CSS * fix(challenge): correct clone button behavior --------- Co-authored-by: Julius Jung Co-authored-by: SabreCat Co-authored-by: Sabe Jones --- .../challenges/POST-challenge_flag.test.js | 62 ++++ .../POST-challenge_flag_clear.test.js | 58 ++++ .../components/challenges/challengeDetail.vue | 91 +++++- .../components/challenges/challengeItem.vue | 1 - .../challenges/closeChallengeModal.vue | 66 +++-- .../challenges/reportChallengeModal.vue | 273 ++++++++++++++++++ .../client/src/store/actions/challenges.js | 12 + website/common/locales/en/challenge.json | 16 +- website/common/locales/en/groups.json | 2 +- .../server/controllers/api-v3/challenges.js | 179 ++++++++++-- website/server/libs/challenges/index.js | 5 + website/server/libs/challenges/reporting.js | 82 ++++++ website/server/libs/slack.js | 36 +++ website/server/models/challenge.js | 2 + 14 files changed, 820 insertions(+), 65 deletions(-) create mode 100644 test/api/v3/integration/challenges/POST-challenge_flag.test.js create mode 100644 test/api/v3/integration/challenges/POST-challenge_flag_clear.test.js create mode 100644 website/client/src/components/challenges/reportChallengeModal.vue create mode 100644 website/server/libs/challenges/reporting.js diff --git a/test/api/v3/integration/challenges/POST-challenge_flag.test.js b/test/api/v3/integration/challenges/POST-challenge_flag.test.js new file mode 100644 index 00000000000..0f5f353b6c5 --- /dev/null +++ b/test/api/v3/integration/challenges/POST-challenge_flag.test.js @@ -0,0 +1,62 @@ +import { v4 as generateUUID } from 'uuid'; +import { + generateChallenge, + createAndPopulateGroup, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /challenges/:challengeId/flag', () => { + let user; + let challenge; + + beforeEach(async () => { + const { group, groupLeader } = await createAndPopulateGroup({ + groupDetails: { + name: 'TestParty', + type: 'party', + privacy: 'private', + }, + members: 1, + }); + + user = groupLeader; + + challenge = await generateChallenge(user, group); + }); + + it('returns an error when challenge is not found', async () => { + await expect(user.post(`/challenges/${generateUUID()}/flag`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('challengeNotFound'), + }); + }); + + it('flags a challenge', async () => { + const flagResult = await user.post(`/challenges/${challenge._id}/flag`); + + expect(flagResult.challenge.flags[user._id]).to.equal(true); + expect(flagResult.challenge.flagCount).to.equal(1); + }); + + it('flags a challenge with a higher count when from an admin', async () => { + await user.update({ 'contributor.admin': true }); + + const flagResult = await user.post(`/challenges/${challenge._id}/flag`); + + expect(flagResult.challenge.flags[user._id]).to.equal(true); + expect(flagResult.challenge.flagCount).to.equal(5); + }); + + it('returns an error when user tries to flag a challenge that is already flagged', async () => { + await user.post(`/challenges/${challenge._id}/flag`); + + await expect(user.post(`/challenges/${challenge._id}/flag`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('messageChallengeFlagAlreadyReported'), + }); + }); +}); diff --git a/test/api/v3/integration/challenges/POST-challenge_flag_clear.test.js b/test/api/v3/integration/challenges/POST-challenge_flag_clear.test.js new file mode 100644 index 00000000000..b1cfaaf6674 --- /dev/null +++ b/test/api/v3/integration/challenges/POST-challenge_flag_clear.test.js @@ -0,0 +1,58 @@ +import { v4 as generateUUID } from 'uuid'; +import { + generateChallenge, + createAndPopulateGroup, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /challenges/:challengeId/clearflags', () => { + let admin; + let nonAdmin; + let challenge; + + beforeEach(async () => { + const { group, groupLeader, members } = await createAndPopulateGroup({ + groupDetails: { + name: 'TestParty', + type: 'party', + privacy: 'private', + }, + members: 1, + }); + + admin = groupLeader; + [nonAdmin] = members; + + await admin.update({ 'permissions.moderator': true }); + + challenge = await generateChallenge(admin, group); + await admin.post(`/challenges/${challenge._id}/flag`); + }); + + it('returns error when non-admin attempts to clear flags', async () => { + await expect(nonAdmin.post(`/challenges/${generateUUID()}/clearflags`)) + .to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('messageGroupChatAdminClearFlagCount'), + }); + }); + + it('returns an error when challenge is not found', async () => { + await expect(admin.post(`/challenges/${generateUUID()}/clearflags`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('challengeNotFound'), + }); + }); + + it('clears flags and sets mod flag to false', async () => { + await nonAdmin.post(`/challenges/${challenge._id}/flag`); + const flagResult = await admin.post(`/challenges/${challenge._id}/clearflags`); + + expect(flagResult.challenge.flagCount).to.eql(0); + expect(flagResult.challenge.flags).to.have.property(admin._id, false); + expect(flagResult.challenge.flags).to.have.property(nonAdmin._id, true); + }); +}); diff --git a/website/client/src/components/challenges/challengeDetail.vue b/website/client/src/components/challenges/challengeDetail.vue index b86a2c28de6..67c437c390a 100644 --- a/website/client/src/components/challenges/challengeDetail.vue +++ b/website/client/src/components/challenges/challengeDetail.vue @@ -1,5 +1,6 @@