diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue index 0fbeaf408b..1cebc6c074 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/DetailsTabView.vue @@ -32,45 +32,80 @@ box @focus="trackClick('Title')" /> - - - - - - + + + + + + + + + + + + + + + + + + @@ -328,6 +363,10 @@ import SubtitlesList from '../../views/files/supplementaryLists/SubtitlesList'; import { isImportedContent, importedChannelLink } from '../../utils'; import AccessibilityOptions from './AccessibilityOptions.vue'; + import LevelsOptions from './LevelsOptions.vue'; + import ResourcesNeededOptions from './ResourcesNeededOptions.vue'; + import LearningActivityOptions from './LearningActivityOptions.vue'; + import { getTitleValidators, getCopyrightHolderValidators, @@ -386,6 +425,7 @@ * - `grade_levels` (sometimes referred to as `content_levels`) * - `learner_needs` (resources needed) * - `accessibility_labels` (accessibility options) + * - `learning_activities` (learning activities) */ function generateNestedNodesGetterSetter(key) { return { @@ -416,6 +456,9 @@ ContentNodeThumbnail, Checkbox, AccessibilityOptions, + LevelsOptions, + ResourcesNeededOptions, + LearningActivityOptions, }, mixins: [constantsTranslationMixin, metadataTranslationMixin], props: { @@ -495,6 +538,10 @@ role: generateGetterSetter('role_visibility'), language: generateGetterSetter('language'), accessibility: generateNestedNodesGetterSetter('accessibility_labels'), + contentLevel: generateNestedNodesGetterSetter('grade_levels'), + resourcesNeeded: generateNestedNodesGetterSetter('learner_needs'), + contentLearningActivities: generateNestedNodesGetterSetter('learning_activities'), + mastery_model() { return this.getExtraFieldsValueFromNodes('mastery_model'); }, @@ -825,6 +872,18 @@ } } } + + .basicInfoColumn { + display: flex; + /deep/ .v-input { + // Stretches the "Description" text area to fill the column vertically + align-items: stretch; + } + /deep/ .v-input__control { + // Makes sure that the character count does not get pushed to second column + flex-wrap: nowrap; + } + } } } diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/LearningActivityOptions.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/LearningActivityOptions.vue new file mode 100644 index 0000000000..c9a7e33351 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/LearningActivityOptions.vue @@ -0,0 +1,56 @@ + + + + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/LevelsOptions.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/LevelsOptions.vue new file mode 100644 index 0000000000..d87a9f0f29 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/LevelsOptions.vue @@ -0,0 +1,70 @@ + + + + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/ResourcesNeededOptions.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/ResourcesNeededOptions.vue new file mode 100644 index 0000000000..b866b1853f --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/ResourcesNeededOptions.vue @@ -0,0 +1,76 @@ + + + + diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/detailsTabView.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/detailsTabView.spec.js index 50efa2b3cf..bda02c2d7e 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/detailsTabView.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/detailsTabView.spec.js @@ -61,6 +61,8 @@ describe.skip('detailsTabView', () => { }); describe('on render', () => { // TODO: add defaults for 'accessibility' field + // TODO: add defaults for 'grade_levels' field + // TODO: add defaults for 'learner_needs' field it('all fields should match node field values', () => { let keys = [ 'language', diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/learningActivityOptions.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/learningActivityOptions.spec.js new file mode 100644 index 0000000000..e0547bf13d --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/learningActivityOptions.spec.js @@ -0,0 +1,57 @@ +import Vue from 'vue'; +import Vuetify from 'vuetify'; +import { shallowMount, mount } from '@vue/test-utils'; +import LearningActivityOptions from '../LearningActivityOptions.vue'; +import { LearningActivities } from 'shared/constants'; + +Vue.use(Vuetify); + +function makeWrapper(value) { + return mount(LearningActivityOptions, { + propsData: { + value, + }, + }); +} + +describe('LearningActivityOptions', () => { + it('smoke test', () => { + const wrapper = shallowMount(LearningActivityOptions); + expect(wrapper.isVueInstance()).toBe(true); + }); + + it('number of items in the dropdown should be equal to number of items available in ', () => { + const wrapper = shallowMount(LearningActivityOptions); + const numberOfDropdownItems = Object.keys(LearningActivities).length; + const dropdownItems = wrapper.attributes()['items'].split(',').length; + + expect(dropdownItems).toBe(numberOfDropdownItems); + }); + + describe('updating state', () => { + it('should update learning_activity field with new values received from a parent', () => { + const learningActivity = ['activity_1', 'activity_2']; + const wrapper = makeWrapper(learningActivity); + const dropdown = wrapper.find({ name: 'v-select' }); + + expect(dropdown.props('value')).toEqual(learningActivity); + + wrapper.setProps({ + value: ['activity_4'], + }); + expect(dropdown.props('value')).toEqual(['activity_4']); + }); + + it('should emit new input values', () => { + const learningActivity = ['activity_1', 'activity_2', 'activity_3']; + const wrapper = makeWrapper({}); + const dropdown = wrapper.find({ name: 'v-select' }); + dropdown.vm.$emit('input', learningActivity); + + return Vue.nextTick().then(() => { + const emittedLevels = wrapper.emitted('input').pop()[0]; + expect(emittedLevels).toEqual(learningActivity); + }); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/levelsOptions.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/levelsOptions.spec.js new file mode 100644 index 0000000000..a3541e494a --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/levelsOptions.spec.js @@ -0,0 +1,58 @@ +import Vue from 'vue'; +import Vuetify from 'vuetify'; +import { shallowMount, mount } from '@vue/test-utils'; +import LevelsOptions from '../LevelsOptions.vue'; +import { ContentLevels } from 'shared/constants'; + +Vue.use(Vuetify); + +function makeWrapper(value) { + return mount(LevelsOptions, { + propsData: { + value, + }, + }); +} + +describe('LevelsOptions', () => { + it('smoke test', () => { + const wrapper = shallowMount(LevelsOptions); + + expect(wrapper.isVueInstance()).toBe(true); + }); + + it('number of items in the dropdown should be equal to number of items available in ContentLevels', () => { + const wrapper = shallowMount(LevelsOptions); + const numberOfAvailableLevels = Object.keys(ContentLevels).length; + const dropdownItems = wrapper.attributes()['items'].split(',').length; + + expect(dropdownItems).toBe(numberOfAvailableLevels); + }); + + describe('updating state', () => { + it('should update levels field with new values received from a parent', () => { + const levels = ['abc', 'gefo']; + const wrapper = makeWrapper(levels); + const dropdown = wrapper.find({ name: 'v-select' }); + + expect(dropdown.props('value')).toEqual(levels); + + wrapper.setProps({ + value: ['def'], + }); + expect(dropdown.props('value')).toEqual(['def']); + }); + + it('should emit new input values', () => { + const levels = ['abc', 'gefo', '8hw']; + const wrapper = makeWrapper({}); + const dropdown = wrapper.find({ name: 'v-select' }); + dropdown.vm.$emit('input', levels); + + return Vue.nextTick().then(() => { + const emittedLevels = wrapper.emitted('input').pop()[0]; + expect(emittedLevels).toEqual(levels); + }); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/resourcesNeededOptions.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/resourcesNeededOptions.spec.js new file mode 100644 index 0000000000..28b6348ab0 --- /dev/null +++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/resourcesNeededOptions.spec.js @@ -0,0 +1,66 @@ +import Vue from 'vue'; +import Vuetify from 'vuetify'; +import { shallowMount, mount } from '@vue/test-utils'; +import ResourcesNeededOptions, { updateResourcesDropdown } from '../ResourcesNeededOptions.vue'; +import { ResourcesNeededTypes } from 'shared/constants'; + +Vue.use(Vuetify); + +function makeWrapper(value) { + return mount(ResourcesNeededOptions, { + propsData: { + value, + }, + }); +} + +describe('ResourcesNeededOptions', () => { + it('smoke test', () => { + const wrapper = shallowMount(ResourcesNeededOptions); + + expect(wrapper.isVueInstance()).toBe(true); + }); + + it('when there is a list of keys to remove from ResourcesNeededTypes, return updated map for ResourcesNeededTypes for dropdown', () => { + const list = ['FOR_BEGINNERS', 'INTERNET']; + const numberOfAvailableResources = Object.keys(ResourcesNeededTypes).length - list.length; + const dropdownItemsLength = Object.keys(updateResourcesDropdown(list)).length; + + expect(dropdownItemsLength).toBe(numberOfAvailableResources); + }); + + it('when there are no keys to remove from ResourcesNeededTypes, dropdown should contain all resources', () => { + const list = []; + const numberOfAvailableResources = Object.keys(ResourcesNeededTypes).length - list.length; + const dropdownItemsLength = Object.keys(updateResourcesDropdown(list)).length; + + expect(dropdownItemsLength).toBe(numberOfAvailableResources); + }); + + describe('updating state', () => { + it('should update resources field with new values received from a parent', () => { + const resourcesNeeded = ['person', 'book']; + const wrapper = makeWrapper(resourcesNeeded); + const dropdown = wrapper.find({ name: 'v-select' }); + + expect(dropdown.props('value')).toEqual(resourcesNeeded); + + wrapper.setProps({ + value: ['cat'], + }); + expect(dropdown.props('value')).toEqual(['cat']); + }); + + it('should emit new input values', () => { + const resourcesNeeded = ['person', 'book', 'train']; + const wrapper = makeWrapper({}); + const dropdown = wrapper.find({ name: 'v-select' }); + dropdown.vm.$emit('input', resourcesNeeded); + + return Vue.nextTick().then(() => { + const emittedLevels = wrapper.emitted('input').pop()[0]; + expect(emittedLevels).toEqual(resourcesNeeded); + }); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/__tests__/actions.spec.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/__tests__/actions.spec.js index 11e8c7d606..9067852d65 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/__tests__/actions.spec.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/__tests__/actions.spec.js @@ -108,6 +108,7 @@ describe('contentNode actions', () => { title: 'notatest', description: 'very', language: 'no', + learning_activities: { test: true }, }) .then(() => { expect(updateSpy).toHaveBeenCalledWith(id, { @@ -116,6 +117,7 @@ describe('contentNode actions', () => { language: 'no', changed: true, complete: false, + learning_activities: { test: true }, }); updateSpy.mockRestore(); }); diff --git a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js index 9120c8e488..c608db3528 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js +++ b/contentcuration/contentcuration/frontend/channelEdit/vuex/contentNode/actions.js @@ -178,6 +178,9 @@ export function createContentNode(context, { parent, kind, ...payload }) { ...contentDefaults, role_visibility: contentDefaults.role_visibility || RolesNames.LEARNER, accessibility_labels: {}, + grade_levels: {}, + learner_needs: {}, + learning_activities: {}, ...payload, }; @@ -211,6 +214,9 @@ function generateContentNodeData({ prerequisite = NOVALUE, complete = NOVALUE, accessibility_labels = NOVALUE, + grade_levels = NOVALUE, + learner_needs = NOVALUE, + learning_activities = NOVALUE, } = {}) { const contentNodeData = {}; if (title !== NOVALUE) { @@ -252,6 +258,15 @@ function generateContentNodeData({ if (accessibility_labels !== NOVALUE) { contentNodeData.accessibility_labels = accessibility_labels; } + if (grade_levels !== NOVALUE) { + contentNodeData.grade_levels = grade_levels; + } + if (learner_needs !== NOVALUE) { + contentNodeData.learner_needs = learner_needs; + } + if (learning_activities !== NOVALUE) { + contentNodeData.learning_activities = learning_activities; + } if (extra_fields !== NOVALUE) { contentNodeData.extra_fields = contentNodeData.extra_fields || {}; diff --git a/contentcuration/contentcuration/frontend/shared/constants.js b/contentcuration/contentcuration/frontend/shared/constants.js index 04a76f2059..c7af961529 100644 --- a/contentcuration/contentcuration/frontend/shared/constants.js +++ b/contentcuration/contentcuration/frontend/shared/constants.js @@ -5,6 +5,8 @@ export { default as CompletionCriteriaModels } from 'kolibri-constants/Completio export { default as ContentLevel } from 'kolibri-constants/labels/Levels'; export { default as Categories } from 'kolibri-constants/labels/Subjects'; export { default as AccessibilityCategories } from 'kolibri-constants/labels/AccessibilityCategories'; +export { default as ContentLevels } from 'kolibri-constants/labels/Levels'; +export { default as ResourcesNeededTypes } from 'kolibri-constants/labels/Needs'; export const ContentDefaults = { author: 'author', @@ -159,6 +161,7 @@ export const ValidationErrors = { INVALID_NUMBER_OF_CORRECT_ANSWERS: 'INVALID_NUMBER_OF_CORRECT_ANSWERS', NO_VALID_PRIMARY_FILES: 'NO_VALID_PRIMARY_FILES', INVALID_COMPLETION_CRITERIA_MODEL: 'INVALID_COMPLETION_CRITERIA_MODEL', + LEARNING_ACTIVITY_REQUIRED: 'LEARNING_ACTIVITY_REQUIRED', ...fileErrors, }; diff --git a/contentcuration/contentcuration/frontend/shared/mixins.js b/contentcuration/contentcuration/frontend/shared/mixins.js index 60e657accf..b7bedbaa60 100644 --- a/contentcuration/contentcuration/frontend/shared/mixins.js +++ b/contentcuration/contentcuration/frontend/shared/mixins.js @@ -669,13 +669,18 @@ const nonconformingKeys = { PEOPLE: 'ToUseWithTeachersAndPeers', PAPER_PENCIL: 'ToUseWithPaperAndPencil', INTERNET: 'NeedsInternet', - MATERIALS: 'NeedsMaterials', FOR_BEGINNERS: 'ForBeginners', digitalLiteracy: 'digitialLiteracy', BASIC_SKILLS: 'allLevelsBasicSkills', FOUNDATIONS: 'basicSkills', - toolsAndSoftwareTraining: 'softwareToolsAndTraining', foundationsLogicAndCriticalThinking: 'logicAndCriticalThinking', + + /* + * TODO: the following are in ResourcesNeededTypes map from le-utils, but not in Kolibri, + * and should be tracked in the issue - https://github.com/learningequality/kolibri/issues/9245 + */ + OTHER_SUPPLIES: 'NeedsMaterials', + SPECIAL_SOFTWARE: 'softwareToolsAndTraining', }; /** diff --git a/contentcuration/contentcuration/frontend/shared/translator.js b/contentcuration/contentcuration/frontend/shared/translator.js index a30ed48453..5188bc5f9c 100644 --- a/contentcuration/contentcuration/frontend/shared/translator.js +++ b/contentcuration/contentcuration/frontend/shared/translator.js @@ -14,6 +14,7 @@ const MESSAGES = { masteryModelNGtZero: 'Must be at least 1', masteryModelNWholeNumber: 'Must be a whole number', confirmLogout: 'Changes you made may not be saved. Are you sure you want to leave this page?', + learningActivityRequired: 'Learning activity is required', }; export default createTranslator('sharedVue', MESSAGES); diff --git a/contentcuration/contentcuration/frontend/shared/utils/validation.js b/contentcuration/contentcuration/frontend/shared/utils/validation.js index d89b416990..8ae07297e1 100644 --- a/contentcuration/contentcuration/frontend/shared/utils/validation.js +++ b/contentcuration/contentcuration/frontend/shared/utils/validation.js @@ -94,6 +94,10 @@ function _getMasteryModel(node) { return node.extra_fields; } +function _getLearningActivity(node) { + return Object.keys(node.learning_activities); +} + function _getErrorMsg(error) { const messages = { [ValidationErrors.TITLE_REQUIRED]: translator.$tr('titleRequired'), @@ -108,6 +112,7 @@ function _getErrorMsg(error) { [ValidationErrors.MASTERY_MODEL_N_REQUIRED]: translator.$tr('masteryModelNRequired'), [ValidationErrors.MASTERY_MODEL_N_WHOLE_NUMBER]: translator.$tr('masteryModelNWholeNumber'), [ValidationErrors.MASTERY_MODEL_N_GT_ZERO]: translator.$tr('masteryModelNGtZero'), + [ValidationErrors.LEARNING_ACTIVITY_REQUIRED]: translator.$tr('learningActivityRequired'), }; return messages[error]; @@ -134,6 +139,10 @@ export function getCopyrightHolderValidators() { return [value => Boolean(value && value.trim()) || ValidationErrors.COPYRIGHT_HOLDER_REQUIRED]; } +export function getLearningActivityValidators() { + return [value => Boolean(value.length) || ValidationErrors.LEARNING_ACTIVITY_REQUIRED]; +} + export function getLicenseDescriptionValidators() { return [value => Boolean(value && value.trim()) || ValidationErrors.LICENSE_DESCRIPTION_REQUIRED]; } @@ -184,6 +193,13 @@ export function getNodeCopyrightHolderErrors(node) { .filter(value => value !== true); } +export function getNodeLearningActivityErrors(node) { + const learningActivity = _getLearningActivity(node); + return getLearningActivityValidators() + .map(validator => validator(learningActivity)) + .filter(value => value !== true); +} + export function getNodeLicenseDescriptionErrors(node) { const license = _getLicense(node); if (!license || !license.is_custom) { @@ -251,6 +267,14 @@ export function getNodeDetailsErrors(node) { } } + // learning activity is a required field for resources + if (node.kind !== ContentKindsNames.TOPIC) { + const learningActivityErrors = getNodeLearningActivityErrors(node); + if (learningActivityErrors.length) { + errors = errors.concat(learningActivityErrors); + } + } + // mastery is required on exercises if (node.kind === ContentKindsNames.EXERCISE) { const masteryModelErrors = getNodeMasteryModelErrors(node); @@ -267,7 +291,6 @@ export function getNodeDetailsErrors(node) { errors = errors.concat(masteryModelNErrors); } } - return errors; } diff --git a/contentcuration/contentcuration/frontend/shared/utils/validation.spec.js b/contentcuration/contentcuration/frontend/shared/utils/validation.spec.js index 539426b741..9c2d4dbe24 100644 --- a/contentcuration/contentcuration/frontend/shared/utils/validation.spec.js +++ b/contentcuration/contentcuration/frontend/shared/utils/validation.spec.js @@ -18,6 +18,7 @@ import { sanitizeAssessmentItemHints, sanitizeAssessmentItem, getAssessmentItemErrors, + getNodeLearningActivityErrors, } from './validation'; import { MasteryModelsNames } from 'shared/leUtils/MasteryModels'; import { ContentKindsNames } from 'shared/leUtils/ContentKinds'; @@ -175,6 +176,23 @@ describe('channelEdit utils', () => { }); }); + describe('getNodeLearningActivityErrors', () => { + it(`returns an error for an empty learning activity input`, () => { + const node = { + learning_activities: {}, + }; + expect(getNodeLearningActivityErrors(node)).toEqual([ + ValidationErrors.LEARNING_ACTIVITY_REQUIRED, + ]); + }); + it('returns no errors when learning activity is specified', () => { + const node = { + learning_activities: { test: true }, + }; + expect(getNodeLearningActivityErrors(node)).toEqual([]); + }); + }); + describe('getNodeMasteryModelErrors', () => { it('returns an error for an empty mastery model', () => { const node = { extra_fields: null }; @@ -347,6 +365,7 @@ describe('channelEdit utils', () => { title: 'Exercise', kind: ContentKindsNames.EXERCISE, license: { id: 8 }, + learning_activities: { test: true }, extra_fields: { mastery_model: MasteryModelsNames.DO_ALL, options: { @@ -457,6 +476,7 @@ describe('channelEdit utils', () => { title: 'A node', license: { id: 8 }, kind, + learning_activities: { test: true }, extra_fields: { options: { completion_criteria: { @@ -548,6 +568,7 @@ describe('channelEdit utils', () => { title: '', kind: 'document', license: 8, + learning_activities: { test: true }, }) ).toEqual([ValidationErrors.TITLE_REQUIRED]); }); @@ -558,6 +579,7 @@ describe('channelEdit utils', () => { title: 'Title', kind: 'document', license: null, + learning_activities: { test: true }, }, [ValidationErrors.LICENSE_REQUIRED], ], @@ -566,6 +588,7 @@ describe('channelEdit utils', () => { title: 'Title', kind: 'document', license: 8, + learning_activities: { test: true }, }, [], ], @@ -575,6 +598,7 @@ describe('channelEdit utils', () => { title: 'Title', kind: 'topic', license: null, + learning_activities: { test: true }, }, [], ], @@ -584,6 +608,7 @@ describe('channelEdit utils', () => { title: 'Title', freeze_authoring_data: true, license: null, + learning_activities: { test: true }, }, [], ], @@ -597,6 +622,7 @@ describe('channelEdit utils', () => { { title: 'Title', license: 1, + learning_activities: { test: true }, }, [ValidationErrors.COPYRIGHT_HOLDER_REQUIRED], ], @@ -605,6 +631,7 @@ describe('channelEdit utils', () => { title: 'Title', license: 1, copyright_holder: 'Copyright holder', + learning_activities: { test: true }, }, [], ], @@ -619,6 +646,7 @@ describe('channelEdit utils', () => { title: 'Title', license: 9, copyright_holder: 'Copyright holder', + learning_activities: { test: true }, }, [ValidationErrors.LICENSE_DESCRIPTION_REQUIRED], ], @@ -628,6 +656,7 @@ describe('channelEdit utils', () => { license: 9, copyright_holder: 'Copyright holder', license_description: 'My custom license', + learning_activities: { test: true }, }, [], ], @@ -641,6 +670,7 @@ describe('channelEdit utils', () => { title: 'Title', kind: 'exercise', license: 8, + learning_activities: { test: true }, }, [ValidationErrors.MASTERY_MODEL_REQUIRED], ], @@ -649,6 +679,7 @@ describe('channelEdit utils', () => { title: 'Title', kind: 'exercise', license: 8, + learning_activities: { test: true }, extra_fields: { mastery_model: 'do_all', }, @@ -660,6 +691,7 @@ describe('channelEdit utils', () => { title: 'Title', kind: 'exercise', license: 8, + learning_activities: { test: true }, extra_fields: { mastery_model: 'm_of_n', m: 3, @@ -677,6 +709,7 @@ describe('channelEdit utils', () => { title: 'Title', kind: 'exercise', license: 8, + learning_activities: { test: true }, extra_fields: { mastery_model: 'm_of_n', m: 3, @@ -690,6 +723,7 @@ describe('channelEdit utils', () => { title: 'Title', kind: 'exercise', license: 8, + learning_activities: { test: true }, extra_fields: { mastery_model: 'm_of_n', m: 2,