diff --git a/src/discussions/learners/LearnerPostsView.jsx b/src/discussions/learners/LearnerPostsView.jsx index e1368f7f..ccf65207 100644 --- a/src/discussions/learners/LearnerPostsView.jsx +++ b/src/discussions/learners/LearnerPostsView.jsx @@ -14,6 +14,7 @@ import { ArrowBack } from '@edx/paragon/icons'; import { RequestStatus, Routes } from '../../data/constants'; import { DiscussionContext } from '../common/context'; +import { selectUserHasModerationPrivileges, selectUserIsStaff } from '../data/selectors'; import { selectAllThreads, selectThreadNextPage, @@ -34,14 +35,19 @@ function LearnerPostsView({ intl }) { const loadingStatus = useSelector(threadsLoadingStatus()); const { courseId, learnerUsername: username } = useContext(DiscussionContext); const nextPage = useSelector(selectThreadNextPage()); + const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges); + const userIsStaff = useSelector(selectUserIsStaff); + const countFlagged = userHasModerationPrivileges || userIsStaff; useEffect(() => { - dispatch(fetchUserPosts(courseId, username)); + dispatch(fetchUserPosts(courseId, { username, countFlagged })); }, [courseId, username]); const loadMorePosts = () => ( - dispatch(fetchUserPosts(courseId, username, { + dispatch(fetchUserPosts(courseId, { + username, page: nextPage, + countFlagged, })) ); diff --git a/src/discussions/learners/LearnerPostsView.test.jsx b/src/discussions/learners/LearnerPostsView.test.jsx new file mode 100644 index 00000000..af57e7d7 --- /dev/null +++ b/src/discussions/learners/LearnerPostsView.test.jsx @@ -0,0 +1,90 @@ +import React from 'react'; + +import { render, screen } from '@testing-library/react'; +import MockAdapter from 'axios-mock-adapter'; +import { act } from 'react-dom/test-utils'; +import { IntlProvider } from 'react-intl'; +import { MemoryRouter, Route } from 'react-router'; +import { Factory } from 'rosie'; + +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { AppProvider } from '@edx/frontend-platform/react'; + +import { initializeStore } from '../../store'; +import { executeThunk } from '../../test-utils'; +import { DiscussionContext } from '../common/context'; +import { courseConfigApiUrl } from '../data/api'; +import { fetchCourseConfig } from '../data/thunks'; +import { coursesApiUrl } from './data/api'; +import LearnerPostsView from './LearnerPostsView'; + +import './data/__factories__'; + +let store; +let axiosMock; +const courseId = 'course-v1:edX+TestX+Test_Course'; +const username = 'abc123'; + +function renderComponent(path = `/${courseId}/learners/${username}/posts`) { + return render( + + + + + + + + + + + , + ); +} + +describe('LearnerPostsView', () => { + beforeEach(async () => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username, + administrator: true, + roles: [], + }, + }); + + store = initializeStore(); + Factory.resetAll(); + const learnerPosts = Factory.build('learnerPosts', {}, { + abuseFlaggedCount: 1, + }); + const apiUrl = `${coursesApiUrl}${courseId}/learner/`; + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock.onGet(apiUrl, { username, count_flagged: true }) + .reply(() => [200, learnerPosts]); + }); + + describe('Basic', () => { + test('Reported icon is visible to moderator for post with reported comment', async () => { + 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); + await act(async () => { + renderComponent(); + }); + expect(screen.queryAllByTestId('reported-post')[0]).toBeInTheDocument(); + }); + + test('Reported icon is not visible to learner for post with reported comment', async () => { + await renderComponent(); + expect(screen.queryByTestId('reported-post')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/discussions/learners/data/__factories__/learners.factory.js b/src/discussions/learners/data/__factories__/learners.factory.js index c148a33e..f0c574b1 100644 --- a/src/discussions/learners/data/__factories__/learners.factory.js +++ b/src/discussions/learners/data/__factories__/learners.factory.js @@ -86,3 +86,63 @@ Factory.define('learnersProfile') })); return profiles; }); + +Factory.define('learnerPosts') + .option('abuseFlaggedCount', null, null) + .option('courseId', null, 'course-v1:edX+TestX+Test_Course') + .attr( + 'results', + ['abuseFlaggedCount', 'courseId'], + (abuseFlaggedCount, courseId) => { + const threads = []; + for (let i = 0; i < 2; i++) { + threads.push({ + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + editable_fields: [ + 'abuse_flagged', + 'following', + 'group_id', + 'raw_body', + 'closed', + 'read', + 'title', + 'topic_id', + 'type', + 'voted', + 'pinned', + ], + id: `post_id${i}`, + author: 'test_user', + author_label: 'Staff', + abuse_flagged: false, + can_delete: true, + voted: false, + vote_count: 1, + title: `Title ${i}`, + raw_body: `

body ${i}

`, + preview_body: `

body ${i}

`, + course_id: courseId, + group_id: null, + group_name: null, + abuse_flagged_count: abuseFlaggedCount, + following: false, + comment_count: 8, + unread_comment_count: 0, + endorsed_comment_list_url: null, + non_endorsed_comment_list_url: null, + read: false, + has_endorsed: false, + pinned: false, + topic_id: 'topic', + }); + } + return threads; + }, + ) + .attr('pagination', [], () => ({ + next: null, + prev: null, + count: 2, + num_pages: 1, + })); diff --git a/src/discussions/learners/data/api.js b/src/discussions/learners/data/api.js index 5aca91d3..97738739 100644 --- a/src/discussions/learners/data/api.js +++ b/src/discussions/learners/data/api.js @@ -1,5 +1,5 @@ /* eslint-disable import/prefer-default-export */ -import { ensureConfig, getConfig } from '@edx/frontend-platform'; +import { ensureConfig, getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; ensureConfig([ @@ -45,10 +45,11 @@ export async function getUserProfiles(usernames) { * pagination: {count, num_pages, next, previous} * } */ -export async function getUserPosts(courseId, username, { page }) { +export async function getUserPosts(courseId, { username, page, countFlagged }) { const learnerPostsApiUrl = `${coursesApiUrl}${courseId}/learner/`; + const params = snakeCaseObject({ username, page, countFlagged }); const { data } = await getAuthenticatedHttpClient() - .get(learnerPostsApiUrl, { params: { username, page } }); + .get(learnerPostsApiUrl, { params }); return data; } diff --git a/src/discussions/learners/data/thunks.js b/src/discussions/learners/data/thunks.js index 36adbf97..673039f2 100644 --- a/src/discussions/learners/data/thunks.js +++ b/src/discussions/learners/data/thunks.js @@ -67,12 +67,12 @@ export function fetchLearners(courseId, { * @param page * @returns a promise that will update the state with the learner's posts */ -export function fetchUserPosts(courseId, username, { page = 1 } = {}) { +export function fetchUserPosts(courseId, { username, page = 1, countFlagged = false } = {}) { return async (dispatch) => { try { dispatch(fetchLearnerThreadsRequest({ courseId, author: username })); - const data = await getUserPosts(courseId, username, { page }); + const data = await getUserPosts(courseId, { username, page, countFlagged }); const normalisedData = normaliseThreads(camelCaseObject(data)); dispatch(fetchThreadsSuccess({ ...normalisedData, page, author: username }));