Skip to content

Commit

Permalink
Editor performance improvements (adaptlearning#1798)
Browse files Browse the repository at this point in the history
* Rename model attributes

* Remove unnecessary collections from editor data

* Update attribute references

* Remove unused functions

* Update models to use actual collection item names

* Refactor editor menu code to allow for async loading

* Stop re-rendering if item already selected

* Remove references to unused model attributes

* Improve comments

* Stop code executing for each item

* Refactor get functions

* Remove unused bits

* Fix various issues

* Remove unused imports

* Refactor to only call listenTo once

* Update ContentModel to return children as array

Due to issue with Backbone.Collections and multiple model types

* Remove unused cut functionality

* Remove unused code

* Update component render to work with async code

* Handle re-rendering in parent

* Make re-render async

* Fix model accessor

* Remove unused imports

* Simplify component delete code

* Stop fetchSiblings from returning self

* Add data-id to all editorOriginViews for convenience

* Removed unused ‘ancestors’ code

* Fix page getter

* Refactor new block code

* Refactor layout code

* Make destroy async to ensure we catch any errors

* Refactor for brevity

* Refactor block rendering for readability

* Refactor new article code

* Remove sibling fetch

* Fix server copy/paste

* Remove unused var

* Remove unused route

* Remove unused sync classes

* Allow for proper async rendering in page views

* Fix whitespace

* Refactor for readability

* Remove logs

* Remove asset URL helpers

As they’re now async…

* Fix courseassets

* Fix async course validation

* Remove noisy warning

* Remove log

* Fix scope issue

* Fix issue with external assets

* Move stuff around

* Remove spaghetti logic from scaffoldAsset template

* Remove log

* Remove log

* Stop unnecessary 404 errors

* Fix courseassets clean-up

* Improve helper to allow for iterators which modify the original list

* Improve performance

* Make content fetches run in parallel

* Make helper parallel

* Improve page editor rendering time

* Remove mandatory fetch from contentModel.initialize

* Make sure model is fetched before rendering

* Remove unused import

* Move async rendering to EditorPageBlockView

* Remove client clipboard data

Clipboard data now automatically deleted by back-end

* Fix issue with hidden blocks

* Fix issues

* Make content fetches run in parallel

* Make helper parallel

* Improve page editor rendering time

* Remove mandatory fetch from contentModel.initialize

* Make sure model is fetched before rendering

* Remove unused import

* Move async rendering to EditorPageBlockView

* Remove client clipboard data

Clipboard data now automatically deleted by back-end

* Fix issue with hidden blocks

* Add mid-render style to editor page blocks

* Fix preview with popup blockers

* Stop page re-rendering on paste

* Remove logs

* Handle destroy async

* Fix menu layer rendering for new/removed COs

* Make rendering series

* Fix menu item state restoration

* Fix merge issues

* Don’t break for components with no supportedLayout

* Move async fetch code to index so scaffold can render correctly

Added a multi-fetch convenience method

* Refactor for readability

* Fix var reference

* Update error messages
  • Loading branch information
taylortom authored and Annoraaq committed Feb 21, 2018
1 parent 76d4baf commit 9be32b5
Show file tree
Hide file tree
Showing 37 changed files with 1,354 additions and 1,515 deletions.
35 changes: 35 additions & 0 deletions frontend/src/core/collections/contentCollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE
define(function(require) {
var Backbone = require('backbone');
var Origin = require('core/origin');
var Helpers = require('core/helpers');

var ContentCollection = Backbone.Collection.extend({
initialize : function(models, options) {
this._type = options._type;
this.model = Helpers.contentModelMap(this._type);
this._courseId = options._courseId;
this._parentId = options._parentId;
this.url = options.url || 'api/content/' + options._type + this.buildQuery();

this.on('reset', this.loadedData, this);
},

buildQuery: function() {
var query = '';
if(this._courseId) {
query += '_courseId=' + this._courseId
}
if(this._parentId) {
query += '_parentId=' + this._parentId
}
return query ? '?' + query : '';
},

loadedData: function() {
Origin.trigger('contentCollection:dataLoaded', this._type);
}
});

return ContentCollection;
});
204 changes: 119 additions & 85 deletions frontend/src/core/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,21 +205,6 @@ define(function(require){
}
},

getAssetFromValue: function(url) {
var urlSplit = url.split('/')
var fileName = urlSplit[urlSplit.length - 1];
// Get courseAsset model
var courseAsset = Origin.editor.data.courseassets.findWhere({_fieldName: fileName});

if (courseAsset) {
var courseAssetId = courseAsset.get('_assetId');

return '/api/asset/serve/' + courseAssetId;
} else {
return '';
}
},

ifImageIsCourseAsset: function(url, block) {
if (url.length !== 0 && url.indexOf('course/assets') == 0) {
return block.fn(this);
Expand All @@ -228,21 +213,6 @@ define(function(require){
}
},

getThumbnailFromValue: function(url) {

var urlSplit = url.split('/')
var fileName = urlSplit[urlSplit.length - 1];
// Get courseAsset model
var courseAsset = Origin.editor.data.courseassets.findWhere({_fieldName: fileName});
if (courseAsset) {
var courseAssetId = courseAsset.get('_assetId');
return '/api/asset/thumb/' + courseAssetId;
} else {
return '/api/asset/thumb/' + url;
}

},

ifAssetIsExternal: function(url, block) {
if(Handlebars.helpers.isAssetExternal(url)) {
return block.fn(this);
Expand Down Expand Up @@ -297,75 +267,139 @@ define(function(require){

return success;
},

validateCourseContent: function(currentCourse) {
// Let's do a standard check for at least one child object
var containsAtLeastOneChild = true;

// checks for at least one child object
validateCourseContent: function(currentCourse, callback) {
var containsAtLeastOneChild = true;
var alerts = [];

function iterateOverChildren(model) {
// Return the function if no children - on components
if(!model._children) return;

var currentChildren = model.getChildren();

// Do validate across each item
if (currentChildren.length == 0) {
containsAtLeastOneChild = false;

var children = _.isArray(model._children) ? model._children.join('/') : model._children;
alerts.push(
"There seems to be a "
+ model.get('_type')
+ " with the title - '"
+ model.get('title')
+ "' with no "
+ children
);

return;
} else {
// Go over each child and call validation again
currentChildren.each(function(childModel) {
iterateOverChildren(childModel);
});
var iterateOverChildren = function(model, index, doneIterator) {
if(!model._childTypes) {
return doneIterator();
}

}

iterateOverChildren(currentCourse);

if(alerts.length > 0) {
model.fetchChildren(function(currentChildren) {
if (currentChildren.length > 0) {
return helpers.forParallelAsync(currentChildren, iterateOverChildren, doneIterator);
}
containsAtLeastOneChild = false;
var children = _.isArray(model._childTypes) ? model._childTypes.join('/') : model._childTypes;
alerts.push(model.get('_type') + " '" + model.get('title') + "' missing " + children);
doneIterator();
});
};
// start recursion
iterateOverChildren(currentCourse, null, function() {
var errorMessage = "";
for(var i = 0, len = alerts.length; i < len; i++) {
errorMessage += "<li>" + alerts[i] + "</li>";
if(alerts.length > 0) {
for(var i = 0, len = alerts.length; i < len; i++) {
errorMessage += "<li>" + alerts[i] + "</li>";
}
return callback(new Error(errorMessage));
}

Origin.Notify.alert({
type: 'error',
title: Origin.l10n.t('app.validationfailed'),
text: errorMessage,
callback: _.bind(this.validateCourseConfirm, this)
});
}

return containsAtLeastOneChild;
callback(null, true);
});
},

validateCourseConfirm: function(isConfirmed) {
if (isConfirmed) {
Origin.trigger('editor:courseValidation');
}
},

isValidEmail: function(value) {
var regEx = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (value.length === 0 || !regEx.test(value)) {
return false;
} else {
return true;
}
},

contentModelMap: function(type) {
var contentModels = {
contentobject: 'core/models/contentObjectModel',
article: 'core/models/articleModel',
block: 'core/models/blockModel',
component: 'core/models/componentModel',
courseasset: 'core/models/courseAssetModel'
};
if(contentModels.hasOwnProperty(type)) {
return require(contentModels[type]);
}
},

/**
* Ensures list is iterated (doesn't guarantee order), even if using async iterator
* @param list Array or Backbone.Collection
* @param func Function to use as iterator. Will be passed item, index and callback function
* @param callback Function to be called on completion
*/
forParallelAsync: function(list, func, callback) {
if(!list.hasOwnProperty('length') || list.length === 0) {
if(typeof callback === 'function') callback();
return;
}
// make a copy in case func modifies the original
var listCopy = list.models ? list.models.slice() : list.slice();
var doneCount = 0;
var _checkCompletion = function() {
if((++doneCount === listCopy.length) && typeof callback === 'function') {
callback();
}
};
for(var i = 0, count = listCopy.length; i < count; i++) {
func(listCopy[i], i, _checkCompletion);
}
},

/**
* Ensures list is iterated in order, even if using async iterator
* @param list Array or Backbone.Collection
* @param func Function to use as iterator. Will be passed item, index and callback function
* @param callback Function to be called on completion
*/
forSeriesAsync: function(list, func, callback) {
if(!list.hasOwnProperty('length') || list.length === 0) {
if(typeof callback === 'function') callback();
return;
}
// make a copy in case func modifies the original
var listCopy = list.models ? list.models.slice() : list.slice();
var doneCount = -1;
var _doAsync = function() {
if(++doneCount === listCopy.length) {
if(typeof callback === 'function') callback();
return;
}
var nextItem = listCopy[doneCount];
if(!nextItem) {
console.error('Invalid item at', doneCount + ':', nextItem);
}
func(nextItem, doneCount, _doAsync);
};
_doAsync();
},

/**
* Does a fetch for model in models, and returns the latest data in the
* passed callback
* @param models {Array of Backbone.Models}
* @param callback {Function to call when complete}
*/
multiModelFetch: function(models, callback) {
var collatedData = {};
helpers.forParallelAsync(models, function(model, index, done) {
model.fetch({
success: function(data) {
collatedData[index] = data;
done();
},
error: function(data) {
console.error('Failed to fetch data for', model.get('_id'), + data.responseText);
done();
}
});
}, function doneAll() {
var orderedKeys = Object.keys(collatedData).sort();
var returnArr = [];
for(var i = 0, count = orderedKeys.length; i < count; i++) {
returnArr.push(collatedData[orderedKeys[i]]);
}
callback(returnArr);
});
}
};

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/core/models/articleModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ define(function(require) {

var ArticleModel = ContentModel.extend({
urlRoot: '/api/content/article',
_parent: 'contentObjects',
_siblings: 'articles',
_children: 'blocks'
_parentType: 'contentobject',
_siblingTypes: 'article',
_childTypes: 'block'
});

return ArticleModel;
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/core/models/blockModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ define(function(require) {

var BlockModel = ContentModel.extend({
urlRoot: '/api/content/block',
_parent: 'articles',
_siblings: 'blocks',
_children: 'components',
_parentType: 'article',
_siblingTypes: 'block',
_childTypes: 'component',
// Block specific properties
layoutOptions: null,
dragLayoutOptions: null,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/core/models/componentModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ define(function(require) {

var ComponentModel = ContentModel.extend({
urlRoot: '/api/content/component',
_parent: 'blocks',
_siblings: 'components',
_parentType: 'block',
_siblingTypes: 'component',
// These are the only attributes which should be permitted on a save
// TODO look into this...
whitelistAttributes: [
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/core/models/componentTypeModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ define(function(require) {
var ComponentTypeModel = ContentModel.extend({
idAttribute: '_id',
urlRoot: '/api/componenttype',
_parent: 'blocks',
_parent: 'block',

comparator: function(model) {
return model.get('displayName');
Expand Down
Loading

0 comments on commit 9be32b5

Please sign in to comment.