Compare commits

...

2 Commits

Author SHA1 Message Date
Adolfo R. Brandes
8d35a729d2 feat: Support runtime configuration
frontend-platform supports runtime configuration since 2.5.0 (see the PR
that introduced it[1], but it requires MFE cooperation.  This implements
just that: by avoiding making configuration values constant, it should
now be possible to change them after initialization.

Almost all changes here relate to the `LMS_BASE_URL` setting, which in
most places was treated as a constant.

[1] https://github.com/openedx/frontend-platform/pull/335

This is a backport to Olive of
https://github.com/openedx/frontend-app-discussions/pull/377
2022-12-09 10:40:46 +00:00
Mehak Nasir
e89792b8d8 fix: handled thread not found result on frontend 2022-12-08 10:44:41 +00:00
32 changed files with 136 additions and 95 deletions

View File

@@ -2,7 +2,7 @@
import { camelCaseObject } from '@edx/frontend-platform'; import { camelCaseObject } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { API_BASE_URL } from '../../../data/constants'; import { getApiBaseUrl } from '../../../data/constants';
function normalizeCourseHomeCourseMetadata(metadata, rootSlug) { function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
const data = camelCaseObject(metadata); const data = camelCaseObject(metadata);
@@ -21,7 +21,7 @@ function normalizeCourseHomeCourseMetadata(metadata, rootSlug) {
} }
export async function getCourseHomeCourseMetadata(courseId, rootSlug) { export async function getCourseHomeCourseMetadata(courseId, rootSlug) {
const url = `${API_BASE_URL}/api/course_home/course_metadata/${courseId}`; const url = `${getApiBaseUrl()}/api/course_home/course_metadata/${courseId}`;
// don't know the context of adding timezone in url. hence omitting it // don't know the context of adding timezone in url. hence omitting it
// url = appendBrowserTimezoneToUrl(url); // url = appendBrowserTimezoneToUrl(url);
const { data } = await getAuthenticatedHttpClient().get(url); const { data } = await getAuthenticatedHttpClient().get(url);

View File

@@ -1,9 +1,9 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { API_BASE_URL } from './constants'; import { getApiBaseUrl } from './constants';
export const blocksAPIURL = `${API_BASE_URL}/api/courses/v1/blocks/`; export const getBlocksAPIURL = () => `${getApiBaseUrl()}/api/courses/v1/blocks/`;
export async function getCourseBlocks(courseId, username) { export async function getCourseBlocks(courseId, username) {
const params = { const params = {
course_id: courseId, course_id: courseId,
@@ -14,6 +14,6 @@ export async function getCourseBlocks(courseId, username) {
student_view_data: 'discussion', student_view_data: 'discussion',
}; };
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get(blocksAPIURL, { params }); .get(getBlocksAPIURL(), { params });
return data; return data;
} }

View File

@@ -1,6 +1,6 @@
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
export const API_BASE_URL = getConfig().LMS_BASE_URL; export const getApiBaseUrl = () => getConfig().LMS_BASE_URL;
/** /**
* Enum for thread types. * Enum for thread types.

View File

@@ -7,10 +7,11 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
import { initializeStore } from '../store'; import { initializeStore } from '../store';
import { executeThunk } from '../test-utils'; import { executeThunk } from '../test-utils';
import { getBlocksAPIResponse } from './__factories__'; import { getBlocksAPIResponse } from './__factories__';
import { blocksAPIURL } from './api'; import { getBlocksAPIURL } from './api';
import { RequestStatus } from './constants'; import { RequestStatus } from './constants';
import { fetchCourseBlocks } from './thunks'; import { fetchCourseBlocks } from './thunks';
const blocksAPIURL = getBlocksAPIURL();
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
let axiosMock; let axiosMock;

View File

@@ -6,9 +6,7 @@ ensureConfig([
'LMS_BASE_URL', 'LMS_BASE_URL',
], 'Comments API service'); ], 'Comments API service');
const apiBaseUrl = getConfig().LMS_BASE_URL; export const getCohortsApiUrl = courseId => `${getConfig().LMS_BASE_URL}/api/cohorts/v1/courses/${courseId}/cohorts/`;
export const getCohortsApiUrl = courseId => `${apiBaseUrl}/api/cohorts/v1/courses/${courseId}/cohorts/`;
export async function getCourseCohorts(courseId) { export async function getCourseCohorts(courseId) {
const params = snakeCaseObject({ courseId }); const params = snakeCaseObject({ courseId });

View File

@@ -18,6 +18,7 @@ import {
import { useDispatchWithState } from '../../data/hooks'; import { useDispatchWithState } from '../../data/hooks';
import { DiscussionContext } from '../common/context'; import { DiscussionContext } from '../common/context';
import { useIsOnDesktop } from '../data/hooks'; import { useIsOnDesktop } from '../data/hooks';
import { EmptyPage } from '../empty-posts';
import { Post } from '../posts'; import { Post } from '../posts';
import { selectThread } from '../posts/data/selectors'; import { selectThread } from '../posts/data/selectors';
import { fetchThread, markThreadAsRead } from '../posts/data/thunks'; import { fetchThread, markThreadAsRead } from '../posts/data/thunks';
@@ -133,9 +134,9 @@ DiscussionCommentsView.propTypes = {
}; };
function CommentsView({ intl }) { function CommentsView({ intl }) {
const [isLoading, submitDispatch] = useDispatchWithState();
const { postId } = useParams(); const { postId } = useParams();
const thread = usePost(postId); const thread = usePost(postId);
const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const isOnDesktop = useIsOnDesktop(); const isOnDesktop = useIsOnDesktop();
@@ -143,8 +144,16 @@ function CommentsView({ intl }) {
courseId, learnerUsername, category, topicId, page, inContext, courseId, learnerUsername, category, topicId, page, inContext,
} = useContext(DiscussionContext); } = useContext(DiscussionContext);
useEffect(() => {
if (!thread) { submitDispatch(fetchThread(postId, courseId, true)); }
}, [postId]);
if (!thread) { if (!thread) {
dispatch(fetchThread(postId, true)); if (!isLoading) {
return (
<EmptyPage title={intl.formatMessage(messages.noThreadFound)} />
);
}
return ( return (
<Spinner animation="border" variant="primary" data-testid="loading-indicator" /> <Spinner animation="border" variant="primary" data-testid="loading-indicator" />
); );

View File

@@ -13,16 +13,19 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils'; import { executeThunk } from '../../test-utils';
import { DiscussionContext } from '../common/context'; import { DiscussionContext } from '../common/context';
import { courseConfigApiUrl } from '../data/api'; import { getCourseConfigApiUrl } from '../data/api';
import { fetchCourseConfig } from '../data/thunks'; import { fetchCourseConfig } from '../data/thunks';
import DiscussionContent from '../discussions-home/DiscussionContent'; import DiscussionContent from '../discussions-home/DiscussionContent';
import { threadsApiUrl } from '../posts/data/api'; import { getThreadsApiUrl } from '../posts/data/api';
import { fetchThreads } from '../posts/data/thunks'; import { fetchThreads } from '../posts/data/thunks';
import { commentsApiUrl } from './data/api'; import { getCommentsApiUrl } from './data/api';
import '../posts/data/__factories__'; import '../posts/data/__factories__';
import './data/__factories__'; import './data/__factories__';
const courseConfigApiUrl = getCourseConfigApiUrl();
const commentsApiUrl = getCommentsApiUrl();
const threadsApiUrl = getThreadsApiUrl();
const discussionPostId = 'thread-1'; const discussionPostId = 'thread-1';
const questionPostId = 'thread-2'; const questionPostId = 'thread-2';
const closedPostId = 'thread-2'; const closedPostId = 'thread-2';
@@ -102,7 +105,7 @@ function renderComponent(postId) {
} }
describe('CommentsView', () => { describe('CommentsView', () => {
beforeEach(async () => { beforeEach(() => {
initializeMockApp({ initializeMockApp({
authenticatedUser: { authenticatedUser: {
userId: 3, userId: 3,
@@ -147,7 +150,7 @@ describe('CommentsView', () => {
)]; )];
}); });
await executeThunk(fetchThreads(courseId), store.dispatch, store.getState); executeThunk(fetchThreads(courseId), store.dispatch, store.getState);
mockAxiosReturnPagedComments(); mockAxiosReturnPagedComments();
mockAxiosReturnPagedCommentsResponses(); mockAxiosReturnPagedCommentsResponses();
}); });
@@ -449,9 +452,9 @@ describe('CommentsView', () => {
describe('for discussion thread', () => { describe('for discussion thread', () => {
const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments'); const findLoadMoreCommentsButton = () => screen.findByTestId('load-more-comments');
it('shown spinner when post isn\'t loaded', async () => { it('shown post not found when post id does not belong to course', async () => {
renderComponent('unloaded-id'); renderComponent('unloaded-id');
expect(await screen.findByTestId('loading-indicator')) expect(await screen.findByText('Thread not found', { exact: true }))
.toBeInTheDocument(); .toBeInTheDocument();
}); });

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -10,6 +10,7 @@ import { Button, useToggle } from '@edx/paragon';
import HTMLLoader from '../../../components/HTMLLoader'; import HTMLLoader from '../../../components/HTMLLoader';
import { ContentActions } from '../../../data/constants'; import { ContentActions } from '../../../data/constants';
import { AlertBanner, DeleteConfirmation, EndorsedAlertBanner } from '../../common'; import { AlertBanner, DeleteConfirmation, EndorsedAlertBanner } from '../../common';
import { DiscussionContext } from '../../common/context';
import { selectBlackoutDate } from '../../data/selectors'; import { selectBlackoutDate } from '../../data/selectors';
import { fetchThread } from '../../posts/data/thunks'; import { fetchThread } from '../../posts/data/thunks';
import { inBlackoutDateRange } from '../../utils'; import { inBlackoutDateRange } from '../../utils';
@@ -39,7 +40,9 @@ function Comment({
const hasMorePages = useSelector(selectCommentHasMorePages(comment.id)); const hasMorePages = useSelector(selectCommentHasMorePages(comment.id));
const currentPage = useSelector(selectCommentCurrentPage(comment.id)); const currentPage = useSelector(selectCommentCurrentPage(comment.id));
const blackoutDateRange = useSelector(selectBlackoutDate); const blackoutDateRange = useSelector(selectBlackoutDate);
const {
courseId,
} = useContext(DiscussionContext);
useEffect(() => { useEffect(() => {
// If the comment has a parent comment, it won't have any children, so don't fetch them. // If the comment has a parent comment, it won't have any children, so don't fetch them.
if (hasChildren && !currentPage && showFullThread) { if (hasChildren && !currentPage && showFullThread) {
@@ -51,7 +54,7 @@ function Comment({
[ContentActions.EDIT_CONTENT]: () => setEditing(true), [ContentActions.EDIT_CONTENT]: () => setEditing(true),
[ContentActions.ENDORSE]: async () => { [ContentActions.ENDORSE]: async () => {
await dispatch(editComment(comment.id, { endorsed: !comment.endorsed }, ContentActions.ENDORSE)); await dispatch(editComment(comment.id, { endorsed: !comment.endorsed }, ContentActions.ENDORSE));
await dispatch(fetchThread(comment.threadId)); await dispatch(fetchThread(comment.threadId, courseId));
}, },
[ContentActions.DELETE]: showDeleteConfirmation, [ContentActions.DELETE]: showDeleteConfirmation,
[ContentActions.REPORT]: () => dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged })), [ContentActions.REPORT]: () => dispatch(editComment(comment.id, { flagged: !comment.abuseFlagged })),

View File

@@ -8,9 +8,7 @@ ensureConfig([
'LMS_BASE_URL', 'LMS_BASE_URL',
], 'Comments API service'); ], 'Comments API service');
const apiBaseUrl = getConfig().LMS_BASE_URL; export const getCommentsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/comments/`;
export const commentsApiUrl = `${apiBaseUrl}/api/discussion/v1/comments/`;
/** /**
* Returns all the comments for the specified thread. * Returns all the comments for the specified thread.
@@ -36,7 +34,7 @@ export async function getThreadComments(
}); });
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.get(commentsApiUrl, { params }); .get(getCommentsApiUrl(), { params });
return data; return data;
} }
@@ -53,7 +51,7 @@ export async function getCommentResponses(
pageSize, pageSize,
} = {}, } = {},
) { ) {
const url = `${commentsApiUrl}${commentId}/`; const url = `${getCommentsApiUrl()}${commentId}/`;
const params = snakeCaseObject({ const params = snakeCaseObject({
page, page,
pageSize, pageSize,
@@ -73,7 +71,7 @@ export async function getCommentResponses(
*/ */
export async function postComment(comment, threadId, parentId = null) { export async function postComment(comment, threadId, parentId = null) {
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.post(commentsApiUrl, snakeCaseObject({ threadId, raw_body: comment, parentId })); .post(getCommentsApiUrl(), snakeCaseObject({ threadId, raw_body: comment, parentId }));
return data; return data;
} }
@@ -94,7 +92,7 @@ export async function updateComment(commentId, {
endorsed, endorsed,
editReasonCode, editReasonCode,
}) { }) {
const url = `${commentsApiUrl}${commentId}/`; const url = `${getCommentsApiUrl()}${commentId}/`;
const postData = snakeCaseObject({ const postData = snakeCaseObject({
raw_body: comment, raw_body: comment,
voted, voted,
@@ -113,7 +111,7 @@ export async function updateComment(commentId, {
* @param {string} commentId ID of comment to delete * @param {string} commentId ID of comment to delete
*/ */
export async function deleteComment(commentId) { export async function deleteComment(commentId) {
const url = `${commentsApiUrl}${commentId}/`; const url = `${getCommentsApiUrl()}${commentId}/`;
await getAuthenticatedHttpClient() await getAuthenticatedHttpClient()
.delete(url); .delete(url);
} }

View File

@@ -7,13 +7,14 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
import { EndorsementStatus } from '../../../data/constants'; import { EndorsementStatus } from '../../../data/constants';
import { initializeStore } from '../../../store'; import { initializeStore } from '../../../store';
import { executeThunk } from '../../../test-utils'; import { executeThunk } from '../../../test-utils';
import { commentsApiUrl } from './api'; import { getCommentsApiUrl } from './api';
import { import {
addComment, editComment, fetchCommentResponses, fetchThreadComments, removeComment, addComment, editComment, fetchCommentResponses, fetchThreadComments, removeComment,
} from './thunks'; } from './thunks';
import './__factories__'; import './__factories__';
const commentsApiUrl = getCommentsApiUrl();
let axiosMock; let axiosMock;
let store; let store;

View File

@@ -182,6 +182,11 @@ const messages = defineMessages({
defaultMessage: '{time} ago', defaultMessage: '{time} ago',
description: 'Time text for endorse banner', description: 'Time text for endorse banner',
}, },
noThreadFound: {
id: 'discussion.thread.notFound',
defaultMessage: 'Thread not found',
description: 'message to show on screen if the request thread is not found in course',
},
}); });
export default messages; export default messages;

View File

@@ -10,12 +10,13 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils'; import { executeThunk } from '../../test-utils';
import { courseConfigApiUrl } from '../data/api'; import { getCourseConfigApiUrl } from '../data/api';
import { fetchCourseConfig } from '../data/thunks'; import { fetchCourseConfig } from '../data/thunks';
import AuthorLabel from './AuthorLabel'; import AuthorLabel from './AuthorLabel';
import { DiscussionContext } from './context'; import { DiscussionContext } from './context';
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const courseConfigApiUrl = getCourseConfigApiUrl();
let store; let store;
let axiosMock; let axiosMock;
let container; let container;

View File

@@ -7,16 +7,14 @@ ensureConfig([
'LMS_BASE_URL', 'LMS_BASE_URL',
], 'Posts API service'); ], 'Posts API service');
const apiBaseUrl = getConfig().LMS_BASE_URL; export const getCourseConfigApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
export const courseConfigApiUrl = `${apiBaseUrl}/api/discussion/v1/courses/`;
/** /**
* Get discussions course config * Get discussions course config
* @param {string} courseId * @param {string} courseId
*/ */
export async function getDiscussionsConfig(courseId) { export async function getDiscussionsConfig(courseId) {
const url = `${courseConfigApiUrl}${courseId}/`; const url = `${getCourseConfigApiUrl()}${courseId}/`;
const { data } = await getAuthenticatedHttpClient().get(url); const { data } = await getAuthenticatedHttpClient().get(url);
return data; return data;
} }
@@ -26,7 +24,7 @@ export async function getDiscussionsConfig(courseId) {
* @param {string} courseId * @param {string} courseId
*/ */
export async function getDiscussionsSettings(courseId) { export async function getDiscussionsSettings(courseId) {
const url = `${courseConfigApiUrl}${courseId}/settings`; const url = `${getCourseConfigApiUrl()}${courseId}/settings`;
const { data } = await getAuthenticatedHttpClient().get(url); const { data } = await getAuthenticatedHttpClient().get(url);
return data; return data;
} }

View File

@@ -13,7 +13,7 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import { DiscussionContext } from '../common/context'; import { DiscussionContext } from '../common/context';
import { fetchConfigSuccess } from '../data/slices'; import { fetchConfigSuccess } from '../data/slices';
import { threadsApiUrl } from '../posts/data/api'; import { getThreadsApiUrl } from '../posts/data/api';
import DiscussionSidebar from './DiscussionSidebar'; import DiscussionSidebar from './DiscussionSidebar';
import '../posts/data/__factories__'; import '../posts/data/__factories__';
@@ -21,6 +21,7 @@ import '../posts/data/__factories__';
let store; let store;
let container; let container;
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const threadsApiUrl = getThreadsApiUrl();
let axiosMock; let axiosMock;
function renderComponent(displaySidebar = true, location = `/${courseId}/`) { function renderComponent(displaySidebar = true, location = `/${courseId}/`) {

View File

@@ -12,7 +12,7 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils'; import { executeThunk } from '../../test-utils';
import messages from '../messages'; import messages from '../messages';
import { threadsApiUrl } from '../posts/data/api'; import { getThreadsApiUrl } from '../posts/data/api';
import { fetchThreads } from '../posts/data/thunks'; import { fetchThreads } from '../posts/data/thunks';
import EmptyPosts from './EmptyPosts'; import EmptyPosts from './EmptyPosts';
@@ -20,6 +20,7 @@ import '../posts/data/__factories__';
let store; let store;
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const threadsApiUrl = getThreadsApiUrl();
function renderComponent(location = `/${courseId}/`) { function renderComponent(location = `/${courseId}/`) {
return render( return render(

View File

@@ -9,7 +9,7 @@ import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import { API_BASE_URL } from '../../data/constants'; import { getApiBaseUrl } from '../../data/constants';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils'; import { executeThunk } from '../../test-utils';
import messages from '../messages'; import messages from '../messages';
@@ -20,7 +20,7 @@ import '../topics/data/__factories__';
let store; let store;
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`; const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
function renderComponent(location = `/${courseId}/topics/`) { function renderComponent(location = `/${courseId}/topics/`) {
return render( return render(

View File

@@ -14,15 +14,17 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils'; import { executeThunk } from '../../test-utils';
import { DiscussionContext } from '../common/context'; import { DiscussionContext } from '../common/context';
import { courseConfigApiUrl } from '../data/api'; import { getCourseConfigApiUrl } from '../data/api';
import { fetchCourseConfig } from '../data/thunks'; import { fetchCourseConfig } from '../data/thunks';
import { coursesApiUrl } from './data/api'; import { getCoursesApiUrl } from './data/api';
import LearnerPostsView from './LearnerPostsView'; import LearnerPostsView from './LearnerPostsView';
import './data/__factories__'; import './data/__factories__';
let store; let store;
let axiosMock; let axiosMock;
const coursesApiUrl = getCoursesApiUrl();
const courseConfigApiUrl = getCourseConfigApiUrl();
const courseId = 'course-v1:edX+TestX+Test_Course'; const courseId = 'course-v1:edX+TestX+Test_Course';
const username = 'abc123'; const username = 'abc123';

View File

@@ -13,9 +13,9 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
import { executeThunk } from '../../test-utils'; import { executeThunk } from '../../test-utils';
import { courseConfigApiUrl } from '../data/api'; import { getCourseConfigApiUrl } from '../data/api';
import { fetchCourseConfig } from '../data/thunks'; import { fetchCourseConfig } from '../data/thunks';
import { coursesApiUrl, userProfileApiUrl } from './data/api'; import { getCoursesApiUrl, getUserProfileApiUrl } from './data/api';
import { fetchLearners } from './data/thunks'; import { fetchLearners } from './data/thunks';
import LearnersView from './LearnersView'; import LearnersView from './LearnersView';
@@ -23,6 +23,9 @@ import './data/__factories__';
let store; let store;
let axiosMock; let axiosMock;
const coursesApiUrl = getCoursesApiUrl();
const courseConfigApiUrl = getCourseConfigApiUrl();
const userProfileApiUrl = getUserProfileApiUrl();
const courseId = 'course-v1:edX+TestX+Test_Course'; const courseId = 'course-v1:edX+TestX+Test_Course';
function renderComponent() { function renderComponent() {

View File

@@ -8,10 +8,8 @@ ensureConfig([
'LMS_BASE_URL', 'LMS_BASE_URL',
], 'Posts API service'); ], 'Posts API service');
const apiBaseUrl = getConfig().LMS_BASE_URL; export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
export const getUserProfileApiUrl = () => `${getConfig().LMS_BASE_URL}/api/user/v1/accounts`;
export const coursesApiUrl = `${apiBaseUrl}/api/discussion/v1/courses/`;
export const userProfileApiUrl = `${apiBaseUrl}/api/user/v1/accounts`;
/** /**
* Fetches all the learners in the given course. * Fetches all the learners in the given course.
@@ -20,7 +18,7 @@ export const userProfileApiUrl = `${apiBaseUrl}/api/user/v1/accounts`;
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
export async function getLearners(courseId, params) { export async function getLearners(courseId, params) {
const url = `${coursesApiUrl}${courseId}/activity_stats/`; const url = `${getCoursesApiUrl()}${courseId}/activity_stats/`;
const { data } = await getAuthenticatedHttpClient().get(url, { params }); const { data } = await getAuthenticatedHttpClient().get(url, { params });
return data; return data;
} }
@@ -30,7 +28,7 @@ export async function getLearners(courseId, params) {
* @param {string} usernames * @param {string} usernames
*/ */
export async function getUserProfiles(usernames) { export async function getUserProfiles(usernames) {
const url = `${userProfileApiUrl}?username=${usernames.join()}`; const url = `${getUserProfileApiUrl()}?username=${usernames.join()}`;
const { data } = await getAuthenticatedHttpClient().get(url); const { data } = await getAuthenticatedHttpClient().get(url);
return data; return data;
} }
@@ -67,7 +65,7 @@ export async function getUserPosts(courseId, {
countFlagged, countFlagged,
cohort, cohort,
} = {}) { } = {}) {
const learnerPostsApiUrl = `${coursesApiUrl}${courseId}/learner/`; const learnerPostsApiUrl = `${getCoursesApiUrl()}${courseId}/learner/`;
const params = snakeCaseObject({ const params = snakeCaseObject({
page, page,

View File

@@ -13,8 +13,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import { getBlocksAPIResponse } from '../../../data/__factories__'; import { getBlocksAPIResponse } from '../../../data/__factories__';
import { blocksAPIURL } from '../../../data/api'; import { getBlocksAPIURL } from '../../../data/api';
import { API_BASE_URL, Routes } from '../../../data/constants'; import { getApiBaseUrl, Routes } from '../../../data/constants';
import { fetchCourseBlocks } from '../../../data/thunks'; import { fetchCourseBlocks } from '../../../data/thunks';
import { initializeStore } from '../../../store'; import { initializeStore } from '../../../store';
import { executeThunk } from '../../../test-utils'; import { executeThunk } from '../../../test-utils';
@@ -25,7 +25,7 @@ import { BreadcrumbMenu } from '../index';
import '../../topics/data/__factories__'; import '../../topics/data/__factories__';
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v2/course_topics/${courseId}`; const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v2/course_topics/${courseId}`;
let store; let store;
let axiosMock; let axiosMock;
@@ -78,7 +78,7 @@ describe('BreadcrumbMenu', () => {
Factory.resetAll(); Factory.resetAll();
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
blocksAPIResponse = getBlocksAPIResponse(); blocksAPIResponse = getBlocksAPIResponse();
axiosMock.onGet(blocksAPIURL) axiosMock.onGet(getBlocksAPIURL())
.reply(200, blocksAPIResponse); .reply(200, blocksAPIResponse);
await executeThunk(fetchCourseBlocks(courseId, 'test-user'), store.dispatch, store.getState); await executeThunk(fetchCourseBlocks(courseId, 'test-user'), store.dispatch, store.getState);
const data = [ const data = [

View File

@@ -12,7 +12,7 @@ import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import { API_BASE_URL, Routes } from '../../../data/constants'; import { getApiBaseUrl, Routes } from '../../../data/constants';
import { initializeStore } from '../../../store'; import { initializeStore } from '../../../store';
import { executeThunk } from '../../../test-utils'; import { executeThunk } from '../../../test-utils';
import { fetchCourseTopics } from '../../topics/data/thunks'; import { fetchCourseTopics } from '../../topics/data/thunks';
@@ -21,7 +21,7 @@ import { LegacyBreadcrumbMenu } from '../index';
import '../../topics/data/__factories__'; import '../../topics/data/__factories__';
const courseId = 'course-v1:edX+TestX+Test_Course'; const courseId = 'course-v1:edX+TestX+Test_Course';
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`; const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
let store; let store;
let axiosMock; let axiosMock;

View File

@@ -18,14 +18,16 @@ import { initializeStore } from '../../store';
import { getCohortsApiUrl } from '../cohorts/data/api'; import { getCohortsApiUrl } from '../cohorts/data/api';
import { DiscussionContext } from '../common/context'; import { DiscussionContext } from '../common/context';
import { fetchConfigSuccess } from '../data/slices'; import { fetchConfigSuccess } from '../data/slices';
import { coursesApiUrl } from '../learners/data/api'; import { getCoursesApiUrl } from '../learners/data/api';
import { threadsApiUrl } from './data/api'; import { getThreadsApiUrl } from './data/api';
import { PostsView } from './index'; import { PostsView } from './index';
import './data/__factories__'; import './data/__factories__';
import '../cohorts/data/__factories__'; import '../cohorts/data/__factories__';
const courseId = 'course-v1:edX+TestX+Test_Course'; const courseId = 'course-v1:edX+TestX+Test_Course';
const coursesApiUrl = getCoursesApiUrl();
const threadsApiUrl = getThreadsApiUrl();
let store; let store;
let axiosMock; let axiosMock;
const username = 'abc123'; const username = 'abc123';

View File

@@ -8,10 +8,8 @@ ensureConfig([
'LMS_BASE_URL', 'LMS_BASE_URL',
], 'Posts API service'); ], 'Posts API service');
const apiBaseUrl = getConfig().LMS_BASE_URL; export const getThreadsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/threads/`;
export const getCoursesApiUrl = () => `${getConfig().LMS_BASE_URL}/api/discussion/v1/courses/`;
export const threadsApiUrl = `${apiBaseUrl}/api/discussion/v1/threads/`;
export const coursesApiUrl = `${apiBaseUrl}/api/discussion/v1/courses/`;
/** /**
* Fetches all the threads in the given course and topic. * Fetches all the threads in the given course and topic.
@@ -62,7 +60,7 @@ export async function getThreads(
countFlagged, countFlagged,
groupId: cohort, groupId: cohort,
}); });
const { data } = await getAuthenticatedHttpClient().get(threadsApiUrl, { params }); const { data } = await getAuthenticatedHttpClient().get(getThreadsApiUrl(), { params });
return data; return data;
} }
@@ -71,9 +69,9 @@ export async function getThreads(
* @param {string} threadId * @param {string} threadId
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
export async function getThread(threadId) { export async function getThread(threadId, courseId) {
const params = { requested_fields: 'profile_image' }; const params = { requested_fields: 'profile_image', course_id: courseId };
const url = `${threadsApiUrl}${threadId}/`; const url = `${getThreadsApiUrl()}${threadId}/`;
const { data } = await getAuthenticatedHttpClient().get(url, { params }); const { data } = await getAuthenticatedHttpClient().get(url, { params });
return data; return data;
} }
@@ -117,7 +115,7 @@ export async function postThread(
}); });
const { data } = await getAuthenticatedHttpClient() const { data } = await getAuthenticatedHttpClient()
.post(threadsApiUrl, postData); .post(getThreadsApiUrl(), postData);
return data; return data;
} }
@@ -152,7 +150,7 @@ export async function updateThread(threadId, {
editReasonCode, editReasonCode,
closeReasonCode, closeReasonCode,
} = {}) { } = {}) {
const url = `${threadsApiUrl}${threadId}/`; const url = `${getThreadsApiUrl()}${threadId}/`;
const patchData = snakeCaseObject({ const patchData = snakeCaseObject({
topicId, topicId,
abuse_flagged: flagged, abuse_flagged: flagged,
@@ -177,7 +175,7 @@ export async function updateThread(threadId, {
* @param {string} threadId * @param {string} threadId
*/ */
export async function deleteThread(threadId) { export async function deleteThread(threadId) {
const url = `${threadsApiUrl}${threadId}/`; const url = `${getThreadsApiUrl()}${threadId}/`;
await getAuthenticatedHttpClient() await getAuthenticatedHttpClient()
.delete(url); .delete(url);
} }
@@ -191,7 +189,7 @@ export async function deleteThread(threadId) {
* @returns {Promise<{ location: string }>} * @returns {Promise<{ location: string }>}
*/ */
export async function uploadFile(blob, filename, courseId, threadKey) { export async function uploadFile(blob, filename, courseId, threadKey) {
const uploadUrl = `${coursesApiUrl}${courseId}/upload`; const uploadUrl = `${getCoursesApiUrl()}${courseId}/upload`;
const formData = new FormData(); const formData = new FormData();
formData.append('thread_key', threadKey); formData.append('thread_key', threadKey);
formData.append('uploaded_file', blob, filename); formData.append('uploaded_file', blob, filename);

View File

@@ -3,9 +3,10 @@ import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeMockApp } from '@edx/frontend-platform/testing'; import { initializeMockApp } from '@edx/frontend-platform/testing';
import { coursesApiUrl, uploadFile } from './api'; import { getCoursesApiUrl, uploadFile } from './api';
const courseId = 'course-v1:edX+TestX+Test_Course'; const courseId = 'course-v1:edX+TestX+Test_Course';
const coursesApiUrl = getCoursesApiUrl();
let axiosMock = null; let axiosMock = null;

View File

@@ -6,7 +6,7 @@ import { initializeMockApp } from '@edx/frontend-platform/testing';
import { initializeStore } from '../../../store'; import { initializeStore } from '../../../store';
import { executeThunk } from '../../../test-utils'; import { executeThunk } from '../../../test-utils';
import { threadsApiUrl } from './api'; import { getThreadsApiUrl } from './api';
import { import {
createNewThread, fetchThread, fetchThreads, removeThread, updateExistingThread, createNewThread, fetchThread, fetchThreads, removeThread, updateExistingThread,
} from './thunks'; } from './thunks';
@@ -14,6 +14,7 @@ import {
import './__factories__'; import './__factories__';
const courseId = 'course-v1:edX+TestX+Test_Course'; const courseId = 'course-v1:edX+TestX+Test_Course';
const threadsApiUrl = getThreadsApiUrl();
let axiosMock; let axiosMock;
let store; let store;

View File

@@ -154,18 +154,18 @@ export function fetchThreads(courseId, {
}; };
} }
export function fetchThread(threadId, isDirectLinkPost = false) { export function fetchThread(threadId, courseId, isDirectLinkPost = false) {
return async (dispatch) => { return async (dispatch) => {
try { try {
dispatch(fetchThreadRequest({ threadId })); dispatch(fetchThreadRequest({ threadId }));
const data = await getThread(threadId); const data = await getThread(threadId, courseId);
if (isDirectLinkPost) { if (isDirectLinkPost) {
dispatch(fetchThreadByDirectLinkSuccess({ ...normaliseThreads(camelCaseObject(data)), page: 1 })); dispatch(fetchThreadByDirectLinkSuccess({ ...normaliseThreads(camelCaseObject(data)), page: 1 }));
} else { } else {
dispatch(fetchThreadSuccess(normaliseThreads(camelCaseObject(data)))); dispatch(fetchThreadSuccess(normaliseThreads(camelCaseObject(data))));
} }
} catch (error) { } catch (error) {
if (getHttpErrorStatus(error) === 403) { if (getHttpErrorStatus(error) === 403 || getHttpErrorStatus(error) === 404) {
dispatch(fetchThreadDenied()); dispatch(fetchThreadDenied());
} else { } else {
dispatch(fetchThreadFailed()); dispatch(fetchThreadFailed());

View File

@@ -33,6 +33,7 @@ import {
selectUserIsGroupTa, selectUserIsGroupTa,
selectUserIsStaff, selectUserIsStaff,
} from '../../data/selectors'; } from '../../data/selectors';
import { EmptyPage } from '../../empty-posts';
import { selectCoursewareTopics, selectNonCoursewareIds, selectNonCoursewareTopics } from '../../topics/data/selectors'; import { selectCoursewareTopics, selectNonCoursewareIds, selectNonCoursewareTopics } from '../../topics/data/selectors';
import { import {
discussionsPath, formikCompatibleHandler, isFormikFieldInvalid, useCommentsPagePath, discussionsPath, formikCompatibleHandler, isFormikFieldInvalid, useCommentsPagePath,
@@ -193,17 +194,26 @@ function PostEditor({
dispatch(fetchCourseCohorts(courseId)); dispatch(fetchCourseCohorts(courseId));
} }
if (editExisting) { if (editExisting) {
dispatch(fetchThread(postId)); dispatchSubmit(fetchThread(postId, courseId));
} }
}, [courseId, editExisting]); }, [courseId, editExisting]);
if (editExisting && !post) { if (editExisting && !post) {
if (submitting) {
return ( return (
<div className="m-4 card p-4 align-items-center"> <div className="m-4 card p-4 align-items-center">
<Spinner animation="border" variant="primary" /> <Spinner animation="border" variant="primary" />
</div> </div>
); );
} }
if (!submitting) {
return (
<EmptyPage
title={intl.formatMessage(messages.noThreadFound)}
/>
);
}
}
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
postType: Yup.mixed() postType: Yup.mixed()

View File

@@ -12,13 +12,13 @@ import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import { API_BASE_URL, Routes } from '../../../data/constants'; import { getApiBaseUrl, Routes } from '../../../data/constants';
import { initializeStore } from '../../../store'; import { initializeStore } from '../../../store';
import { executeThunk } from '../../../test-utils'; import { executeThunk } from '../../../test-utils';
import { getCohortsApiUrl } from '../../cohorts/data/api'; import { getCohortsApiUrl } from '../../cohorts/data/api';
import { DiscussionContext } from '../../common/context'; import { DiscussionContext } from '../../common/context';
import { fetchCourseTopics } from '../../topics/data/thunks'; import { fetchCourseTopics } from '../../topics/data/thunks';
import { threadsApiUrl } from '../data/api'; import { getThreadsApiUrl } from '../data/api';
import { fetchThread } from '../data/thunks'; import { fetchThread } from '../data/thunks';
import { PostEditor } from '../index'; import { PostEditor } from '../index';
@@ -28,7 +28,8 @@ import '../../topics/data/__factories__';
import '../data/__factories__'; import '../data/__factories__';
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`; const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
const threadsApiUrl = getThreadsApiUrl();
let store; let store;
let axiosMock; let axiosMock;

View File

@@ -131,6 +131,11 @@ const messages = defineMessages({
defaultMessage: 'Unnamed subcategory', defaultMessage: 'Unnamed subcategory',
description: 'display string for topics with missing names', description: 'display string for topics with missing names',
}, },
noThreadFound: {
id: 'discussion.thread.notFound',
defaultMessage: 'Thread not found',
description: 'message to show on screen if the request thread is not found in course',
},
}); });
export default messages; export default messages;

View File

@@ -11,11 +11,12 @@ import { AppProvider } from '@edx/frontend-platform/react';
import { initializeStore } from '../../../store'; import { initializeStore } from '../../../store';
import { executeThunk } from '../../../test-utils'; import { executeThunk } from '../../../test-utils';
import { DiscussionContext } from '../../common/context'; import { DiscussionContext } from '../../common/context';
import { courseConfigApiUrl } from '../../data/api'; import { getCourseConfigApiUrl } from '../../data/api';
import { fetchCourseConfig } from '../../data/thunks'; import { fetchCourseConfig } from '../../data/thunks';
import PostLink from './PostLink'; import PostLink from './PostLink';
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const courseConfigApiUrl = getCourseConfigApiUrl();
let store; let store;
let axiosMock; let axiosMock;

View File

@@ -11,8 +11,8 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import { getBlocksAPIResponse } from '../../data/__factories__'; import { getBlocksAPIResponse } from '../../data/__factories__';
import { blocksAPIURL } from '../../data/api'; import { getBlocksAPIURL } from '../../data/api';
import { API_BASE_URL, DiscussionProvider } from '../../data/constants'; import { DiscussionProvider, getApiBaseUrl } from '../../data/constants';
import { selectSequences } from '../../data/selectors'; import { selectSequences } from '../../data/selectors';
import { fetchCourseBlocks } from '../../data/thunks'; import { fetchCourseBlocks } from '../../data/thunks';
import { initializeStore } from '../../store'; import { initializeStore } from '../../store';
@@ -26,8 +26,8 @@ import './data/__factories__';
const courseId = 'course-v1:edX+DemoX+Demo_Course'; const courseId = 'course-v1:edX+DemoX+Demo_Course';
const topicsApiUrl = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`; const topicsApiUrl = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
const topicsv2ApiUrl = `${API_BASE_URL}/api/discussion/v2/course_topics/${courseId}`; const topicsv2ApiUrl = `${getApiBaseUrl()}/api/discussion/v2/course_topics/${courseId}`;
let store; let store;
let axiosMock; let axiosMock;
let lastLocation; let lastLocation;
@@ -113,7 +113,7 @@ describe('TopicsView', () => {
axiosMock axiosMock
.onGet(topicsv2ApiUrl) .onGet(topicsv2ApiUrl)
.reply(200, data); .reply(200, data);
axiosMock.onGet(blocksAPIURL) axiosMock.onGet(getBlocksAPIURL())
.reply(200, getBlocksAPIResponse(true)); .reply(200, getBlocksAPIResponse(true));
axiosMock.onAny().networkError(); axiosMock.onAny().networkError();
await executeThunk(fetchCourseBlocks(courseId, 'abc123'), store.dispatch, store.getState); await executeThunk(fetchCourseBlocks(courseId, 'abc123'), store.dispatch, store.getState);

View File

@@ -1,10 +1,10 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { API_BASE_URL } from '../../../data/constants'; import { getApiBaseUrl } from '../../../data/constants';
export async function getCourseTopics(courseId, topicIds) { export async function getCourseTopics(courseId, topicIds) {
const url = `${API_BASE_URL}/api/discussion/v1/course_topics/${courseId}`; const url = `${getApiBaseUrl()}/api/discussion/v1/course_topics/${courseId}`;
const params = {}; const params = {};
if (topicIds) { if (topicIds) {
params.topic_id = topicIds.join(','); params.topic_id = topicIds.join(',');
@@ -15,7 +15,7 @@ export async function getCourseTopics(courseId, topicIds) {
} }
export async function getCourseTopicsV2(courseId, topicIds) { export async function getCourseTopicsV2(courseId, topicIds) {
const url = `${API_BASE_URL}/api/discussion/v2/course_topics/${courseId}`; const url = `${getApiBaseUrl()}/api/discussion/v2/course_topics/${courseId}`;
const params = {}; const params = {};
if (topicIds) { if (topicIds) {
params.topic_id = topicIds.join(','); params.topic_id = topicIds.join(',');