mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
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 <me@matchajune.io> Co-authored-by: SabreCat <sabe@habitica.com> Co-authored-by: Sabe Jones <sabrecat@gmail.com>
This commit is contained in:
@@ -20,7 +20,6 @@ import csvStringify from '../../libs/csvStringify';
|
||||
import {
|
||||
createTasks,
|
||||
} from '../../libs/tasks';
|
||||
|
||||
import {
|
||||
addUserJoinChallengeNotification,
|
||||
getChallengeGroupResponse,
|
||||
@@ -29,8 +28,12 @@ import {
|
||||
createChallengeQuery,
|
||||
} from '../../libs/challenges';
|
||||
import apiError from '../../libs/apiError';
|
||||
|
||||
import common from '../../../common';
|
||||
import {
|
||||
clearFlags,
|
||||
flagChallenge,
|
||||
notifyOfFlaggedChallenge,
|
||||
} from '../../libs/challenges/reporting';
|
||||
|
||||
const { MAX_SUMMARY_SIZE_FOR_CHALLENGES } = common.constants;
|
||||
|
||||
@@ -366,7 +369,7 @@ api.leaveChallenge = {
|
||||
* @apiParam (Query) {Number} page This parameter can be used to specify the page number
|
||||
for the user challenges result (the initial page is number 0).
|
||||
* @apiParam (Query) {String} [member] If set to `true` it limits results to challenges where the
|
||||
user is a member.
|
||||
user is a member, or the user owns the challenge.
|
||||
* @apiParam (Query) {String} [owned] If set to `owned` it limits results to challenges owned
|
||||
by the user. If set to `not_owned` it limits results
|
||||
to challenges not owned by the user.
|
||||
@@ -394,10 +397,30 @@ api.getUserChallenges = {
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const CHALLENGES_PER_PAGE = 10;
|
||||
const { page } = req.query;
|
||||
|
||||
const {
|
||||
categories,
|
||||
member,
|
||||
owned,
|
||||
page,
|
||||
search,
|
||||
} = req.query;
|
||||
const { user } = res.locals;
|
||||
|
||||
const query = {
|
||||
$and: [],
|
||||
};
|
||||
|
||||
if (!user.hasPermission('moderator')) {
|
||||
query.$and.push(
|
||||
{
|
||||
$or: [
|
||||
{ flagCount: { $not: { $gt: 1 } } },
|
||||
{ leader: user._id },
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Challenges the user owns
|
||||
const orOptions = [{ leader: user._id }];
|
||||
|
||||
@@ -407,7 +430,7 @@ api.getUserChallenges = {
|
||||
}
|
||||
|
||||
// Challenges in groups user is a member of, plus public challenges
|
||||
if (!req.query.member) {
|
||||
if (!member) {
|
||||
const userGroups = await Group.getGroups({
|
||||
user,
|
||||
types: ['party', 'guilds', 'tavern'],
|
||||
@@ -417,33 +440,29 @@ api.getUserChallenges = {
|
||||
group: { $in: userGroupIds },
|
||||
});
|
||||
}
|
||||
|
||||
const query = {
|
||||
$and: [{ $or: orOptions }],
|
||||
};
|
||||
|
||||
const { owned } = req.query;
|
||||
if (owned) {
|
||||
if (owned === 'not_owned') {
|
||||
query.$and.push({ leader: { $ne: user._id } });
|
||||
}
|
||||
|
||||
if (owned === 'owned') {
|
||||
query.$and.push({ leader: user._id });
|
||||
}
|
||||
if (owned === 'not_owned') {
|
||||
query.leader = { $ne: user._id }; // Show only Challenges user does not own
|
||||
} else if (owned === 'owned') {
|
||||
query.leader = user._id; // Show only Challenges user owns
|
||||
} else {
|
||||
orOptions.push(
|
||||
{ leader: user._id }, // Additionally show Challenges user owns
|
||||
);
|
||||
}
|
||||
|
||||
if (req.query.search) {
|
||||
query.$and.push({ $or: orOptions });
|
||||
|
||||
if (search) {
|
||||
const searchOr = { $or: [] };
|
||||
const searchWords = _.escapeRegExp(req.query.search).split(' ').join('|');
|
||||
const searchWords = _.escapeRegExp(search).split(' ').join('|');
|
||||
const searchQuery = { $regex: new RegExp(`${searchWords}`, 'i') };
|
||||
searchOr.$or.push({ name: searchQuery });
|
||||
searchOr.$or.push({ description: searchQuery });
|
||||
query.$and.push(searchOr);
|
||||
}
|
||||
|
||||
if (req.query.categories) {
|
||||
const categorySlugs = req.query.categories.split(',');
|
||||
if (categories) {
|
||||
const categorySlugs = categories.split(',');
|
||||
query.categories = { $elemMatch: { slug: { $in: categorySlugs } } };
|
||||
}
|
||||
|
||||
@@ -502,7 +521,7 @@ api.getGroupChallenges = {
|
||||
url: '/challenges/groups/:groupId',
|
||||
middlewares: [authWithHeaders({
|
||||
// Some fields (including _id) are always loaded (see middlewares/auth)
|
||||
userFieldsToInclude: ['party', 'guilds'], // Some fields are always loaded (see middlewares/auth)
|
||||
userFieldsToInclude: ['party', 'guilds', 'contributor'], // Some fields are always loaded (see middlewares/auth)
|
||||
})],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
@@ -524,7 +543,18 @@ api.getGroupChallenges = {
|
||||
// .populate('leader', nameFields)
|
||||
.exec();
|
||||
|
||||
const resChals = challenges.map(challenge => (new Challenge(challenge)).toJSON());
|
||||
const resChals = challenges.map(challenge => {
|
||||
// filter out challenges that the non-admin user isn't participating in, nor created
|
||||
const nonParticipant = !user.challenges
|
||||
|| (user.challenges
|
||||
&& user.challenges.findIndex(cId => cId === challenge._id) === -1);
|
||||
const isFlaggedForNonAdminUser = challenge.flagCount > 1
|
||||
&& !user.hasPermission('moderator')
|
||||
&& nonParticipant
|
||||
&& challenge.leader !== user._id;
|
||||
|
||||
return isFlaggedForNonAdminUser ? null : (new Challenge(challenge)).toJSON();
|
||||
}).filter(challenge => !!challenge);
|
||||
|
||||
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||
await Promise.all(resChals.map((chal, index) => User
|
||||
@@ -573,6 +603,15 @@ api.getChallenge = {
|
||||
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
const nonParticipant = !user.challenges
|
||||
|| (user.challenges
|
||||
&& user.challenges.findIndex(cId => cId === challenge._id) === -1);
|
||||
const isFlaggedForNonAdminUser = challenge.flagCount > 1
|
||||
&& !user.hasPermission('moderator')
|
||||
&& nonParticipant
|
||||
&& challenge.leader !== user._id;
|
||||
if (isFlaggedForNonAdminUser) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
// Fetching basic group data
|
||||
const group = await Group.getGroup({
|
||||
user, groupId: challenge.group, fields: `${basicGroupFields} purchased`,
|
||||
@@ -828,6 +867,15 @@ api.selectChallengeWinner = {
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
if (!challenge.canModify(user)) throw new NotAuthorized(res.t('onlyLeaderDeleteChal'));
|
||||
|
||||
const nonParticipant = !user.challenges
|
||||
|| (user.challenges
|
||||
&& user.challenges.findIndex(cId => cId === challenge._id) === -1);
|
||||
const isFlaggedForNonAdminUser = challenge.flagCount > 1
|
||||
&& !user.hasPermission('moderator')
|
||||
&& nonParticipant
|
||||
&& challenge.leader !== user._id;
|
||||
if (isFlaggedForNonAdminUser) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
const winner = await User.findOne({ _id: req.params.winnerId }).exec();
|
||||
if (!winner || winner.challenges.indexOf(challenge._id) === -1) throw new NotFound(res.t('winnerNotFound', { userId: req.params.winnerId }));
|
||||
|
||||
@@ -877,6 +925,15 @@ api.cloneChallenge = {
|
||||
const challengeToClone = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challengeToClone) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
const nonParticipant = !user.challenges
|
||||
|| (user.challenges
|
||||
&& user.challenges.findIndex(cId => cId === challengeToClone._id) === -1);
|
||||
const isFlaggedForNonAdminUser = challengeToClone.flagCount > 1
|
||||
&& !user.hasPermission('moderator')
|
||||
&& nonParticipant
|
||||
&& challengeToClone.leader !== user._id;
|
||||
if (isFlaggedForNonAdminUser) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
const { savedChal } = await createChallenge(user, req, res);
|
||||
|
||||
const challengeTaskIds = [
|
||||
@@ -910,4 +967,74 @@ api.cloneChallenge = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/challenges/:challengeId/flag Flag a challenge
|
||||
* @apiName FlagChallenge
|
||||
* @apiGroup Challenge
|
||||
*
|
||||
* @apiParam (Path) {UUID} challengeId The _id for the challenge to flag
|
||||
* @apiParam (Body) {String} [comment] Why the message was flagged
|
||||
*
|
||||
* @apiSuccess {Object} data The flagged challenge message
|
||||
*
|
||||
* @apiUse ChallengeNotFound
|
||||
*/
|
||||
api.flagChallenge = {
|
||||
method: 'POST',
|
||||
url: '/challenges/:challengeId/flag',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const challenge = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
await flagChallenge(challenge, user, res);
|
||||
await notifyOfFlaggedChallenge(challenge, user, req.body.comment);
|
||||
|
||||
res.respond(200, { challenge });
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/challenges/:challengeId/clearflags Clears flags on a challenge
|
||||
* @apiName ClearFlagsChallenge
|
||||
* @apiGroup Challenge
|
||||
*
|
||||
* @apiParam (Path) {UUID} challengeId The _id for the challenge to clear flags from
|
||||
*
|
||||
* @apiSuccess {Object} data The flagged challenge message
|
||||
*
|
||||
* @apiUse ChallengeNotFound
|
||||
*/
|
||||
api.clearFlagsChallenge = {
|
||||
method: 'POST',
|
||||
url: '/challenges/:challengeId/clearflags',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
if (!user.hasPermission('moderator')) {
|
||||
throw new NotAuthorized(res.t('messageGroupChatAdminClearFlagCount'));
|
||||
}
|
||||
|
||||
const challenge = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
|
||||
await clearFlags(challenge, user);
|
||||
|
||||
res.respond(200, { challenge });
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
Reference in New Issue
Block a user