From f2332f652a8d1e3ad08885ffe96b20568c320cb6 Mon Sep 17 00:00:00 2001 From: Blaine Jester Date: Tue, 4 May 2021 14:41:06 -0700 Subject: [PATCH 1/3] User-scoped channel quiz feature --- .../components/edit/DetailsTabView.vue | 24 ++++++++++++++++++- .../channelEdit/vuex/contentNode/actions.js | 17 +++++++++++-- .../frontend/shared/constants.js | 6 ++++- .../contentcuration/static/feature_flags.json | 5 ++++ .../contentcuration/viewsets/contentnode.py | 5 ++++ 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue index 189f00c1f3..33e9df7d85 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: 'Allowed 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..fbe8af45a8 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..c30a320906 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): From dcd57297be5f52c6db014e2f21d3b27eda549ee1 Mon Sep 17 00:00:00 2001 From: Blaine Jester Date: Tue, 4 May 2021 15:01:25 -0700 Subject: [PATCH 2/3] Change copy --- .../frontend/channelEdit/components/edit/DetailsTabView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue index 33e9df7d85..fdc9f3b151 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue @@ -677,7 +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: 'Allowed as a channel quiz', + channelQuizzesLabel: 'Allow as a channel quiz', }, }; From ba5b51d673aa2fe7a6163c4cecccb4f7b51c69d0 Mon Sep 17 00:00:00 2001 From: Blaine Jester Date: Tue, 4 May 2021 15:45:15 -0700 Subject: [PATCH 3/3] Linting fixes --- .../frontend/channelEdit/vuex/contentNode/actions.js | 4 ++-- contentcuration/contentcuration/frontend/shared/constants.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js index fbe8af45a8..15c44623a3 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js @@ -285,14 +285,14 @@ export function updateContentNode(context, { id, ...payload } = {}) { // Don't overwrite existing extra_fields data if (contentNodeData.extra_fields) { - const extraFields = (node.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 = { diff --git a/contentcuration/contentcuration/frontend/shared/constants.js b/contentcuration/contentcuration/frontend/shared/constants.js index c30a320906..01068ff400 100644 --- a/contentcuration/contentcuration/frontend/shared/constants.js +++ b/contentcuration/contentcuration/frontend/shared/constants.js @@ -166,5 +166,5 @@ export const FeatureFlagKeys = Object.keys(FeatureFlagsSchema.properties).reduce ); export const ContentModalities = { - QUIZ: 'QUIZ' + QUIZ: 'QUIZ', };