From 45c596b7706d4d9f8519c42bd9a76fdb0fac8438 Mon Sep 17 00:00:00 2001 From: Awais Ansari <79941147+awais-ansari@users.noreply.github.com> Date: Wed, 24 Aug 2022 12:31:31 +0500 Subject: [PATCH] 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 --- .../comments/CommentsView.test.jsx | 2 +- .../comments/comment/CommentEditor.jsx | 11 ++++++--- src/discussions/common/AlertBanner.jsx | 14 +++++++---- src/discussions/common/AlertBanner.test.jsx | 2 +- .../common/EndorsedAlertBanner.test.jsx | 2 +- .../data/__factories__/config.factory.js | 4 ++-- src/discussions/data/hooks.js | 18 ++++++++++----- src/discussions/data/selectors.js | 4 +++- src/discussions/data/slices.js | 3 ++- src/discussions/data/thunks.js | 14 +++++++++-- .../discussions-home/DiscussionSidebar.jsx | 23 ++++++++++++------- .../learners/learner/LearnerFilterBar.jsx | 7 +++--- .../learners/learner/LearnerFooter.jsx | 9 +++++++- src/discussions/posts/PostsList.jsx | 6 ++--- src/discussions/posts/PostsView.test.jsx | 2 +- .../posts/post-editor/PostEditor.jsx | 15 +++++++----- .../posts/post-editor/PostEditor.test.jsx | 18 ++++++++++----- .../posts/post-filter-bar/PostFilterBar.jsx | 14 +++++------ src/discussions/posts/post/PostFooter.jsx | 6 +++-- .../posts/post/PostFooter.test.jsx | 6 ++++- src/discussions/posts/post/PostLink.jsx | 7 +++++- src/discussions/posts/post/PostLink.test.jsx | 10 +++++++- 22 files changed, 134 insertions(+), 63 deletions(-) diff --git a/src/discussions/comments/CommentsView.test.jsx b/src/discussions/comments/CommentsView.test.jsx index e5a9c423..52c01a81 100644 --- a/src/discussions/comments/CommentsView.test.jsx +++ b/src/discussions/comments/CommentsView.test.jsx @@ -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' }, diff --git a/src/discussions/comments/comment/CommentEditor.jsx b/src/discussions/comments/comment/CommentEditor.jsx index 3708ef0a..0fec7c8a 100644 --- a/src/discussions/comments/comment/CommentEditor.jsx +++ b/src/discussions/comments/comment/CommentEditor.jsx @@ -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 ); diff --git a/src/discussions/common/AlertBanner.jsx b/src/discussions/common/AlertBanner.jsx index 27fb084b..90987694 100644 --- a/src/discussions/common/AlertBanner.jsx +++ b/src/discussions/common/AlertBanner.jsx @@ -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 && ( {intl.formatMessage(messages.abuseFlaggedMessage)} )} - {reasonCodesEnabled && (userIsPrivileged || userIsContentAuthor) && ( + {reasonCodesEnabled && canSeeLastEditOrClosedAlert && ( <> {content.lastEdit?.reason && ( @@ -50,7 +54,7 @@ function AlertBanner({ - {intl.formatMessage(messages.reason)}: {content.closeReason} + {content.closeReason && (`${intl.formatMessage(messages.reason)}: ${content.closeReason}`)} )} diff --git a/src/discussions/common/AlertBanner.test.jsx b/src/discussions/common/AlertBanner.test.jsx index e6f888a8..388f75e9 100644 --- a/src/discussions/common/AlertBanner.test.jsx +++ b/src/discussions/common/AlertBanner.test.jsx @@ -82,7 +82,7 @@ describe.each([ }); store = initializeStore({ config: { - userIsPrivileged: true, + hasModerationPrivileges: true, reasonCodesEnabled: true, }, }); diff --git a/src/discussions/common/EndorsedAlertBanner.test.jsx b/src/discussions/common/EndorsedAlertBanner.test.jsx index bfc9de8a..b9ab9b5b 100644 --- a/src/discussions/common/EndorsedAlertBanner.test.jsx +++ b/src/discussions/common/EndorsedAlertBanner.test.jsx @@ -76,7 +76,7 @@ describe.each([ }); store = initializeStore({ config: { - userIsPrivileged: true, + hasModerationPrivileges: true, reasonCodesEnabled: true, }, }); diff --git a/src/discussions/data/__factories__/config.factory.js b/src/discussions/data/__factories__/config.factory.js index 7d3118f9..768a1ac8 100644 --- a/src/discussions/data/__factories__/config.factory.js +++ b/src/discussions/data/__factories__/config.factory.js @@ -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'])); diff --git a/src/discussions/data/hooks.js b/src/discussions/data/hooks.js index c2b6c8cc..04f1a63c 100644 --- a/src/discussions/data/hooks.js +++ b/src/discussions/data/hooks.js @@ -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) ); }; diff --git a/src/discussions/data/selectors.js b/src/discussions/data/selectors.js index a767a7b3..72378de6 100644 --- a/src/discussions/data/selectors.js +++ b/src/discussions/data/selectors.js @@ -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; diff --git a/src/discussions/data/slices.js b/src/discussions/data/slices.js index 210c8911..37ca4bad 100644 --- a/src/discussions/data/slices.js +++ b/src/discussions/data/slices.js @@ -11,7 +11,8 @@ const configSlice = createSlice({ allowAnonymous: false, allowAnonymousToPeers: false, userRoles: [], - userIsPrivileged: false, + hasModerationPrivileges: false, + isGroupTa: false, isUserAdmin: false, learnersTabEnabled: false, settings: { diff --git a/src/discussions/data/thunks.js b/src/discussions/data/thunks.js index e0ff9e69..06164089 100644 --- a/src/discussions/data/thunks.js +++ b/src/discussions/data/thunks.js @@ -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()); diff --git a/src/discussions/discussions-home/DiscussionSidebar.jsx b/src/discussions/discussions-home/DiscussionSidebar.jsx index 29a3df6c..3689c47c 100644 --- a/src/discussions/discussions-home/DiscussionSidebar.jsx +++ b/src/discussions/discussions-home/DiscussionSidebar.jsx @@ -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 (
- + {configStatus === RequestStatus.SUCCESSFUL && ( + + )}
); diff --git a/src/discussions/learners/learner/LearnerFilterBar.jsx b/src/discussions/learners/learner/LearnerFilterBar.jsx index 49d4522b..856bac9a 100644 --- a/src/discussions/learners/learner/LearnerFilterBar.jsx +++ b/src/discussions/learners/learner/LearnerFilterBar.jsx @@ -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) && (
@@ -24,7 +31,7 @@ function LearnerFooter({ {learner.replies + learner.responses}
- {Boolean(activeFlags || inactiveFlags) && ( + {canSeeLearnerReportedStats && ( diff --git a/src/discussions/posts/PostsList.jsx b/src/discussions/posts/PostsList.jsx index 056db78f..134557ae 100644 --- a/src/discussions/posts/PostsList.jsx +++ b/src/discussions/posts/PostsList.jsx @@ -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, })) ); diff --git a/src/discussions/posts/PostsView.test.jsx b/src/discussions/posts/PostsView.test.jsx index 9962e824..0e46726f 100644 --- a/src/discussions/posts/PostsView.test.jsx +++ b/src/discussions/posts/PostsView.test.jsx @@ -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(); diff --git a/src/discussions/posts/post-editor/PostEditor.jsx b/src/discussions/posts/post-editor/PostEditor.jsx index 5910a555..fd78dd82 100644 --- a/src/discussions/posts/post-editor/PostEditor.jsx +++ b/src/discussions/posts/post-editor/PostEditor.jsx @@ -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) { diff --git a/src/discussions/posts/post-editor/PostEditor.test.jsx b/src/discussions/posts/post-editor/PostEditor.test.jsx index bc997171..43e04363 100644 --- a/src/discussions/posts/post-editor/PostEditor.test.jsx +++ b/src/discussions/posts/post-editor/PostEditor.test.jsx @@ -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); diff --git a/src/discussions/posts/post-filter-bar/PostFilterBar.jsx b/src/discussions/posts/post-filter-bar/PostFilterBar.jsx index 3ce4e52a..797d9169 100644 --- a/src/discussions/posts/post-filter-bar/PostFilterBar.jsx +++ b/src/discussions/posts/post-filter-bar/PostFilterBar.jsx @@ -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 ( - {(userIsPrivileged || userIsStaff) && ( + {(userHasModerationPrivileges || userIsGroupTa) && ( - {userIsPrivileged && ( + {userHasModerationPrivileges && ( <>
{status === RequestStatus.IN_PROGRESS ? ( diff --git a/src/discussions/posts/post/PostFooter.jsx b/src/discussions/posts/post/PostFooter.jsx index 0b9b2095..a855222b 100644 --- a/src/discussions/posts/post/PostFooter.jsx +++ b/src/discussions/posts/post/PostFooter.jsx @@ -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({ )}
- {post.groupId && ( + {post.groupId && userHasModerationPrivileges && ( <> { roles: [], }, }); - store = initializeStore(); + store = initializeStore({ + config: { + hasModerationPrivileges: true, + }, + }); }); it("shows 'x new' badge for new comments", () => { diff --git a/src/discussions/posts/post/PostLink.jsx b/src/discussions/posts/post/PostLink.jsx index 3474c179..882907b5 100644 --- a/src/discussions/posts/post/PostLink.jsx +++ b/src/discussions/posts/post/PostLink.jsx @@ -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({ )} - {postReported && ( + {postReported && canSeeReportedBadge && ( { }, }); 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); });