fix: allow actions according to user role and privileges (#260)

* fix: allow actions according to user role and privileges

* test: fix failed test case for has_moderation_privileges change
This commit is contained in:
Awais Ansari
2022-08-24 12:31:31 +05:00
committed by GitHub
parent d1dcf20312
commit 45c596b770
22 changed files with 134 additions and 63 deletions

View File

@@ -257,7 +257,7 @@ describe('CommentsView', () => {
async function setupCourseConfig(reasonCodesEnabled = true) {
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
user_is_privileged: true,
has_moderation_privileges: true,
reason_codes_enabled: reasonCodesEnabled,
editReasons: [
{ code: 'reason-1', label: 'reason 1' },

View File

@@ -13,7 +13,11 @@ import { TinyMCEEditor } from '../../../components';
import FormikErrorFeedback from '../../../components/FormikErrorFeedback';
import PostPreviewPane from '../../../components/PostPreviewPane';
import { useDispatchWithState } from '../../../data/hooks';
import { selectModerationSettings, selectUserIsPrivileged } from '../../data/selectors';
import {
selectModerationSettings,
selectUserHasModerationPrivileges,
selectUserIsGroupTa,
} from '../../data/selectors';
import { formikCompatibleHandler, isFormikFieldInvalid } from '../../utils';
import { addComment, editComment } from '../data/thunks';
import messages from '../messages';
@@ -26,11 +30,12 @@ function CommentEditor({
}) {
const editorRef = useRef(null);
const { authenticatedUser } = useContext(AppContext);
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
const [submitting, dispatch] = useDispatchWithState();
const canDisplayEditReason = (reasonCodesEnabled && userIsPrivileged
const canDisplayEditReason = (reasonCodesEnabled && (userHasModerationPrivileges || userIsGroupTa)
&& edit && comment.author !== authenticatedUser.username
);

View File

@@ -10,7 +10,7 @@ import { Error } from '@edx/paragon/icons';
import { commentShape } from '../comments/comment/proptypes';
import messages from '../comments/messages';
import { selectModerationSettings, selectUserIsPrivileged } from '../data/selectors';
import { selectModerationSettings, selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../data/selectors';
import { postShape } from '../posts/post/proptypes';
import AuthorLabel from './AuthorLabel';
@@ -18,18 +18,22 @@ function AlertBanner({
intl,
content,
}) {
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
const userIsContentAuthor = getAuthenticatedUser().username === content.author;
const canSeeLastEditOrClosedAlert = (userHasModerationPrivileges || userIsContentAuthor || userIsGroupTa);
const isReportedByCurrentUser = getAuthenticatedUser().username === content?.abuseFlaggedBy;
const canSeeReportedBanner = (userHasModerationPrivileges || userIsGroupTa || isReportedByCurrentUser);
return (
<>
{content.abuseFlagged && (
{content.abuseFlagged && canSeeReportedBanner && (
<Alert icon={Error} variant="danger" className="px-3 mb-2 py-10px shadow-none flex-fill">
{intl.formatMessage(messages.abuseFlaggedMessage)}
</Alert>
)}
{reasonCodesEnabled && (userIsPrivileged || userIsContentAuthor) && (
{reasonCodesEnabled && canSeeLastEditOrClosedAlert && (
<>
{content.lastEdit?.reason && (
<Alert variant="info" className="px-3 shadow-none mb-2 py-10px bg-light-200">
@@ -50,7 +54,7 @@ function AlertBanner({
<AuthorLabel author={content.closedBy} linkToProfile />
</span>
<span className="mx-1" />
{intl.formatMessage(messages.reason)}:&nbsp;{content.closeReason}
{content.closeReason && (`${intl.formatMessage(messages.reason)}: ${content.closeReason}`)}
</div>
</Alert>
)}

View File

@@ -82,7 +82,7 @@ describe.each([
});
store = initializeStore({
config: {
userIsPrivileged: true,
hasModerationPrivileges: true,
reasonCodesEnabled: true,
},
});

View File

@@ -76,7 +76,7 @@ describe.each([
});
store = initializeStore({
config: {
userIsPrivileged: true,
hasModerationPrivileges: true,
reasonCodesEnabled: true,
},
});

View File

@@ -4,6 +4,6 @@ Factory.define('config')
.attrs({
allow_anonymous: false,
allow_anonymous_to_peers: false,
user_is_privileged: false,
has_moderation_privileges: false,
})
.attr('user_roles', ['user_is_privileged'], (userIsPrivileged) => (userIsPrivileged ? ['Student', 'Moderator'] : ['Student']));
.attr('user_roles', ['has_moderation_privileges'], (hasModerationPrivileges) => (hasModerationPrivileges ? ['Student', 'Moderator'] : ['Student']));

View File

@@ -15,7 +15,11 @@ import { selectTopics } from '../topics/data/selectors';
import { fetchCourseTopics } from '../topics/data/thunks';
import { discussionsPath, postMessageToParent } from '../utils';
import {
selectAreThreadsFiltered, selectModerationSettings, selectPostThreadCount, selectUserIsPrivileged,
selectAreThreadsFiltered,
selectModerationSettings,
selectPostThreadCount,
selectUserHasModerationPrivileges,
selectUserIsGroupTa,
} from './selectors';
import { fetchCourseConfig } from './thunks';
@@ -150,14 +154,16 @@ export function useContainerSizeForParent(refContainer) {
}
export const useAlertBannerVisible = (content) => {
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const { reasonCodesEnabled } = useSelector(selectModerationSettings);
const userIsContentAuthor = getAuthenticatedUser().username === content.author;
const canSeeLastEditOrClosedAlert = (userHasModerationPrivileges || userIsContentAuthor || userIsGroupTa);
const isReportedByCurrentUser = getAuthenticatedUser().username === content?.abuseFlaggedBy;
const canSeeReportedBanner = (userHasModerationPrivileges || userIsGroupTa || isReportedByCurrentUser);
return (
(reasonCodesEnabled
&& (userIsPrivileged || userIsContentAuthor)
&& (content.lastEdit?.reason || content.closed)
) || content.abuseFlagged
(reasonCodesEnabled && canSeeLastEditOrClosedAlert && (content.lastEdit?.reason || content.closed))
|| (content.abuseFlagged && canSeeReportedBanner)
);
};

View File

@@ -6,10 +6,12 @@ export const selectAnonymousPostingConfig = state => ({
allowAnonymousToPeers: state.config.allowAnonymousToPeers,
});
export const selectUserIsPrivileged = state => state.config.userIsPrivileged;
export const selectUserHasModerationPrivileges = state => state.config.hasModerationPrivileges;
export const selectUserIsStaff = state => state.config.isUserAdmin;
export const selectUserIsGroupTa = state => state.config.isGroupTa;
export const selectconfigLoadingStatus = state => state.config.status;
export const selectLearnersTabEnabled = state => state.config.learnersTabEnabled;

View File

@@ -11,7 +11,8 @@ const configSlice = createSlice({
allowAnonymous: false,
allowAnonymousToPeers: false,
userRoles: [],
userIsPrivileged: false,
hasModerationPrivileges: false,
isGroupTa: false,
isUserAdmin: false,
learnersTabEnabled: false,
settings: {

View File

@@ -2,8 +2,12 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { LearnersOrdering } from '../../data/constants';
import {
LearnersOrdering,
PostsStatusFilter,
} from '../../data/constants';
import { setSortedBy } from '../learners/data';
import { setStatusFilter } from '../posts/data';
import { getHttpErrorStatus } from '../utils';
import { getDiscussionsConfig, getDiscussionsSettings } from './api';
import {
@@ -19,17 +23,23 @@ export function fetchCourseConfig(courseId) {
return async (dispatch) => {
try {
let learnerSort = LearnersOrdering.BY_LAST_ACTIVITY;
let postsFilterStatus = PostsStatusFilter.ALL;
dispatch(fetchConfigRequest());
const config = await getDiscussionsConfig(courseId);
if (config.is_user_admin || config.user_is_privileged) {
if (config.has_moderation_privileges) {
const settings = await getDiscussionsSettings(courseId);
Object.assign(config, { settings });
}
if ((config.has_moderation_privileges || config.is_group_ta)) {
learnerSort = LearnersOrdering.BY_FLAG;
postsFilterStatus = PostsStatusFilter.REPORTED;
}
dispatch(fetchConfigSuccess(camelCaseObject(config)));
dispatch(setSortedBy(learnerSort));
dispatch(setStatusFilter(postsFilterStatus));
} catch (error) {
if (getHttpErrorStatus(error) === 403) {
dispatch(fetchConfigDenied());

View File

@@ -2,12 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import {
Redirect, Route, Switch, useLocation,
} from 'react-router';
import { Routes } from '../../data/constants';
import { RequestStatus, Routes } from '../../data/constants';
import { useIsOnDesktop, useIsOnXLDesktop } from '../data/hooks';
import { selectconfigLoadingStatus, selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../data/selectors';
import { LearnerPostsView, LearnersView } from '../learners';
import { PostsView } from '../posts';
import { TopicsView } from '../topics';
@@ -16,6 +18,9 @@ export default function DiscussionSidebar({ displaySidebar }) {
const location = useLocation();
const isOnDesktop = useIsOnDesktop();
const isOnXLDesktop = useIsOnXLDesktop();
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const configStatus = useSelector(selectconfigLoadingStatus);
return (
<div
@@ -36,13 +41,15 @@ export default function DiscussionSidebar({ displaySidebar }) {
<Route path={Routes.TOPICS.PATH} component={TopicsView} />
<Route path={Routes.LEARNERS.POSTS} component={LearnerPostsView} />
<Route path={Routes.LEARNERS.PATH} component={LearnersView} />
<Redirect
from={Routes.DISCUSSIONS.PATH}
to={{
...location,
pathname: Routes.POSTS.ALL_POSTS,
}}
/>
{configStatus === RequestStatus.SUCCESSFUL && (
<Redirect
from={Routes.DISCUSSIONS.PATH}
to={{
...location,
pathname: (userHasModerationPrivileges || userIsGroupTa) ? Routes.POSTS.ALL_POSTS : Routes.POSTS.MY_POSTS,
}}
/>
)}
</Switch>
</div>
);

View File

@@ -9,7 +9,7 @@ import { Collapsible, Form, Icon } from '@edx/paragon';
import { Check, Tune } from '@edx/paragon/icons';
import { LearnersOrdering } from '../../../data/constants';
import { selectUserIsPrivileged } from '../../data/selectors';
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
import { setSortedBy } from '../data';
import { selectLearnerSorting } from '../data/selectors';
import messages from '../messages';
@@ -48,7 +48,8 @@ function LearnerFilterBar({
intl,
}) {
const dispatch = useDispatch();
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const currentSorting = useSelector(selectLearnerSorting());
const [isOpen, setOpen] = useState(false);
@@ -94,7 +95,7 @@ function LearnerFilterBar({
value={LearnersOrdering.BY_LAST_ACTIVITY}
selected={currentSorting}
/>
{userIsPrivileged && (
{(userHasModerationPrivileges || userIsGroupTa) && (
<ActionItem
id="sort-reported"
label={intl.formatMessage(messages.reportedActivity)}

View File

@@ -1,10 +1,13 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon, OverlayTrigger, Tooltip } from '@edx/paragon';
import { Edit, Report } from '@edx/paragon/icons';
import { QuestionAnswerOutline } from '../../../components/icons';
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
import messages from '../messages';
import { learnerShape } from './proptypes';
@@ -12,8 +15,12 @@ function LearnerFooter({
learner,
intl,
}) {
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const { inactiveFlags } = learner;
const { activeFlags } = learner;
const canSeeLearnerReportedStats = (activeFlags || inactiveFlags) && (userHasModerationPrivileges || userIsGroupTa);
return (
<div className="d-flex align-items-center pt-1 mt-2.5" style={{ marginBottom: '2px' }}>
<div className="d-flex align-items-center">
@@ -24,7 +31,7 @@ function LearnerFooter({
<Icon src={Edit} className="icon-size mr-2 ml-4" />
{learner.replies + learner.responses}
</div>
{Boolean(activeFlags || inactiveFlags) && (
{canSeeLearnerReportedStats && (
<OverlayTrigger
overlay={(
<Tooltip id={`learner-${learner.username}`}>

View File

@@ -10,7 +10,7 @@ import { Button, Spinner } from '@edx/paragon';
import { RequestStatus } from '../../data/constants';
import { DiscussionContext } from '../common/context';
import { selectconfigLoadingStatus, selectUserIsPrivileged, selectUserIsStaff } from '../data/selectors';
import { selectconfigLoadingStatus, selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors';
import messages from '../messages';
import {
selectThreadFilters, selectThreadNextPage, selectThreadSorting, threadsLoadingStatus,
@@ -31,7 +31,7 @@ function PostsList({ posts, topics, intl }) {
const filters = useSelector(selectThreadFilters());
const nextPage = useSelector(selectThreadNextPage());
const showOwnPosts = page === 'my-posts';
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsStaff = useSelector(selectUserIsStaff);
const configStatus = useSelector(selectconfigLoadingStatus);
@@ -42,7 +42,7 @@ function PostsList({ posts, topics, intl }) {
filters,
page: pageNum,
author: showOwnPosts ? authenticatedUser.username : null,
countFlagged: userIsPrivileged || userIsStaff,
countFlagged: userHasModerationPrivileges || userIsStaff,
}))
);

View File

@@ -88,7 +88,7 @@ describe('PostsView', () => {
store = initializeStore({
blocks: { blocks: { 'test-usage-key': { topics: ['some-topic-2', 'some-topic-0'] } } },
config: { userIsPrivileged: true },
config: { hasModerationPrivileges: true },
});
store.dispatch(fetchConfigSuccess({}));
Factory.resetAll();

View File

@@ -27,7 +27,8 @@ import {
selectAnonymousPostingConfig,
selectDivisionSettings,
selectModerationSettings,
selectUserIsPrivileged,
selectUserHasModerationPrivileges,
selectUserIsGroupTa,
} from '../../data/selectors';
import { selectCoursewareTopics, selectNonCoursewareIds, selectNonCoursewareTopics } from '../../topics/data/selectors';
import {
@@ -96,13 +97,14 @@ function PostEditor({
const coursewareTopics = useSelector(selectCoursewareTopics);
const cohorts = useSelector(selectCourseCohorts);
const post = useSelector(selectThread(postId));
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const settings = useSelector(selectDivisionSettings);
const { allowAnonymous, allowAnonymousToPeers } = useSelector(selectAnonymousPostingConfig);
const { reasonCodesEnabled, editReasons } = useSelector(selectModerationSettings);
const canDisplayEditReason = (reasonCodesEnabled && editExisting
&& userIsPrivileged && post.author !== authenticatedUser.username
&& (userHasModerationPrivileges || userIsGroupTa) && post?.author !== authenticatedUser.username
);
const editReasonCodeValidation = canDisplayEditReason && {
@@ -112,13 +114,14 @@ function PostEditor({
const canSelectCohort = (tId) => {
// If the user isn't privileged, they can't edit the cohort.
// If the topic is being edited the cohort can't be changed.
if (!userIsPrivileged || editExisting) {
if (!userHasModerationPrivileges) {
return false;
}
if (nonCoursewareIds.includes(tId)) {
return settings.dividedCourseWideDiscussions.includes(tId);
}
return settings.alwaysDivideInlineDiscussions || settings.dividedInlineDiscussions.includes(tId);
const isCohorting = settings.alwaysDivideInlineDiscussions || settings.dividedInlineDiscussions.includes(tId);
return isCohorting;
};
const hideEditor = () => {
if (editExisting) {
@@ -166,7 +169,7 @@ function PostEditor({
};
useEffect(() => {
if (userIsPrivileged && isEmpty(cohorts)) {
if (userHasModerationPrivileges && isEmpty(cohorts)) {
dispatch(fetchCourseCohorts(courseId));
}
if (editExisting) {

View File

@@ -151,8 +151,7 @@ describe('PostEditor', () => {
config: {
provider: 'legacy',
userRoles: ['Student', 'Moderator'],
userIsPrivileged: true,
moderationSettings: {},
hasModerationPrivileges: true,
settings: {
dividedInlineDiscussions: dividedcw,
dividedCourseWideDiscussions: dividedncw,
@@ -252,7 +251,7 @@ describe('PostEditor', () => {
});
});
test('test unprivileged user', async () => {
await setupData({ userIsPrivileged: false });
await setupData({ hasModerationPrivileges: false });
await renderComponent();
['ncw-topic 1', 'ncw-topic 2', 'category-1-topic 1', 'category-2-topic 1'].forEach((topicName) => {
act(() => {
@@ -271,9 +270,9 @@ describe('PostEditor', () => {
.toBeInTheDocument();
});
});
test('edit existing post should not show cohort selector', async () => {
test('edit existing post should not show cohort selector to unprivileged users', async () => {
const threadId = 'thread-1';
await setupData();
await setupData({ hasModerationPrivileges: false });
axiosMock.onGet(`${threadsApiUrl}${threadId}/`)
.reply(200, Factory.build('thread'));
await executeThunk(fetchThread(threadId), store.dispatch, store.getState);
@@ -315,10 +314,13 @@ describe('PostEditor', () => {
describe('Edit codes', () => {
const threadId = 'thread-1';
beforeEach(async () => {
const dividedncw = ['ncw-topic-2'];
const dividedcw = ['category-1-topic-2', 'category-2-topic-1', 'category-2-topic-2'];
store = initializeStore({
config: {
provider: 'legacy',
userIsPrivileged: true,
hasModerationPrivileges: true,
reasonCodesEnabled: true,
editReasons: [
{
@@ -330,6 +332,10 @@ describe('PostEditor', () => {
label: 'Reason 2',
},
],
settings: {
dividedInlineDiscussions: dividedcw,
dividedCourseWideDiscussions: dividedncw,
},
},
});
await executeThunk(fetchCourseTopics(courseId), store.dispatch, store.getState);

View File

@@ -18,7 +18,7 @@ import {
} from '../../../data/constants';
import { selectCourseCohorts } from '../../cohorts/data/selectors';
import { fetchCourseCohorts } from '../../cohorts/data/thunks';
import { selectUserIsPrivileged, selectUserIsStaff } from '../../data/selectors';
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
import {
setCohortFilter, setPostsTypeFilter, setSortedBy, setStatusFilter,
} from '../data';
@@ -61,12 +61,12 @@ function PostFilterBar({
}) {
const dispatch = useDispatch();
const { courseId } = useParams();
const userIsPrivileged = useSelector(selectUserIsPrivileged);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const currentSorting = useSelector(selectThreadSorting());
const currentFilters = useSelector(selectThreadFilters());
const { status } = useSelector(state => state.cohorts);
const cohorts = useSelector(selectCourseCohorts);
const userIsStaff = useSelector(selectUserIsStaff);
const [isOpen, setOpen] = useState(false);
@@ -106,10 +106,10 @@ function PostFilterBar({
};
useEffect(() => {
if (userIsPrivileged && isEmpty(cohorts)) {
if (userHasModerationPrivileges && isEmpty(cohorts)) {
dispatch(fetchCourseCohorts(courseId));
}
}, [courseId, userIsPrivileged]);
}, [courseId, userHasModerationPrivileges]);
return (
<Collapsible.Advanced
@@ -187,7 +187,7 @@ function PostFilterBar({
value={PostsStatusFilter.FOLLOWING}
selected={currentFilters.status}
/>
{(userIsPrivileged || userIsStaff) && (
{(userHasModerationPrivileges || userIsGroupTa) && (
<ActionItem
id="status-reported"
label={intl.formatMessage(messages.filterReported)}
@@ -228,7 +228,7 @@ function PostFilterBar({
/>
</Form.RadioSet>
</div>
{userIsPrivileged && (
{userHasModerationPrivileges && (
<>
<div className="border-bottom my-2" />
{status === RequestStatus.IN_PROGRESS ? (

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import * as timeago from 'timeago.js';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -20,6 +20,7 @@ import {
StarOutline,
} from '../../../components/icons';
import timeLocale from '../../common/time-locale';
import { selectUserHasModerationPrivileges } from '../../data/selectors';
import { updateExistingThread } from '../data/thunks';
import LikeButton from './LikeButton';
import messages from './messages';
@@ -31,6 +32,7 @@ function PostFooter({
preview,
}) {
const dispatch = useDispatch();
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
timeago.register('time-locale', timeLocale);
return (
@@ -77,7 +79,7 @@ function PostFooter({
</Badge>
)}
<div className="d-flex flex-fill justify-content-end align-items-center">
{post.groupId && (
{post.groupId && userHasModerationPrivileges && (
<>
<OverlayTrigger
overlay={(

View File

@@ -57,7 +57,11 @@ describe('PostFooter', () => {
roles: [],
},
});
store = initializeStore();
store = initializeStore({
config: {
hasModerationPrivileges: true,
},
});
});
it("shows 'x new' badge for new comments", () => {

View File

@@ -2,6 +2,7 @@ import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -11,6 +12,7 @@ import { PushPin } from '../../../components/icons';
import { AvatarOutlineAndLabelColors, Routes, ThreadType } from '../../../data/constants';
import AuthorLabel from '../../common/AuthorLabel';
import { DiscussionContext } from '../../common/context';
import { selectUserHasModerationPrivileges, selectUserIsGroupTa } from '../../data/selectors';
import { discussionsPath, isPostPreviewAvailable } from '../../utils';
import messages from './messages';
import PostFooter from './PostFooter';
@@ -40,9 +42,12 @@ function PostLink({
category,
learnerUsername,
});
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const showAnsweredBadge = post.hasEndorsed && post.type === ThreadType.QUESTION;
const authorLabelColor = AvatarOutlineAndLabelColors[post.authorLabel];
const postReported = post.abuseFlagged || post.abuseFlaggedCount;
const canSeeReportedBadge = (userHasModerationPrivileges || userIsGroupTa);
return (
<>
@@ -86,7 +91,7 @@ function PostLink({
</Badge>
)}
{postReported && (
{postReported && canSeeReportedBadge && (
<Badge
variant="danger"
data-testid="reported-post"

View File

@@ -65,9 +65,15 @@ describe('PostFooter', () => {
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
has_moderation_privileges: true,
});
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
});
it('has reported text only when abuseFlagged is true', () => {
it('has reported text only when abuseFlagged is true', async () => {
renderComponent(mockPost);
expect(screen.queryByTestId('reported-post')).toBeFalsy();
@@ -90,7 +96,9 @@ describe('Post username', () => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/`).reply(200, {
learners_tab_enabled: true,
has_moderation_privileges: true,
});
axiosMock.onGet(`${courseConfigApiUrl}${courseId}/settings`).reply(200, {});
await executeThunk(fetchCourseConfig(courseId), store.dispatch, store.getState);
});