From ebdabc57a2ecd926271cc51c541c494b49d31e3d Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Mon, 6 Apr 2020 09:39:24 +0100 Subject: [PATCH 1/5] issue/2645 Added manifest creation and loading --- grunt/config/watch.js | 4 +- grunt/tasks/build.js | 1 + grunt/tasks/dev.js | 1 + grunt/tasks/diff.js | 1 + grunt/tasks/language-data-manifests.js | 7 + grunt/tasks/server-build.js | 1 + src/core/js/collections/adaptCollection.js | 6 - .../js/collections/adaptSubsetCollection.js | 23 ++ src/core/js/data.js | 256 +++++++----------- src/core/js/models/buildModel.js | 2 +- src/core/js/models/courseModel.js | 14 +- src/core/js/mpabc.js | 78 +++++- 12 files changed, 217 insertions(+), 177 deletions(-) create mode 100644 grunt/tasks/language-data-manifests.js create mode 100644 src/core/js/collections/adaptSubsetCollection.js diff --git a/grunt/config/watch.js b/grunt/config/watch.js index 46fe07361..c40ffa001 100644 --- a/grunt/config/watch.js +++ b/grunt/config/watch.js @@ -17,8 +17,8 @@ module.exports = { tasks: ['handlebars', 'javascript:dev'] }, courseJson: { - files: ['<%= sourcedir %>course/**/*.<%= jsonext %>'], - tasks: ['jsonlint', 'check-json', 'newer:copy:courseJson', 'schema-defaults'] + files: ['<%= sourcedir %>course/**/*.<%= jsonext %>', '<%= outputdir %>course/*/language_data_manifest.js'], + tasks: ['language-data-manifests', 'jsonlint', 'check-json', 'newer:copy:courseJson', 'schema-defaults'] }, courseAssets: { files: ['<%= sourcedir %>course/<%=languages%>/*', '!<%= sourcedir %>course/<%=languages%>/*.<%= jsonext %>'], diff --git a/grunt/tasks/build.js b/grunt/tasks/build.js index 76e5d3d00..042977768 100644 --- a/grunt/tasks/build.js +++ b/grunt/tasks/build.js @@ -9,6 +9,7 @@ module.exports = function(grunt) { 'build-config', 'copy', 'schema-defaults', + 'language-data-manifests', 'handlebars', 'tracking-insert', 'javascript:compile', diff --git a/grunt/tasks/dev.js b/grunt/tasks/dev.js index 9a3f0aeef..450f903bf 100644 --- a/grunt/tasks/dev.js +++ b/grunt/tasks/dev.js @@ -8,6 +8,7 @@ module.exports = function(grunt) { 'build-config', 'copy', 'schema-defaults', + 'language-data-manifests', 'handlebars', 'tracking-insert', 'javascript:dev', diff --git a/grunt/tasks/diff.js b/grunt/tasks/diff.js index 9f6f93da9..53f7694d3 100644 --- a/grunt/tasks/diff.js +++ b/grunt/tasks/diff.js @@ -8,6 +8,7 @@ module.exports = function(grunt) { 'build-config', 'copy', 'schema-defaults', + 'language-data-manifests', 'newer:handlebars:compile', 'tracking-insert', 'newer:javascript:dev', diff --git a/grunt/tasks/language-data-manifests.js b/grunt/tasks/language-data-manifests.js new file mode 100644 index 000000000..a38f232e7 --- /dev/null +++ b/grunt/tasks/language-data-manifests.js @@ -0,0 +1,7 @@ +module.exports = function(grunt) { + const Helpers = require('../helpers')(grunt); + grunt.registerTask('language-data-manifests', 'Creates a manifest for each set of language data files', function() { + const languages = Helpers.getFramework({ useOutputData: true }).getData().languages; + languages.forEach(language => language.saveManifest()); + }); +}; diff --git a/grunt/tasks/server-build.js b/grunt/tasks/server-build.js index ff82e5983..afebcf936 100644 --- a/grunt/tasks/server-build.js +++ b/grunt/tasks/server-build.js @@ -9,6 +9,7 @@ module.exports = function(grunt) { '_log-vars', 'build-config', 'copy', + 'language-data-manifests', 'less:' + requireMode, 'handlebars', 'javascript:' + requireMode, diff --git a/src/core/js/collections/adaptCollection.js b/src/core/js/collections/adaptCollection.js index 03566c559..500e65b04 100644 --- a/src/core/js/collections/adaptCollection.js +++ b/src/core/js/collections/adaptCollection.js @@ -5,13 +5,7 @@ define([ class AdaptCollection extends Backbone.Collection { initialize(models, options) { - this.url = options.url; this.once('reset', this.loadedData, this); - if (!this.url) return; - this.fetch({ - reset: true, - error: () => console.error('ERROR: unable to load file ' + this.url) - }); } loadedData() { diff --git a/src/core/js/collections/adaptSubsetCollection.js b/src/core/js/collections/adaptSubsetCollection.js new file mode 100644 index 000000000..ba6c188a1 --- /dev/null +++ b/src/core/js/collections/adaptSubsetCollection.js @@ -0,0 +1,23 @@ +define([ + 'core/js/adapt', + './adaptCollection' +], function(Adapt, AdaptCollection) { + + class AdaptSubsetCollection extends AdaptCollection { + + initialize(models, options) { + super.initialize(models, options); + this.parent = options.parent; + this.listenTo(this.parent, 'reset', this.loadSubset); + } + + loadSubset() { + this.set(this.parent.filter(model => model instanceof this.model)); + this._byAdaptID = this.groupBy('_id'); + } + + } + + return AdaptSubsetCollection; + +}); diff --git a/src/core/js/data.js b/src/core/js/data.js index f87d32ffd..258a9bcbf 100644 --- a/src/core/js/data.js +++ b/src/core/js/data.js @@ -1,38 +1,37 @@ define([ 'core/js/adapt', 'core/js/collections/adaptCollection', + 'core/js/models/buildModel', 'core/js/models/configModel', 'core/js/models/courseModel', 'core/js/models/lockingModel', - 'core/js/models/buildModel', 'core/js/startController' -], function(Adapt, AdaptCollection, ConfigModel, CourseModel) { +], function(Adapt, AdaptCollection, BuildModel, ConfigModel, CourseModel) { - class Data extends Backbone.Controller { + class Data extends AdaptCollection { - initialize() { - this.mappedIds = {}; + model(json) { + const ModelClass = Adapt.getModelClass(json); + if (!ModelClass) { + return new Backbone.Model(json); + } + return new ModelClass(json); } - init () { - Adapt.build.whenReady().then(this.onBuildDataLoaded.bind(this)); + async init () { + Adapt.build = new BuildModel(null, { url: 'adapt/js/build.min.js', reset: true }); + await Adapt.build.whenReady(); + $('html').attr('data-adapt-framework-version', Adapt.build.get('package').version); + this.loadConfigData(); } - onBuildDataLoaded() { - $('html').attr('data-adapt-framework-version', Adapt.build.get('package').version); + loadConfigData() { Adapt.config = new ConfigModel(null, { url: 'course/config.' + Adapt.build.get('jsonext'), reset: true }); + this.listenToOnce(Adapt, 'configModel:loadCourseData', this.onLoadCourseData); this.listenTo(Adapt.config, { 'change:_activeLanguage': this.onLanguageChange, 'change:_defaultDirection': this.onDirectionChange }); - - // Events that are triggered by the main Adapt content collections and models - this.listenToOnce(Adapt, 'configModel:loadCourseData', this.onLoadCourseData); - } - - onLanguageChange(model, language) { - Adapt.offlineStorage.set('lang', language); - this.loadCourseData(this.triggerDataReady.bind(this), language); } onDirectionChange(model, direction) { @@ -48,141 +47,116 @@ define([ * If it has we can go ahead and start loading; if it hasn't, apply the defaultLanguage from config.json */ onLoadCourseData() { - if (Adapt.config.get('_activeLanguage')) { - this.loadCourseData(this.triggerDataReady.bind(this)); - } else { + if (!Adapt.config.get('_activeLanguage')) { Adapt.config.set('_activeLanguage', Adapt.config.get('_defaultLanguage')); + return; } + this.loadCourseData(); } - loadCourseData(callback, newLanguage) { - this.listenTo(Adapt, 'adaptCollection:dataLoaded courseModel:dataLoaded', () => { - this.checkDataIsLoaded(callback, newLanguage); - }); + onLanguageChange(model, language) { + Adapt.offlineStorage.set('lang', language); + this.loadCourseData(language); + } + + async loadCourseData(newLanguage) { // All code that needs to run before adapt starts should go here const language = Adapt.config.get('_activeLanguage'); - const jsonext = Adapt.build.get('jsonext'); + const courseFolder = 'course/' + language + '/'; $('html').attr('lang', language); - const getContentObjectModel = json => { - const ModelClass = Adapt.getModelClass(json) || Adapt.getModelClass('menu'); - if (!ModelClass) { - throw new Error(`Cannot find model for: ${Adapt.getModelName(json)}`); - } - return new ModelClass(json); - }; - - const getPath = name => `course/${language}/${name}.${jsonext}`; - - const getModel = json => { - const ModelClass = Adapt.getModelClass(json); - if (!ModelClass) { - throw new Error(`Cannot find model for: ${Adapt.getModelName(json)}`); - } - return new ModelClass(json); - }; - - Adapt.course = new CourseModel(null, { url: getPath('course'), reset: true }); - - Adapt.contentObjects = new AdaptCollection(null, { - model: getContentObjectModel, - url: getPath('contentObjects') - }); - - Adapt.articles = new AdaptCollection(null, { - model: getModel, - url: getPath('articles') - }); + await this.loadManifestFiles(courseFolder); + await this.triggerDataLoaded(); + await this.triggerDataReady(newLanguage); + this.triggerInit(); - Adapt.blocks = new AdaptCollection(null, { - model: getModel, - url: getPath('blocks') - }); + } - Adapt.components = new AdaptCollection(null, { - model: getModel, - url: getPath('components') + getJSON(path) { + return new Promise((resolve, reject) => { + $.getJSON(path, data => { + // Add path to data incase it's necessary later + data.__path__ = path; + resolve(data); + }).fail(reject); }); } - checkDataIsLoaded(callback, newLanguage) { - if (Adapt.contentObjects.models.length > 0 && - Adapt.articles.models.length > 0 && - Adapt.blocks.models.length > 0 && - Adapt.components.models.length > 0 && - Adapt.course.get('_id')) { - - this.mapAdaptIdsToObjects(); - - Adapt.log.debug('Firing app:dataLoaded'); - - try { - Adapt.trigger('app:dataLoaded');// Triggered to setup model connections in AdaptModel.js - } catch (e) { - Adapt.log.error('Error during app:dataLoading trigger', e); + async loadManifestFiles(languagePath) { + this.trigger('loading'); + this.reset(); + const manifestPath = languagePath + 'language_data_manifest.js'; + let manifest; + try { + manifest = await this.getJSON(manifestPath); + } catch (err) { + manifest = ['course.json', 'contentObjects.json', 'articles.json', 'blocks.json', 'components.json']; + Adapt.log.warnOnce(`Manifest path '${manifestPath} not found. Using traditional files: ${manifest.join(', ')}`); + } + const allFileData = await Promise.all(manifest.map(filePath => { + return this.getJSON(`${languagePath}${filePath}`); + })); + // Flatten all file data into a single array of model data + const allModelData = allFileData.reduce((result, fileData) => { + if (fileData instanceof Array) { + result.push(...fileData); + } else if (fileData instanceof Object) { + result.push(fileData); + } else { + Adapt.log.warnOnce(`File data isn't an array or object: ${fileData.__path__}`); } - - this.setupMapping(); - - Adapt.wait.queue(() => { - callback(newLanguage); - }); - + return result; + }, []); + // Add course model first to allow other model/views to utilize its settings + const course = allModelData.find(modelData => modelData._type === 'course'); + if (!course) { + throw new Error(`Expected a model data with "_type": "course", none found.`); } + this.push(course); + // Add other models + allModelData.forEach(modelData => { + if (modelData._type === 'course') { + return; + } + this.push(modelData); + }); + // index by id + this._byAdaptID = this.indexBy('_id'); + this.trigger('reset'); + this.trigger('loaded'); + await Adapt.wait.queue(); } - mapAdaptIdsToObjects() { - Adapt.contentObjects._byAdaptID = Adapt.contentObjects.groupBy('_id'); - Adapt.articles._byAdaptID = Adapt.articles.groupBy('_id'); - Adapt.blocks._byAdaptID = Adapt.blocks.groupBy('_id'); - Adapt.components._byAdaptID = Adapt.components.groupBy('_id'); - } - - setupMapping() { - this.mappedIds = {}; - - // Setup course Id - this.mappedIds[Adapt.course.get('_id')] = 'course'; - - const collections = ['contentObjects', 'articles', 'blocks', 'components']; - - collections.forEach(collection => { - Adapt[collection].models.forEach(model => { - const id = model.get('_id'); - this.mappedIds[id] = collection; - }); - }); + async triggerDataLoaded() { + Adapt.log.debug('Firing app:dataLoaded'); + try { + Adapt.trigger('app:dataLoaded');// Triggered to setup model connections in AdaptModel.js + } catch (e) { + Adapt.log.error('Error during app:dataLoading trigger', e); + } + await Adapt.wait.queue(); } - triggerDataReady(newLanguage) { + async triggerDataReady(newLanguage) { if (newLanguage) { - Adapt.trigger('app:languageChanged', newLanguage); - _.defer(() => { Adapt.startController.loadCourseData(); - let hash = '#/'; - - if (Adapt.startController.isEnabled()) { - hash = Adapt.startController.getStartHash(true); - } - - Backbone.history.navigate(hash, { trigger: true, replace: true }); + const hash = Adapt.startController.isEnabled() ? + '#/' : + Adapt.startController.getStartHash(true); + Adapt.router.navigate(hash, { trigger: true, replace: true }); }); } - Adapt.log.debug('Firing app:dataReady'); - try { Adapt.trigger('app:dataReady'); } catch (e) { Adapt.log.error('Error during app:dataReady trigger', e); } - - Adapt.wait.queue(this.triggerInit.bind(this)); - + await Adapt.wait.queue(); } triggerInit() { @@ -192,59 +166,23 @@ define([ whenReady() { if (this.isReady) return Promise.resolve(); - return new Promise(resolve => { this.once('ready', resolve); }); } - /** - * Looks up which collection a model belongs to - * @param {string} id The id of the item you want to look up e.g. `"co-05"` - * @return {string} One of the following (or `undefined` if not found): - * - "course" - * - "contentObjects" - * - "blocks" - * - "articles" - * - "components" - */ - mapById(id) { - return this.mappedIds[id]; - } - /** * Looks up a model by its `_id` property * @param {string} id The id of the item e.g. "co-05" * @return {Backbone.Model} */ findById(id) { - if (id === Adapt.course.get('_id')) { - return Adapt.course; - } - - const collectionType = Adapt.mapById(id); - - if (!collectionType) { - console.warn('Adapt.findById() unable to find collection type for id: ' + id); + const model = this._byAdaptID[id]; + if (!model) { + console.warn(`Adapt.findById() unable to find collection type for id: ${id}`); return; } - - return Adapt[collectionType]._byAdaptID[id][0]; - } - - /** - * Filter all models. - * @param {Function} filter - * @returns {Array} - */ - filter(filter) { - const result = []; - filter(Adapt.course) && result.push(Adapt.course); - result.push(...Adapt.contentObjects.filter(filter)); - result.push(...Adapt.articles.filter(filter)); - result.push(...Adapt.blocks.filter(filter)); - result.push(...Adapt.components.filter(filter)); - return result; + return model; } } diff --git a/src/core/js/models/buildModel.js b/src/core/js/models/buildModel.js index 142186fa6..47ed6b69e 100644 --- a/src/core/js/models/buildModel.js +++ b/src/core/js/models/buildModel.js @@ -37,6 +37,6 @@ define([ } - return (Adapt.build = new BuildModel(null, { url: 'adapt/js/build.min.js', reset: true })); + return BuildModel; }); diff --git a/src/core/js/models/courseModel.js b/src/core/js/models/courseModel.js index 6065a40b5..b90ba61a1 100644 --- a/src/core/js/models/courseModel.js +++ b/src/core/js/models/courseModel.js @@ -23,23 +23,21 @@ define([ return 'course'; } - initialize(attrs, options) { - super.initialize(arguments); + initialize(...args) { Adapt.trigger('courseModel:dataLoading'); - this.url = options.url; - this.on('sync', this.loadedData, this); - if (!this.url) return; - this.fetch({ - error: () => console.error(`ERROR: unable to load file ${this.url}`) - }); + super.initialize(...args); + this.loadedData(); } loadedData() { + Adapt.course = this; Adapt.trigger('courseModel:dataLoaded'); } } + Adapt.register('course', { model: CourseModel }); + return CourseModel; }); diff --git a/src/core/js/mpabc.js b/src/core/js/mpabc.js index cbb2aa09a..c6a5157bc 100644 --- a/src/core/js/mpabc.js +++ b/src/core/js/mpabc.js @@ -1,9 +1,85 @@ define([ + 'core/js/adapt', + 'core/js/data', + 'core/js/collections/adaptSubsetCollection', + 'core/js/models/courseModel', + 'core/js/models/contentObjectModel', 'core/js/models/menuModel', 'core/js/models/pageModel', 'core/js/models/articleModel', 'core/js/models/blockModel', + 'core/js/models/componentModel', 'core/js/views/pageView', 'core/js/views/articleView', 'core/js/views/blockView' -], function(MenuModel, PageModel, ArticleModel, BlockModel, PageView, ArticleView, BlockView) {}); +], function(Adapt, Data, AdaptSubsetCollection, CourseModel, ContentObjectModel, MenuModel, PageModel, ArticleModel, BlockModel, ComponentModel, PageView, ArticleView, BlockView) { + + class MPABC extends Backbone.Controller { + + initialize() { + // Example of how to cause the data loader to wait for another module to setup + this.listenTo(Data, { + 'loading': this.waitForDataLoaded, + 'loaded': this.onDataLoaded + }); + this.setupDeprecatedSubsetCollections(); + } + + waitForDataLoaded() { + // Tell the data loader to wait + Adapt.wait.begin(); + } + + onDataLoaded() { + // Tell the data loader that we have finished + Adapt.wait.end(); + } + + setupDeprecatedSubsetCollections() { + let contentObjects = new AdaptSubsetCollection(null, { parent: Data, model: ContentObjectModel }); + let articles = new AdaptSubsetCollection(null, { parent: Data, model: ArticleModel }); + let blocks = new AdaptSubsetCollection(null, { parent: Data, model: BlockModel }); + let components = new AdaptSubsetCollection(null, { parent: Data, model: ComponentModel }); + Object.defineProperty(Adapt, 'contentObjects', { + get: function() { + Adapt.log.deprecated('Adapt.contentObjects, please use Adapt.data instead'); + return contentObjects; + }, + set: function(value) { + contentObjects = value; + } + }); + Object.defineProperty(Adapt, 'articles', { + get: function() { + Adapt.log.deprecated('Adapt.articles, please use Adapt.data instead'); + return articles; + }, + set: function(value) { + articles = value; + } + }); + Object.defineProperty(Adapt, 'blocks', { + get: function() { + Adapt.log.deprecated('Adapt.blocks, please use Adapt.data instead'); + return blocks; + }, + set: function(value) { + blocks = value; + } + }); + Object.defineProperty(Adapt, 'components', { + get: function() { + Adapt.log.deprecated('Adapt.components, please use Adapt.data instead'); + return components; + }, + set: function(value) { + components = value; + } + }); + } + + } + + return (Adapt.mpabc = new MPABC()); + +}); From a861913c9a037caccb736043be729b0329d83500 Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Mon, 6 Apr 2020 09:39:24 +0100 Subject: [PATCH 2/5] issue/2645 Added manifest creation and loading --- grunt/config/watch.js | 4 +- grunt/tasks/build.js | 1 + grunt/tasks/dev.js | 1 + grunt/tasks/diff.js | 1 + grunt/tasks/language-data-manifests.js | 7 + grunt/tasks/server-build.js | 1 + src/core/js/collections/adaptCollection.js | 6 - .../js/collections/adaptSubsetCollection.js | 23 ++ src/core/js/data.js | 256 +++++++----------- src/core/js/models/buildModel.js | 2 +- src/core/js/models/courseModel.js | 14 +- src/core/js/mpabc.js | 78 +++++- 12 files changed, 217 insertions(+), 177 deletions(-) create mode 100644 grunt/tasks/language-data-manifests.js create mode 100644 src/core/js/collections/adaptSubsetCollection.js diff --git a/grunt/config/watch.js b/grunt/config/watch.js index 46fe07361..c40ffa001 100644 --- a/grunt/config/watch.js +++ b/grunt/config/watch.js @@ -17,8 +17,8 @@ module.exports = { tasks: ['handlebars', 'javascript:dev'] }, courseJson: { - files: ['<%= sourcedir %>course/**/*.<%= jsonext %>'], - tasks: ['jsonlint', 'check-json', 'newer:copy:courseJson', 'schema-defaults'] + files: ['<%= sourcedir %>course/**/*.<%= jsonext %>', '<%= outputdir %>course/*/language_data_manifest.js'], + tasks: ['language-data-manifests', 'jsonlint', 'check-json', 'newer:copy:courseJson', 'schema-defaults'] }, courseAssets: { files: ['<%= sourcedir %>course/<%=languages%>/*', '!<%= sourcedir %>course/<%=languages%>/*.<%= jsonext %>'], diff --git a/grunt/tasks/build.js b/grunt/tasks/build.js index 76e5d3d00..042977768 100644 --- a/grunt/tasks/build.js +++ b/grunt/tasks/build.js @@ -9,6 +9,7 @@ module.exports = function(grunt) { 'build-config', 'copy', 'schema-defaults', + 'language-data-manifests', 'handlebars', 'tracking-insert', 'javascript:compile', diff --git a/grunt/tasks/dev.js b/grunt/tasks/dev.js index 9a3f0aeef..450f903bf 100644 --- a/grunt/tasks/dev.js +++ b/grunt/tasks/dev.js @@ -8,6 +8,7 @@ module.exports = function(grunt) { 'build-config', 'copy', 'schema-defaults', + 'language-data-manifests', 'handlebars', 'tracking-insert', 'javascript:dev', diff --git a/grunt/tasks/diff.js b/grunt/tasks/diff.js index 9f6f93da9..53f7694d3 100644 --- a/grunt/tasks/diff.js +++ b/grunt/tasks/diff.js @@ -8,6 +8,7 @@ module.exports = function(grunt) { 'build-config', 'copy', 'schema-defaults', + 'language-data-manifests', 'newer:handlebars:compile', 'tracking-insert', 'newer:javascript:dev', diff --git a/grunt/tasks/language-data-manifests.js b/grunt/tasks/language-data-manifests.js new file mode 100644 index 000000000..a38f232e7 --- /dev/null +++ b/grunt/tasks/language-data-manifests.js @@ -0,0 +1,7 @@ +module.exports = function(grunt) { + const Helpers = require('../helpers')(grunt); + grunt.registerTask('language-data-manifests', 'Creates a manifest for each set of language data files', function() { + const languages = Helpers.getFramework({ useOutputData: true }).getData().languages; + languages.forEach(language => language.saveManifest()); + }); +}; diff --git a/grunt/tasks/server-build.js b/grunt/tasks/server-build.js index ff82e5983..afebcf936 100644 --- a/grunt/tasks/server-build.js +++ b/grunt/tasks/server-build.js @@ -9,6 +9,7 @@ module.exports = function(grunt) { '_log-vars', 'build-config', 'copy', + 'language-data-manifests', 'less:' + requireMode, 'handlebars', 'javascript:' + requireMode, diff --git a/src/core/js/collections/adaptCollection.js b/src/core/js/collections/adaptCollection.js index 03566c559..500e65b04 100644 --- a/src/core/js/collections/adaptCollection.js +++ b/src/core/js/collections/adaptCollection.js @@ -5,13 +5,7 @@ define([ class AdaptCollection extends Backbone.Collection { initialize(models, options) { - this.url = options.url; this.once('reset', this.loadedData, this); - if (!this.url) return; - this.fetch({ - reset: true, - error: () => console.error('ERROR: unable to load file ' + this.url) - }); } loadedData() { diff --git a/src/core/js/collections/adaptSubsetCollection.js b/src/core/js/collections/adaptSubsetCollection.js new file mode 100644 index 000000000..ba6c188a1 --- /dev/null +++ b/src/core/js/collections/adaptSubsetCollection.js @@ -0,0 +1,23 @@ +define([ + 'core/js/adapt', + './adaptCollection' +], function(Adapt, AdaptCollection) { + + class AdaptSubsetCollection extends AdaptCollection { + + initialize(models, options) { + super.initialize(models, options); + this.parent = options.parent; + this.listenTo(this.parent, 'reset', this.loadSubset); + } + + loadSubset() { + this.set(this.parent.filter(model => model instanceof this.model)); + this._byAdaptID = this.groupBy('_id'); + } + + } + + return AdaptSubsetCollection; + +}); diff --git a/src/core/js/data.js b/src/core/js/data.js index f87d32ffd..258a9bcbf 100644 --- a/src/core/js/data.js +++ b/src/core/js/data.js @@ -1,38 +1,37 @@ define([ 'core/js/adapt', 'core/js/collections/adaptCollection', + 'core/js/models/buildModel', 'core/js/models/configModel', 'core/js/models/courseModel', 'core/js/models/lockingModel', - 'core/js/models/buildModel', 'core/js/startController' -], function(Adapt, AdaptCollection, ConfigModel, CourseModel) { +], function(Adapt, AdaptCollection, BuildModel, ConfigModel, CourseModel) { - class Data extends Backbone.Controller { + class Data extends AdaptCollection { - initialize() { - this.mappedIds = {}; + model(json) { + const ModelClass = Adapt.getModelClass(json); + if (!ModelClass) { + return new Backbone.Model(json); + } + return new ModelClass(json); } - init () { - Adapt.build.whenReady().then(this.onBuildDataLoaded.bind(this)); + async init () { + Adapt.build = new BuildModel(null, { url: 'adapt/js/build.min.js', reset: true }); + await Adapt.build.whenReady(); + $('html').attr('data-adapt-framework-version', Adapt.build.get('package').version); + this.loadConfigData(); } - onBuildDataLoaded() { - $('html').attr('data-adapt-framework-version', Adapt.build.get('package').version); + loadConfigData() { Adapt.config = new ConfigModel(null, { url: 'course/config.' + Adapt.build.get('jsonext'), reset: true }); + this.listenToOnce(Adapt, 'configModel:loadCourseData', this.onLoadCourseData); this.listenTo(Adapt.config, { 'change:_activeLanguage': this.onLanguageChange, 'change:_defaultDirection': this.onDirectionChange }); - - // Events that are triggered by the main Adapt content collections and models - this.listenToOnce(Adapt, 'configModel:loadCourseData', this.onLoadCourseData); - } - - onLanguageChange(model, language) { - Adapt.offlineStorage.set('lang', language); - this.loadCourseData(this.triggerDataReady.bind(this), language); } onDirectionChange(model, direction) { @@ -48,141 +47,116 @@ define([ * If it has we can go ahead and start loading; if it hasn't, apply the defaultLanguage from config.json */ onLoadCourseData() { - if (Adapt.config.get('_activeLanguage')) { - this.loadCourseData(this.triggerDataReady.bind(this)); - } else { + if (!Adapt.config.get('_activeLanguage')) { Adapt.config.set('_activeLanguage', Adapt.config.get('_defaultLanguage')); + return; } + this.loadCourseData(); } - loadCourseData(callback, newLanguage) { - this.listenTo(Adapt, 'adaptCollection:dataLoaded courseModel:dataLoaded', () => { - this.checkDataIsLoaded(callback, newLanguage); - }); + onLanguageChange(model, language) { + Adapt.offlineStorage.set('lang', language); + this.loadCourseData(language); + } + + async loadCourseData(newLanguage) { // All code that needs to run before adapt starts should go here const language = Adapt.config.get('_activeLanguage'); - const jsonext = Adapt.build.get('jsonext'); + const courseFolder = 'course/' + language + '/'; $('html').attr('lang', language); - const getContentObjectModel = json => { - const ModelClass = Adapt.getModelClass(json) || Adapt.getModelClass('menu'); - if (!ModelClass) { - throw new Error(`Cannot find model for: ${Adapt.getModelName(json)}`); - } - return new ModelClass(json); - }; - - const getPath = name => `course/${language}/${name}.${jsonext}`; - - const getModel = json => { - const ModelClass = Adapt.getModelClass(json); - if (!ModelClass) { - throw new Error(`Cannot find model for: ${Adapt.getModelName(json)}`); - } - return new ModelClass(json); - }; - - Adapt.course = new CourseModel(null, { url: getPath('course'), reset: true }); - - Adapt.contentObjects = new AdaptCollection(null, { - model: getContentObjectModel, - url: getPath('contentObjects') - }); - - Adapt.articles = new AdaptCollection(null, { - model: getModel, - url: getPath('articles') - }); + await this.loadManifestFiles(courseFolder); + await this.triggerDataLoaded(); + await this.triggerDataReady(newLanguage); + this.triggerInit(); - Adapt.blocks = new AdaptCollection(null, { - model: getModel, - url: getPath('blocks') - }); + } - Adapt.components = new AdaptCollection(null, { - model: getModel, - url: getPath('components') + getJSON(path) { + return new Promise((resolve, reject) => { + $.getJSON(path, data => { + // Add path to data incase it's necessary later + data.__path__ = path; + resolve(data); + }).fail(reject); }); } - checkDataIsLoaded(callback, newLanguage) { - if (Adapt.contentObjects.models.length > 0 && - Adapt.articles.models.length > 0 && - Adapt.blocks.models.length > 0 && - Adapt.components.models.length > 0 && - Adapt.course.get('_id')) { - - this.mapAdaptIdsToObjects(); - - Adapt.log.debug('Firing app:dataLoaded'); - - try { - Adapt.trigger('app:dataLoaded');// Triggered to setup model connections in AdaptModel.js - } catch (e) { - Adapt.log.error('Error during app:dataLoading trigger', e); + async loadManifestFiles(languagePath) { + this.trigger('loading'); + this.reset(); + const manifestPath = languagePath + 'language_data_manifest.js'; + let manifest; + try { + manifest = await this.getJSON(manifestPath); + } catch (err) { + manifest = ['course.json', 'contentObjects.json', 'articles.json', 'blocks.json', 'components.json']; + Adapt.log.warnOnce(`Manifest path '${manifestPath} not found. Using traditional files: ${manifest.join(', ')}`); + } + const allFileData = await Promise.all(manifest.map(filePath => { + return this.getJSON(`${languagePath}${filePath}`); + })); + // Flatten all file data into a single array of model data + const allModelData = allFileData.reduce((result, fileData) => { + if (fileData instanceof Array) { + result.push(...fileData); + } else if (fileData instanceof Object) { + result.push(fileData); + } else { + Adapt.log.warnOnce(`File data isn't an array or object: ${fileData.__path__}`); } - - this.setupMapping(); - - Adapt.wait.queue(() => { - callback(newLanguage); - }); - + return result; + }, []); + // Add course model first to allow other model/views to utilize its settings + const course = allModelData.find(modelData => modelData._type === 'course'); + if (!course) { + throw new Error(`Expected a model data with "_type": "course", none found.`); } + this.push(course); + // Add other models + allModelData.forEach(modelData => { + if (modelData._type === 'course') { + return; + } + this.push(modelData); + }); + // index by id + this._byAdaptID = this.indexBy('_id'); + this.trigger('reset'); + this.trigger('loaded'); + await Adapt.wait.queue(); } - mapAdaptIdsToObjects() { - Adapt.contentObjects._byAdaptID = Adapt.contentObjects.groupBy('_id'); - Adapt.articles._byAdaptID = Adapt.articles.groupBy('_id'); - Adapt.blocks._byAdaptID = Adapt.blocks.groupBy('_id'); - Adapt.components._byAdaptID = Adapt.components.groupBy('_id'); - } - - setupMapping() { - this.mappedIds = {}; - - // Setup course Id - this.mappedIds[Adapt.course.get('_id')] = 'course'; - - const collections = ['contentObjects', 'articles', 'blocks', 'components']; - - collections.forEach(collection => { - Adapt[collection].models.forEach(model => { - const id = model.get('_id'); - this.mappedIds[id] = collection; - }); - }); + async triggerDataLoaded() { + Adapt.log.debug('Firing app:dataLoaded'); + try { + Adapt.trigger('app:dataLoaded');// Triggered to setup model connections in AdaptModel.js + } catch (e) { + Adapt.log.error('Error during app:dataLoading trigger', e); + } + await Adapt.wait.queue(); } - triggerDataReady(newLanguage) { + async triggerDataReady(newLanguage) { if (newLanguage) { - Adapt.trigger('app:languageChanged', newLanguage); - _.defer(() => { Adapt.startController.loadCourseData(); - let hash = '#/'; - - if (Adapt.startController.isEnabled()) { - hash = Adapt.startController.getStartHash(true); - } - - Backbone.history.navigate(hash, { trigger: true, replace: true }); + const hash = Adapt.startController.isEnabled() ? + '#/' : + Adapt.startController.getStartHash(true); + Adapt.router.navigate(hash, { trigger: true, replace: true }); }); } - Adapt.log.debug('Firing app:dataReady'); - try { Adapt.trigger('app:dataReady'); } catch (e) { Adapt.log.error('Error during app:dataReady trigger', e); } - - Adapt.wait.queue(this.triggerInit.bind(this)); - + await Adapt.wait.queue(); } triggerInit() { @@ -192,59 +166,23 @@ define([ whenReady() { if (this.isReady) return Promise.resolve(); - return new Promise(resolve => { this.once('ready', resolve); }); } - /** - * Looks up which collection a model belongs to - * @param {string} id The id of the item you want to look up e.g. `"co-05"` - * @return {string} One of the following (or `undefined` if not found): - * - "course" - * - "contentObjects" - * - "blocks" - * - "articles" - * - "components" - */ - mapById(id) { - return this.mappedIds[id]; - } - /** * Looks up a model by its `_id` property * @param {string} id The id of the item e.g. "co-05" * @return {Backbone.Model} */ findById(id) { - if (id === Adapt.course.get('_id')) { - return Adapt.course; - } - - const collectionType = Adapt.mapById(id); - - if (!collectionType) { - console.warn('Adapt.findById() unable to find collection type for id: ' + id); + const model = this._byAdaptID[id]; + if (!model) { + console.warn(`Adapt.findById() unable to find collection type for id: ${id}`); return; } - - return Adapt[collectionType]._byAdaptID[id][0]; - } - - /** - * Filter all models. - * @param {Function} filter - * @returns {Array} - */ - filter(filter) { - const result = []; - filter(Adapt.course) && result.push(Adapt.course); - result.push(...Adapt.contentObjects.filter(filter)); - result.push(...Adapt.articles.filter(filter)); - result.push(...Adapt.blocks.filter(filter)); - result.push(...Adapt.components.filter(filter)); - return result; + return model; } } diff --git a/src/core/js/models/buildModel.js b/src/core/js/models/buildModel.js index 142186fa6..47ed6b69e 100644 --- a/src/core/js/models/buildModel.js +++ b/src/core/js/models/buildModel.js @@ -37,6 +37,6 @@ define([ } - return (Adapt.build = new BuildModel(null, { url: 'adapt/js/build.min.js', reset: true })); + return BuildModel; }); diff --git a/src/core/js/models/courseModel.js b/src/core/js/models/courseModel.js index 6065a40b5..b90ba61a1 100644 --- a/src/core/js/models/courseModel.js +++ b/src/core/js/models/courseModel.js @@ -23,23 +23,21 @@ define([ return 'course'; } - initialize(attrs, options) { - super.initialize(arguments); + initialize(...args) { Adapt.trigger('courseModel:dataLoading'); - this.url = options.url; - this.on('sync', this.loadedData, this); - if (!this.url) return; - this.fetch({ - error: () => console.error(`ERROR: unable to load file ${this.url}`) - }); + super.initialize(...args); + this.loadedData(); } loadedData() { + Adapt.course = this; Adapt.trigger('courseModel:dataLoaded'); } } + Adapt.register('course', { model: CourseModel }); + return CourseModel; }); diff --git a/src/core/js/mpabc.js b/src/core/js/mpabc.js index cbb2aa09a..c6a5157bc 100644 --- a/src/core/js/mpabc.js +++ b/src/core/js/mpabc.js @@ -1,9 +1,85 @@ define([ + 'core/js/adapt', + 'core/js/data', + 'core/js/collections/adaptSubsetCollection', + 'core/js/models/courseModel', + 'core/js/models/contentObjectModel', 'core/js/models/menuModel', 'core/js/models/pageModel', 'core/js/models/articleModel', 'core/js/models/blockModel', + 'core/js/models/componentModel', 'core/js/views/pageView', 'core/js/views/articleView', 'core/js/views/blockView' -], function(MenuModel, PageModel, ArticleModel, BlockModel, PageView, ArticleView, BlockView) {}); +], function(Adapt, Data, AdaptSubsetCollection, CourseModel, ContentObjectModel, MenuModel, PageModel, ArticleModel, BlockModel, ComponentModel, PageView, ArticleView, BlockView) { + + class MPABC extends Backbone.Controller { + + initialize() { + // Example of how to cause the data loader to wait for another module to setup + this.listenTo(Data, { + 'loading': this.waitForDataLoaded, + 'loaded': this.onDataLoaded + }); + this.setupDeprecatedSubsetCollections(); + } + + waitForDataLoaded() { + // Tell the data loader to wait + Adapt.wait.begin(); + } + + onDataLoaded() { + // Tell the data loader that we have finished + Adapt.wait.end(); + } + + setupDeprecatedSubsetCollections() { + let contentObjects = new AdaptSubsetCollection(null, { parent: Data, model: ContentObjectModel }); + let articles = new AdaptSubsetCollection(null, { parent: Data, model: ArticleModel }); + let blocks = new AdaptSubsetCollection(null, { parent: Data, model: BlockModel }); + let components = new AdaptSubsetCollection(null, { parent: Data, model: ComponentModel }); + Object.defineProperty(Adapt, 'contentObjects', { + get: function() { + Adapt.log.deprecated('Adapt.contentObjects, please use Adapt.data instead'); + return contentObjects; + }, + set: function(value) { + contentObjects = value; + } + }); + Object.defineProperty(Adapt, 'articles', { + get: function() { + Adapt.log.deprecated('Adapt.articles, please use Adapt.data instead'); + return articles; + }, + set: function(value) { + articles = value; + } + }); + Object.defineProperty(Adapt, 'blocks', { + get: function() { + Adapt.log.deprecated('Adapt.blocks, please use Adapt.data instead'); + return blocks; + }, + set: function(value) { + blocks = value; + } + }); + Object.defineProperty(Adapt, 'components', { + get: function() { + Adapt.log.deprecated('Adapt.components, please use Adapt.data instead'); + return components; + }, + set: function(value) { + components = value; + } + }); + } + + } + + return (Adapt.mpabc = new MPABC()); + +}); From d6dfe8d3f2a558125dcd79e5872fe85f96290b6b Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Tue, 14 Apr 2020 10:43:55 +0100 Subject: [PATCH 3/5] Recommendations --- src/core/js/mpabc.js | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/core/js/mpabc.js b/src/core/js/mpabc.js index c6a5157bc..0fa77a9dd 100644 --- a/src/core/js/mpabc.js +++ b/src/core/js/mpabc.js @@ -19,8 +19,8 @@ define([ initialize() { // Example of how to cause the data loader to wait for another module to setup this.listenTo(Data, { - 'loading': this.waitForDataLoaded, - 'loaded': this.onDataLoaded + loading: this.waitForDataLoaded, + loaded: this.onDataLoaded }); this.setupDeprecatedSubsetCollections(); } @@ -41,40 +41,32 @@ define([ let blocks = new AdaptSubsetCollection(null, { parent: Data, model: BlockModel }); let components = new AdaptSubsetCollection(null, { parent: Data, model: ComponentModel }); Object.defineProperty(Adapt, 'contentObjects', { - get: function() { + get: () => { Adapt.log.deprecated('Adapt.contentObjects, please use Adapt.data instead'); return contentObjects; }, - set: function(value) { - contentObjects = value; - } + set: value => contentObjects = value }); Object.defineProperty(Adapt, 'articles', { - get: function() { + get: () => { Adapt.log.deprecated('Adapt.articles, please use Adapt.data instead'); return articles; }, - set: function(value) { - articles = value; - } + set: value => articles = value }); Object.defineProperty(Adapt, 'blocks', { - get: function() { + get: () => { Adapt.log.deprecated('Adapt.blocks, please use Adapt.data instead'); return blocks; }, - set: function(value) { - blocks = value; - } + set: value => blocks = value }); Object.defineProperty(Adapt, 'components', { - get: function() { + get: () => { Adapt.log.deprecated('Adapt.components, please use Adapt.data instead'); return components; }, - set: function(value) { - components = value; - } + set: value => components = value }); } From 4dd0ba80611117e3fc1423ee7180710807cc7b0e Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Tue, 14 Apr 2020 11:34:00 +0100 Subject: [PATCH 4/5] Improved babel source mapping --- grunt/config/babel.js | 60 +++++++++++++++++++++++++++---------- grunt/tasks/build.js | 2 +- grunt/tasks/dev.js | 2 +- grunt/tasks/diff.js | 2 +- grunt/tasks/server-build.js | 2 +- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/grunt/config/babel.js b/grunt/config/babel.js index dd9178c5e..3e6415f60 100644 --- a/grunt/config/babel.js +++ b/grunt/config/babel.js @@ -1,22 +1,50 @@ module.exports = { - options: { - sourceMap: true, - inputSourceMap: true, - sourceType: 'script', - minified: true, - comments: false, - presets: [ - [ - '@babel/preset-env', - { - "targets": { - "ie": "11" + compile: { + options: { + inputSourceMap: false, + minified: true, + comments: false, + presets: [ + [ + '@babel/preset-env', + { + "targets": { + "ie": "11" + } } - } + ] ] - ] + }, + files: [{ + "expand": true, + "cwd": "<%= tempdir %>", + "src": [ + "adapt.min.js" + ], + "dest": "<%= outputdir %>adapt/js/", + "ext": ".min.js" + }] }, - dist: { + dev: { + options: { + sourceMap: true, + inputSourceMap: true, + sourceType: 'script', + retainLines: true, + minified: false, + compact: false, + comments: true, + presets: [ + [ + '@babel/preset-env', + { + "targets": { + "ie": "11" + } + } + ] + ] + }, files: [{ "expand": true, "cwd": "<%= tempdir %>", @@ -27,4 +55,4 @@ module.exports = { "ext": ".min.js" }] } -}; \ No newline at end of file +}; diff --git a/grunt/tasks/build.js b/grunt/tasks/build.js index 042977768..7c2803138 100644 --- a/grunt/tasks/build.js +++ b/grunt/tasks/build.js @@ -13,7 +13,7 @@ module.exports = function(grunt) { 'handlebars', 'tracking-insert', 'javascript:compile', - 'babel', + 'babel:compile', 'clean:dist', 'less:compile', 'replace', diff --git a/grunt/tasks/dev.js b/grunt/tasks/dev.js index 450f903bf..29c3f8d34 100644 --- a/grunt/tasks/dev.js +++ b/grunt/tasks/dev.js @@ -12,7 +12,7 @@ module.exports = function(grunt) { 'handlebars', 'tracking-insert', 'javascript:dev', - 'babel', + 'babel:dev', 'less:dev', 'replace', 'scripts:adaptpostbuild', diff --git a/grunt/tasks/diff.js b/grunt/tasks/diff.js index 53f7694d3..3939933aa 100644 --- a/grunt/tasks/diff.js +++ b/grunt/tasks/diff.js @@ -12,7 +12,7 @@ module.exports = function(grunt) { 'newer:handlebars:compile', 'tracking-insert', 'newer:javascript:dev', - 'babel', + 'babel:dev', 'newer:less:dev', 'replace', 'scripts:adaptpostbuild', diff --git a/grunt/tasks/server-build.js b/grunt/tasks/server-build.js index afebcf936..5397490aa 100644 --- a/grunt/tasks/server-build.js +++ b/grunt/tasks/server-build.js @@ -13,7 +13,7 @@ module.exports = function(grunt) { 'less:' + requireMode, 'handlebars', 'javascript:' + requireMode, - 'babel', + 'babel:' + requireMode, 'replace', 'scripts:adaptpostbuild', 'clean:temp' From 3c7b65644793c85810150e9a916c04fbeff095ab Mon Sep 17 00:00:00 2001 From: Oliver Foster Date: Tue, 14 Apr 2020 12:23:57 +0100 Subject: [PATCH 5/5] Readded sourceType to babel config --- grunt/config/babel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/grunt/config/babel.js b/grunt/config/babel.js index 3e6415f60..e794ef21c 100644 --- a/grunt/config/babel.js +++ b/grunt/config/babel.js @@ -2,6 +2,7 @@ module.exports = { compile: { options: { inputSourceMap: false, + sourceType: 'script', minified: true, comments: false, presets: [