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:
@@ -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' },
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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)}: {content.closeReason}
|
||||
{content.closeReason && (`${intl.formatMessage(messages.reason)}: ${content.closeReason}`)}
|
||||
</div>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -82,7 +82,7 @@ describe.each([
|
||||
});
|
||||
store = initializeStore({
|
||||
config: {
|
||||
userIsPrivileged: true,
|
||||
hasModerationPrivileges: true,
|
||||
reasonCodesEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ describe.each([
|
||||
});
|
||||
store = initializeStore({
|
||||
config: {
|
||||
userIsPrivileged: true,
|
||||
hasModerationPrivileges: true,
|
||||
reasonCodesEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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']));
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,7 +11,8 @@ const configSlice = createSlice({
|
||||
allowAnonymous: false,
|
||||
allowAnonymousToPeers: false,
|
||||
userRoles: [],
|
||||
userIsPrivileged: false,
|
||||
hasModerationPrivileges: false,
|
||||
isGroupTa: false,
|
||||
isUserAdmin: false,
|
||||
learnersTabEnabled: false,
|
||||
settings: {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}`}>
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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={(
|
||||
|
||||
@@ -57,7 +57,11 @@ describe('PostFooter', () => {
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore();
|
||||
store = initializeStore({
|
||||
config: {
|
||||
hasModerationPrivileges: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("shows 'x new' badge for new comments", () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user