From 7f4c28f4bafbbfa42b68b7df26492779d48f1468 Mon Sep 17 00:00:00 2001
From: Awais Ansari <79941147+awais-ansari@users.noreply.github.com>
Date: Thu, 3 Jun 2021 17:32:18 +0500
Subject: [PATCH] feat: Add Divide discussions by cohorts funtionality (#122)
* feat: Add Divide discussions by cohorts funtionality
* feat: Destructure values in DivisionByGroupField and remove unnecessary async calls from thunks
* feat: update checkbox component in DivisionByGroupFields
* refactor: change variable names
---
src/i18n/messages/ar.json | 2 +-
src/i18n/messages/es_419.json | 2 +-
src/i18n/messages/fr.json | 2 +-
src/i18n/messages/zh_CN.json | 2 +-
.../discussions/DiscussionsSettings.test.jsx | 4 +-
.../app-config-form/AppConfigForm.jsx | 4 +-
.../apps/legacy/LegacyConfigForm.jsx | 13 +-
.../apps/legacy/LegacyConfigForm.test.jsx | 70 +++++---
.../apps/shared/DivisionByGroupFields.jsx | 154 ++++++++++++++----
.../discussion-topics/DiscussionTopics.jsx | 23 ++-
.../app-config-form/apps/shared/messages.js | 8 +-
.../discussions/data/api.js | 26 +--
.../discussions/data/redux.test.js | 55 +++++--
.../discussions/data/slice.js | 10 +-
.../discussions/data/thunks.js | 19 +--
.../discussions/factories/mockApiResponses.js | 7 +-
16 files changed, 265 insertions(+), 136 deletions(-)
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json
index 0899e37df..c34acdc77 100644
--- a/src/i18n/messages/ar.json
+++ b/src/i18n/messages/ar.json
@@ -171,4 +171,4 @@
"header.label.main.header": "الرئيسية",
"header.label.secondary.nav": "ثانوي",
"header.label.courseOutline": "العودة إلى مخطط المساق في استوديو"
-}
\ No newline at end of file
+}
diff --git a/src/i18n/messages/es_419.json b/src/i18n/messages/es_419.json
index b7d4d67cc..45ea0d5bd 100644
--- a/src/i18n/messages/es_419.json
+++ b/src/i18n/messages/es_419.json
@@ -171,4 +171,4 @@
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.courseOutline": "Back to course outline in Studio"
-}
\ No newline at end of file
+}
diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json
index b7d4d67cc..45ea0d5bd 100644
--- a/src/i18n/messages/fr.json
+++ b/src/i18n/messages/fr.json
@@ -171,4 +171,4 @@
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.courseOutline": "Back to course outline in Studio"
-}
\ No newline at end of file
+}
diff --git a/src/i18n/messages/zh_CN.json b/src/i18n/messages/zh_CN.json
index b7d4d67cc..45ea0d5bd 100644
--- a/src/i18n/messages/zh_CN.json
+++ b/src/i18n/messages/zh_CN.json
@@ -171,4 +171,4 @@
"header.label.main.header": "Main",
"header.label.secondary.nav": "Secondary",
"header.label.courseOutline": "Back to course outline in Studio"
-}
\ No newline at end of file
+}
diff --git a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
index 4a80501ef..1961c134d 100644
--- a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
+++ b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
@@ -24,7 +24,7 @@ import DiscussionsSettings from './DiscussionsSettings';
import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
import initializeStore from '../../store';
import { getAppsUrl } from './data/api';
-import { piazzaApiResponse } from './factories/mockApiResponses';
+import { piazzaApiResponse, legacyApiResponse } from './factories/mockApiResponses';
import appMessages from './app-config-form/messages';
import appListMessages from './app-list/messages';
@@ -117,6 +117,8 @@ describe('DiscussionsSettings', () => {
});
test('successfully advances to settings step for legacy', async () => {
+ axiosMock.onGet(getAppsUrl(courseId)).reply(200, legacyApiResponse);
+ renderComponent();
history.push(`/course/${courseId}/pages-and-resources/discussions`);
// This is an important line that ensures the spinner has been removed - and thus our main
diff --git a/src/pages-and-resources/discussions/app-config-form/AppConfigForm.jsx b/src/pages-and-resources/discussions/app-config-form/AppConfigForm.jsx
index 682f36905..bd1333d6e 100644
--- a/src/pages-and-resources/discussions/app-config-form/AppConfigForm.jsx
+++ b/src/pages-and-resources/discussions/app-config-form/AppConfigForm.jsx
@@ -30,7 +30,7 @@ function AppConfigForm({
const { path: pagesAndResourcesPath } = useContext(PagesAndResourcesContext);
const { params: { appId: routeAppId } } = useRouteMatch();
const {
- selectedAppId, status, saveStatus, discussionTopicIds,
+ selectedAppId, status, saveStatus, discussionTopicIds, divideDiscussionIds,
} = useSelector(state => state.discussions);
const app = useModel('apps', selectedAppId);
// appConfigs have no ID of their own, so we use the active app ID to reference them.
@@ -38,7 +38,7 @@ function AppConfigForm({
// if we're configuring a new app.
const appConfigObj = useModel('appConfigs', selectedAppId);
const discussionTopics = useModels('discussionTopics', discussionTopicIds);
- const appConfig = { ...appConfigObj, discussionTopics };
+ const appConfig = { ...appConfigObj, discussionTopics, divideDiscussionIds };
useEffect(() => {
if (status === LOADED) {
diff --git a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx
index a7cbd1f8c..8e2edd2c9 100644
--- a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx
+++ b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.jsx
@@ -75,7 +75,7 @@ function LegacyConfigForm({
@@ -96,14 +96,13 @@ function LegacyConfigForm({
LegacyConfigForm.propTypes = {
appConfig: PropTypes.shape({
divideByCohorts: PropTypes.bool.isRequired,
- divideCourseWideTopics: PropTypes.bool.isRequired,
- divideGeneralTopic: PropTypes.bool.isRequired,
- divideQuestionsForTAsTopic: PropTypes.bool.isRequired,
+ divideCourseTopicsByCohorts: PropTypes.bool.isRequired,
allowAnonymousPosts: PropTypes.bool.isRequired,
allowAnonymousPostsPeers: PropTypes.bool.isRequired,
blackoutDates: PropTypes.string.isRequired,
discussionTopics: PropTypes.arrayOf(PropTypes.shape({
- name: PropTypes.string.isRequired,
+ name: PropTypes.string,
+ id: PropTypes.string,
})),
}),
onSubmit: PropTypes.func.isRequired,
@@ -117,9 +116,7 @@ LegacyConfigForm.defaultProps = {
appConfig: {
divideByCohorts: false,
allowDivisionByUnit: false,
- divideCourseWideTopics: false,
- divideGeneralTopic: false,
- divideQuestionsForTAsTopic: false,
+ divideCourseTopicsByCohorts: false,
allowAnonymousPosts: false,
allowAnonymousPostsPeers: false,
blackoutDates: '[]',
diff --git a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.test.jsx b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.test.jsx
index 429a621a3..bf8c9da66 100644
--- a/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.test.jsx
+++ b/src/pages-and-resources/discussions/app-config-form/apps/legacy/LegacyConfigForm.test.jsx
@@ -20,15 +20,18 @@ const courseId = 'course-v1:edX+TestX+Test_Course';
const defaultAppConfig = {
id: 'legacy',
divideByCohorts: false,
- divideCourseWideTopics: false,
- divideGeneralTopic: false,
- divideQuestionsForTAsTopic: false,
+ divideCourseTopicsByCohorts: false,
discussionTopics: [
- { name: 'General', id: 'course-generated-id-123-client-made-this-up' },
+ { name: 'General', id: 'course' },
{ name: 'Edx', id: '13f106c6-6735-4e84-b097-0456cff55960' },
],
+ divideDiscussionIds: [
+ 'course',
+ '13f106c6-6735-4e84-b097-0456cff55960',
+ ],
allowAnonymousPosts: false,
allowAnonymousPostsPeers: false,
+ allowDivisionByUnit: false,
blackoutDates: '[]',
};
@@ -89,7 +92,10 @@ describe('LegacyConfigForm', () => {
const handleSubmit = jest.fn();
await mockStore(legacyApiResponse);
- createComponent(defaultAppConfig, handleSubmit, formRef);
+ createComponent({
+ ...defaultAppConfig,
+ divideByCohorts: true,
+ }, handleSubmit, formRef);
await act(async () => {
formRef.current.submit();
@@ -99,7 +105,10 @@ describe('LegacyConfigForm', () => {
// Because we use defaultAppConfig as the initialValues of the form, and we haven't changed
// any of the form inputs, this exact object shape is returned back to us, so we're reusing
// it here. It's not supposed to be 'the same object', it just happens to be.
- defaultAppConfig,
+ {
+ ...defaultAppConfig,
+ divideByCohorts: true,
+ },
);
});
@@ -111,15 +120,13 @@ describe('LegacyConfigForm', () => {
expect(container.querySelector('#divideByCohorts')).toBeInTheDocument();
expect(container.querySelector('#divideByCohorts')).not.toBeChecked();
expect(
- container.querySelector('#divideCourseWideTopics'),
- ).not.toBeInTheDocument();
- expect(
- container.querySelector('#divideGeneralTopic'),
- ).not.toBeInTheDocument();
- expect(
- container.querySelector('#divideQuestionsForTAsTopic'),
+ container.querySelector('#divideCourseTopicsByCohorts'),
).not.toBeInTheDocument();
+ defaultAppConfig.divideDiscussionIds.forEach(id => expect(
+ container.querySelector(`#checkbox-${id}`),
+ ).not.toBeInTheDocument());
+
// AnonymousPostingFields
expect(container.querySelector('#allowAnonymousPosts')).toBeInTheDocument();
expect(container.querySelector('#allowAnonymousPosts')).not.toBeChecked();
@@ -144,20 +151,16 @@ describe('LegacyConfigForm', () => {
expect(container.querySelector('#divideByCohorts')).toBeInTheDocument();
expect(container.querySelector('#divideByCohorts')).toBeChecked();
expect(
- container.querySelector('#divideCourseWideTopics'),
+ container.querySelector('#divideCourseTopicsByCohorts'),
).toBeInTheDocument();
expect(
- container.querySelector('#divideCourseWideTopics'),
- ).not.toBeChecked();
- expect(container.querySelector('#divideGeneralTopic')).toBeInTheDocument();
- expect(container.querySelector('#divideGeneralTopic')).not.toBeChecked();
- expect(
- container.querySelector('#divideQuestionsForTAsTopic'),
- ).toBeInTheDocument();
- expect(
- container.querySelector('#divideQuestionsForTAsTopic'),
+ container.querySelector('#divideCourseTopicsByCohorts'),
).not.toBeChecked();
+ defaultAppConfig.divideDiscussionIds.forEach(id => expect(
+ container.querySelector(`#checkbox-${id}`),
+ ).not.toBeInTheDocument());
+
// AnonymousPostingFields
expect(container.querySelector('#allowAnonymousPosts')).toBeInTheDocument();
expect(container.querySelector('#allowAnonymousPosts')).toBeChecked();
@@ -168,4 +171,25 @@ describe('LegacyConfigForm', () => {
container.querySelector('#allowAnonymousPostsPeers'),
).not.toBeChecked();
});
+
+ test('folded discussion topics are in the DOM when divideByCohorts and divideCourseWideTopicsare enabled',
+ async () => {
+ await mockStore(legacyApiResponse);
+ createComponent({
+ ...defaultAppConfig,
+ divideByCohorts: true,
+ divideCourseTopicsByCohorts: true,
+ });
+
+ // DivisionByGroupFields
+ expect(container.querySelector('#divideByCohorts')).toBeInTheDocument();
+ expect(container.querySelector('#divideByCohorts')).toBeChecked();
+ expect(container.querySelector('#divideCourseTopicsByCohorts')).toBeInTheDocument();
+ expect(container.querySelector('#divideCourseTopicsByCohorts')).toBeChecked();
+
+ defaultAppConfig.divideDiscussionIds.forEach(id => {
+ expect(container.querySelector(`#checkbox-${id}`)).toBeInTheDocument();
+ expect(container.querySelector(`#checkbox-${id}`)).toBeChecked();
+ });
+ });
});
diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/DivisionByGroupFields.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/DivisionByGroupFields.jsx
index 61f40fe44..faaaa3f1d 100644
--- a/src/pages-and-resources/discussions/app-config-form/apps/shared/DivisionByGroupFields.jsx
+++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/DivisionByGroupFields.jsx
@@ -1,61 +1,142 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form, TransitionReplace } from '@edx/paragon';
+import { FieldArray, useFormikContext } from 'formik';
import FormSwitchGroup from '../../../../../generic/FormSwitchGroup';
import messages from './messages';
import AppConfigFormDivider from './AppConfigFormDivider';
+import { updateDividedDiscussionsIds } from '../../../data/slice';
+import { updateModel } from '../../../../../generic/model-store';
function DivisionByGroupFields({
- onBlur,
- onChange,
- intl,
- values,
+ onBlur, onChange, intl, appConfig,
}) {
+ const dispatch = useDispatch();
+ const { setFieldValue } = useFormikContext();
+ const {
+ divideDiscussionIds,
+ discussionTopics,
+ divideByCohorts,
+ divideCourseTopicsByCohorts,
+ } = appConfig;
+
+ useEffect(() => {
+ dispatch(updateDividedDiscussionsIds({ divideDiscussionIds }));
+ }, [divideDiscussionIds]);
+
+ useEffect(() => {
+ const discussionTopicIds = discussionTopics.map(
+ (topic) => topic.id,
+ );
+ const divideCourseTopicsByCohortsOff = (
+ discussionTopicIds.length === divideDiscussionIds.length
+ && discussionTopicIds.every((topicId) => divideDiscussionIds.includes(topicId))
+ ) || !divideDiscussionIds.length;
+
+ if (divideByCohorts) {
+ if (divideCourseTopicsByCohortsOff && !divideCourseTopicsByCohorts) {
+ setFieldValue('divideCourseTopicsByCohorts', false);
+ setFieldValue('divideDiscussionIds', discussionTopicIds);
+ } else {
+ setFieldValue('divideCourseTopicsByCohorts', true);
+ }
+ } else {
+ setFieldValue('divideDiscussionIds', []);
+ setFieldValue('divideCourseTopicsByCohorts', false);
+ }
+
+ const {
+ divideDiscussionIds: courseDividedDiscussionsIds,
+ discussionTopics: courseDiscussionTopics,
+ ...payload
+ } = appConfig;
+ dispatch(updateModel({ modelType: 'appConfigs', model: payload }));
+ }, [
+ divideByCohorts,
+ divideCourseTopicsByCohorts,
+ discussionTopics,
+ ]);
+
+ const handleCheckBoxToggle = (event, push, remove) => {
+ const { checked, value } = event.target;
+ if (checked) {
+ push(value);
+ } else {
+ remove(divideDiscussionIds.indexOf(value));
+ }
+ };
+
+ const handleDivideCourseTopicsByCohortsToggle = (event) => {
+ const { checked } = event.target;
+ if (!checked) {
+ setFieldValue('divideDiscussionIds', []);
+ }
+ onChange(event);
+ };
+
return (
<>
-
{intl.formatMessage(messages.divisionByGroup)}
-
+
+ {intl.formatMessage(messages.divisionByGroup)}
+
- {values.divideByCohorts ? (
+ {divideByCohorts ? (
handleDivideCourseTopicsByCohortsToggle(event)}
onBlur={onBlur}
className="ml-4 mt-3"
- id="divideCourseWideTopics"
- checked={values.divideCourseWideTopics}
- label={intl.formatMessage(messages.divideCourseWideTopicsLabel)}
- helpText={intl.formatMessage(messages.divideCourseWideTopicsHelp)}
+ id="divideCourseTopicsByCohorts"
+ checked={divideCourseTopicsByCohorts}
+ label={intl.formatMessage(messages.divideCourseTopicsByCohortsLabel)}
+ helpText={intl.formatMessage(messages.divideCourseTopicsByCohortsHelp)}
/>
-
-
-
-
+
+ {divideCourseTopicsByCohorts ? (
+
+ (
+
+ handleCheckBoxToggle(event, push, remove)}
+ onBlur={onBlur}
+ defaultValue={divideDiscussionIds}
+ >
+ {discussionTopics.map((topic) => (
+
+ {topic.name}
+
+ ))}
+
+
+ )}
+ />
+
+ ) : (
+
+ )}
+
- ) : }
+ ) : (
+
+ )}
>
);
@@ -65,11 +146,14 @@ DivisionByGroupFields.propTypes = {
onBlur: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
intl: intlShape.isRequired,
- values: PropTypes.shape({
+ appConfig: PropTypes.shape({
divideByCohorts: PropTypes.bool,
- divideCourseWideTopics: PropTypes.bool,
- divideGeneralTopic: PropTypes.bool,
- divideQuestionsForTAsTopic: PropTypes.bool,
+ divideCourseTopicsByCohorts: PropTypes.bool,
+ discussionTopics: PropTypes.arrayOf(PropTypes.shape({
+ name: PropTypes.string,
+ id: PropTypes.string,
+ })),
+ divideDiscussionIds: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
};
diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx
index 090132d9c..dc1f02e98 100644
--- a/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx
+++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/discussion-topics/DiscussionTopics.jsx
@@ -8,31 +8,36 @@ import { v4 as uuid } from 'uuid';
import messages from '../messages';
import TopicItem from './TopicItem';
-import { removeModel } from '../../../../../../generic/model-store';
+import { removeModel, updateModels } from '../../../../../../generic/model-store';
import { updateDiscussionTopicIds } from '../../../../data/slice';
-import { updatedDiscussionTopics } from '../../../../data/thunks';
const DiscussionTopics = ({ intl }) => {
const dispatch = useDispatch();
- const { values } = useFormikContext();
- const [topics, setTopics] = useState(values.discussionTopics);
+ const { values: appConfig, setFieldValue } = useFormikContext();
+ const { discussionTopics, divideDiscussionIds } = appConfig;
+ const [topics, setTopics] = useState(discussionTopics);
useEffect(() => {
- const updatedDiscussionTopicIds = values.discussionTopics?.map(topic => topic.id);
+ const updatedDiscussionTopicIds = discussionTopics.map(topic => topic.id);
- setTopics(values.discussionTopics);
- dispatch(updateDiscussionTopicIds(updatedDiscussionTopicIds));
- dispatch(updatedDiscussionTopics(values.discussionTopics));
- }, [values.discussionTopics]);
+ setTopics(discussionTopics);
+ dispatch(updateDiscussionTopicIds({ updatedDiscussionTopicIds }));
+ dispatch(updateModels({ modelType: 'discussionTopics', models: discussionTopics }));
+ }, [discussionTopics]);
const handleTopicDelete = (topicIndex, topicId, remove) => {
remove(topicIndex);
dispatch(removeModel({ modelType: 'discussionTopics', id: topicId }));
+ const updatedDividedDiscussionsIds = divideDiscussionIds.filter(
+ (id) => id !== topicId,
+ );
+ setFieldValue('divideDiscussionIds', updatedDividedDiscussionsIds);
};
const addNewTopic = (push) => {
const payload = { name: '', id: uuid() };
push(payload);
+ setFieldValue('divideDiscussionIds', [...divideDiscussionIds, payload.id]);
};
return (
diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/messages.js b/src/pages-and-resources/discussions/app-config-form/apps/shared/messages.js
index 99c5f08b2..1b3c109fb 100644
--- a/src/pages-and-resources/discussions/app-config-form/apps/shared/messages.js
+++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/messages.js
@@ -16,13 +16,13 @@ const messages = defineMessages({
defaultMessage: 'Learners will only be able to view and respond to discussions posted by members of their cohort.',
description: 'Help text for a switch that enables dividing discussions by cohorts.',
},
- divideCourseWideTopicsLabel: {
- id: 'authoring.discussions.builtIn.divideCourseWideTopics.label',
+ divideCourseTopicsByCohortsLabel: {
+ id: 'authoring.discussions.builtIn.divideCourseTopicsByCohorts.label',
defaultMessage: 'Divide course wide discussion topics',
description: 'Label for a switch that enables dividing course wide topics by cohorts.',
},
- divideCourseWideTopicsHelp: {
- id: 'authoring.discussions.builtIn.divideCourseWideTopics.help',
+ divideCourseTopicsByCohortsHelp: {
+ id: 'authoring.discussions.builtIn.divideCourseTopicsByCohorts.help',
defaultMessage: 'Choose which of your general course wide discussion topics you would like to divide.',
description: 'Help text asking the user to pick course-wide topics that should be divided by cohort.',
},
diff --git a/src/pages-and-resources/discussions/data/api.js b/src/pages-and-resources/discussions/data/api.js
index 56466138c..e27337159 100644
--- a/src/pages-and-resources/discussions/data/api.js
+++ b/src/pages-and-resources/discussions/data/api.js
@@ -33,22 +33,13 @@ function normalizePluginConfig(data) {
if (!data || Object.keys(data).length < 1) {
return {};
}
-
return {
allowAnonymousPosts: data.allow_anonymous,
allowAnonymousPostsPeers: data.allow_anonymous_to_peers,
blackoutDates: JSON.stringify(data.discussion_blackouts),
- // TODO: We need all these added to the API. ... default them for now
- divideByCohorts: false,
allowDivisionByUnit: false,
- divideCourseWideTopics: false,
- // TODO: Note that these last two are in the `discussion_topics` data, but we haven't been able
- // to add them properly here yet. I'm not sure the data is in a usable state as is, since it
- // only seems to include the topics for which these would be "true". Assuming its only these
- // two, I think we could check for the existence of "General" for the first, but I'm not sure
- // what the topic title is for the second. "Questions for TAs" maybe?
- divideGeneralTopic: false,
- divideQuestionsForTAsTopic: false,
+ divideByCohorts: data.divided_course_wide_discussions.length > 0,
+ divideCourseTopicsByCohorts: false,
};
}
@@ -77,19 +68,11 @@ function normalizeApps(data) {
? extractDiscussionTopicIds(data.plugin_configuration.discussion_topics) : [],
discussionTopics: data.plugin_configuration.discussion_topics
? normalizeDiscussionTopic(data.plugin_configuration.discussion_topics) : [],
+ divideDiscussionIds: data.plugin_configuration.divided_course_wide_discussions,
};
}
function denormalizeData(courseId, appId, data) {
- /*
- TODO: What about these? Some are from the instructor dashboard, presumably... but I don't think they all are.
-
- divideByCohorts
- allowDivisionByUnit
- divideCourseWideTopics
- divideGeneralTopic
- divideQuestionsForTAsTopic
- */
const pluginConfiguration = {};
if (data.allowAnonymousPosts) {
@@ -108,6 +91,9 @@ function denormalizeData(courseId, appId, data) {
return newTopics;
}, {});
}
+ if (data.divideDiscussionIds) {
+ pluginConfiguration.divided_course_wide_discussions = data.divideDiscussionIds;
+ }
const ltiConfiguration = {};
diff --git a/src/pages-and-resources/discussions/data/redux.test.js b/src/pages-and-resources/discussions/data/redux.test.js
index 8d630c315..b0716b4cd 100644
--- a/src/pages-and-resources/discussions/data/redux.test.js
+++ b/src/pages-and-resources/discussions/data/redux.test.js
@@ -61,6 +61,8 @@ const piazzaApp = {
let axiosMock;
let store;
+let divideDiscussionIds;
+let discussionTopicIds;
describe('Data layer integration tests', () => {
beforeEach(() => {
@@ -76,6 +78,14 @@ describe('Data layer integration tests', () => {
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
store = initializeStore();
+ divideDiscussionIds = [
+ '13f106c6-6735-4e84-b097-0456cff55960',
+ 'course',
+ ];
+ discussionTopicIds = [
+ '13f106c6-6735-4e84-b097-0456cff55960',
+ 'course',
+ ];
});
afterEach(() => {
@@ -158,10 +168,8 @@ describe('Data layer integration tests', () => {
status: LOADED,
saveStatus: SAVED,
hasValidationError: false,
- discussionTopicIds: [
- '13f106c6-6735-4e84-b097-0456cff55960',
- 'course-generated-id-123-client-made-this-up',
- ],
+ discussionTopicIds,
+ divideDiscussionIds,
});
expect(store.getState().models.apps.legacy).toEqual(legacyApp);
expect(store.getState().models.apps.piazza).toEqual(piazzaApp);
@@ -173,11 +181,9 @@ describe('Data layer integration tests', () => {
blackoutDates: '[]',
// TODO: Note! As of this writing, all the data below this line is NOT returned in the API
// but we add it in during normalization.
- divideByCohorts: false,
+ divideByCohorts: true,
allowDivisionByUnit: false,
- divideCourseWideTopics: false,
- divideGeneralTopic: false,
- divideQuestionsForTAsTopic: false,
+ divideCourseTopicsByCohorts: false,
});
});
});
@@ -324,6 +330,14 @@ describe('Data layer integration tests', () => {
allow_anonymous: true,
allow_anonymous_to_peers: true,
discussion_blackouts: [['2015-09-15', '2015-09-21'], ['2015-10-01', '2015-10-08']],
+ discussion_topics: {
+ Edx: { id: '13f106c6-6735-4e84-b097-0456cff55960' },
+ General: { id: 'course' },
+ },
+ divided_course_wide_discussions: [
+ '13f106c6-6735-4e84-b097-0456cff55960',
+ 'course',
+ ],
},
provider_type: 'legacy',
}).reply(200, {
@@ -333,6 +347,14 @@ describe('Data layer integration tests', () => {
allow_anonymous: true,
allow_anonymous_to_peers: true,
discussion_blackouts: [['2015-09-15', '2015-09-21'], ['2015-10-01', '2015-10-08']],
+ discussion_topics: {
+ Edx: { id: '13f106c6-6735-4e84-b097-0456cff55960' },
+ General: { id: 'course' },
+ },
+ divided_course_wide_discussions: [
+ '13f106c6-6735-4e84-b097-0456cff55960',
+ 'course',
+ ],
},
});
@@ -350,9 +372,12 @@ describe('Data layer integration tests', () => {
// but we technically send it to the thunk, so here it is.
divideByCohorts: true,
allowDivisionByUnit: true,
- divideCourseWideTopics: true,
- divideGeneralTopic: true,
- divideQuestionsForTAsTopic: true,
+ divideCourseTopicsByCohorts: false,
+ divideDiscussionIds,
+ discussionTopics: [
+ { name: 'Edx', id: '13f106c6-6735-4e84-b097-0456cff55960' },
+ { name: 'General', id: 'course' },
+ ],
},
pagesAndResourcesPath,
), store.dispatch);
@@ -367,6 +392,8 @@ describe('Data layer integration tests', () => {
status: LOADED,
saveStatus: SAVED,
hasValidationError: false,
+ divideDiscussionIds,
+ discussionTopicIds,
}),
);
expect(store.getState().models.appConfigs.legacy).toEqual({
@@ -377,11 +404,9 @@ describe('Data layer integration tests', () => {
blackoutDates: '[["2015-09-15","2015-09-21"],["2015-10-01","2015-10-08"]]',
// TODO: Note! The values we tried to save were ignored, this test reflects what currently
// happens, but NOT what we want to have happen!
- divideByCohorts: false,
+ divideByCohorts: true,
allowDivisionByUnit: false,
- divideCourseWideTopics: false,
- divideGeneralTopic: false,
- divideQuestionsForTAsTopic: false,
+ divideCourseTopicsByCohorts: false,
});
});
});
diff --git a/src/pages-and-resources/discussions/data/slice.js b/src/pages-and-resources/discussions/data/slice.js
index e6dcfa0a6..1d79b06ed 100644
--- a/src/pages-and-resources/discussions/data/slice.js
+++ b/src/pages-and-resources/discussions/data/slice.js
@@ -24,6 +24,7 @@ const slice = createSlice({
// ValidationError is the Flag that represents a form validation status.
hasValidationError: false,
discussionTopicIds: [],
+ divideDiscussionIds: [],
},
reducers: {
loadApps: (state, { payload }) => {
@@ -33,6 +34,7 @@ const slice = createSlice({
state.status = LOADED;
state.saveStatus = SAVED;
state.discussionTopicIds = payload.discussionTopicIds;
+ state.divideDiscussionIds = payload.divideDiscussionIds;
},
selectApp: (state, { payload }) => {
const { appId } = payload;
@@ -51,7 +53,12 @@ const slice = createSlice({
state.hasValidationError = hasError;
},
updateDiscussionTopicIds: (state, { payload }) => {
- state.discussionTopicIds = payload;
+ const { updatedDiscussionTopicIds } = payload;
+ state.discussionTopicIds = updatedDiscussionTopicIds;
+ },
+ updateDividedDiscussionsIds: (state, { payload }) => {
+ const { divideDiscussionIds } = payload;
+ state.divideDiscussionIds = divideDiscussionIds;
},
},
});
@@ -63,6 +70,7 @@ export const {
updateSaveStatus,
updateValidationStatus,
updateDiscussionTopicIds,
+ updateDividedDiscussionsIds,
} = slice.actions;
export const {
diff --git a/src/pages-and-resources/discussions/data/thunks.js b/src/pages-and-resources/discussions/data/thunks.js
index 246842ca2..6e58e44e7 100644
--- a/src/pages-and-resources/discussions/data/thunks.js
+++ b/src/pages-and-resources/discussions/data/thunks.js
@@ -1,8 +1,5 @@
import { history } from '@edx/frontend-platform';
-import {
- addModel, addModels, updateModels,
-} from '../../../generic/model-store';
-
+import { addModel, addModels } from '../../../generic/model-store';
import { getApps, postAppConfig } from './api';
import {
FAILED,
@@ -26,11 +23,12 @@ export function fetchApps(courseId) {
appConfig,
discussionTopicIds,
discussionTopics,
+ divideDiscussionIds,
} = await getApps(courseId);
+ dispatch(addModel({ modelType: 'appConfigs', model: appConfig }));
dispatch(addModels({ modelType: 'apps', models: apps }));
dispatch(addModels({ modelType: 'features', models: features }));
- dispatch(addModel({ modelType: 'appConfigs', model: appConfig }));
dispatch(addModels({ modelType: 'discussionTopics', models: discussionTopics }));
dispatch(loadApps({
@@ -38,6 +36,7 @@ export function fetchApps(courseId) {
appIds: apps.map(app => app.id),
featureIds: features.map(feature => feature.id),
discussionTopicIds,
+ divideDiscussionIds,
}));
} catch (error) {
if (error.response && error.response.status === 403) {
@@ -61,11 +60,12 @@ export function saveAppConfig(courseId, appId, drafts, successPath) {
appConfig,
discussionTopicIds,
discussionTopics,
+ divideDiscussionIds,
} = await postAppConfig(courseId, appId, drafts);
+ dispatch(addModel({ modelType: 'appConfigs', model: appConfig }));
dispatch(addModels({ modelType: 'apps', models: apps }));
dispatch(addModels({ modelType: 'features', models: features }));
- dispatch(addModel({ modelType: 'appConfigs', model: appConfig }));
dispatch(addModels({ modelType: 'discussionTopics', models: discussionTopics }));
dispatch(loadApps({
@@ -73,6 +73,7 @@ export function saveAppConfig(courseId, appId, drafts, successPath) {
appIds: apps.map(app => app.id),
featureIds: features.map(feature => feature.id),
discussionTopicIds,
+ divideDiscussionIds,
}));
dispatch(updateSaveStatus({ status: SAVED }));
// Note that we redirect here to avoid having to work with the promise over in AppConfigForm.
@@ -88,9 +89,3 @@ export function saveAppConfig(courseId, appId, drafts, successPath) {
}
};
}
-
-export function updatedDiscussionTopics(payload) {
- return async (dispatch) => {
- dispatch(updateModels({ modelType: 'discussionTopics', models: payload }));
- };
-}
diff --git a/src/pages-and-resources/discussions/factories/mockApiResponses.js b/src/pages-and-resources/discussions/factories/mockApiResponses.js
index 585c8e5ed..ac731e9fa 100644
--- a/src/pages-and-resources/discussions/factories/mockApiResponses.js
+++ b/src/pages-and-resources/discussions/factories/mockApiResponses.js
@@ -52,9 +52,12 @@ export const legacyApiResponse = {
available_division_schemes: ['enrollment_track'],
discussion_topics: {
Edx: { id: '13f106c6-6735-4e84-b097-0456cff55960' },
- General: { id: 'course-generated-id-123-client-made-this-up' },
+ General: { id: 'course' },
},
- divided_course_wide_discussions: [],
+ divided_course_wide_discussions: [
+ '13f106c6-6735-4e84-b097-0456cff55960',
+ 'course',
+ ],
divided_inline_discussions: [],
division_scheme: 'none',
// Note, this gets stringified when normalized into the app, but the API returns it as an