diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue index 189f00c1f3..fdc9f3b151 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue @@ -104,6 +104,14 @@ :label="$tr('randomizeQuestionLabel')" :indeterminate="!isUnique(randomizeOrder)" /> + + + @@ -308,7 +316,7 @@ import VisibilityDropdown from 'shared/views/VisibilityDropdown'; import Checkbox from 'shared/views/form/Checkbox'; import { ContentKindsNames } from 'shared/leUtils/ContentKinds'; - import { NEW_OBJECT } from 'shared/constants'; + import { NEW_OBJECT, FeatureFlagKeys, ContentModalities } from 'shared/constants'; // Define an object to act as the place holder for non unique values. const nonUniqueValue = {}; @@ -479,6 +487,16 @@ }, }, thumbnailEncoding: generateGetterSetter('thumbnail_encoding'), + channelQuiz: { + get() { + const options = this.getExtraFieldsValueFromNodes('options') || {}; + return options.modality === ContentModalities.QUIZ; + }, + set(val) { + const options = { modality: val ? ContentModalities.QUIZ : null }; + this.updateExtraFields({ options }); + }, + }, /* COMPUTED PROPS */ disableAuthEdits() { @@ -521,6 +539,9 @@ newContent() { return !this.nodes.some(n => n[NEW_OBJECT]); }, + allowChannelQuizzes() { + return this.$store.getters.hasFeatureEnabled(FeatureFlagKeys.channel_quizzes); + }, }, watch: { nodes: { @@ -656,6 +677,7 @@ tagsLabel: 'Tags', noTagsFoundText: 'No results found for "{text}". Press \'Enter\' key to create a new tag', randomizeQuestionLabel: 'Randomize question order for learners', + channelQuizzesLabel: 'Allow as a channel quiz', }, }; diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js index a8d2298884..15c44623a3 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js @@ -258,6 +258,9 @@ function generateContentNodeData({ if (extra_fields.randomize !== undefined) { contentNodeData.extra_fields.randomize = extra_fields.randomize; } + if (extra_fields.options) { + contentNodeData.extra_fields.options = extra_fields.options; + } } if (prerequisite !== NOVALUE) { contentNodeData.prerequisite = prerequisite; @@ -282,11 +285,21 @@ export function updateContentNode(context, { id, ...payload } = {}) { // Don't overwrite existing extra_fields data if (contentNodeData.extra_fields) { + const extraFields = node.extra_fields || {}; + + // Don't overwrite existing options data + if (contentNodeData.extra_fields.options) { + contentNodeData.extra_fields.options = { + ...(extraFields.options || {}), + ...contentNodeData.extra_fields.options, + }; + } + contentNodeData = { ...contentNodeData, extra_fields: { - ...(node.extra_fields || {}), - ...(contentNodeData.extra_fields || {}), + ...extraFields, + ...contentNodeData.extra_fields, }, }; } diff --git a/contentcuration/contentcuration/frontend/shared/constants.js b/contentcuration/contentcuration/frontend/shared/constants.js index 9e5cf375d9..01068ff400 100644 --- a/contentcuration/contentcuration/frontend/shared/constants.js +++ b/contentcuration/contentcuration/frontend/shared/constants.js @@ -157,10 +157,14 @@ export const ValidationErrors = { export const FeatureFlagsSchema = featureFlagsSchema; -export const FeatureFlagKeys = Object.keys(FeatureFlagsSchema).reduce( +export const FeatureFlagKeys = Object.keys(FeatureFlagsSchema.properties).reduce( (featureFlags, featureFlag) => { featureFlags[featureFlag] = featureFlag; return featureFlags; }, {} ); + +export const ContentModalities = { + QUIZ: 'QUIZ', +}; diff --git a/contentcuration/contentcuration/static/feature_flags.json b/contentcuration/contentcuration/static/feature_flags.json index 605261b96f..ac96fbad2f 100644 --- a/contentcuration/contentcuration/static/feature_flags.json +++ b/contentcuration/contentcuration/static/feature_flags.json @@ -8,6 +8,11 @@ "title": "Test development feature", "description": "This no-op feature flag is excluded from non-dev environments", "$env": "development" + }, + "channel_quizzes": { + "type": "boolean", + "title": "Channel quizzes", + "description": "Allows an exercise to be marked as a channel quiz" } }, "examples": [ diff --git a/contentcuration/contentcuration/viewsets/contentnode.py b/contentcuration/contentcuration/viewsets/contentnode.py index 416c583d10..0ba70444bc 100644 --- a/contentcuration/contentcuration/viewsets/contentnode.py +++ b/contentcuration/contentcuration/viewsets/contentnode.py @@ -243,6 +243,10 @@ def update(self, queryset, all_validated_data): return all_objects +class ExtraFieldsOptionsSerializer(JSONFieldDictSerializer): + modality = ChoiceField(choices=(("QUIZ", "Quiz"),), allow_null=True, required=False) + + class ExtraFieldsSerializer(JSONFieldDictSerializer): mastery_model = ChoiceField( choices=exercises.MASTERY_MODELS, allow_null=True, required=False @@ -250,6 +254,7 @@ class ExtraFieldsSerializer(JSONFieldDictSerializer): randomize = BooleanField() m = IntegerField(allow_null=True, required=False) n = IntegerField(allow_null=True, required=False) + options = ExtraFieldsOptionsSerializer(required=False) class TagField(DotPathValueMixin, DictField):