Skip to content

Commit 5061a93

Browse files
authored
Merge pull request #5001 from ozer550/implement-free-response-type-question-survey
Implement free response type question survey
2 parents 9adee5a + 92a35c8 commit 5061a93

File tree

11 files changed

+172
-29
lines changed

11 files changed

+172
-29
lines changed

contentcuration/contentcuration/frontend/channelEdit/components/AssessmentEditor/AssessmentEditor.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
:item="item"
4646
:errors="itemErrors(item)"
4747
:openDialog="openDialog"
48+
:nodeId="nodeId"
4849
data-test="editor"
4950
@update="onItemUpdate"
5051
@close="closeActiveItem"
@@ -207,7 +208,6 @@
207208
if (!this.items) {
208209
return [];
209210
}
210-
211211
return [...this.items].sort((item1, item2) => (item1.order > item2.order ? 1 : -1));
212212
},
213213
firstItem() {

contentcuration/contentcuration/frontend/channelEdit/components/AssessmentItemEditor/AssessmentItemEditor.vue

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
</VFlex>
6767
</VLayout>
6868

69-
<VLayout mt-4>
69+
<VLayout v-if="kind !== AssessmentItemTypes.FREE_RESPONSE" mt-4>
7070
<VFlex>
7171
<ErrorList
7272
:errors="answersErrorMessages"
@@ -112,7 +112,12 @@
112112
import translator from '../../translator';
113113
import { updateAnswersToQuestionType, assessmentItemKey } from '../../utils';
114114
import { AssessmentItemTypeLabels } from '../../constants';
115-
import { AssessmentItemTypes, ValidationErrors } from 'shared/constants';
115+
import {
116+
ContentModalities,
117+
AssessmentItemTypes,
118+
ValidationErrors,
119+
FeatureFlagKeys,
120+
} from 'shared/constants';
116121
import ErrorList from 'shared/views/ErrorList/ErrorList';
117122
import Uploader from 'shared/views/files/Uploader';
118123
import MarkdownEditor from 'shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor';
@@ -147,6 +152,10 @@
147152
* ...
148153
* }
149154
*/
155+
nodeId: {
156+
type: String,
157+
required: true,
158+
},
150159
item: {
151160
type: Object,
152161
default: null,
@@ -184,10 +193,13 @@
184193
openHintIdx: null,
185194
openAnswerIdx: null,
186195
kindSelectKey: 0,
196+
AssessmentItemTypes,
187197
};
188198
},
189199
computed: {
190200
...mapGetters('file', ['getFileUpload']),
201+
...mapGetters(['hasFeatureEnabled']),
202+
...mapGetters('contentNode', ['getContentNode']),
191203
question() {
192204
if (!this.item || !this.item.question) {
193205
return '';
@@ -198,6 +210,9 @@
198210
imagePreset() {
199211
return FormatPresetsNames.EXERCISE_IMAGE;
200212
},
213+
modality() {
214+
return this.getContentNode(this.nodeId)?.extra_fields?.options?.modality;
215+
},
201216
kind() {
202217
if (!this.item || !this.item.type) {
203218
return AssessmentItemTypes.SINGLE_SELECTION;
@@ -206,7 +221,7 @@
206221
return this.item.type;
207222
},
208223
kindSelectItems() {
209-
return [
224+
const items = [
210225
{
211226
value: AssessmentItemTypes.SINGLE_SELECTION,
212227
text: translator.$tr(AssessmentItemTypeLabels[AssessmentItemTypes.SINGLE_SELECTION]),
@@ -224,6 +239,18 @@
224239
text: translator.$tr(AssessmentItemTypeLabels[AssessmentItemTypes.TRUE_FALSE]),
225240
},
226241
];
242+
243+
if (
244+
this.hasFeatureEnabled(FeatureFlagKeys.survey) &&
245+
this.modality === ContentModalities.SURVEY
246+
) {
247+
items.push({
248+
value: AssessmentItemTypes.FREE_RESPONSE,
249+
text: translator.$tr(AssessmentItemTypeLabels[AssessmentItemTypes.FREE_RESPONSE]),
250+
});
251+
}
252+
253+
return items;
227254
},
228255
answers() {
229256
if (!this.item || !this.item.answers) {
@@ -244,8 +271,12 @@
244271
245272
if (this.errors && this.errors.includes(ValidationErrors.QUESTION_REQUIRED)) {
246273
errorMessages.push(translator.$tr(`errorQuestionRequired`));
274+
} else if (
275+
this.errors &&
276+
this.errors.includes(ValidationErrors.INVALID_COMPLETION_TYPE_FOR_FREE_RESPONSE_QUESTION)
277+
) {
278+
errorMessages.push(translator.$tr(`errorInvalidQuestionType`));
247279
}
248-
249280
return errorMessages;
250281
},
251282
answersErrorMessages() {
@@ -292,12 +323,10 @@
292323
...assessmentItemKey(this.item),
293324
...payload,
294325
};
295-
296326
this.$emit('update', payload);
297327
},
298328
changeKind(newKind) {
299329
const newAnswers = updateAnswersToQuestionType(newKind, this.answers);
300-
301330
this.closeAnswer();
302331
this.updateItem({
303332
type: newKind,
@@ -365,6 +394,21 @@
365394
366395
break;
367396
397+
case AssessmentItemTypes.FREE_RESPONSE:
398+
if (typeof this.openDialog === 'function' && this.answers.length > 0) {
399+
this.openDialog({
400+
title: this.$tr('dialogTitle'),
401+
message: this.$tr('dialogMessageChangeToFreeResponse'),
402+
submitLabel: this.$tr('dialogSubmitBtnLabel'),
403+
onSubmit: () => this.changeKind(newKind),
404+
onCancel: this.rerenderKindSelect,
405+
});
406+
} else {
407+
this.changeKind(newKind);
408+
}
409+
410+
break;
411+
368412
default:
369413
this.changeKind(newKind);
370414
break;
@@ -415,6 +459,8 @@
415459
"Switching to 'true or false' will remove all current answers. Continue?",
416460
dialogMessageChangeToInput:
417461
"Switching to 'numeric input' will set all answers as correct and remove all non-numeric answers. Continue?",
462+
dialogMessageChangeToFreeResponse:
463+
"Switching to 'free response' will remove all current answers. Continue?",
418464
},
419465
};
420466

contentcuration/contentcuration/frontend/channelEdit/components/AssessmentTab/AssessmentTab.vue

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,21 +91,26 @@
9191
return this.getAssessmentItems(this.nodeId);
9292
},
9393
areAssessmentItemsValid() {
94-
return this.getAssessmentItemsAreValid({ contentNodeId: this.nodeId, ignoreDelayed: true });
94+
return this.getAssessmentItemsAreValid({
95+
contentNodeId: this.nodeId,
96+
ignoreDelayed: true,
97+
});
9598
},
9699
assessmentItemsErrors() {
97-
return this.getAssessmentItemsErrors({ contentNodeId: this.nodeId, ignoreDelayed: true });
100+
const errorMap = this.getAssessmentItemsErrors({
101+
contentNodeId: this.nodeId,
102+
ignoreDelayed: true,
103+
});
104+
return errorMap;
98105
},
99106
invalidItemsErrorMessage() {
100107
const invalidItemsCount = this.getInvalidAssessmentItemsCount({
101108
contentNodeId: this.nodeId,
102109
ignoreDelayed: true,
103110
});
104-
105111
if (!invalidItemsCount) {
106112
return '';
107113
}
108-
109114
return this.$tr('incompleteItemsCountMessage', { invalidItemsCount });
110115
},
111116
},

contentcuration/contentcuration/frontend/channelEdit/components/edit/EditView.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@
7878
</li>
7979
</ul>
8080
</VAlert>
81-
<DetailsTabView :key="nodeIds.join('-')" ref="detailsTab" :nodeIds="nodeIds" />
81+
<DetailsTabView
82+
:key="nodeIds.join('-')"
83+
ref="detailsTab"
84+
:nodeIds="nodeIds"
85+
/>
8286
</VTabItem>
8387
<VTabItem :key="tabs.QUESTIONS" ref="questionwindow" :value="tabs.QUESTIONS" lazy>
8488
<AssessmentTab :nodeId="nodeIds[0]" />

contentcuration/contentcuration/frontend/channelEdit/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const AssessmentItemTypeLabels = {
4848
[AssessmentItemTypes.TRUE_FALSE]: 'questionTypeTrueFalse',
4949
[AssessmentItemTypes.INPUT_QUESTION]: 'questionTypeInput',
5050
[AssessmentItemTypes.PERSEUS_QUESTION]: 'questionTypePerseus',
51+
[AssessmentItemTypes.FREE_RESPONSE]: 'questionTypeFreeResponse',
5152
};
5253

5354
export const TabNames = {

contentcuration/contentcuration/frontend/channelEdit/translator.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ const MESSAGES = {
1010
questionTypeTrueFalse: 'True/False',
1111
questionTypeInput: 'Numeric input',
1212
questionTypePerseus: 'Perseus',
13+
questionTypeFreeResponse: 'Free response',
1314
errorQuestionRequired: 'Question is required',
15+
errorInvalidQuestionType: 'Invalid question type',
1416
errorMissingAnswer: 'Choose a correct answer',
1517
errorChooseAtLeastOneCorrectAnswer: 'Choose at least one correct answer',
1618
errorProvideAtLeastOneCorrectAnswer: 'Provide at least one correct answer',

contentcuration/contentcuration/frontend/channelEdit/utils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ export function updateAnswersToQuestionType(questionType, answers) {
9393
}
9494
}
9595

96+
if (questionType === AssessmentItemTypes.FREE_RESPONSE) {
97+
return [];
98+
}
99+
96100
const answersCopy = JSON.parse(JSON.stringify(answers));
97101

98102
switch (questionType) {

contentcuration/contentcuration/frontend/channelEdit/vuex/assessmentItem/__tests__/getters.spec.js

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AssessmentItemTypes, DELAYED_VALIDATION, ValidationErrors } from 'share
99

1010
describe('assessmentItem getters', () => {
1111
let state;
12+
let rootGetters;
1213

1314
beforeEach(() => {
1415
state = {
@@ -83,6 +84,13 @@ describe('assessmentItem getters', () => {
8384
},
8485
},
8586
};
87+
88+
rootGetters = {
89+
'contentNode/getContentNode': id => ({
90+
id,
91+
kind: 'exercise',
92+
}),
93+
};
8694
});
8795

8896
describe('getAssessmentItems', () => {
@@ -136,7 +144,9 @@ describe('assessmentItem getters', () => {
136144

137145
describe('getAssessmentItemsErrors', () => {
138146
it('returns validation codes corresponding to invalid assessment items of a content node', () => {
139-
expect(getAssessmentItemsErrors(state)({ contentNodeId: 'content-node-id-2' })).toEqual({
147+
expect(
148+
getAssessmentItemsErrors(state, {}, {}, rootGetters)({ contentNodeId: 'content-node-id-2' })
149+
).toEqual({
140150
'assessment-id-2': [
141151
ValidationErrors.QUESTION_REQUIRED,
142152
ValidationErrors.INVALID_NUMBER_OF_CORRECT_ANSWERS,
@@ -147,7 +157,12 @@ describe('assessmentItem getters', () => {
147157

148158
it("doesn't include invalid nodes errors that are new if `ignoreDelayed` set to true", () => {
149159
expect(
150-
getAssessmentItemsErrors(state)({ contentNodeId: 'content-node-id-2', ignoreDelayed: true })
160+
getAssessmentItemsErrors(
161+
state,
162+
{},
163+
{},
164+
rootGetters
165+
)({ contentNodeId: 'content-node-id-2', ignoreDelayed: true })
151166
).toEqual({
152167
'assessment-id-2': [
153168
ValidationErrors.QUESTION_REQUIRED,
@@ -160,12 +175,24 @@ describe('assessmentItem getters', () => {
160175

161176
describe('getInvalidAssessmentItemsCount', () => {
162177
it('returns a correct number of invalid assessment items of a content node', () => {
163-
expect(getInvalidAssessmentItemsCount(state)({ contentNodeId: 'content-node-id-2' })).toBe(2);
178+
expect(
179+
getInvalidAssessmentItemsCount(
180+
state,
181+
{},
182+
{},
183+
rootGetters
184+
)({ contentNodeId: 'content-node-id-2' })
185+
).toBe(2);
164186
});
165187

166188
it("doesn't count invalid nodes that are new if `ignoreDelayed` set to true", () => {
167189
expect(
168-
getInvalidAssessmentItemsCount(state)({
190+
getInvalidAssessmentItemsCount(
191+
state,
192+
{},
193+
{},
194+
rootGetters
195+
)({
169196
contentNodeId: 'content-node-id-2',
170197
ignoreDelayed: true,
171198
})
@@ -175,16 +202,35 @@ describe('assessmentItem getters', () => {
175202

176203
describe('getAssessmentItemsAreValid', () => {
177204
it('returns true if all assessment items of a content node are valid', () => {
178-
expect(getAssessmentItemsAreValid(state)({ contentNodeId: 'content-node-id-1' })).toBe(true);
205+
expect(
206+
getAssessmentItemsAreValid(
207+
state,
208+
{},
209+
{},
210+
rootGetters
211+
)({ contentNodeId: 'content-node-id-1' })
212+
).toBe(true);
179213
});
180214

181215
it('returns false if all assessment items of a content node are not valid', () => {
182-
expect(getAssessmentItemsAreValid(state)({ contentNodeId: 'content-node-id-2' })).toBe(false);
216+
expect(
217+
getAssessmentItemsAreValid(
218+
state,
219+
{},
220+
{},
221+
rootGetters
222+
)({ contentNodeId: 'content-node-id-2' })
223+
).toBe(false);
183224
});
184225

185226
it('returns true if all assessment items are not valid and marked as new if `ignoreDelayed` set to true', () => {
186227
expect(
187-
getAssessmentItemsAreValid(state)({
228+
getAssessmentItemsAreValid(
229+
state,
230+
{},
231+
{},
232+
rootGetters
233+
)({
188234
contentNodeId: 'content-node-id-4',
189235
ignoreDelayed: true,
190236
})

0 commit comments

Comments
 (0)