From 2f4e6a6188f6f8714d06f0404abf514a1345e59a Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 3 Mar 2016 13:13:32 +0000 Subject: [PATCH 001/111] Move slugify to helpers --- lib/helpers.js | 51 ++++++++++++++++++++++++++++++-------------- lib/outputmanager.js | 15 ------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 7560974e16..9146c85988 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -74,35 +74,35 @@ function grantSuperPermissions (userId, next) { if (!userId) { return next(new Error("Failed to grant super user permissions, invalid userId: " + userId)); } - + var _assignSuperRole = function (userId, cb) { // verify the Super Admin role is installed rolemanager.retrieveRole({ name: "Super Admin" }, function (err, role) { if (err) { return cb(err); } - + if (role) { return rolemanager.assignRole(role._id, userId, cb); } - + // role does not exist, bah! return cb(new Error('Role not found')); }); }; - + _assignSuperRole(userId, function (err) { if (!err) { // no problem, role was assigned return next(null); } - + // otherwise we need to install default roles rolemanager.installDefaultRoles(function (err) { if (err) { return next(err); } - + // the second attempt passes control back to the first callback _assignSuperRole(userId, next); }); @@ -128,10 +128,10 @@ function isMasterPreviewAccessible(courseId, userId, next) { if (course._isShared || course.createdBy == userId) { // Shared courses on the same tenant are open to users on the same tenant return next(null, true); - } - + } + return next(null, false); - + } else { return next(new Error('Course ' + courseId + ' not found')); } @@ -164,23 +164,23 @@ function hasCoursePermission (action, userId, tenantId, contentItem, next) { if ((course._isShared || course.createdBy == userId) && course._tenantId.toString() == tenantId) { // Shared courses on the same tenant are open to users on the same tenant return next(null, true); - } - + } + return next(null, false); - + } else { return next(new Error('Course ' + courseId + ' not found')); } }); - }); + }); } else { // Course permission cannot be verified return next(null, false); - } + } } /** - * Replaces ALL insstances of the search parameter in a given string with a replacement + * Replaces ALL instances of the search parameter in a given string with a replacement * @param {string} str * @param {string} search * @param {string} replacement @@ -189,10 +189,29 @@ function replaceAll(str, search, replacement) { return str.split(search).join(replacement); } +/** + * Returns a slugified string, e.g. for use in a published filename + * Removes non-word/whitespace chars, converts to lowercase and replaces spaces with hyphens + * Multiple arguments are joined with spaces (and therefore hyphenated) + * @return {string} + **/ +function slugify() { + var str = Array.apply(null,arguments).join(' '); + var strip_re = /[^\w\s-]/g; + var hyphenate_re = /[-\s]+/g; + + str = str.replace(strip_re, '').trim() + str = str.toLowerCase(); + str = str.replace(hyphenate_re, '-'); + + return str; +} + exports = module.exports = { schemaToObject: schemaToObject, grantSuperPermissions: grantSuperPermissions, hasCoursePermission: hasCoursePermission, isMasterPreviewAccessible: isMasterPreviewAccessible, - replaceAll: replaceAll + replaceAll: replaceAll, + slugify: slugify }; diff --git a/lib/outputmanager.js b/lib/outputmanager.js index 0f01936787..6e75c7eb8c 100644 --- a/lib/outputmanager.js +++ b/lib/outputmanager.js @@ -898,21 +898,6 @@ OutputPlugin.prototype.export = function (courseId, req, res, next) { throw new Error('OutputPlugin#export must be implemented by extending objects!'); }; -/** - * Returns a slugified string, e.g. for use in a published filename - * - * @return {string} - */ -OutputPlugin.prototype.slugify = function(s) { - var _slugify_strip_re = /[^\w\s-]/g; - var _slugify_hyphenate_re = /[-\s]+/g; - - s = s.replace(_slugify_strip_re, '').trim().toLowerCase(); - s = s.replace(_slugify_hyphenate_re, '-'); - - return s; -}; - /** * Returns a string with double and single quote characters escaped * From 867c757b5b8cbb3fea941e808906d7a7f1013463 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 3 Mar 2016 13:22:15 +0000 Subject: [PATCH 002/111] Only allow single export zip to be stored per-user at any one time Fixes #1056 --- plugins/output/adapt/index.js | 59 ++++++++++++----------------------- routes/export/index.js | 59 +++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 66 deletions(-) diff --git a/plugins/output/adapt/index.js b/plugins/output/adapt/index.js index 341f661627..24a9d34ee7 100644 --- a/plugins/output/adapt/index.js +++ b/plugins/output/adapt/index.js @@ -24,6 +24,7 @@ var origin = require('../../../'), exec = require('child_process').exec, semver = require('semver'), version = require('../../../version'), + helpers = require('../../../lib/helpers'), logger = require('../../../lib/logger'); function AdaptOutput() { @@ -211,7 +212,7 @@ AdaptOutput.prototype.publish = function(courseId, isPreview, request, response, if (!isPreview) { // Now zip the build package var filename = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.AllCourses, tenantId, courseId, Constants.Filenames.Download); - var zipName = self.slugify(outputJson['course'].title); + var zipName = helpers.slugify(outputJson['course'].title); var output = fs.createWriteStream(filename), archive = archiver('zip'); @@ -257,40 +258,16 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) { var self = this; var tenantId = usermanager.getCurrentUser().tenant._id; var userId = usermanager.getCurrentUser()._id; - var timestamp = new Date().toISOString().replace('T', '-').replace(/:/g, '').substr(0,17); var FRAMEWORK_ROOT_FOLDER = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), Constants.Folders.Framework); var COURSE_ROOT_FOLDER = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.AllCourses, tenantId, courseId); - - // set in getCourseName - var exportName; - var exportDir; + var exportDir = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.Exports, userId); async.waterfall([ function publishCourse(callback) { self.publish(courseId, true, request, response, callback); }, - function getCourseName(results, callback) { - database.getDatabase(function (error, db) { - if (error) { - return callback(err); - } - - db.retrieve('course', { _id: courseId }, { jsonOnly: true }, function (error, results) { - if (error) { - return callback(error); - } - if(!results || results.length > 1) { - return callback(new Error('Unexpected results returned for course ' + courseId + ' (' + results.length + ')', self)); - } - - exportName = self.slugify(results[0].title) + '-export-' + timestamp; - exportDir = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.Exports, exportName); - callback(); - }); - }); - }, - function copyFiles(callback) { + function copyFiles(results, callback) { self.generateIncludesForCourse(courseId, function(error, includes) { if(error) { return callback(error); @@ -336,20 +313,24 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) { }, function zipFiles(callback) { var archive = archiver('zip'); - var output = fs.createWriteStream(exportDir + '.zip'); - - archive.on('error', callback); - output.on('close', callback); - archive.pipe(output); - archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize(); - }, - function cleanUp(callback) { - fse.remove(exportDir, function (error) { - callback(error, { zipName: exportName + '.zip' }); - }); + var zipPath = exportDir + '.zip'; + // fse.remove(zipPath, function(error) { + // if(error) return callback(error); + var output = fs.createWriteStream(zipPath); + archive.on('error', callback); + output.on('close', callback); + archive.pipe(output); + archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize(); + // }); } ], - next); + function onDone(asyncError) { + // remove the exportDir, if there is one + fse.remove(exportDir, function(removeError) { + // async error more important + next(asyncError || removeError); + }); + }); }; /** diff --git a/routes/export/index.js b/routes/export/index.js index ee7c09fb67..36b270f57a 100644 --- a/routes/export/index.js +++ b/routes/export/index.js @@ -41,23 +41,20 @@ server.get('/export/:tenant/:course', function (req, res, next) { plugin.export(course, req, res, function (error, result) { if (error) { logger.log('error', 'Unable to export:', error); - res.statusCode = 500; - return res.json({ + return res.status(500).json({ success: false, message: error.message }); } - - result.success = true; - result.message = app.polyglot.t('app.exportcoursesuccess'); - res.statusCode = 200; - return res.json(result); + return res.status(200).json({ + success: true, + message: app.polyglot.t('app.exportcoursesuccess') + }); }); } }); } else { - res.statusCode = 401; - return res.json({ + return res.status(401).json({ success: false, message: app.polyglot.t('app.errorusernoaccess') }); @@ -65,34 +62,42 @@ server.get('/export/:tenant/:course', function (req, res, next) { }); }); // TODO probably needs to be moved to download route -server.get('/export/:tenant/:course/:title/download.zip', function (req, res, next) { +server.get('/export/:tenant/:course/download.zip', function (req, res, next) { var tenantId = req.params.tenant; var courseId = req.params.course; var userId = usermanager.getCurrentUser()._id; - var zipName = req.params.title; // TODO don't like having to specify this here AND in plugins/output/adapt.export->getCourseName()-exportDir var zipDir = path.join( configuration.tempDir, configuration.getConfig('masterTenantID'), Constants.Folders.Framework, Constants.Folders.Exports, - zipName + userId + '.zip' ); - - fs.stat(zipDir, function(err, stat) { - if (err) { - next(err); - } else { - res.writeHead(200, { - 'Content-Type': 'application/zip', - 'Content-Length': stat.size, - 'Content-disposition' : 'attachment; filename=' + zipName, - 'Pragma' : 'no-cache', - 'Expires' : '0' + // get the course name + app.contentmanager.getContentPlugin('course', function (error, plugin) { + if (error) return callback(error); + plugin.retrieve({ _id:courseId }, {}, function(error, results) { + if (error) { + return callback(error); + } + if (results.length !== 1) { + return callback(new Error('Export: cannot find course (' + courseId + ')')); + } + fs.stat(zipDir, function(error, stat) { + if (error) { + return next(error); + } + var zipName = helpers.slugify(results[0].title,'export') + '.zip'; + res.writeHead(200, { + 'Content-Type': 'application/zip', + 'Content-Length': stat.size, + 'Content-disposition' : 'attachment; filename=' + zipName, + 'Pragma' : 'no-cache', + 'Expires' : '0' + }); + fs.createReadStream(zipDir).pipe(res); }); - - var readStream = fs.createReadStream(zipDir); - readStream.pipe(res); - } + }); }); }); From fad78bb89a4234c16185fe22ca0e96d2a889677b Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 3 Mar 2016 16:00:17 +0000 Subject: [PATCH 003/111] Remove unnecessary deletion of old zip The wWrite stream overwrites any existing files with the same name --- plugins/output/adapt/index.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/plugins/output/adapt/index.js b/plugins/output/adapt/index.js index 24a9d34ee7..283ca14371 100644 --- a/plugins/output/adapt/index.js +++ b/plugins/output/adapt/index.js @@ -314,14 +314,11 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) { function zipFiles(callback) { var archive = archiver('zip'); var zipPath = exportDir + '.zip'; - // fse.remove(zipPath, function(error) { - // if(error) return callback(error); - var output = fs.createWriteStream(zipPath); - archive.on('error', callback); - output.on('close', callback); - archive.pipe(output); - archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize(); - // }); + var output = fs.createWriteStream(zipPath); + archive.on('error', callback); + output.on('close', callback); + archive.pipe(output); + archive.bulk([{ expand: true, cwd: exportDir, src: ['**/*'] }]).finalize(); } ], function onDone(asyncError) { From d8778b8e8162d88e54d677673bda5e575e560975 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 3 Mar 2016 16:00:32 +0000 Subject: [PATCH 004/111] Fix export download route --- frontend/src/core/editor/global/views/editorView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/core/editor/global/views/editorView.js b/frontend/src/core/editor/global/views/editorView.js index ce7fe98e8c..a59591758b 100644 --- a/frontend/src/core/editor/global/views/editorView.js +++ b/frontend/src/core/editor/global/views/editorView.js @@ -134,7 +134,7 @@ define(function(require){ // get the zip var form = document.createElement("form"); - form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/' + data.zipName + '/download.zip'); + form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/download.zip'); form.submit(); }, error: function(jqXHR, textStatus, errorThrown) { From 6e03617fc0fbee6e16a7dedb885db5f830bce319 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 27 Jun 2016 12:36:46 +0100 Subject: [PATCH 005/111] Set up 'BrowserStorage' plugin to utilise the Web Storage API --- frontend/src/plugins/browserStorage/index.js | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 frontend/src/plugins/browserStorage/index.js diff --git a/frontend/src/plugins/browserStorage/index.js b/frontend/src/plugins/browserStorage/index.js new file mode 100644 index 0000000000..b48112ecd0 --- /dev/null +++ b/frontend/src/plugins/browserStorage/index.js @@ -0,0 +1,58 @@ +// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE +/** + * TODO Look into setting this up like Notify with registered plugins + */ +define(function(require) { + // don't bother doing anything if there's no storage + if(!Storage) return; + + var _ = require('underscore'); + var Origin = require('coreJS/app/origin'); + + var userData = false; + + var BrowserStorage = { + initialise: function() { + var userId = Origin.sessionModel.get('id'); + userData = { + local: JSON.parse(localStorage.getItem(userId)) || {}, + session: JSON.parse(sessionStorage.getItem(userId)) || {} + }; + }, + + set: function(key, value, sessionOnly, replaceExisting) { + // TODO messy & only handles objects + if(sessionOnly === true) { + if(replaceExisting) userData.session[key] = value; + else userData.session[key] = _.extend({}, userData.session[key], value); + } else { + if(replaceExisting) userData.local[key] = value; + else userData.local[key] = _.extend({}, userData.local[key], value); + } + this.save(); + }, + + get: function(key) { + var value = _.extend({}, userData.local[key], userData.session[key]); + return value; + }, + + // persist data to Storage + save: function() { + var userId = Origin.sessionModel.get('id'); + + if(!_.isEmpty(userData.session)) { + sessionStorage.setItem(userId, JSON.stringify(userData.session)); + } + if(!_.isEmpty(userData.local)) { + localStorage.setItem(userId, JSON.stringify(userData.local)); + } + } + }; + + // init + Origin.on('app:dataReady login:changed', function() { + BrowserStorage.initialise(); + Origin.browserStorage = BrowserStorage; + }); +}); From 69bed18d87c87ae2eff70834c62dd1df19c5bd85 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 27 Jun 2016 12:37:18 +0100 Subject: [PATCH 006/111] Persist dashboard preferences to BrowserStorage --- frontend/src/core/app/views/originView.js | 21 +++++++++---------- .../src/core/dashboard/views/dashboardView.js | 14 ++++++------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/frontend/src/core/app/views/originView.js b/frontend/src/core/app/views/originView.js index 89934e9fce..395201d9c9 100644 --- a/frontend/src/core/app/views/originView.js +++ b/frontend/src/core/app/views/originView.js @@ -51,24 +51,23 @@ define(function(require){ Origin.trigger('router:hideLoading'); }, - setUserPreference: function(key, value) { - if (this.settings.preferencesKey && typeof(Storage) !== "undefined") { - // Get preferences for this view + setUserPreference: function(key, value, sessionOnly) { + if (this.settings.preferencesKey) { var preferences = (Origin.sessionModel.get(this.settings.preferencesKey) || {}); - // Set key and value preferences[key] = value; - // Store in localStorage + // Store in session model Origin.sessionModel.set(this.settings.preferencesKey, preferences); - + // set in browser storage + var data = {}; + data[key] = value; + Origin.browserStorage.set(this.settings.preferencesKey, data, sessionOnly || false, false); } }, getUserPreferences: function() { - if (this.settings.preferencesKey && typeof(Storage) !== "undefined" - && localStorage.getItem(this.settings.preferencesKey)) { - return Origin.sessionModel.get(this.settings.preferencesKey); - } else { - return {}; + if (this.settings.preferencesKey) { + var saveData = Origin.browserStorage.get(this.settings.preferencesKey); + return saveData || {}; } }, diff --git a/frontend/src/core/dashboard/views/dashboardView.js b/frontend/src/core/dashboard/views/dashboardView.js index 165e1b9c0a..fad7323afb 100644 --- a/frontend/src/core/dashboard/views/dashboardView.js +++ b/frontend/src/core/dashboard/views/dashboardView.js @@ -108,7 +108,7 @@ define(function(require){ title: 1 }; - this.setUserPreference('sort','asc'); + this.setUserPreference('sort','asc', true); if (shouldRenderProjects) { this.updateCollection(true); @@ -123,7 +123,7 @@ define(function(require){ title: -1 } - this.setUserPreference('sort','desc'); + this.setUserPreference('sort','desc', true); if (shouldRenderProjects) { @@ -139,7 +139,7 @@ define(function(require){ updatedAt: -1 } - this.setUserPreference('sort','updated'); + this.setUserPreference('sort','updated', true); if (shouldRenderProjects) { @@ -165,9 +165,9 @@ define(function(require){ if (userPreferences) { var searchString = (userPreferences.search || ''); this.search = this.convertFilterTextToPattern(searchString); - this.setUserPreference('search', searchString); + this.setUserPreference('search', searchString, true); this.tags = (_.pluck(userPreferences.tags, 'id') || []); - this.setUserPreference('tags', userPreferences.tags); + this.setUserPreference('tags', userPreferences.tags, true); } // Check if sort is set and sort the collection @@ -306,12 +306,12 @@ define(function(require){ filterBySearchInput: function (filterText) { this.filterText = filterText; this.search = this.convertFilterTextToPattern(filterText); - this.setUserPreference('search', filterText); + this.setUserPreference('search', filterText, true); this.updateCollection(true); }, filterCoursesByTags: function(tags) { - this.setUserPreference('tags', tags); + this.setUserPreference('tags', tags, true); this.tags = _.pluck(tags, 'id'); this.updateCollection(true); }, From 22a0786be37510b2e0e36ed4a62c2883064d05f4 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 22 Aug 2016 15:43:26 +0100 Subject: [PATCH 007/111] Fix broken ifHasPermissions check Fixes #1344 --- frontend/src/core/app/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/core/app/helpers.js b/frontend/src/core/app/helpers.js index 8943a457e6..35bb0b71a6 100644 --- a/frontend/src/core/app/helpers.js +++ b/frontend/src/core/app/helpers.js @@ -190,7 +190,7 @@ define(function(require){ ifHasPermissions: function(permissions, block) { var permissionsArray = permissions.split(','); - if (Origin.permissions.hasPermissions(permissions)) { + if (Origin.permissions.hasPermissions(permissionsArray)) { return block.fn(this); } else { return block.inverse(this); From 068fb7af44b84c54dbeae560d3b866e6f9078f23 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 22 Aug 2016 15:45:01 +0100 Subject: [PATCH 008/111] Standardise Helpers line breaks --- frontend/src/core/app/helpers.js | 55 +++++++++++++++++++------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/frontend/src/core/app/helpers.js b/frontend/src/core/app/helpers.js index 35bb0b71a6..de7a3a38af 100644 --- a/frontend/src/core/app/helpers.js +++ b/frontend/src/core/app/helpers.js @@ -8,18 +8,23 @@ define(function(require){ console: function(context) { return console.log(context); }, + lowerCase: function(text) { return text.toLowerCase(); }, + numbers: function(index) { return index +1; }, + capitalise: function(text) { return text.charAt(0).toUpperCase() + text.slice(1); }, + odd: function (index) { return (index +1) % 2 === 0 ? 'even' : 'odd'; }, + stringToClassName: function(text) { if (!text) { return; @@ -32,6 +37,7 @@ define(function(require){ // Remove _ and spaces with dashes return text.replace(/_| /g, "-").toLowerCase(); }, + keyToTitleString: function(key) { if (!key) { return; @@ -40,29 +46,29 @@ define(function(require){ var string = key.replace(/_/g, "").toLowerCase(); return this.capitalise(string); }, + formatDate: function(timestamp, noZero) { var noDisplay = '-'; // 2014-02-17T17:00:34.196Z if (typeof(timestamp) !== 'undefined') { var date = new Date(timestamp); - // optionally use noDisplay char if 0 dates are to be interpreted as such if (noZero && 0 === date.valueOf()) { return noDisplay; } - return date.toDateString(); } return noDisplay; }, + momentFormat: function(date, format) { if (typeof date == 'undefined') { return '-'; } - return moment(date).format(format); }, + formatDuration: function(duration) { var zero = '0', hh, mm, ss; var time = new Date(0, 0, 0, 0, 0, Math.floor(duration), 0); @@ -78,6 +84,7 @@ define(function(require){ return hh + ':' + mm + ':' + ss; }, + // checks for http/https and www. prefix isAssetExternal: function(url) { if (url && url.length > 0) { @@ -87,6 +94,7 @@ define(function(require){ return true; } }, + ifValueEquals: function(value, text, block) { if (value === text) { return block.fn(this); @@ -94,6 +102,7 @@ define(function(require){ return block.inverse(this); } }, + ifUserIsMe: function(userId, block) { if (userId === Origin.sessionModel.get('id')) { return block.fn(this); @@ -101,6 +110,7 @@ define(function(require){ return block.inverse(this); } }, + selected: function(option, value){ if (option === value) { return ' selected'; @@ -108,27 +118,32 @@ define(function(require){ return '' } }, + counterFromZero: function(n, block) { var sum = ''; for (var i = 0; i <= n; ++i) sum += block.fn(i); return sum; }, + counterFromOne: function(n, block) { var sum = ''; for (var i = 1; i <= n; ++i) sum += block.fn(i); return sum; }, + t: function(str, options) { for (var placeholder in options.hash) { options[placeholder] = options.hash[placeholder]; } return (window.polyglot != undefined ? window.polyglot.t(str, options) : str); }, + stripHtml: function(html) { return new Handlebars.SafeString(html); }, + bytesToSize: function(bytes) { if (bytes == 0) return '0 B'; @@ -138,6 +153,7 @@ define(function(require){ return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; }, + renderBooleanOptions: function(selectedValue) { var options = ["true", "false"]; var html = ''; @@ -149,6 +165,7 @@ define(function(require){ return new Handlebars.SafeString(html); }, + pickCSV: function (list, key, separator) { var vals = []; separator = (separator && separator.length) ? separator : ','; @@ -163,6 +180,7 @@ define(function(require){ } return vals.join(separator); }, + renderTags: function(list, key) { var html = ''; @@ -173,10 +191,8 @@ define(function(require){ var tag = (key && list[i][key]) ? list[i][key] : list[i]; - html += '
  • ' + tag + '
  • '; } - html += '' } @@ -202,7 +218,7 @@ define(function(require){ 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'); @@ -221,7 +237,6 @@ define(function(require){ }, getThumbnailFromValue: function(url) { - var urlSplit = url.split('/') var fileName = urlSplit[urlSplit.length - 1]; // Get courseAsset model @@ -253,47 +268,45 @@ define(function(require){ }, copyStringToClipboard: function(data) { - var textArea = document.createElement("textarea"); - + textArea.value = data; // Place in top-left corner of screen regardless of scroll position. textArea.style.position = 'fixed'; textArea.style.top = 0; textArea.style.left = 0; - + // Ensure it has a small width and height. Setting to 1px / 1em // doesn't work as this gives a negative w/h on some browsers. textArea.style.width = '2em'; textArea.style.height = '2em'; - + // We don't need padding, reducing the size if it does flash render. textArea.style.padding = 0; - + // Clean up any borders. textArea.style.border = 'none'; textArea.style.outline = 'none'; textArea.style.boxShadow = 'none'; - + // Avoid flash of white box if rendered for any reason. textArea.style.background = 'transparent'; - + document.body.appendChild(textArea); - + textArea.select(); - + var success = document.execCommand('copy'); document.body.removeChild(textArea); - + return success; }, - + validateCourseContent: function(currentCourse) { // Let's do a standard check for at least one child object var containsAtLeastOneChild = true; - var alerts = []; function iterateOverChildren(model) { @@ -314,7 +327,6 @@ define(function(require){ + "' with no " + model._children ); - return; } else { // Go over each child and call validation again @@ -322,7 +334,6 @@ define(function(require){ iterateOverChildren(childModel); }); } - } iterateOverChildren(currentCourse); @@ -348,8 +359,6 @@ define(function(require){ if (isConfirmed) { Origin.trigger('editor:courseValidation'); } - } - }; for(var name in helpers) { From 194c7054d301de23780b82f2ce737a445a27c240 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 22 Aug 2016 15:45:15 +0100 Subject: [PATCH 009/111] Minor formatting changes --- frontend/src/core/app/helpers.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/core/app/helpers.js b/frontend/src/core/app/helpers.js index de7a3a38af..40556ebb40 100644 --- a/frontend/src/core/app/helpers.js +++ b/frontend/src/core/app/helpers.js @@ -193,7 +193,7 @@ define(function(require){ : list[i]; html += '
  • ' + tag + '
  • '; } - html += '' + html += ''; } return html; @@ -214,7 +214,7 @@ define(function(require){ }, getAssetFromValue: function(url) { - var urlSplit = url.split('/') + var urlSplit = url.split('/'); var fileName = urlSplit[urlSplit.length - 1]; // Get courseAsset model var courseAsset = Origin.editor.data.courseAssets.findWhere({_fieldName: fileName}); @@ -222,7 +222,7 @@ define(function(require){ if (courseAsset) { var courseAssetId = courseAsset.get('_assetId'); - return '/api/asset/serve/' + courseAssetId; + return '/api/asset/serve/' + courseAssetId; } else { return ''; } @@ -237,7 +237,7 @@ define(function(require){ }, getThumbnailFromValue: function(url) { - var urlSplit = url.split('/') + var urlSplit = url.split('/'); var fileName = urlSplit[urlSplit.length - 1]; // Get courseAsset model var courseAsset = Origin.editor.data.courseAssets.findWhere({_fieldName: fileName}); @@ -316,7 +316,7 @@ define(function(require){ var currentChildren = model.getChildren(); // Do validate across each item - if (currentChildren.length == 0) { + if (currentChildren.length === 0) { containsAtLeastOneChild = false; alerts.push( @@ -336,6 +336,7 @@ define(function(require){ } } + // call iterator iterateOverChildren(currentCourse); if(alerts.length > 0) { @@ -355,9 +356,10 @@ define(function(require){ return containsAtLeastOneChild; }, - validateCourseConfirm: function(isConfirmed) { - if (isConfirmed) { - Origin.trigger('editor:courseValidation'); + validateCourseConfirm: function(isConfirmed) { + if (isConfirmed) { + Origin.trigger('editor:courseValidation'); + } } }; From 2886e78e638eab0950e5e55e31283a0afc4c7225 Mon Sep 17 00:00:00 2001 From: Louise McMahon Date: Tue, 22 Aug 2017 16:09:02 +0100 Subject: [PATCH 010/111] updated from feedback changed icon to paintbrush --- .../editor/selectTheme/less/editorTheme.less | 30 +++++++++---------- .../selectTheme/templates/editorThemeItem.hbs | 2 +- .../selectTheme/views/editorThemeItemView.js | 4 +-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend/src/modules/editor/selectTheme/less/editorTheme.less b/frontend/src/modules/editor/selectTheme/less/editorTheme.less index d19a93ff30..0ecceff996 100644 --- a/frontend/src/modules/editor/selectTheme/less/editorTheme.less +++ b/frontend/src/modules/editor/selectTheme/less/editorTheme.less @@ -6,7 +6,7 @@ } .theme-list-item { - height: 280px; + height: 295px; width: 275px; margin-left:0px; margin-right: 30px; @@ -21,9 +21,9 @@ border: 1px solid #d6f2f9; &-inner { - padding-top: 20px; - .theme-header { + padding: 20px 0; + .display-name { padding-left: 20px; font-size: 175%; @@ -32,23 +32,23 @@ .description { padding-left: 20px; - padding-bottom: 15px; font-size: 110%; color: @quaternary-color; } + } - .theme-preview { - position: relative; - height: 100%; - width: 100%; - - .fa { - color:rgb(214, 242, 249); - font-size: 50px; - padding-top: 70px; - padding-left: 116px; - } + .theme-preview { + position: relative; + height: 205px; + width: 100%; + .fa { + color:rgb(214, 242, 249); + position: absolute; + font-size: 70px; + top: 45%; + left: 50%; + transform: translate(-50%, -50%); } } } diff --git a/frontend/src/modules/editor/selectTheme/templates/editorThemeItem.hbs b/frontend/src/modules/editor/selectTheme/templates/editorThemeItem.hbs index 4c2cccd063..706fecd734 100644 --- a/frontend/src/modules/editor/selectTheme/templates/editorThemeItem.hbs +++ b/frontend/src/modules/editor/selectTheme/templates/editorThemeItem.hbs @@ -2,8 +2,8 @@

    {{displayName}}

    {{description}}
    -
    +
    {{#if settings}}
      diff --git a/frontend/src/modules/editor/selectTheme/views/editorThemeItemView.js b/frontend/src/modules/editor/selectTheme/views/editorThemeItemView.js index 65c7dc8d84..91ba550bb6 100644 --- a/frontend/src/modules/editor/selectTheme/views/editorThemeItemView.js +++ b/frontend/src/modules/editor/selectTheme/views/editorThemeItemView.js @@ -24,7 +24,7 @@ define(function(require){ }, postRender: function() { - var previewUrl = '/api/theme/preview/' + this.model.get('name') + '/' + this.model.get('name') + var previewUrl = '/api/theme/preview/' + this.model.get('name') + '/' + this.model.get('name'); var $previewLoc = this.$('.theme-preview'); $.ajax(previewUrl, { @@ -33,7 +33,7 @@ define(function(require){ $previewLoc.prepend($('', { src: previewUrl, alt: Origin.l10n.t('app.themepreviewalt') })); }, 204: function() { - $previewLoc.prepend($('', { class: 'fa fa-file-image-o' })); + $previewLoc.prepend($('', { 'class': 'fa fa-paint-brush' })); } } }); From 4e4a5670fd225b37db98ca3be066fe136fd8dcc4 Mon Sep 17 00:00:00 2001 From: Tom Greenfield Date: Tue, 29 Aug 2017 13:59:46 +0100 Subject: [PATCH 011/111] Add instruction to component schema --- frontend/src/core/models/componentModel.js | 3 ++- plugins/content/component/model.schema | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/core/models/componentModel.js b/frontend/src/core/models/componentModel.js index 8ab83cedf9..7ea75bbef1 100644 --- a/frontend/src/core/models/componentModel.js +++ b/frontend/src/core/models/componentModel.js @@ -26,7 +26,8 @@ define(function(require) { 'title', 'version', 'themeSettings', - '_onScreen' + '_onScreen', + 'instruction' ] }); diff --git a/plugins/content/component/model.schema b/plugins/content/component/model.schema index 4c37a8d403..114ef684b1 100644 --- a/plugins/content/component/model.schema +++ b/plugins/content/component/model.schema @@ -81,6 +81,12 @@ "validators": [], "translatable": true }, + "instruction": { + "type": "string", + "default" : "", + "inputType": "Text", + "translatable": true + }, "_onScreen": { "type": "object", "title": "", From efa1678505623786fff02bf0c07a3f85d6613df3 Mon Sep 17 00:00:00 2001 From: Ulf Christensen Date: Tue, 3 Oct 2017 13:34:09 +0200 Subject: [PATCH 012/111] Upgrade unzip to unzip2 This fixes #1732 --- package.json | 2 +- plugins/content/bower/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 13cf898a36..1ccca5af29 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "semver": "^5.0.3", "serve-favicon": "^2.3.0", "underscore": "~1.5.2", - "unzip": "0.1.8", + "unzip2": "0.2.5", "validator": "4.2.1", "winston": "1.0.2" }, diff --git a/plugins/content/bower/index.js b/plugins/content/bower/index.js index fdc2d273ba..92fe4be8f1 100644 --- a/plugins/content/bower/index.js +++ b/plugins/content/bower/index.js @@ -30,7 +30,7 @@ var origin = require('../../../'), _ = require('underscore'), util = require('util'), path = require('path'), - unzip = require('unzip'), + unzip = require('unzip2'), exec = require('child_process').exec, IncomingForm = require('formidable').IncomingForm, version = require('../../../version.json'); From 4004e21e4783589e845ffa194ba22d4178dd5ecb Mon Sep 17 00:00:00 2001 From: Dan Gray Date: Wed, 30 Aug 2017 11:23:11 +0100 Subject: [PATCH 013/111] fixes 1474 --- lib/outputmanager.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/outputmanager.js b/lib/outputmanager.js index f62ee00d71..254ede6738 100644 --- a/lib/outputmanager.js +++ b/lib/outputmanager.js @@ -766,21 +766,9 @@ OutputPlugin.prototype.writeCourseAssets = function(tenantId, courseId, destinat var newAssetPath = "course/" + lang + "/assets/" + encodeURIComponent(asset.filename); Object.keys(Constants.CourseCollections).forEach(function(key) { - if (key === 'contentobject') { - return; - } jsonObject[key] = JSON.parse(JSON.stringify(jsonObject[key]).replace(replaceRegex, newAssetPath)); }); - // Substitute in the friendly file name for contentObjects too - for (var i = 0; i < jsonObject['contentobject'].length; i++) { - var co = jsonObject['contentobject'][i]; - if (co.hasOwnProperty('_graphic') && co._graphic.hasOwnProperty('src') - && co._graphic.src.search(replaceRegex) !== -1) { - co._graphic.src = newAssetPath; - } - } - // AB-59 - can't use asset record directly - need to use storage plugin filestorage.getStorage(asset.repository, function (err, storage) { if (err) { From f54d5015f7128a4f5c865a5e64b60fa1c16c3dd7 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 31 Aug 2017 10:17:32 +0100 Subject: [PATCH 014/111] Amend contentModel._children to allow for arrays Fix #1719 --- frontend/src/core/helpers.js | 3 ++- frontend/src/core/models/contentModel.js | 26 +++++++++++++++---- .../src/core/models/contentObjectModel.js | 4 +-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/frontend/src/core/helpers.js b/frontend/src/core/helpers.js index d914ee2c83..b41b7bad8b 100644 --- a/frontend/src/core/helpers.js +++ b/frontend/src/core/helpers.js @@ -314,13 +314,14 @@ define(function(require){ 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 " - + model._children + + children ); return; diff --git a/frontend/src/core/models/contentModel.js b/frontend/src/core/models/contentModel.js index 01b5a074a3..5eee95b80b 100644 --- a/frontend/src/core/models/contentModel.js +++ b/frontend/src/core/models/contentModel.js @@ -21,12 +21,28 @@ define(function(require) { }, getChildren: function() { - if (Origin.editor.data[this._children]) { - var children = Origin.editor.data[this._children].where({ _parentId: this.get('_id') }); - var childrenCollection = new Backbone.Collection(children); - return childrenCollection; + var self = this; + var getChildrenDelegate = function(type) { + if (Origin.editor.data[type]) { + var children = Origin.editor.data[type].where({ _parentId: self.get('_id') }); + var childrenCollection = new Backbone.Collection(children); + return childrenCollection; + } + return null; + }; + if(_.isArray(this._children)) { + var allChildren; + for(var i = 0, count = this._children.length; i < count; i++) { + var children = getChildrenDelegate(this._children[i]); + if(children) { + if(!allChildren) allChildren = children; + else allChildren.add(children.models); + } + } + return allChildren; + } else { + return getChildrenDelegate(this._children); } - return null; }, getParent: function() { diff --git a/frontend/src/core/models/contentObjectModel.js b/frontend/src/core/models/contentObjectModel.js index ece3f7d98b..1ba306df62 100644 --- a/frontend/src/core/models/contentObjectModel.js +++ b/frontend/src/core/models/contentObjectModel.js @@ -6,8 +6,8 @@ define(function(require) { var ContentObjectModel = ContentModel.extend({ urlRoot: '/api/content/contentobject', _parent: 'contentObjects', - _siblings:'contentObjects', - _children: 'articles', + _siblings: 'contentObjects', + _children: ['contentObjects', 'articles'], defaults: { _isSelected: false, From 535e544071caab38bfe940d667860e84eb42348c Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Tue, 17 Oct 2017 13:43:32 +0100 Subject: [PATCH 015/111] Enchancements to install/update (#1726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * can upgrades to upgrade and install script * removed version.json * fix issues caused by removing version.json fix pacakge.json version number. fix issue that stopped automatic upgrade added error catching to gitub api calls * Merge lib/application.js changes from canstudios/latest-release * Add refactored file * Refactor common code into helper file * Move framework & authoring git code into helper * Switch to use helper function Also reduce other duplication * Fix issue with framework folder path * Switch comparison to use semver * Refactor, and switch to use configuration module * Add spinner * Add delegate function to check for update data * Adjust spinner code * Make log a bit more user-friendly * Amend code style * Remove all dependencies on version.json * Remove whole framework folder before clone * Fix import errors * Remove comments * Reword message * Refactor * Start install refactor * Amend imports * Update style * Refactor * Fix broken references * Add newlines * Add temporary callback * Move config items to point of ref, as need to make use of dynamic vars * Set configResults * Refactor tenant code * Add bracket for readability * Remove line * Update tenantmanager to use local framework * Add missing import * Fix to framework install * Keep config.frameworkRevision for now * Add logs * Add error checks * Add custom directory * Fix application update check * Add missing import * Fix error handling * Update functions to use opts values * Disable logging for now * Remove framework install This is now done by tenantmanager when setting up master tenant * Add frontend build function to helpers * Update logs * Amend colouring * Hide exec logs * Add error handling * Move spinner code to helpers * Move exit code to helpers * Abstract input code * Refactor functions for more general use * Add missing bits * Update logs for readability * Fix logic * Remove extraneous code * Add helpful error for GitHub API limit error Amend some other logs for readability * Add missing error var * Update log messages * Amend to use log wrapper * Add spinner to install * Fix issue with halted install process * Fix upgrade version check issue * Allow framework version to be ‘locked’ in config.json Requires ‘framework’ attribute, can be 1, 2, or 3 figures (e.g. ‘1’, ‘1.1’ or ‘1.1.1’) * Allow undefined frameworkRevision value * Add dependency * Set env var to silence install output * Remove adapt_framework folder from .gitignore * Amend in-line with review by @tomgreenfield * Remove unused functions * Refactor * Fix error handling * Fix typos * Add config.json install overrides Also refactor input configs * Switch to use writeJSON * Add input validators/transforms * Refactor for readability * Fix error bug * Fix error check * Remove .env file generation as we don’t support foreman * Update prompt dev to allow password replace * Remove .env and .vagrant * Fix config output * Update feedback texts * Remove reference to version.json * Fix issue with framework install directory * Removed unused lines * Move into single applyEachSeries * Don’t modify existing object * Pass opts to bowermanager * Fix issue with framework plugin isntall * Refactor code * Change prompt.delimiter to match install script * Fix issue with undefined masterTenantID * Make SMTP input optional * Remove redundant code * Tidy output logs * Move init * Stop newline after passwords * Move installHelpers * Move further up * Uncouple function from framework lock * Move input code to installHelpers * Update instructional text * Refactor error handling code * Fix issue with authoring update erroring on git fetch * Move log * Rewrite for readability * Make revision input optional But must specify one… * Refactor for error handling * Fix framework update Now updateRepos always does a fetch Resets to remote/branch * Fix updateRepo to allow any valid revision * Remove logs * Remove node_modules before installing deps * Amend user instruction text * Fix typo * Use backticks * Amend logging * Update log * Fix issue with returning error after exec * Default framework URL to adaptlearning * Return error if trying to upgrade with custom repos * Refactor error handling code * Split up server config items to let us dynamically set rootUrl.default * Remove unused function * Switch processes to use remote name ‘origin’ * Fix config defaults * Return data with callback * Switch spinner library due to issue on windows * Fix broken var reference * Update log for usability * Allow for config.json value * Fix whitespace * Remove log from config.json install * Add db connection check * Move out of async.race due to execution time * Reduce animation framerate * Fix error handler --- .gitignore | 6 - install.js | 958 +++++++++++++++------------------ lib/application.js | 271 +++------- lib/bowermanager.js | 107 ++-- lib/database.js | 24 + lib/frameworkhelper.js | 52 +- lib/installHelpers.js | 531 ++++++++++++++++++ lib/tenantmanager.js | 70 ++- package.json | 26 +- plugins/content/bower/index.js | 91 ++-- plugins/output/adapt/index.js | 14 +- test/entry.js | 2 + upgrade.js | 499 +++++------------ version.json | 4 - 14 files changed, 1382 insertions(+), 1273 deletions(-) create mode 100644 lib/installHelpers.js delete mode 100644 version.json diff --git a/.gitignore b/.gitignore index 20b3452841..8d11a415a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -/adapt_framework - /conf/config.json /data @@ -31,9 +29,5 @@ /.settings -version.json - .idea -.vagrant -.env .project diff --git a/install.js b/install.js index 00086a69b8..281f23a94d 100644 --- a/install.js +++ b/install.js @@ -1,545 +1,479 @@ -// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE -var prompt = require('prompt'), - async = require('async'), - fs = require('fs'), - path = require('path'), - rimraf = require('rimraf'), - exec = require('child_process').exec, - origin = require('./lib/application'), - frameworkHelper = require('./lib/frameworkhelper'), - auth = require('./lib/auth'), - database = require('./lib/database'), - helpers = require('./lib/helpers'), - localAuth = require('./plugins/auth/local'), - logger = require('./lib/logger'), - optimist = require('optimist'), - util = require('util'); +var _ = require('underscore'); +var async = require('async'); +var chalk = require('chalk'); +var fs = require('fs-extra'); +var optimist = require('optimist'); +var path = require('path'); +var prompt = require('prompt'); + +var auth = require('./lib/auth'); +var database = require('./lib/database'); +var helpers = require('./lib/helpers'); +var installHelpers = require('./lib/installHelpers'); +var localAuth = require('./plugins/auth/local'); +var logger = require('./lib/logger'); +var origin = require('./lib/application'); + +var IS_INTERACTIVE = process.argv.length === 2; +var USE_CONFIG; -// set overrides from command line arguments -prompt.override = optimist.argv; -prompt.start(); - -prompt.message = '> '; -prompt.delimiter = ''; - -// get available db drivers and auth plugins -var drivers = database.getAvailableDriversSync(); -var auths = auth.getAvailableAuthPluginsSync(); var app = origin(); +// config for prompt inputs +var inputData; var masterTenant = false; var superUser = false; +// from user input +var configResults; -var isVagrant = function () { - if (process.argv.length > 2) { - return true; +// we need the framework version for the config items, so let's go +installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) { + if(error) { + return handleError(error, 1, 'Failed to get the latest framework version. Check package.json.'); } - - return false; -}; - -// config items -var configItems = [ - { - name: 'serverPort', - type: 'number', - description: 'Server port', - pattern: /^[0-9]+\W*$/, - default: 5000 - }, - { - name: 'serverName', - type: 'string', - description: 'Server name', - default: 'localhost' - }, - // { - // name: 'dbType', - // type: 'string', - // description: getDriversPrompt(), - // conform: function (v) { - // // validate against db drivers - // v = parseInt(v, 10); - // return v > 0 && v <= drivers.length; - // }, - // before: function (v) { - // // convert's the numeric answer to one of the available drivers - // return drivers[(parseInt(v, 10) - 1)]; - // }, - // default: '1' - // }, - { - name: 'dbHost', - type: 'string', - description: 'Database host', - default: 'localhost' - }, - { - name: 'dbName', - type: 'string', - description: 'Master database name', - pattern: /^[A-Za-z0-9_-]+\W*$/, - default: 'adapt-tenant-master' - }, - { - name: 'dbPort', - type: 'number', - description: 'Database server port', - pattern: /^[0-9]+\W*$/, - default: 27017 - }, - { - name: 'dataRoot', - type: 'string', - description: 'Data directory path', - pattern: /^[A-Za-z0-9_-]+\W*$/, - default: 'data' - }, - { - name: 'sessionSecret', - type: 'string', - description: 'Session secret', - pattern: /^.+$/, - default: 'your-session-secret' - }, - // { - // name: 'auth', - // type: 'string', - // description: getAuthPrompt(), - // conform: function (v) { - // // validate against auth types - // v = parseInt(v, 10); - // return v > 0 && v <= auths.length; - // }, - // before: function (v) { - // // convert's the numeric answer to one of the available auth types - // return auths[(parseInt(v, 10) - 1)]; - // }, - // default: '1' - // }, - { - name: 'useffmpeg', - type: 'string', - description: "Will ffmpeg be used? y/N", - before: function (v) { - if (/(Y|y)[es]*/.test(v)) { - return true; + inputData = { + useConfigJSON: { + name: 'useJSON', + description: 'Use existing config values? y/N', + type: 'string', + before: installHelpers.inputHelpers.toBoolean, + default: 'N' + }, + startInstall: { + name: 'install', + description: 'Continue? Y/n', + type: 'string', + before: installHelpers.inputHelpers.toBoolean, + default: 'Y' + }, + server: [ + { + name: 'serverPort', + type: 'number', + description: 'Server port', + pattern: installHelpers.inputHelpers.numberValidator, + default: 5000 + }, + { + name: 'serverName', + type: 'string', + description: 'Server name', + default: 'localhost' + }, + { + name: 'dbHost', + type: 'string', + description: 'Database host', + default: 'localhost' + }, + { + name: 'dbName', + type: 'string', + description: 'Master database name', + pattern: installHelpers.inputHelpers.alphanumValidator, + default: 'adapt-tenant-master' + }, + { + name: 'dbPort', + type: 'number', + description: 'Database server port', + pattern: installHelpers.inputHelpers.numberValidator, + default: 27017 + }, + { + name: 'dataRoot', + type: 'string', + description: 'Data directory path', + pattern: installHelpers.inputHelpers.alphanumValidator, + default: 'data' + }, + { + name: 'sessionSecret', + type: 'string', + description: 'Session secret (value used when saving session cookie data)', + pattern: /^.+$/, + default: 'your-session-secret' + }, + { + name: 'authoringToolRepository', + type: 'string', + description: "Git repository URL to be used for the authoring tool source code", + default: 'https://github.com/adaptlearning/adapt_authoring.git' + }, + { + name: 'frameworkRepository', + type: 'string', + description: "Git repository URL to be used for the framework source code", + default: 'https://github.com/adaptlearning/adapt_framework.git' + }, + { + name: 'frameworkRevision', + type: 'string', + description: 'Specific git revision to be used for the framework. Accepts any valid revision type (e.g. branch/tag/commit)', + default: 'tags/' + latestFrameworkTag + } + ], + features: { + ffmpeg: { + name: 'useffmpeg', + type: 'string', + description: "Are you using ffmpeg? y/N", + before: installHelpers.inputHelpers.toBoolean, + default: 'N' + }, + smtp: { + confirm: { + name: 'useSmtp', + type: 'string', + description: "Will you be using an SMTP server? (used for sending emails) y/N", + before: installHelpers.inputHelpers.toBoolean, + default: 'N' + }, + configure: [ + { + name: 'smtpService', + type: 'string', + description: "Which SMTP service (if any) will be used? (see https://github.com/andris9/nodemailer-wellknown#supported-services for a list of supported services.)", + default: 'none', + }, + { + name: 'smtpUsername', + type: 'string', + description: "SMTP username", + default: '', + }, + { + name: 'smtpPassword', + type: 'string', + description: "SMTP password", + hidden: true, + replace: installHelpers.inputHelpers.passwordReplace, + default: '', + before: installHelpers.inputHelpers.passwordBefore + }, + { + name: 'fromAddress', + type: 'string', + description: "Sender email address", + default: '', + }, + { + name: 'rootUrl', + type: 'string', + description: "The url this install will be accessible from", + default: '' // set using default server options + } + ] } - return false; }, - default: 'N' - }, - { - name: 'smtpService', - type: 'string', - description: "Which SMTP service (if any) will be used? (see https://github.com/andris9/nodemailer-wellknown#supported-services for a list of supported services.)", - default: 'none' - }, - { - name: 'smtpUsername', - type: 'string', - description: "SMTP username", - default: '' - }, - { - name: 'smtpPassword', - type: 'string', - description: "SMTP password", - hidden: true - }, - { - name: 'fromAddress', - type: 'string', - description: "Sender email address", - default: '' - }, - // { - // name: 'outputPlugin', - // type: 'string', - // description: "Which output plugin will be used?", - // default: 'adapt' - // } -]; + tenant: [ + { + name: 'masterTenantName', + type: 'string', + description: "Set a unique name for your tenant", + pattern: installHelpers.inputHelpers.alphanumValidator, + default: 'master' + }, + { + name: 'masterTenantDisplayName', + type: 'string', + description: 'Set the display name for your tenant', + default: 'Master' + } + ], + tenantDelete: { + name: "confirm", + description: "Continue? (Y/n)", + before: installHelpers.inputHelpers.toBoolean, + default: "Y" + }, + superUser: [ + { + name: 'suEmail', + type: 'string', + description: "Email address", + required: true + }, + { + name: 'suPassword', + type: 'string', + description: "Password", + hidden: true, + replace: installHelpers.inputHelpers.passwordReplace, + required: true, + before: installHelpers.inputHelpers.passwordBefore + }, + { + name: 'suRetypePassword', + type: 'string', + description: "Confirm Password", + hidden: true, + replace: installHelpers.inputHelpers.passwordReplace, + required: true, + before: installHelpers.inputHelpers.passwordBefore + } + ] + }; + if(!IS_INTERACTIVE) { + return start(); + } + console.log(''); + if(!fs.existsSync('conf/config.json')) { + return start(); + } + console.log('Found an existing config.json file. Do you want to use the values in this file during install?'); + installHelpers.getInput(inputData.useConfigJSON, function(result) { + console.log(''); + USE_CONFIG = result.useJSON; + start(); + }); +}); -tenantConfig = [ - { - name: 'name', - type: 'string', - description: "Set a unique name for your tenant", - pattern: /^[A-Za-z0-9_-]+\W*$/, - default: 'master' - }, - { - name: 'displayName', - type: 'string', - description: 'Set the display name for your tenant', - required: true, - default: 'Master' +function generatePromptOverrides() { + if(USE_CONFIG) { + var configJson = require('./conf/config.json'); + var configData = JSON.parse(JSON.stringify(configJson).replace('true', '"y"').replace('false', '"n"')); + configData.install = 'y'; } -]; + // NOTE config.json < cmd args + return _.extend({}, configData, optimist.argv); +} -userConfig = [ - { - name: 'email', - type: 'string', - description: "Email address", - required: true - }, - { - name: 'password', - type: 'string', - description: "Password", - hidden: true, - required: true - }, - { - name: 'retypePassword', - type: 'string', - description: "Retype Password", - hidden: true, - required: true +function start() { + // set overrides from command line arguments and config.json + prompt.override = generatePromptOverrides(); + // Prompt the user to begin the install + if(!IS_INTERACTIVE || USE_CONFIG) { + console.log('This script will install the application. Please wait ...'); + } else { + console.log('This script will install the application. \nWould you like to continue?'); } -]; + installHelpers.getInput(inputData.startInstall, function(result) { + if(!result.install) { + return handleError(null, 0, 'User cancelled the install'); + } + async.series([ + configureServer, + configureFeatures, + configureMasterTenant, + createMasterTenant, + createSuperUser, + buildFrontend + ], function(error, results) { + if(error) { + console.error('ERROR: ', error); + return exit(1, 'Install was unsuccessful. Please check the console output.'); + } + exit(0, `Installation completed successfully, the application can now be started with 'node server'.`); + }); + }); +} -/** - * Installer steps - * - * 1. install the framework - * 2. add config vars - * 3. configure master tenant - * 4. create admin account - * 5. TODO install plugins - */ -var steps = [ - // install the framework - function installFramework (next) { - // AB-277 always remove framework folder on install - rimraf(path.resolve(__dirname, 'adapt_framework'), function () { - // now clone the framework - frameworkHelper.cloneFramework(function (err) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Framework install failed. See console output for possible reasons.'); - } +function configureServer(callback) { + console.log(''); + if(!IS_INTERACTIVE || USE_CONFIG) { + console.log('Now setting configuration items.'); + } else { + console.log('We need to configure the tool before install. \nTip: just press ENTER to accept the default value in brackets.'); + } + installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) { + if(error) { + return handleError(error, 1, 'Failed to get latest framework version'); + } + installHelpers.getInput(inputData.server, function(result) { + addConfig(result); + callback(); + }); + }); +} - // Remove the default course - rimraf(path.resolve(__dirname, 'adapt_framework', 'src', 'course'), function(err) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Framework install error -- unable to remove default course.'); +function configureFeatures(callback) { + async.series([ + function ffmpeg(cb) { + installHelpers.getInput(inputData.features.ffmpeg, function(result) { + addConfig(configResults); + cb(); + }); + }, + function smtp(cb) { + installHelpers.getInput(inputData.features.smtp.confirm, function(result) { + if(!result.useSmtp || USE_CONFIG && configResults.useSmtp !== 'y') { + return cb(); + } + for(var i = 0, count = inputData.features.smtp.configure.length; i < count; i++) { + if(inputData.features.smtp.configure[i].name === 'rootUrl') { + inputData.features.smtp.configure[i].default = `http://${configResults.serverName}:${configResults.serverPort}`; } - - return next(); + } + installHelpers.getInput(inputData.features.smtp.configure, function(result) { + addConfig(configResults); + cb(); }); }); - }); - }, - - function configureEnvironment(next) { - if (isVagrant()) { - console.log('Now setting configuration items.'); - } else { - console.log('Now set configuration items. Just press ENTER to accept the default value (in brackets).'); - } - prompt.get(configItems, function (err, results) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Could not save configuration items.'); - } - - saveConfig(results, next); - }); - }, - // configure tenant - function configureTenant (next) { - console.log("Checking configuration, please wait a moment ... "); - // suppress app log output - logger.clear(); + } + ], function() { + saveConfig(configResults, callback); + }); +} - // run the app - app.run(); - app.on('serverStarted', function () { - if (isVagrant()) { - console.log('Creating your tenant. Please wait ...'); - } else { - console.log('Now create your tenant. Just press ENTER to accept the default value (in brackets). Please wait ...'); +function configureMasterTenant(callback) { + var onError = function(error) { + console.error('ERROR: ', error); + return exit(1, 'Failed to configure master tenant. Please check the console output.'); + }; + if(!IS_INTERACTIVE || USE_CONFIG) { + console.log('Now configuring the master tenant. \n'); + } else { + console.log('Now we need to configure the master tenant. \nTip: just press ENTER to accept the default value in brackets.\n'); + } + logger.clear(); + + installHelpers.showSpinner('Starting server'); + // run the app + app.run({ skipVersionCheck: true }); + app.on('serverStarted', function() { + installHelpers.hideSpinner(); + database.checkConnection(function(error) { + if(error) { + return callback(error); } - prompt.get(tenantConfig, function (err, result) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Tenant creation was unsuccessful. Please check the console output.'); - } + installHelpers.getInput(inputData.tenant, function(result) { + console.log(''); + // add the input to our cached config + addConfig({ + masterTenant: { + name: result.masterTenantName, + displayName: result.masterTenantName + } + }); // check if the tenant name already exists - app.tenantmanager.retrieveTenant({ name: result.name }, function (err, tenant) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Tenant creation was unsuccessful. Please check the console output.'); + app.tenantmanager.retrieveTenant({ name: result.masterTenantName }, function(error, tenant) { + if(error) { + return onError(error); } - - var tenantName = result.name; - var tenantDisplayName = result.displayName; - - // create the tenant according to the user provided details - var _createTenant = function (cb) { - console.log("Creating file system for tenant: " + tenantName + ", please wait ..."); - app.tenantmanager.createTenant({ - name: tenantName, - displayName: tenantDisplayName, - isMaster: true, - database: { - dbName: app.configuration.getConfig('dbName'), - dbHost: app.configuration.getConfig('dbHost'), - dbUser: app.configuration.getConfig('dbUser'), - dbPass: app.configuration.getConfig('dbPass'), - dbPort: app.configuration.getConfig('dbPort') - } - }, - function (err, tenant) { - if (err || !tenant) { - console.log('ERROR: ', err); - return exitInstall(1, 'Tenant creation was unsuccessful. Please check the console output.'); - } - - masterTenant = tenant; - console.log("Tenant " + tenant.name + " was created. Now saving configuration, please wait ..."); - // save master tenant name to config - app.configuration.setConfig('masterTenantName', tenant.name); - app.configuration.setConfig('masterTenantID', tenant._id); - saveConfig(app.configuration.getConfig(), cb); - } - ); - }; - - // deletes all collections in the db - var _deleteCollections = function (cb) { - async.eachSeries( - app.db.getModelNames(), - function (modelName, nxt) { - app.db.destroy(modelName, null, nxt); - }, - cb - ); - }; - - if (tenant) { - // deal with duplicate tenant. permanently. - console.log("Tenant already exists. It will be deleted."); - return prompt.get({ name: "confirm", description: "Continue? (Y/n)", default: "Y" }, function (err, result) { - if (err || !/(Y|y)[es]*/.test(result.confirm)) { - return exitInstall(1, 'Exiting install ... '); - } - - // buh-leted - _deleteCollections(function (err) { - if (err) { - return next(err); - } - - return _createTenant(next); - }); - }); + if(!tenant) { + return callback(); } - - // tenant is fresh - return _createTenant(next); + if(!IS_INTERACTIVE) { + return exit(1, `Tenant '${tenant.name}' already exists, automatic install cannot continue.`); + } + console.log(chalk.yellow(`Tenant '${tenant.name}' already exists. ${chalk.underline('It must be deleted for install to continue.')}`)); + installHelpers.getInput(inputData.tenantDelete, function(result) { + console.log(''); + if(!result.confirm) { + return exit(1, 'Exiting install.'); + } + // delete tenant + async.eachSeries(app.db.getModelNames(), function(modelName, cb) { + app.db.destroy(modelName, null, cb); + }, callback); + }); }); }); - }); - }, - // install content plugins - function installContentPlugins (next) { - // Interrogate the adapt.json file from the adapt_framework folder and install the latest versions of the core plugins - fs.readFile(path.join(process.cwd(), 'temp', app.configuration.getConfig('masterTenantID').toString(), 'adapt_framework', 'adapt.json'), function (err, data) { - if (err) { - console.log('ERROR: ' + err); - return next(err); - } - - var json = JSON.parse(data); - // 'dependencies' contains a key-value pair representing the plugin name and the semver - var plugins = Object.keys(json.dependencies); + }, configResults.dbName); + }); +} - async.eachSeries(plugins, function(plugin, pluginCallback) { - if(json.dependencies[plugin] === '*') { - app.bowermanager.installLatestCompatibleVersion(plugin, pluginCallback); - } else { - app.bowermanager.installPlugin(plugin, json.dependencies[plugin], pluginCallback); - } - }, next); - }); - }, - // configure the super awesome user - function createSuperUser (next) { - if (isVagrant()) { - console.log("Creating the super user account. This account can be used to manage everything on your " + app.polyglot.t('app.productname') + " instance."); - } else { - console.log("Create the super user account. This account can be used to manage everything on your " + app.polyglot.t('app.productname') + " instance."); +function createMasterTenant(callback) { + app.tenantmanager.createTenant({ + name: configResults.masterTenant.name, + displayName: configResults.masterTenant.displayName, + isMaster: true, + database: { + dbName: app.configuration.getConfig('dbName'), + dbHost: app.configuration.getConfig('dbHost'), + dbUser: app.configuration.getConfig('dbUser'), + dbPass: app.configuration.getConfig('dbPass'), + dbPort: app.configuration.getConfig('dbPort') } + }, function(error, tenant) { + if(error) { + return handleError(error, 1, 'Failed to create master tenant. Please check the console output.'); + } + console.log('Master tenant created successfully.'); + masterTenant = tenant; + saveConfig(app.configuration.getConfig(), callback); + }); +} - prompt.get(userConfig, function (err, result) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Tenant creation was unsuccessful. Please check the console output.'); - } - - var userEmail = result.email; - var userPassword = result.password; - var userRetypePassword = result.retypePassword; - // ruthlessly remove any existing users (we're already nuclear if we've deleted the existing tenant) - app.usermanager.deleteUser({ email: userEmail }, function (err, userRec) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'User account creation was unsuccessful. Please check the console output.'); +function createSuperUser(callback) { + var onError = function(error) { + handleError(error, 1, 'Failed to create admin user account. Please check the console output.'); + }; + console.log(`\nNow we need to set up a 'Super Admin' account. This account can be used to manage everything on your ${app.polyglot.t('app.productname')} instance.`); + installHelpers.getInput(inputData.superUser, function(result) { + console.log(''); + app.usermanager.deleteUser({ email: result.suEmail }, function(error, userRec) { + if(error) return onError(error); + // add a new user using default auth plugin + new localAuth().internalRegisterUser(true, { + email: result.suEmail, + password: result.suPassword, + retypePassword: result.suRetypePassword, + _tenantId: masterTenant._id + }, function(error, user) { + // TODO should we allow a retry if the passwords don't match? + if(error) { + return onError(error); } - - // add a new user using default auth plugin - new localAuth().internalRegisterUser(true, { - email: userEmail, - password: userPassword, - retypePassword: userRetypePassword, - _tenantId: masterTenant._id - }, function (err, user) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'User account creation was unsuccessful. Please check the console output.'); - } - - superUser = user; - // grant super permissions! - helpers.grantSuperPermissions(user._id, function (err) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'User account creation was unsuccessful. Please check the console output.'); - } - - return next(); - }); - } - ); + superUser = user; + helpers.grantSuperPermissions(user._id, function(error) { + if(error) return onError(error); + return callback(); + }); }); }); - }, - // run grunt build - function gruntBuild (next) { - console.log('Compiling the ' + app.polyglot.t('app.productname') + ' web application, please wait a moment ... '); - var proc = exec('grunt build:prod', { stdio: [0, 'pipe', 'pipe'] }, function (err) { - if (err) { - console.log('ERROR: ', err); - console.log('grunt build:prod command failed. Is the grunt-cli module installed? You can install using ' + 'npm install -g grunt grunt-cli'); - console.log('Install will continue. Try running ' + 'grunt build:prod' + ' after installation completes.'); - return next(); - } - - console.log('The ' + app.polyglot.t('app.productname') + ' web application was compiled and is now ready to use.'); - return next(); - }); - - // pipe through any output from grunt - proc.stdout.on('data', console.log); - proc.stderr.on('data', console.log); - }, - // all done - function finalize (next) { - if (isVagrant()) { - console.log("Installation complete.\nTo restart your instance run the command 'pm2 restart all'"); - } else { - console.log("Installation complete.\n To restart your instance run the command 'node server' (or 'foreman start' if using heroku toolbelt)."); - } - - return next(); - } -]; - -// set overrides from command line arguments -prompt.override = optimist.argv; - -prompt.start(); - -// Prompt the user to begin the install -if (isVagrant()) { - console.log('This script will install the application. Please wait ...'); -} else { - console.log('This script will install the application. Would you like to continue?'); + }); } -prompt.get({ name: 'install', description: 'Y/n', type: 'string', default: 'Y' }, function (err, result) { - if (!/(Y|y)[es]*$/.test(result['install'])) { - return exitInstall(); - } - - // run steps - async.series(steps, function (err, results) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Install was unsuccessful. Please check the console output.'); +function buildFrontend(callback) { + installHelpers.buildAuthoring(function(error) { + if(error) { + return callback(`Failed to build the web application, (${error}) \nInstall will continue. Try again after installation completes using 'grunt build:prod'.`); } - - exitInstall(); + callback(); }); -}); +} // helper functions -/** - * This will write out the config items both as a config.json file and - * as a .env file for foreman - * - * @param {object} configItems - * @param {callback} next - */ - -function saveConfig (configItems, next) { - var env = []; - Object.keys(configItems).forEach(function (key) { - env.push(key + "=" + configItems[key]); - }); - - // write the env file! - if (0 === fs.writeSync(fs.openSync('.env', 'w'), env.join("\n"))) { - console.log('ERROR: Failed to write .env file. Do you have write permissions for the current directory?'); - process.exit(1, 'Install Failed.'); - } - - // Defaulting these config settings until there are actual options. - configItems.outputPlugin = 'adapt'; - configItems.dbType = 'mongoose'; - configItems.auth = 'local'; - - // write the config.json file! - if (0 === fs.writeSync(fs.openSync(path.join('conf', 'config.json'), 'w'), JSON.stringify(configItems))) { - console.log('ERROR: Failed to write conf/config.json file. Do you have write permissions for the directory?'); - process.exit(1, 'Install Failed.'); - } - return next(); +function addConfig(newConfigItems) { + configResults = _.extend({}, configResults, newConfigItems); } /** - * writes an indexed prompt for available db drivers + * This will write out the config items both as a config.json file * - * @return {string} + * @param {object} configItems + * @param {callback} callback */ -function getDriversPrompt() { - var str = "Choose your database driver type (enter a number)\n"; - drivers.forEach(function (d, index) { - str += (index+1) + ". " + d + "\n"; +function saveConfig(configItems, callback) { + // add some default values as these aren't set + var config = { + outputPlugin: 'adapt', + dbType: 'mongoose', + auth: 'local', + root: process.cwd() + }; + // copy over the input values + _.each(configItems, function(value, key) { + config[key] = value; }); - - return str; -} - -/** - * writes an indexed prompt for available authentication plugins - * - * @return {string} - */ - -function getAuthPrompt () { - var str = "Choose your authentication method (enter a number)\n"; - auths.forEach(function (a, index) { - str += (index+1) + ". " + a + "\n"; + fs.writeJson(path.join('conf', 'config.json'), config, { spaces: 2 }, function(error) { + if(error) { + handleError(`Failed to write configuration file to ${chalk.underline('conf/config.json')}.\n${error}`, 1, 'Install Failed.'); + } + return callback(); }); +} - return str; +function handleError(error, exitCode, exitMessage) { + if(error) { + console.error(`ERROR: ${error}`); + } + if(exitCode) { + exit(exitCode, exitMessage); + } } /** @@ -549,27 +483,15 @@ function getAuthPrompt () { * @param {string} msg */ -function exitInstall (code, msg) { - code = code || 0; - msg = msg || 'Bye!'; - console.log(msg); - - // handle borked tenant, users, in case of a non-zero exit - if (0 !== code) { - if (app && app.db) { - if (masterTenant) { - return app.db.destroy('tenant', { _id: masterTenant._id }, function (err) { - if (superUser) { - return app.db.destroy('user', { _id: superUser._id }, function (err) { - return process.exit(code); - }); - } - - return process.exit(code); - }); - } +function exit(code, msg) { + installHelpers.exit(code, msg, function(callback) { + if(0 === code || app && !app.db || !masterTenant) { + return callback(); } - } - - process.exit(code); + // handle borked tenant, users, in case of a non-zero exit + app.db.destroy('tenant', { _id: masterTenant._id }, function(error) { + if(!superUser) return callback(); + app.db.destroy('user', { _id: superUser._id }, callback); + }); + }); } diff --git a/lib/application.js b/lib/application.js index ab59adba1a..68c9b1ce4a 100644 --- a/lib/application.js +++ b/lib/application.js @@ -25,9 +25,11 @@ var EventEmitter = require('events').EventEmitter, Mailer = require('./mailer').Mailer, configuration = require('./configuration'); -var request = require('request'); +var installHelpers = require('./installHelpers') + var async = require('async'); var chalk = require('chalk'); +var semver = require('semver'); // Express middleware - separated out in express 4 var favicon = require('serve-favicon'); @@ -279,7 +281,7 @@ Origin.prototype.createServer = function (options, cb) { // new session store using connect-mongo (issue #544) var sessionStore = new MongoStore({ mongooseConnection: db.conn - }); + }); server.use(compression()); /*server.use(favicon());*/ @@ -320,9 +322,6 @@ Origin.prototype.createServer = function (options, cb) { } server.use(permissions.policyChecker()); - - /*server.use(server.router);*/ - // loop through the plugins for middleware var pluginManager = pluginmanager.getManager(); var plugins = pluginManager.getPlugins(); @@ -345,209 +344,78 @@ Origin.prototype.createServer = function (options, cb) { server.use(app.clientErrorHandler()); - - /*if ('development' == server.get('env')) { - server.use(errorHandler({ - dumpExceptions: false, - showStack: false - })); - }*/ - return cb(null, server); }, configuration.getConfig('dbName')); }; Origin.prototype.startServer = function (options) { - + var app = this; // Ensure that the options object is set. - options = typeof options === 'undefined' - ? { skipVersionCheck: false, skipStartLog: false} - : options; - - // configure server - var serverOptions = {}; - if (!this.configuration || !this.configuration.getConfig('dbName')) { - serverOptions.minimal = true; + if(typeof options === 'undefined') { + options = { skipVersionCheck: false, skipStartLog: false }; } - - var app = this; - - var installedServerVersion = '', installedFrameworkVersion = ''; - var latestServerTag = ''; - var latestFrameworkTag = ''; - - async.series([ - function(callback) { - // Read the current versions - var versionFile = JSON.parse(fs.readFileSync('version.json'), {encoding: 'utf8'}); - - if (versionFile) { - installedServerVersion = versionFile.adapt_authoring; - installedFrameworkVersion = versionFile.adapt_framework; + var _checkForUpdates = function(callback) { + if(configuration.getConfig('isTestEnvironment') || options.skipVersionCheck) { + return callback(); + } + checkForUpdates(function(error) { + if(error) { + logger.log('error', `Check for updates failed, ${error}`); } - callback(); - }, - function(callback) { - if (true === configuration.getConfig('isTestEnvironment') || options.skipVersionCheck) { - return callback(); - } - - // Check the latest version of the project - request({ - headers: { - 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36' - }, - uri: 'https://api.github.com/repos/adaptlearning/adapt_authoring/tags', - method: 'GET' - }, function (error, response, body) { - if (error) { - logger.log('error', error); - } else if (response.statusCode == 200) { - var tagInfo = JSON.parse(body); - - if (tagInfo) { - latestServerTag = tagInfo[0].name; - } - } - - callback(); - }); - }, - function(callback) { - if (true === configuration.getConfig('isTestEnvironment') || options.skipVersionCheck) { - return callback(); - } - - // Check the latest version of the framework - request({ - headers: { - 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36' - }, - uri: 'https://api.github.com/repos/adaptlearning/adapt_framework/tags', - method: 'GET' - }, function (error, response, body) { - if (error) { - logger.log('error', error); - } else if (response.statusCode == 200) { - var tagInfo = JSON.parse(body); - - if (tagInfo) { - latestFrameworkTag = tagInfo[0].name; - } - } - - callback(); - }); - }, - function(callback) { - if (true === configuration.getConfig('isTestEnvironment') || options.skipVersionCheck) { - return callback(); - } - - var isUpdateAvailable = false; - - if (installedServerVersion == latestServerTag) { - logger.log('info', chalk.green('%s %s'), app.polyglot.t('app.productname'), installedServerVersion); - } else { - logger.log('info', chalk.yellow('You are currently running %s %s - %s is now available'), app.polyglot.t('app.productname'), installedServerVersion, latestServerTag); - isUpdateAvailable = true; - } - - if (installedFrameworkVersion == latestFrameworkTag) { - logger.log('info', chalk.green('Adapt Framework %s'), installedFrameworkVersion); - } else { - logger.log('info', chalk.yellow('The Adapt Framework being used is %s - %s is now available'), installedFrameworkVersion, latestFrameworkTag); - isUpdateAvailable = true; - } - - if (isUpdateAvailable) { - logger.log('info', "Run " + chalk.bgRed('"node upgrade.js"') + " to update to the latest version"); + }); + }; + _checkForUpdates(function(err, result) { + // configure server + var serverOptions = { + minimal: !app.configuration || !app.configuration.getConfig('dbName') + }; + app.createServer(serverOptions, function (error, server) { + if (error) { + logger.log('fatal', 'error creating server', error); + return process.exit(1); } + // use default port if configuration is not available + var port = app.configuration ? app.configuration.getConfig('serverPort') : DEFAULT_SERVER_PORT; + + app.server = server; + // Create a http server + var httpServer = require('http').createServer(server); + app._httpServer = httpServer.listen(port, function() { + // set up routes + app.router = router(app); + // handle different server states + app.emit(serverOptions.minimal ? "minimalServerStarted" : "serverStarted", app.server); + + var writeRebuildFile = function(courseFolder, callback) { + var OutputConstants = require('./outputmanager').Constants; + var buildFolder = path.join(courseFolder, OutputConstants.Folders.Build); + + fs.exists(buildFolder, function (exists) { + if (!exists) return callback(null); + // Write an empty lock file + logger.log('info', 'Writing build to ' + path.join(buildFolder, OutputConstants.Filenames.Rebuild)); + fs.writeFile(path.join(buildFolder, OutputConstants.Filenames.Rebuild), '', callback); + }); + }; - callback(); - }, - function(callback) { - app.createServer(serverOptions, function (error, server) { - if (error) { - logger.log('fatal', 'error creating server', error); - return process.exit(1); - } - - // use default port if configuration is not available - var port = app.configuration - ? app.configuration.getConfig('serverPort') - : DEFAULT_SERVER_PORT; - - app.server = server; - - // Create a http server - var httpServer = require('http').createServer(server); + app.on("rebuildCourse", function(tenantId, courseId) { + logger.log('info', 'Event:rebuildCourse triggered for Tenant: ' + tenantId + ' Course: ' + courseId); - app._httpServer = httpServer.listen(port, function(){ - // set up routes - app.router = router(app); + var OutputConstants = require('./outputmanager').Constants; + var courseFolder = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), OutputConstants.Folders.Framework, OutputConstants.Folders.AllCourses, tenantId, courseId); - // handle different server states - if (serverOptions.minimal) { - app.emit("minimalServerStarted", app.server); - } else { - app.emit("serverStarted", app.server); - } + fs.exists(courseFolder, function(exists) { + if (!exists) return; - var writeRebuildFile = function(courseFolder, callback) { - var OutputConstants = require('./outputmanager').Constants; - var buildFolder = path.join(courseFolder, OutputConstants.Folders.Build); - - fs.exists(buildFolder, function (exists) { - if (exists) { - // Write an empty lock file called .rebuild - logger.log('info', 'Writing build to ' + path.join(buildFolder, OutputConstants.Filenames.Rebuild)); - fs.writeFile(path.join(buildFolder, OutputConstants.Filenames.Rebuild), '', function (err) { - if (err) { - return callback(err); - } - - return callback(null); - }); - } else { - return callback(null); - } - }); - }; - - app.on("rebuildCourse", function(tenantId, courseId) { - logger.log('info', 'Event:rebuildCourse triggered for Tenant: ' + tenantId + ' Course: ' + courseId); - // Not ideal, but there is a timing issue which prevente d - var OutputConstants = require('./outputmanager').Constants; - var courseFolder = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), OutputConstants.Folders.Framework, OutputConstants.Folders.AllCourses, tenantId, courseId); - - fs.exists(courseFolder, function(exists) { - if (exists) { - writeRebuildFile(courseFolder, function(err) { - if (err) { - logger.log('error', err); - } - - return; - }); - } - - return; + writeRebuildFile(courseFolder, function(err) { + if (err) logger.log('error', err); }); }); - - if (!options.skipStartLog) { - logger.log('info', 'Server started listening on port ' + port); - } - - callback(); }); }); - } - ]); - - + }); + }); }; Origin.prototype.restartServer = function () { @@ -667,6 +535,29 @@ Origin.prototype.ModulePreloader.defOpts = { } }; +function checkForUpdates(callback) { + installHelpers.getInstalledVersions(function(error, installedData) { + installHelpers.getUpdateData(function(error, updateData) { + if(error) { + return callback(error); + } + if (updateData && updateData.adapt_authoring) { + logger.log('info', chalk.yellow(`${app.polyglot.t('app.productname')} v${installedData.adapt_authoring} (${updateData.adapt_authoring} is now available).`)); + } else { + logger.log('info', chalk.green(`${app.polyglot.t('app.productname')} ${installedData.adapt_authoring}.`)); + } + if (updateData && updateData.adapt_framework) { + logger.log('info', chalk.yellow(`Adapt framework v${installedData.adapt_framework} (${updateData.adapt_framework} is now available).`)); + } else { + logger.log('info', chalk.green(`Adapt framework ${installedData.adapt_framework}.`)); + } + if (updateData) { + logger.log('info', `Run ${chalk.bgRed('node upgrade.js')} to update your install.`); + } + callback(); + }); + }); +} /** * boostraps the application or returns it if it exists diff --git a/lib/bowermanager.js b/lib/bowermanager.js index 5f58f7ad87..a96fd1090a 100644 --- a/lib/bowermanager.js +++ b/lib/bowermanager.js @@ -8,6 +8,7 @@ var path = require('path'); var rimraf = require('rimraf'); var semver = require('semver'); +var installHelpers = require('./installHelpers'); var Constants = require('./outputmanager').Constants; var database = require('./database'); var logger = require('./logger'); @@ -71,22 +72,11 @@ BowerManager.prototype.extractPackageInfo = function(plugin, pkgMeta, schema) { */ BowerManager.prototype.installPlugin = function(pluginName, pluginVersion, callback) { var self = this; - // Formulate the package name. var packageName = (pluginVersion == '*') ? pluginName : pluginName + '#' + pluginVersion; - // Interrogate the version.json file on installation as we cannot rely on including it via a 'require' call. - fs.readFile(path.join(app.configuration.getConfig('root'), 'version.json'), 'utf8', function(err, version) { - // Ensure the JSON is parsed. - version = JSON.parse(version); - - if (err) { - logger.log('error', err); - return callback(err); - } - // Clear the bower cache for this plugin. rimraf(path.join(bowerOptions.directory, pluginName), function (err) { if (err) { @@ -135,23 +125,25 @@ BowerManager.prototype.installPlugin = function(pluginName, pluginVersion, callb if (err) { return callback(err); } - - if (packageInfo[pluginName].pkgMeta.framework) { + installHelpers.getLatestFrameworkVersion(function(error, frameworkVersion) { + if (error) { + return callback(error); + } + if (!packageInfo[pluginName].pkgMeta.framework) { + return self.importPackage(plugin, packageInfo[pluginName], bowerOptions, callback); + } // If the plugin defines a framework, ensure that it is compatible - if (semver.satisfies(semver.clean(version.adapt_framework), packageInfo[pluginName].pkgMeta.framework)) { - self.importPackage(plugin, packageInfo[pluginName], bowerOptions, callback); - } else { - logger.log('error', 'Unable to install ' + packageInfo[pluginName].pkgMeta.name + '(' + packageInfo[pluginName].framework + ') as it is not supported in the current version of of the Adapt framework (' + version.adapt_framework + ')'); - return callback('Unable to install ' + packageInfo[pluginName].pkgMeta.name + ' as it is not supported in the current version of of the Adapt framework'); + if (!semver.satisfies(semver.clean(frameworkVersion), packageInfo[pluginName].pkgMeta.framework)) { + var error = `Unable to install ${packageInfo[pluginName].pkgMeta.name} (${packageInfo[pluginName].framework}) as it is not supported in the current version of the Adapt framework (${frameworkVersion})`; + logger.log('error', error); + return callback(error); } - } else { self.importPackage(plugin, packageInfo[pluginName], bowerOptions, callback); - } + }); }); }); }); }); - }); } /** @@ -162,43 +154,44 @@ BowerManager.prototype.installPlugin = function(pluginName, pluginVersion, callb */ BowerManager.prototype.installLatestCompatibleVersion = function (pluginName, callback) { var self = this; - fs.readJson(path.join(app.configuration.getConfig('root'), 'version.json'), function(error, versionJson) { - if (error) { - logger.log('error', error); - return callback(err); + // Query bower to verify that the specified plugin exists. + bower.commands.search(pluginName, bowerOptions) + .on('error', callback) + .on('end', function (results) { + if (!results || results.length == 0) { + logger.log('warn', 'Plugin ' + packageName + ' not found!'); + return callback('Plugin ' + packageName + ' not found!'); } - // Query bower to verify that the specified plugin exists. - bower.commands.search(pluginName, bowerOptions) + // The plugin exists -- remove any fuzzy matches, e.g. adapt-contrib-assessment would + // also bring in adapt-contrib-assessmentResults, etc. + var bowerPackage = _.findWhere(results, {name: pluginName}); + bower.commands.info(bowerPackage.url) .on('error', callback) - .on('end', function (results) { - if (!results || results.length == 0) { - logger.log('warn', 'Plugin ' + packageName + ' not found!'); - return callback('Plugin ' + packageName + ' not found!'); - } - // The plugin exists -- remove any fuzzy matches, e.g. adapt-contrib-assessment would - // also bring in adapt-contrib-assessmentResults, etc. - var bowerPackage = _.findWhere(results, {name: pluginName}); - bower.commands.info(bowerPackage.url) - .on('error', callback) - .on('end', function (latestInfo) { - // the versions will be in version order, rather than release date, - // so no need for ordering - var installedFrameworkVersion = versionJson.adapt_framework; - var requiredFrameworkVersion; - var index = -1; - async.doUntil(function iterator(cb) { - bower.commands.info(bowerPackage.url + '#' + latestInfo.versions[++index]) - .on('error', cb) - .on('end', function (result) { - requiredFrameworkVersion = result.framework; - cb(); - }); - }, function isCompatible() { - return semver.satisfies(installedFrameworkVersion, requiredFrameworkVersion); - }, function(error, version) { - self.installPlugin(pluginName, latestInfo.versions[index], callback); - }); + .on('end', function (latestInfo) { + // the versions will be in version order, rather than release date, + // so no need for ordering + installHelpers.getInstalledFrameworkVersion(function(error, installedFrameworkVersion) { + if(error) { + return callback(error); + } + var requiredFrameworkVersion; + var index = -1; + async.doUntil(function iterator(cb) { + bower.commands.info(bowerPackage.url + '#' + latestInfo.versions[++index]) + .on('error', cb) + .on('end', function (result) { + requiredFrameworkVersion = result.framework; + cb(); + }); + }, function isCompatible() { + return semver.satisfies(installedFrameworkVersion, requiredFrameworkVersion); + }, function(error, version) { + if(error) { + return callback(error); + } + self.installPlugin(pluginName, latestInfo.versions[index], callback); }); + }); }); }); } @@ -257,8 +250,8 @@ BowerManager.prototype.importPackage = function (plugin, packageInfo, options, c return callback(null); } - // Build a path to the destination working folder. - var destination = path.join(app.configuration.getConfig('root').toString(), 'temp', app.configuration.getConfig('masterTenantID').toString(), 'adapt_framework', 'src', plugin.bowerConfig.srcLocation, pkgMeta.name); + // use the passed dest, or build a path to the destination working folder + var destination = path.join(app.configuration.getConfig('root').toString(), 'temp', app.configuration.getConfig('masterTenantID'), 'adapt_framework', 'src', plugin.bowerConfig.srcLocation, pkgMeta.name); // Remove whatever version of the plugin is there already. rimraf(destination, function(err) { diff --git a/lib/database.js b/lib/database.js index 1f26ff407d..f4a8c6c1c1 100644 --- a/lib/database.js +++ b/lib/database.js @@ -491,6 +491,29 @@ function getDatabase(next, tenantId, type) { }); } +/** + * Attempts to connect to the database, and returns an error on failure + * + * @param {callback} cb - function + */ +function checkConnection(cb) { + var name = configuration.getConfig('dbName'); + var host = configuration.getConfig('dbHost'); + var port = configuration.getConfig('dbPort'); + if(!name || !host || !port) { + return cb('Cannot check database connection, missing settings in config.json.'); + } + getDatabase(function(error, db) { + if(error) { + return cb(error); + } + if(db.conn.readyState !== 1) { + return cb(`Cannot connect to the MongoDB '${name}' at '${host}:${port}'. Please check the details are correct and the database is running.`); + } + cb(); + }, name); +} + /** * returns a list of available drivers * @@ -589,6 +612,7 @@ function preloadHandle(app, instance){ */ module.exports.addDatabaseHook = addDatabaseHook; module.exports.getDatabase = getDatabase; +module.exports.checkConnection = checkConnection; module.exports.resolveSchemaPath = resolveSchemaPath; module.exports.getAvailableDrivers = getAvailableDrivers; module.exports.getAvailableDriversSync = getAvailableDriversSync; diff --git a/lib/frameworkhelper.js b/lib/frameworkhelper.js index 1e6d7ad6b3..3482356b78 100644 --- a/lib/frameworkhelper.js +++ b/lib/frameworkhelper.js @@ -1,10 +1,10 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE var path = require('path'), - fs = require('fs'), - util = require('util'), - rimraf = require('rimraf'), - exec = require('child_process').exec, - serverRoot = require('./configuration').serverRoot; + fs = require('fs'), + util = require('util'), + rimraf = require('rimraf'), + exec = require('child_process').exec, + serverRoot = require('./configuration').serverRoot; // errors function FrameworkError (message) { @@ -15,15 +15,13 @@ function FrameworkError (message) { util.inherits(FrameworkError, Error); -var FRAMEWORK_DIR = 'adapt_framework', - DEFAULT_BRANCH = 'master', - GIT_FRAMEWORK_CLONE_URL = 'https://github.com/adaptlearning/adapt_framework.git'; - +var FRAMEWORK_DIR = 'adapt_framework'; + function flog(msg) { console.log(' ' + msg); } -function cloneFramework (next) { +function cloneFramework (next, frameworkRepository, frameworkRevision) { fs.exists (path.join(serverRoot, FRAMEWORK_DIR), function (exists) { if (exists) { // don't bother installing again @@ -32,42 +30,34 @@ function cloneFramework (next) { } console.log('The Adapt Framework was not found. It will now be installed...'); - var child = exec('git clone ' + GIT_FRAMEWORK_CLONE_URL, { + var child = exec('git clone ' + frameworkRepository, { stdio: [0, 'pipe', 'pipe'] }); - + child.stdout.on('data', flog); child.stderr.on('data', flog); - + child.on('exit', function (error, stdout, stderr) { if (error) { console.log('ERROR: ' + error); return next(error); } - + console.log("Clone from GitHub was successful."); - return installFramework(next); + return installFramework(next, frameworkRevision); }); }); }; -function installFramework (next) { - var frameworkVersion = DEFAULT_BRANCH; +function installFramework (next, frameworkRevision) { - try { - var versionFile = JSON.parse(fs.readFileSync('version.json'), {encoding: 'utf8'}); - frameworkVersion = versionFile.adapt_framework; - } catch (e) { - console.log('Warning: Failed to determine compatible Adapt Framework version from version.json, so using ' + DEFAULT_BRANCH); - } - console.log('Running \'npm install\' for the Adapt Framework...'); - - var child = exec('git checkout --quiet ' + frameworkVersion + ' && npm install', { - cwd: FRAMEWORK_DIR, + + var child = exec('git checkout --quiet ' + frameworkRevision + ' && npm install', { + cwd: FRAMEWORK_DIR, stdio: [0, 'pipe', 'pipe'] }); - + child.stdout.on('data', flog); child.stderr.on('data', flog); @@ -76,15 +66,15 @@ function installFramework (next) { console.log('ERROR: ', error); return next(error); } - + console.log("Completed installing NodeJS modules.\n"); - + // Remove the default course. rimraf(path.join(serverRoot, FRAMEWORK_DIR, 'src', 'course'), function(err) { if (err) { console.log('ERROR:', error); } - + return next(); }); }); diff --git a/lib/installHelpers.js b/lib/installHelpers.js new file mode 100644 index 0000000000..2e3a7319e5 --- /dev/null +++ b/lib/installHelpers.js @@ -0,0 +1,531 @@ +var _ = require('underscore'); +var async = require('async'); +var chalk = require('chalk'); +var exec = require('child_process').exec; +var fs = require('fs-extra'); +var logUpdate = require('log-update'); +var path = require('path'); +var prompt = require('prompt'); +var readline = require('readline'); +var request = require('request'); +var semver = require('semver'); + +var configuration = require('./configuration'); +var pkg = fs.readJSONSync(path.join(__dirname, '..', 'package.json')); + +var DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'; +var DEFAULT_GITHUB_ORG = 'adaptlearning'; // used to pull releases from +var DEFAULT_SERVER_REPO = `https://github.com/${DEFAULT_GITHUB_ORG}/adapt_authoring.git`; +var DEFAULT_FRAMEWORK_REPO = `https://github.com/${DEFAULT_GITHUB_ORG}/adapt_framework.git`; +var REMOTE_NAME = 'origin'; + +var spinnerInt = -1; + +var inputHelpers = { + passwordReplace: '*', + numberValidator: /^[0-9]+\W*$/, + alphanumValidator: /^[A-Za-z0-9_-]+\W*$/, + toBoolean: function(v) { + if(/(Y|y)[es]*/.test(v)) return true; + return false; + }, + passwordBefore: function(v) { + /** + * HACK because read module used by prompt adds a blank line when + * hidden & replace attrs are set + */ + readline.moveCursor(process.stdout, 0, -1); + return v; + } +}; + +var exports = module.exports = { + DEFAULT_SERVER_REPO, + DEFAULT_FRAMEWORK_REPO, + exit, + showSpinner, + hideSpinner, + getInput, + inputHelpers, + getInstalledServerVersion, + getLatestServerVersion, + getInstalledFrameworkVersion, + getLatestFrameworkVersion, + getInstalledVersions, + getLatestVersions, + getUpdateData, + installFramework, + updateFramework, + updateFrameworkPlugins, + updateAuthoring, + buildAuthoring +}; + +function exit(code, msg, preCallback) { + var _exit = function() { + hideSpinner(); + code = code || 0; + msg = msg || 'Bye!'; + log('\n' + (code === 0 ? chalk.green(msg) : chalk.red(msg)) + '\n'); + process.exit(code); + } + if(preCallback) { + preCallback(_exit); + } else { + _exit(); + } +} + +function showSpinner(text) { + if(isSilent()) return; + // NOTE we stop the existing spinner (not ideal) + hideSpinner(); + var frames = ['-', '\\', '|', '/']; + var i = 0; + spinnerInt = setInterval(function() { + var frame = frames[i = ++i % frames.length]; + logUpdate(`${frame} ${text}`); + }, 120); +} + +function hideSpinner() { + if(isSilent()) return; + clearInterval(spinnerInt); + logUpdate.clear(); +} + +function getInput(items, callback) { + prompt.message = '> '; + prompt.delimiter = ''; + prompt.start(); + prompt.get(items, function(error, result) { + if(error) { + if(error.message === 'canceled') error = new Error('User cancelled the process'); + return exit(1, error); + } + callback(result); + }); +} + +function getInstalledServerVersion(callback) { + try { + var pkg = fs.readJSONSync('package.json'); + callback(null, pkg.version); + } catch(e) { + callback(`Cannot determine authoring tool version\n${e}`); + } +} + +function getLatestServerVersion(callback) { + checkLatestAdaptRepoVersion('adapt_authoring', callback); +} + +function getInstalledFrameworkVersion(callback) { + try { + var pkg = fs.readJSONSync(path.join(getFrameworkRoot(), 'package.json')); + callback(null, pkg.version); + } catch(e) { + return callback(`Cannot determine framework version\n${e}`); + } +} + +function getLatestFrameworkVersion(callback) { + checkLatestAdaptRepoVersion('adapt_framework', pkg.framework, callback); +} + +function getInstalledVersions(callback) { + async.parallel([ + exports.getInstalledServerVersion, + exports.getInstalledFrameworkVersion + ], function(error, results) { + callback(error, { + adapt_authoring: results[0], + adapt_framework: results[1] + }); + }); +} + +function getLatestVersions(callback) { + async.parallel([ + exports.getLatestServerVersion, + exports.getLatestFrameworkVersion + ], function(error, results) { + callback(error, { + adapt_authoring: results[0], + adapt_framework: results[1] + }); + }); +} + +function getUpdateData(callback) { + async.parallel([ + exports.getInstalledVersions, + exports.getLatestVersions + ], function(error, results) { + if(error) { + return callback(error); + } + var updateData = {}; + if(semver.lt(results[0].adapt_authoring, results[1].adapt_authoring)) { + updateData.adapt_authoring = results[1].adapt_authoring; + } + if(semver.lt(results[0].adapt_framework, results[1].adapt_framework)) { + updateData.adapt_framework = results[1].adapt_framework; + } + if(_.isEmpty(updateData)) { + return callback(); + } + callback(null, updateData); + }); +} + +function getFrameworkRoot() { + return path.join(configuration.serverRoot, 'temp', configuration.getConfig('masterTenantID'), 'adapt_framework'); +} + +/** +* Checks all releases for the latest to match framework value in config.json +* Recusion required for pagination. +*/ +function checkLatestAdaptRepoVersion(repoName, versionLimit, callback) { + if(typeof versionLimit === 'function') { + callback = versionLimit; + versionLimit = undefined; + } + // used in pagination + var nextPage = `https://api.github.com/repos/${DEFAULT_GITHUB_ORG}/${repoName}/releases`; + + var _getReleases = function(done) { + request({ + headers: { + 'User-Agent': DEFAULT_USER_AGENT , + Authorization: 'token 15e160298d59a7a70ac7895c9766b0802735ac99' + }, + uri: nextPage, + method: 'GET' + }, done); + }; + var _requestHandler = function(error, response, body) { + // we've exceeded the API limit + if(response.statusCode === 403 && response.headers['x-ratelimit-remaining'] === '0') { + var reqsReset = new Date(response.headers['x-ratelimit-reset']*1000); + error = `You have exceeded GitHub's request limit of ${response.headers['x-ratelimit-limit']} requests per hour. Please wait until at least ${reqsReset.toTimeString()} before trying again.`; + } + else if (response.statusCode !== 200) { + error = 'GitubAPI did not respond with a 200 status code.'; + } + + if (error) { + return callback(`Couldn't check latest version of ${repoName}\n${error}`); + } + nextPage = parseLinkHeader(response.headers.link).next; + try { + var releases = JSON.parse(body); + } catch(e) { + return callback(`Failed to parse GitHub release data\n${e}`); + } + var compatibleRelease; + if(!versionLimit) { + return callback(null, releases[0].tag_name); + } + async.someSeries(releases, function(release, cb) { + var isFullRelease = !release.draft && !release.prerelease; + if(isFullRelease && semver.satisfies(release.tag_name, versionLimit)) { + compatibleRelease = release; + return cb(null, true); + } + cb(null, false); + }, function(error, satisfied) { + if(!satisfied) { + if(nextPage) { + return _getReleases(_requestHandler); + } + error = `Couldn't find any releases compatible with specified framework version (${versionLimit}), please check that it is a valid version.`; + } + if(error) { + return callback(error); + } + callback(error, compatibleRelease.tag_name); + }); + }; + // start recursion + _getReleases(_requestHandler); +} + +// taken from https://gist.github.com/niallo/3109252 +function parseLinkHeader(header) { + if (!header || header.length === 0) { + return []; + } + var links = {}; + // Parse each part into a named link + _.each(header.split(','), function(p) { + var section = p.split(';'); + if (section.length !== 2) { + throw new Error("section could not be split on ';'"); + } + var url = section[0].replace(/<(.*)>/, '$1').trim(); + var name = section[1].replace(/rel="(.*)"/, '$1').trim(); + links[name] = url; + }); + return links; +} + +/** +* Clones/updates the temp/ framework folder +* Accepts the following options: { +* repository: URL to pull framework from, +* revision: in the format tags/[TAG] or remote/[BRANCH], +* force: forces a clone regardless of whether we have an existing clone, +* } +*/ +function installFramework(opts, callback) { + if(arguments.length !== 2 || !opts.directory) { + return callback('Cannot install framework, invalid options passed.'); + } + if(!opts.repository) { + opts.repository = DEFAULT_FRAMEWORK_REPO; + } + if(!opts.revision) { + return getLatestFrameworkVersion(function(error, version) { + if(error) return callback(error); + installFramework(_.extend({ revision: version }, opts), callback); + }); + } + if(!fs.existsSync(opts.directory) || opts.force) { + return async.applyEachSeries([ + cloneRepo, + updateFramework + ], opts, callback); + } + updateFramework(opts, callback); +} + +function updateFramework(opts, callback) { + if(opts && !opts.repository) { + opts.repository = DEFAULT_FRAMEWORK_REPO; + } + async.applyEachSeries([ + updateRepo, + installDependencies, + purgeCourseFolder, + updateFrameworkPlugins + ], opts, callback); +} + +function checkOptions(opts, action, callback) { + if(!opts) { + return callback(`Cannot ${action} repository, invalid options passed.`); + } + if(!opts.repository) { + return callback(`Cannot ${action} repository, no repository specified.`); + } + if(!opts.directory) { + return callback(`Cannot ${action} ${opts.repository}, no target directory specified.`); + } + callback(); +} + +function cloneRepo(opts, callback) { + checkOptions(opts, 'clone', function(error) { + if(error) { + return callback(error); + } + showSpinner(`Cloning ${opts.repository}`); + fs.remove(opts.directory, function(error) { + if(error) { + hideSpinner(); + return callback(error); + } + execCommand(`git clone ${opts.repository} --origin ${REMOTE_NAME} ${opts.directory}`, function(error) { + hideSpinner(); + if(error) { + return callback(error); + } + log(`Cloned ${opts.repository} successfully.`); + callback(); + }); + }); + }); +} + +function fetchRepo(opts, callback) { + checkOptions(opts, 'fetch', function(error) { + if(error) { + return callback(error); + } + execCommand(`git fetch ${REMOTE_NAME}`, { cwd: opts.directory }, function(error) { + // HACK not an ideal way to figure out if it's the right error... + if(error && error.indexOf(`'${REMOTE_NAME}' does not appear to be a git repository`) > -1) { + error = `Remote with name '${REMOTE_NAME}' not found. Check it exists and try again.`; + } + callback(error); + }); + }); +} + +function updateRepo(opts, callback) { + fetchRepo(opts, function(error) { + if(error) { + return callback(error); + } + checkOptions(opts, 'update', function(error) { + if(error) { + return callback(error); + } + var shortDir = opts.directory.replace(configuration.serverRoot, '') || opts.directory; + showSpinner(`Updating ${shortDir} to ${opts.revision}`); + + execCommand(`git reset --hard && git checkout ${opts.revision}`, { + cwd: opts.directory + }, function(error) { + hideSpinner(); + if(error) { + return callback(error); + } + log(`${shortDir} switched to revision ${opts.revision}`); + callback(); + }); + }); + }); +} + +/** +* Uses adapt.json to install the latest plugin versions +*/ +function updateFrameworkPlugins(opts, callback) { + if(arguments.length !== 2) { + return callback('Cannot update Adapt framework plugins, invalid options passed.'); + } + if(!opts.directory) { + return callback('Cannot update Adapt framework plugins, no target directory specified.'); + } + fs.readJSON(path.join(opts.directory, 'adapt.json'), function(error, json) { + if (error) { + return callback(error); + } + var plugins = Object.keys(json.dependencies); + async.eachSeries(plugins, function(plugin, pluginCallback) { + var _done = function() { + hideSpinner(); + pluginCallback.apply(this, arguments); + }; + showSpinner(`Updating Adapt framework plugin '${plugin}'`); + + if(json.dependencies[plugin] === '*') { + app.bowermanager.installLatestCompatibleVersion(plugin, _done); + } else { + app.bowermanager.installPlugin(plugin, json.dependencies[plugin], _done); + } + }, function(error) { + hideSpinner(); + if(error) { + return callback(error); + } + log('Adapt framework plugins updated.'); + callback(); + }); + }); +} + +/** +* This isn't used by the authoring tool +*/ +function purgeCourseFolder(opts, callback) { + if(arguments.length !== 2) { + return callback('Cannot remove course folder, invalid options passed.'); + } + if(!opts.directory) { + return callback('Cannot remove course folder, no target directory specified.'); + } + fs.remove(path.join(opts.directory, 'src', 'course'), callback); +} + +function updateAuthoring(opts, callback) { + if(!opts.revision) { + return callback('Cannot update server, revision not specified.'); + } + if(!opts.repository) { + opts.repository = DEFAULT_SERVER_REPO; + } + async.series([ + function fetchLatest(cb) { + fetchRepo(opts, cb); + }, + function pullLatest(cb) { + updateRepo(opts, cb); + }, + function installDeps(cb) { + installDependencies(cb); + }, + function rebuildApp(cb) { + buildAuthoring(cb); + } + ], function(error) { + if(!error) { + log(`Server has been updated successfully!`); + } + callback(error); + }); +} + +function buildAuthoring(callback) { + showSpinner('Building web application'); + execCommand('grunt build:prod', function(error){ + hideSpinner(); + if(error) { + return callback(error); + } + log('Web application built successfully.'); + callback(); + }); +} + +function installDependencies(opts, callback) { + if(arguments.length === 1) { + callback = opts; + } + showSpinner(`Installing node dependencies`); + + var cwd = opts.directory || configuration.serverRoot; + + fs.remove(path.join(cwd, 'node_modules'), function(error) { + if(error) { + return callback(error); + } + execCommand('npm install --production', { cwd: cwd }, function(error) { + hideSpinner(); + if(error) { + return callback(error); + } + log('Node dependencies installed successfully.'); + callback(); + }); + }); +} + +function execCommand(cmd, opts, callback) { + if(arguments.length === 2) { + callback = opts; + opts = {}; + } + var stdoutData = ''; + var errData = ''; + var child = exec(cmd, _.extend({ stdio: [0, 'pipe', 'pipe'] }, opts)); + child.stdout.on('data', function(data) { stdoutData += data; }); + child.stderr.on('data', function(data) { errData += data; }); + child.on('exit', function(error) { + if(error) { + return callback(errData || error); + } + callback(null, stdoutData); + }); +} + +function log(msg) { + if(!isSilent()) console.log(msg); +} + +function isSilent() { + return process.env.SILENT; +} diff --git a/lib/tenantmanager.js b/lib/tenantmanager.js index 942ff280bf..18ea4e3798 100644 --- a/lib/tenantmanager.js +++ b/lib/tenantmanager.js @@ -11,7 +11,7 @@ var util = require('util'); var configuration = require('./configuration'); var database = require('./database'); -var frameworkhelper = require('./frameworkhelper'); +var installHelpers = require('./installHelpers'); var logger = require('./logger'); // Constants @@ -235,50 +235,35 @@ exports = module.exports = { * @param {function} callback - function of the form function (error, tenant) */ createTenantFilesystem: function(tenant, callback) { - function copyFramework(callback) { - logger.log('info', 'Copying Adapt framework into place for new tenant'); - ncp(path.join(configuration.serverRoot, FRAMEWORK_DIR), path.join(configuration.tempDir, tenant._id.toString(), FRAMEWORK_DIR), function (err) { - if (err) { - logger.log('error', err); - return callback(err); - } else { - return callback(null); - } - }); - }; - - if (tenant.isMaster) { - logger.log('info', 'Creating master tenant filesystem'); + if (!tenant.isMaster) { + logger.log('info', 'No filesystem required for tenant ' + tenant.name); + callback(null); + } + logger.log('info', 'Creating master tenant filesystem'); - mkdirp(path.join(configuration.tempDir, tenant._id.toString()), function (err) { - if (err) { - logger.log('error', err); - return callback(err); + mkdirp(path.join(configuration.tempDir, tenant._id.toString()), function (err) { + if (err) { + logger.log('error', err); + return callback(err); + } + var tenantFrameworkDir = path.join(configuration.tempDir, tenant._id.toString(), FRAMEWORK_DIR); + fs.exists(tenantFrameworkDir, function(exists) { + if (exists) { + return callback(); } - - // Check that framework exists - fs.exists(path.join(configuration.serverRoot, FRAMEWORK_DIR), function(exists) { - if (!exists) { - logger.log('info', 'Framework does not exist'); - frameworkhelper.cloneFramework(function(err, result) { - if (err) { - logger.log('fatal', 'Error cloning framework', err); - return callback(err); - } - else { - return copyFramework(callback) - } - }); - } else { - return copyFramework(callback); + logger.log('info', 'Framework does not exist, will download'); + installHelpers.installFramework({ + repository: configuration.getConfig('frameworkRepository'), + revision: configuration.getConfig('frameworkRevision'), + directory: tenantFrameworkDir + }, function(err, result) { + if (err) { + logger.log('error', `Error downloading the framework ${err}`); } + callback(err); }); }); - } else { - logger.log('info', 'No filesystem required for tenant ' + tenant.name); - callback(null); - } - + }); }, /** @@ -346,6 +331,11 @@ exports = module.exports = { logger.log('error', 'Failed to create tenant: ', tenant); return callback(error); } + // these are needed in createTenantFilesystem + if(tenant.isMaster) { + configuration.setConfig('masterTenantName', result.name); + configuration.setConfig('masterTenantID', result._id.toString()); + } self.createTenantFilesystem(result, function(error) { if (error) { diff --git a/package.json b/package.json index 13cf898a36..930d7c3809 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,18 @@ "adapt", "authoring" ], + "repository": { + "type": "git", + "url": "https://github.com/adaptlearning/adapt_authoring.git" + }, + "framework": "2", + "main": "index", + "engines": { + "node": "4.x" + }, + "scripts": { + "test": "grunt test" + }, "contributors": [ { "name": "Ryan Adams", @@ -77,17 +89,6 @@ "email": "nicola.willis@canstudios.com" } ], - "repository": { - "type": "git", - "url": "https://github.com/adaptlearning/adapt_authoring.git" - }, - "main": "index", - "engines": { - "node": "4.x" - }, - "scripts": { - "test": "grunt test" - }, "dependencies": { "archiver": "~0.16.0", "async": "2.5.0", @@ -126,6 +127,7 @@ "json-schema-mapper": "0.0.2", "junk": "^1.0.2", "less": "^2.7.1", + "log-update": "^2.1.0", "matchdep": "~0.3.0", "method-override": "^2.3.5", "mime": "1.2.x", @@ -142,7 +144,7 @@ "nodemailer": "~2.5.0", "optimist": "*", "passport": "~0.1.17", - "prompt": "0.2.14", + "prompt": "^1.0.0", "request": "^2.53.0", "rimraf": "~2.2.5", "semver": "^5.0.3", diff --git a/plugins/content/bower/index.js b/plugins/content/bower/index.js index fdc2d273ba..3811d84bee 100644 --- a/plugins/content/bower/index.js +++ b/plugins/content/bower/index.js @@ -33,7 +33,7 @@ var origin = require('../../../'), unzip = require('unzip'), exec = require('child_process').exec, IncomingForm = require('formidable').IncomingForm, - version = require('../../../version.json'); + installHelpers = require('../../../lib/installHelpers'); // errors function PluginPackageError (msg) { @@ -776,22 +776,22 @@ BowerPlugin.prototype.updatePackages = function (plugin, options, cb) { }) .on('end', function (packageInfo) { // add details for each to the db - async.eachSeries( - Object.keys(packageInfo), - function (key, next) { - if (packageInfo[key].pkgMeta.framework) { - // If the plugin defines a framework, ensure that it is compatible - if (semver.satisfies(semver.clean(version.adapt_framework), packageInfo[key].pkgMeta.framework)) { - addPackage(plugin, packageInfo[key], options, next); - } else { - logger.log('warn', 'Unable to install ' + packageInfo[key].pkgMeta.name + ' as it is not supported in the current version of of the Adapt framework'); - next(); - } - } else { - addPackage(plugin, packageInfo[key], options, next); + async.eachSeries(Object.keys(packageInfo), function (key, next) { + if (!packageInfo[key].pkgMeta.framework) { + return addPackage(plugin, packageInfo[key], options, next); + } + installHelpers.getInstalledFrameworkVersion(function(error, frameworkVersion) { + if(error) { + return next(error); + } + // If the plugin defines a framework, ensure that it is compatible + if (!semver.satisfies(semver.clean(frameworkVersion), packageInfo[key].pkgMeta.framework)) { + logger.log('warn', 'Unable to install ' + packageInfo[key].pkgMeta.name + ' as it is not supported in the current version of the Adapt framework'); + return next(); } - }, - cb); + addPackage(plugin, packageInfo[key], options, next); + }); + }, cb); }); }); }); @@ -831,21 +831,26 @@ function checkIfHigherVersionExists (package, options, cb) { logger.log('error', `Unexpected number of ${packageName}s found (${results.length})`); return cb(error); } - var installedVersion = results[0].version; - var latestVersionIsNewer = semver.gt(latestPkg.version, installedVersion); - var satisfiesFrameworkReq = semver.satisfies(semver.clean(version.adapt_framework), latestPkg.framework); + installHelpers.getInstalledFrameworkVersion(function(error, frameworkVersion) { + if(error) { + return cb(error); + } + var installedVersion = results[0].version; + var latestVersionIsNewer = semver.gt(latestPkg.version, installedVersion); + var satisfiesFrameworkReq = semver.satisfies(semver.clean(frameworkVersion), latestPkg.framework); - if(!latestVersionIsNewer) { - logger.log('info', `Already using the latest version of ${packageName} (${latestPkg.version})`); - return cb(null, false); - } - if(!satisfiesFrameworkReq) { - // TODO recursively check old versions; we may be several releases behind - logger.log('warn', `A later version of ${packageName} is available but is not supported by the installed version of the Adapt framework (${version.adapt_framework})`); - return cb(null, false); - } - logger.log('info', `A new version of ${packageName} is available (${latestPkg.version})`); - cb(null, true); + if(!latestVersionIsNewer) { + logger.log('info', `Already using the latest version of ${packageName} (${latestPkg.version})`); + return cb(null, false); + } + if(!satisfiesFrameworkReq) { + // TODO recursively check old versions; we may be several releases behind + logger.log('warn', `A later version of ${packageName} is available but is not supported by the installed version of the Adapt framework (${frameworkVersion})`); + return cb(null, false); + } + logger.log('info', `A new version of ${packageName} is available (${latestPkg.version})`); + cb(null, true); + }); }); }); }) @@ -937,23 +942,25 @@ function handleUploadedPlugin (req, res, next) { pkgMeta: packageJson }; - // Check if the framework has been defined on the plugin and that it's not compatible - if (packageInfo.pkgMeta.framework && !semver.satisfies(semver.clean(version.adapt_framework), packageInfo.pkgMeta.framework)) { - return next(new PluginPackageError('This plugin is incompatible with version ' + version.adapt_framework + ' of the Adapt framework')); - } - - app.contentmanager.getContentPlugin(pluginType, function (error, contentPlugin) { - if (error) { + installHelpers.getInstalledFrameworkVersion(function(error, frameworkVersion) { + if(error) { return next(error); } - - addPackage(contentPlugin.bowerConfig, packageInfo, { strict: true }, function (error, results) { + // Check if the framework has been defined on the plugin and that it's not compatible + if (packageInfo.pkgMeta.framework && !semver.satisfies(semver.clean(frameworkVersion), packageInfo.pkgMeta.framework)) { + return next(new PluginPackageError('This plugin is incompatible with version ' + frameworkVersion + ' of the Adapt framework')); + } + app.contentmanager.getContentPlugin(pluginType, function (error, contentPlugin) { if (error) { return next(error); } - - res.statusCode = 200; - return res.json({ success: true, pluginType: pluginType, message: 'successfully added new plugin' }); + addPackage(contentPlugin.bowerConfig, packageInfo, { strict: true }, function (error, results) { + if (error) { + return next(error); + } + res.statusCode = 200; + return res.json({ success: true, pluginType: pluginType, message: 'successfully added new plugin' }); + }); }); }); diff --git a/plugins/output/adapt/index.js b/plugins/output/adapt/index.js index 9e0e46ce86..bc8c1a1c50 100644 --- a/plugins/output/adapt/index.js +++ b/plugins/output/adapt/index.js @@ -23,7 +23,7 @@ var origin = require('../../../'), assetmanager = require('../../../lib/assetmanager'), exec = require('child_process').exec, semver = require('semver'), - version = require('../../../version'), + installHelpers = require('../../../lib/installHelpers'), logger = require('../../../lib/logger'); function AdaptOutput() { @@ -39,7 +39,8 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next outputJson = {}, isRebuildRequired = false, themeName = '', - menuName = Constants.Defaults.MenuName; + menuName = Constants.Defaults.MenuName, + frameworkVersion; var resultObject = {}; @@ -146,10 +147,15 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next if (err) { return callback(err); } - callback(null); }); }, + function(callback) { + installHelpers.getInstalledFrameworkVersion(function(error, version) { + frameworkVersion = version; + callback(error); + }); + }, function(callback) { fs.exists(path.join(BUILD_FOLDER, Constants.Filenames.Main), function(exists) { if (!exists || isRebuildRequired) { @@ -159,7 +165,7 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next var outputFolder = COURSE_FOLDER.replace(FRAMEWORK_ROOT_FOLDER + path.sep,''); // Append the 'build' folder to later versions of the framework - if (semver.gte(semver.clean(version.adapt_framework), semver.clean('2.0.0'))) { + if (semver.gte(semver.clean(frameworkVersion), semver.clean('2.0.0'))) { outputFolder = path.join(outputFolder, Constants.Folders.Build); } diff --git a/test/entry.js b/test/entry.js index 75362309bf..1611ee2150 100644 --- a/test/entry.js +++ b/test/entry.js @@ -20,6 +20,8 @@ var EXTENDED_TIMEOUT = 600000; before(function(done) { this.timeout(EXTENDED_TIMEOUT); + process.env.SILENT = true; + async.series([ removeTestData, function initApp(cb) { diff --git a/upgrade.js b/upgrade.js index e9ac704f0e..2a4f4d8432 100644 --- a/upgrade.js +++ b/upgrade.js @@ -1,397 +1,158 @@ -var builder = require('./lib/application'); -var prompt = require('prompt'); -var fs = require('fs'); -var request = require('request'); +var _ = require('underscore'); var async = require('async'); -var exec = require('child_process').exec; -var rimraf = require('rimraf'); -var path = require('path'); +var chalk = require('chalk'); +var fs = require('fs-extra'); var optimist = require('optimist'); +var path = require('path'); +var semver = require('semver'); -// Constants -var DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'; - -// Helper -var isVagrant = function () { - if (process.argv.length > 2) { - return true; - } - return false; -}; - -// GLOBALS -var app = builder(); -var installedBuilderVersion = ''; -var latestBuilderTag = ''; -var installedFrameworkVersion = ''; -var latestFrameworkTag = ''; -var shouldUpdateBuilder = false; -var shouldUpdateFramework = false; -var versionFile = JSON.parse(fs.readFileSync('version.json'), {encoding: 'utf8'}); -var configFile = JSON.parse(fs.readFileSync('conf/config.json'), {encoding: 'utf8'}); - -var steps = [ - function(callback) { - - console.log('Checking versions'); - - if (versionFile) { - installedBuilderVersion = versionFile.adapt_authoring; - installedFrameworkVersion = versionFile.adapt_framework; - } - - console.log('Currently installed versions:\n- ' + app.polyglot.t('app.productname') + ': ' + installedBuilderVersion + '\n- Adapt Framework: ' + installedFrameworkVersion); - callback(); - - }, - function(callback) { - - console.log('Checking for ' + app.polyglot.t('app.productname') + ' upgrades...'); - // Check the latest version of the project - request({ - headers: { - 'User-Agent' : DEFAULT_USER_AGENT - }, - uri: 'https://api.github.com/repos/adaptlearning/adapt_authoring/tags', - method: 'GET' - }, function (error, response, body) { +var configuration = require('./lib/configuration'); +var logger = require('./lib/logger'); +var origin = require('./lib/application'); +var OutputConstants = require('./lib/outputmanager').Constants; +var installHelpers = require('./lib/installHelpers'); - if (!error && response.statusCode == 200) { - var tagInfo = JSON.parse(body); +var DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'; +var app = origin(); - if (tagInfo) { - latestBuilderTag = tagInfo[0].name; - } +/** +* Start of execution +*/ +start(); + +function start() { + // don't show any logger messages in the console + logger.level('console','error'); + // start the server first + app.run({ skipVersionCheck: true, skipStartLog: true }); + app.on('serverStarted', getUserInput); +} - callback(); +function getUserInput() { + // properties for the prompts + var confirmProperties = { + name: 'continue', + description: 'Continue? Y/n', + type: 'string', + default: 'Y', + before: installHelpers.inputHelpers.toBoolean + }; + var upgradeProperties = { + properties: { + updateAutomatically: { + description: 'Update automatically? Y/n', + type: 'string', + default: 'Y', + before: installHelpers.inputHelpers.toBoolean } - - }); - - }, - function(callback) { - - console.log('Checking for Adapt Framework upgrades...'); - // Check the latest version of the framework - request({ - headers: { - 'User-Agent' : DEFAULT_USER_AGENT + } + }; + var tagProperties = { + properties: { + authoringToolGitTag: { + type: 'string', + description: 'Specific git revision to be used for the authoring tool. Accepts any valid revision type (e.g. branch/tag/commit)', + default: '' }, - uri: 'https://api.github.com/repos/adaptlearning/adapt_framework/tags', - method: 'GET' - }, function (error, response, body) { - if (!error && response.statusCode == 200) { - var tagInfo = JSON.parse(body); - - if (tagInfo) { - latestFrameworkTag = tagInfo[0].name; - } - - callback(); + frameworkGitTag: { + type: 'string', + description: 'Specific git revision to be used for the framework. Accepts any valid revision type (e.g. branch/tag/commit)', + default: '' } - }); - - }, - function(callback) { - // Check what needs upgrading - if (latestBuilderTag != installedBuilderVersion) { - shouldUpdateBuilder = true; - console.log('Update for ' + app.polyglot.t('app.productname') + ' is available: ' + latestBuilderTag); - } - - if (latestFrameworkTag != installedFrameworkVersion) { - shouldUpdateFramework = true; - console.log('Update for Adapt Framework is available: ' + latestFrameworkTag); - } - - // If neither of the Builder or Framework need updating then quit the upgrading process - if (!shouldUpdateFramework && !shouldUpdateBuilder) { - console.log('No updates available at this time\n'); - process.exit(0); - } - - callback(); - - }, function(callback) { - // Upgrade Builder if we need to - if (shouldUpdateBuilder) { - - upgradeBuilder(latestBuilderTag, function(err) { - if (err) { - return callback(err); - } - - versionFile.adapt_authoring = latestBuilderTag; - callback(); - - }); - - } else { - callback(); } - - }, function(callback) { - - // Upgrade Framework if we need to - if (shouldUpdateFramework) { - - upgradeFramework(latestFrameworkTag, function(err) { - if (err) { - return callback(err); - } - - versionFile.adapt_framework = latestFrameworkTag; - callback(); - - }); - - } else { - callback(); + }; + console.log(`\nThis script will update the ${app.polyglot.t('app.productname')} and/or Adapt Framework. Would you like to continue?`); + installHelpers.getInput(confirmProperties, function(result) { + if(!result.continue) { + return installHelpers.exit(); } - - }, function(callback) { - - // After upgrading let's update the version.json to the latest version - fs.writeFile('version.json', JSON.stringify(versionFile, null, 4), function(err) { - if(err) { - callback(err); - } else { - console.log("Version file updated\n"); - callback(); - } - }); - - }, function(callback) { - if (shouldUpdateFramework) { - // If the framework has been updated, interrogate the adapt.json file from the adapt_framework - // folder and install the latest versions of the core plugins - fs.readFile(path.join(configFile.root, 'temp', configFile.masterTenantID, 'adapt_framework', 'adapt.json'), function (err, data) { - if (err) { - return callback(err); - } - - var json = JSON.parse(data); - // 'dependencies' contains a key-value pair representing the plugin name and the semver - var plugins = Object.keys(json.dependencies); - - async.eachSeries(plugins, function(plugin, pluginCallback) { - app.bowermanager.installPlugin(plugin, json.dependencies[plugin], function(err) { - if (err) { - return pluginCallback(err); - } - - pluginCallback(); - }); - - }, function(err) { - if (err) { - console.log(err); - return callback(err); + installHelpers.getInput(upgradeProperties, function(result) { + console.log(''); + if(result.updateAutomatically) { + return checkForUpdates(function(error, updateData) { + if(error) { + return installHelpers.exit(1, error); } - - callback(); + doUpdate(updateData); }); - }); - } else { - callback(); - } - }, - function(callback) { - // Left empty for any upgrade scripts - just remember to call the callback when done. - callback(); - } -]; - -app.run({skipVersionCheck: true, skipStartLog: true}); - -app.on('serverStarted', function () { - prompt.override = optimist.argv; - prompt.start(); - - // Prompt the user to begin the install - if (isVagrant()) { - console.log(`\nUpdate the ${app.polyglot.t('app.productname')} (and/or Adapt Framework) to the latest released version.`); - } else { - console.log(`\nThis script will update the ${app.polyglot.t('app.productname')} (and/or Adapt Framework) to the latest released version. Would you like to continue?`); - } - - prompt.get({ name: 'Y/n', type: 'string', default: 'Y' }, function (err, result) { - if (!/(Y|y)[es]*$/.test(result['Y/n'])) { - return exitUpgrade(); - } - - // run steps - async.series(steps, function (err, results) { - - if (err) { - console.log('ERROR: ', err); - return exitUpgrade(1, 'Upgrade was unsuccessful. Please check the console output.'); } - - console.log(' '); - - exitUpgrade(0, 'Great work! Your ' + app.polyglot.t('app.productname') + ' is now updated.'); - }); - }); -}); - -// This upgrades the Builder -function upgradeBuilder(tagName, callback) { - - console.log('Upgrading the ' + app.polyglot.t('app.productname') + '...please hold on!'); - var child = exec('git fetch origin', { - stdio: [0, 'pipe', 'pipe'] - }); - - child.stdout.on('data', function(err) { - console.log(err); - }); - child.stderr.on('data', function(err) { - console.log(err); - }); - - child.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); - } - - console.log("Fetch from GitHub was successful."); - console.log("Pulling latest changes..."); - - var secondChild = exec('git reset --hard ' + tagName, { - stdio: [0, 'pipe', 'pipe'] - }); - - secondChild.stdout.on('data', function(err) { - console.log(err); - }); - - secondChild.stderr.on('data', function(err) { - console.log(err); - }); - - secondChild.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); - } - - console.log("Installing " + app.polyglot.t('app.productname') + " dependencies.\n"); - - var thirdChild = exec('npm install', { - stdio: [0, 'pipe', 'pipe'] - }); - - thirdChild.stdout.on('data', function(err) { - console.log(err); - }); - - thirdChild.stderr.on('data', function(err) { - console.log(err); - }); - - thirdChild.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); + // no automatic update, so get the intended versions + installHelpers.getInput(tagProperties, function(result) { + console.log(''); + if(!result.authoringToolGitTag && !result.frameworkGitTag) { + return installHelpers.exit(1, 'Cannot update sofware if no revisions are specified.'); } - console.log("Dependencies installed.\n"); - - console.log("Building front-end.\n"); - - var fourthChild = exec('grunt build:prod', { - stdio: [0, 'pipe', 'pipe'] - }); - - fourthChild.stdout.on('data', function(err) { - console.log(err); - }); - - fourthChild.stderr.on('data', function(err) { - console.log(err); - }); - - fourthChild.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); - } - - console.log("front-end built.\n"); - - console.log(app.polyglot.t('app.productname') + " has been updated.\n"); - callback(); + doUpdate({ + adapt_authoring: result.authoringToolGitTag, + adapt_framework: result.frameworkGitTag }); }); }); }); } -// This upgrades the Framework -function upgradeFramework(tagName, callback) { - console.log('Upgrading the Adapt Framework...please hold on!'); - - var child = exec('git fetch origin', { - cwd: 'temp/' + configFile.masterTenantID + '/adapt_framework', - stdio: [0, 'pipe', 'pipe'] - }); - - child.stdout.on('data', function(err) { - console.log(err); - }); - - child.stderr.on('data', function(err) { - console.log(err); - }); - - child.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); +function checkForUpdates(callback) { + var isCustomFramework = configuration.getConfig('frameworkRepository') !== installHelpers.DEFAULT_FRAMEWORK_REPO; + var isCustomServer = configuration.getConfig('authoringToolRepository') !== installHelpers.DEFAULT_SERVER_REPO; + if(isCustomFramework || isCustomServer) { + return callback('Cannot perform an automatic upgrade when custom repositories are used.'); + } + installHelpers.showSpinner('Checking for updates'); + installHelpers.getUpdateData(function(error, data) { + installHelpers.hideSpinner(); + if(error) { + return callback(error); } + if(!data) { + return installHelpers.exit(0, `Your software is already up-to-date, no need to upgrade.`); + } + console.log(chalk.underline('Software updates found.\n')); + callback(null, data); + }); +} - console.log("Fetch from GitHub was successful."); - console.log("Pulling latest changes..."); - - var secondChild = exec('git reset --hard ' + tagName + ' && npm install', { - cwd: 'temp/' + configFile.masterTenantID + '/adapt_framework', - stdio: [0, 'pipe', 'pipe'] - }); - - secondChild.stdout.on('data', function(err) { - console.log(err); - }); - - secondChild.stderr.on('data', function(err) { - console.log(err); - }); - - secondChild.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); +function doUpdate(data) { + async.series([ + function upgradeAuthoring(cb) { + if(!data.adapt_authoring) { + return cb(); } - - console.log("Framework has been updated.\n"); - - rimraf(configFile.root + '/temp/' + configFile.masterTenantID + '/adapt_framework/src/course', function(err) { - if (err) { - console.log(err); + installHelpers.updateAuthoring({ + repository: configuration.getConfig('authoringToolRepository'), + revision: data.adapt_authoring, + directory: configuration.serverRoot + }, function(error) { + if(error) { + console.log(`Failed to update ${configuration.serverRoot} to '${data.adapt_authoring}'`); + return cb(error); } - - callback(); + console.log(`${app.polyglot.t('app.productname')} upgraded to ${data.adapt_authoring}`); + cb(); }); - - }); - + }, + function upgradeFramework(cb) { + if(!data.adapt_framework) { + return cb(); + } + var dir = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), OutputConstants.Folders.Framework); + installHelpers.updateFramework({ + repository: configuration.getConfig('frameworkRepository'), + revision: data.adapt_framework, + directory: dir + }, function(error) { + if(error) { + console.log(`Failed to upgrade ${dir.replace(configuration.serverRoot, '')} to ${data.adapt_framework}`); + return cb(error); + } + console.log(`Adapt framework upgraded to ${data.adapt_framework}`); + cb(); + }); + }, + ], function(error) { + if(error) { + console.error('ERROR:', error); + return installHelpers.exit(1, 'Upgrade was unsuccessful. Please check the console output.'); + } + installHelpers.exit(0, `Your ${app.polyglot.t('app.productname')} was updated successfully.`); }); } - -/** - * Exits the install with some cleanup, should there be an error - * - * @param {int} code - * @param {string} msg - */ - -function exitUpgrade (code, msg) { - code = code || 0; - msg = msg || 'Bye!'; - console.log(msg); - process.exit(code); -} diff --git a/version.json b/version.json deleted file mode 100644 index c4a102800e..0000000000 --- a/version.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "adapt_authoring": "v0.3.1", - "adapt_framework": "v2.0.17" -} From 24d2420a497036aeec5bae9962d6100e13f070a3 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Tue, 17 Oct 2017 20:45:03 +0100 Subject: [PATCH 016/111] Update CHANGELOG for release 0.4.0 (#1739) * Add release notes * Fix formatting * Update date * Update titles * Reword opening paragraph * Remove INSTALL.md content * Add further changes --- CHANGELOG.md | 170 ++++++++++++++++++++++++++++++++++++++------------- INSTALL.md | 82 +------------------------ 2 files changed, 131 insertions(+), 121 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ecee19005..7780b1c05f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,84 @@ # Change Log -All notable changes to this project will be documented in this file.
      -This project adheres to [Semantic Versioning](http://semver.org/). -**IMPORTANT**: For information on how to *correctly* update your installation, consult [INSTALL.md](https://github.com/adaptlearning/adapt_authoring/blob/master/INSTALL.md#updating-the-tool). +All notable changes to the Adapt authoring tool are documented in this file. + +**IMPORTANT**: For information on how to **correctly and safely** update your installation, please consult **INSTALL.md**. + +_Note that we adhere to the [semantic versioning](http://semver.org/) scheme for release numbering._ + +## [0.4.0] - 2017-10-17 + +Major refactor of the front-end application. + +### Upgrade Notes +Due to the changes made to the install script, this release restricts the installed framework version to `v2.x.x` to avoid unsupported breaking changes introduced in framework `v3`. + +There are a few notable changes to the code that may impact customisations: +- `app:dataReady` has been renamed to `origin:dataReady` +- `variables.less` has been renamed to `colours.less`. +- Some editor collections have been renamed: + - `componentTypes` -> `componenttypes` + - `extensionTypes` -> `extensiontypes` + - `courseAssets` -> `courseassets` + +Please check the release notes below for more information. + +### Added +- Framework themes can now display a preview in the theme picker. To enable this, a `preview.jpg` file is needed in the theme folder root +- Can now specify custom auth source for MongoDB ([\#1673](https://github.com/adaptlearning/adapt_authoring/issues/1673)) +- New `contentPane` module takes over view rendering from `Router`, and acts as a consistent container for main app content. Makes sure scrolling is consistent across the application among other things. +- EditorDataLoader has been added to preload editor data. You can use the `EditorDataLoader.waitForLoad` function to halt code until preload has finished. You can also use the `editor:dataPreloaded` event. + +### Changed +- Major refactoring of the frontend folder: + - 'Core' code separated into modules, and core + - Web-app loading rewritten + - Core LESS files are now accessible without needing to specify a relative file path. `variables.less` has been renamed to `colours.less`. + - All duplicate LESS files have been merged, and put in their respective module folder + - The `adaptbuilder` folder has been renamed to `build` + - Editor routing code has been simplified, and moved into the sub-module folders. See [modules/editor/index.js#L27-L55](https://github.com/adaptlearning/adapt_authoring/blob/v0.4.0/frontend/src/modules/editor/index.js#L27-L55) for the routing code, and [modules/editor/article/index.js#L10](https://github.com/adaptlearning/adapt_authoring/blob/release-0.4.0/frontend/src/modules/editor/article/index.js#L10) as an example of the new routing method. + - Events using `app:` replaced with `origin:` for consistency. Most notably: any code using `app:dataReady` will need to be switched over to listen to `origin:dataReady` + - Router has been refactored, and the following convenience functions added: `navigateTo` - wrapper for `Backbone.Router.navigate`, `navigateToLogin`, `setHomeRoute` and `navigateToHome` + - Editor collections have been renamed to reflect the MongoDB collection names: `editor.componentTypes` -> `editor.componenttypes`, `editor.extensionTypes` -> `editor.extensiontypes`, `editor.courseAssets` -> `editor.courseassets` + - `window.polyglot` has been abstracted into the new localisation module, which can be referenced with `Origin.l10n` +- Dashboard module has been renamed to projects, and is the default home route +- User management has moved from plugins to modules +- Install/upgrade scripts overhauled: + - Can now upgrade the server and framework to specific releases + - Can now upgrade the server and framework separately + - Install/upgrade scripts have been made prettier to look at (and more useful) with the introduction of activity spinners, and more helpful log messages + - Upgrade script now ignores draft and prereleases ([\#1723](https://github.com/adaptlearning/adapt_authoring/issues/1723)) + - Upgrade/install script now allows custom git repositories to be used for both the server and framework source + - Framework version can be restricted so as not to automatically upgrade to a version you don't want to support. To enable this, specify the `framework` version in `package.json` (accepts any valid semver range) ([\#1703](https://github.com/adaptlearning/adapt_authoring/issues/1703)) +- Besides the schemas, the user interface is now completely localised ([\#1573](https://github.com/adaptlearning/adapt_authoring/issues/1573)) +- Improved multi-user support for previewing/publishing of courses ([\#1220](https://github.com/adaptlearning/adapt_authoring/issues/1220)) +- User profile page is now correctly styled ([\#1413](https://github.com/adaptlearning/adapt_authoring/issues/1413)) +- Newly created courses can now be built without any editing ([\#1678](https://github.com/adaptlearning/adapt_authoring/issues/1678)) +- Must now input super admin password twice during install to avoid user error ([\#1032][https://github.com/adaptlearning/adapt_authoring/issues/1032]) +- Abstracted polyglot into the new internal `l10n` library (accessible globally via the `Origin` object). Language strings are now obtained using `Origin.l10n.t` +- Backbone forms version updated, and override code tidied up/removed where possible +- Boolean selects are now rendered as checkboxes. + +### Removed +- **Vagrant support has been dropped** ([\#1503](https://github.com/adaptlearning/adapt_authoring/issues/1503)) +- The user management's user list sort is now case-insensitive ([\#1549](https://github.com/adaptlearning/adapt_authoring/issues/1549)) +- Front-end tests removed for now + +### Fixed +- Framework plugin update has been fixed ([\#1415](https://github.com/adaptlearning/adapt_authoring/issues/1415)) +- Unused user password reset data is now cleared on delete of the related user ([\#1553](https://github.com/adaptlearning/adapt_authoring/issues/1553)) +- Copy and paste now correctly includes any extension settings ([\#1484](https://github.com/adaptlearning/adapt_authoring/issues/1484)) +- Dashboard no longer hangs if large images have been used as hero images ([\#1470](https://github.com/adaptlearning/adapt_authoring/issues/1470)) +- Server plugin schemas now correctly reflect the latest state after a plugin has been updated (previously a server restart was needed for any schema changes to be reflected) ([\#1524](https://github.com/adaptlearning/adapt_authoring/issues/1524)) +- Preview loading route added to prevent preview opening in a background tab/window ([\#1636](https://github.com/adaptlearning/adapt_authoring/issues/1636)) +- We now only attempt to load valid routes, avoiding unnecessary server breakdowns ([\#1534](https://github.com/adaptlearning/adapt_authoring/issues/1534)) +- Mocha tests fixed, and integrated with TravisCI +- Dragging is now restricted for components depending on layout ([\#1631](https://github.com/adaptlearning/adapt_authoring/issues/1631)) ## [0.3.0] - 2017-01-24 +User management feature release. + ### Added - User management - Can add users @@ -27,7 +100,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Removed `on_start` from notifications as Travis WebLint shows as deprecated ### Fixed -- Block alignment in page editor +- Block alignment in page editor - Password reset emails now work as intended - The 'enabled' checkbox in Plugin Management now hides plugins from editor - Removed tab/newline chars from CKEditor output to fix tabbing in published courses @@ -36,6 +109,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [0.2.2] - 2016-09-13 +Bugfix release. + ### Added - Support for editing JSON objects, for example, the `_playerOptions` array in the Media component @@ -43,7 +118,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Vagrant issue with preventing updating plugins ## [0.2.1] - 2016-08-16 -This is a patch release which fixes minor issues with v0.2.0 uncovered by community user testing. + +Bugfix release after community user testing. ### Added - LESS sourcemaps @@ -57,9 +133,8 @@ This is a patch release which fixes minor issues with v0.2.0 uncovered by commun - reset.less now loaded before *everything* ## [0.2.0] - 2016-07-27 -This version introduces a new look and feel to the user interface of the authoring tool, and closely matches the new theme of the Adapt community site introduced at the beginning of the year. -In addition, this release also includes the following: +Major theme update to match the new look and feel of [adaptlearning.org](www.adaptlearning.org). ### Changed - Disabled SSL certificate check during Vagrant install @@ -71,12 +146,13 @@ In addition, this release also includes the following: - Saving course settings hangs if nothing has been changed ## [0.1.7] - 2016-04-28 -This version contains numerous minor bug fixes and enhamcements, and supports the new menu locking feature released in version 2.0.9 of the Adapt Framework. - ### Added +Bugfix release to support framework v2.0.9. + +### Added - Support for new Adapt Framework 'menu locking' functionality - Support for v2.0.9 of the Adapt Framework -- Support for _isAvailable flag +- Support for `_isAvailable` flag - Added link to GitHub repositories for plugins ### Changed @@ -87,11 +163,12 @@ This version contains numerous minor bug fixes and enhamcements, and supports th ### Fixed - Role statements not updated on a server restart - Autocomplete enabled on text input fields -- MongoStore does not support replicasets -- Removed @learningpool.com e-mail address hack - +- MongoStore does not support `replicasets` +- Removed `@learningpool.com` e-mail address hack + ## [0.1.6] - 2016-03-29 -This version adds the ability to export the source code for a course that can be built using the Adapt Framework. It also fixes some important issues for installing using Vagrant. + +Release to add source-code export of courses. ### Added - Support for new Adapt Framework 'start page' functionality @@ -113,31 +190,33 @@ This version adds the ability to export the source code for a course that can be - Intermittent error in copy and pasting component ## [0.1.5] - 2016-02-16 -This version aligns closely with the re-work on the Adapt Framework v2.0.7 release, with a focus on performance improvements around preview and downloading courses. + +Bugfix release to support framework v2.0.7. ### Added - Support for v2.0.7 of the Adapt Framework - Optimised build process, i.e. only plugins used are bundled -- Ability to copy the _id value of contentobjects, articles, blocks and components to clipboard +- Ability to copy the `_id` value of `contentobjects`, `articles`, `blocks` and `components` to clipboard - Ability to easily change component layouts without using drag and drop - Ability to export the source code of a particular course - Caching added to assets to improve performance ### Changed -- _isAvailableInEditor flag persisted when a new plugin is uploaded +- `_isAvailableInEditor` flag persisted when a new plugin is uploaded - Optimised performance of processing course assets in preview/download - Preview redirects to index.html rather than main.html - The count of failed logins is reset after a successful login -- Turned off automatic population of Display Title for blocks +- Turned off automatic population of display title for blocks ### Fixed -- Non-essential attributes removed from course.json +- Non-essential attributes removed from `course.json` - ACE JavaScript error when creating a new course - Hard 100kb limit on JSON payload - Corrected Project Details save issue ## [0.1.4] - 2015-11-25 -This version adds support for Node.js v4.2.2 LTS. + +Release to add support for Node.js v4.2.2 LTS. ### Added - Support for Node.js v4.2.2 LTS @@ -149,20 +228,22 @@ This version adds support for Node.js v4.2.2 LTS. - Locking the Title and Display Title by default - Renamed 'Publish' button to 'Download' - Updated package dependencies to correct security issues -- Assets can now be defined in articles.json +- Assets can now be defined in `articles.json` - Tag length has been increased to 30 characters ### Fixed - Error on copying and pasting a block - Custom CSS/LESS not pulling through -- _supportedLayout not working correctly +- `_supportedLayout` not working correctly ## [0.1.3] - 2015-10-21 +Bugfix release. + ### Added -- Support for MongoDB replicasets +- Support for MongoDB `replicasets` - More robust processing for missing schema pluginLocations -- Support for _accessibility added to Configuration Settings +- Support for `_accessibility` added to Configuration Settings - Support for screen breakpoints added to Configuration Settings - Added security to preview route @@ -171,24 +252,26 @@ This version adds support for Node.js v4.2.2 LTS. - Bumped CKEditor version to 4.5.4 ### Fixed -- Page and menu/sections were created without a linkText property set -- IE 9 issue with editor and list formatting -- Problem with isAssetExternal() +- Page and menu/sections were created without a `linkText` property set +- IE9 issue with editor and list formatting +- Problem with `isAssetExternal()` - Dashboard problems when a hero image is not set - Added validation for length of database name -- Added validation to Confugration Settings +- Added validation to Configuration Settings ## [0.1.2] - 2015-09-30 +Bugfix release. + ### Added -- Support for _isOptional (Adapt Framework v2.x) +- Support for `_isOptional` (Adapt Framework v2.x) - Support for accessibility (Adapt Framework v2.x) - Support for plugin 'globals' (Adapt Framework v2.x) - Improved install/upgrade - 'Global' configurations for plugins are conditionally applied - Added basic browser-based spell-check to HTML editor - Table editing is now an option on the HTML editor -- Any tag added in the HTML editor is now preserved +- Any `` tag added in the HTML editor is now preserved - Support for 'Autofill' on graphic components - Confirmation when deleting a component/extension item, such as a narrative or question stem - Ability to delete assets @@ -196,12 +279,12 @@ This version adds support for Node.js v4.2.2 LTS. ### Changed - Course now has a Display Title property -- Default plugins are now taken from the framework adapt.json file, hard-coded references to plugins are +- Default plugins are now taken from the framework `adapt.json` file, hard-coded references to plugins are - Removed the dependency on adapt-cli - Added better logging for Validation Failed errors on database operations - Remove hard-coded references to core plugins -- Upgrade to Express 4, support NodeJS 0.12.6, i.e. removed hard dependency on 0.10.33 -- Any logger.log() calls now support placeholders properly +- Upgrade to Express 4, support NodeJS 0.12.6 (removed hard dependency on 0.10.33) +- Any `logger.log()` calls now support placeholders properly - Authoring tool specific properties now removed from output JSON - Updated logo @@ -218,25 +301,27 @@ This version adds support for Node.js v4.2.2 LTS. - Deleting an article or page does not remove associated assets contained with in - Modal overlay has a few responsive issues when appending content/custom editing views - Issue with long list item attribute values going outside of the list item box -- Issue with nested items in backbone forms showing as [object Object] +- Issue with nested items in backbone forms showing as `[object Object]` - Course tags were removed when a hero image was added or removed ## [0.1.1] - 2015-03-12 +Large bugfix release. + ## Upgrade Notes If upgrading from a previous version, please add the following keys to your config.json -- "outputPlugin" - "adapt" -- "masterTenantName" - {name of the folder containing your master tenant files} -- "masterTenantID" - {MongoDB _id of the initial first row in the 'tenants' collection} +- `outputPlugin` -> `adapt` +- `masterTenantName` -> {name of the folder containing your master tenant files} +- `masterTenantID` - {MongoDB `_id` of the initial first row in the `tenants` collection} ### Added - Support for client-side configs - Proper support for shared courses - Poster images now available on courses - Progress indicator on preview -- Support for _trackingId values +- Support for `_trackingId` values ### Changed - Role permissions synced on a server restart @@ -244,7 +329,7 @@ If upgrading from a previous version, please add the following keys to your conf - Install process updated ### Fixed -- Minor IE 9 fixes +- Minor IE9 fixes - Corrected 'Back to courses' button - Missing language strings - Fixes around drag and drop, copy and paste @@ -256,6 +341,8 @@ If upgrading from a previous version, please add the following keys to your conf ## [0.1.0] - 2015-01-26 +Initial release. + ### Added - Support for menu selection - Support to load configuration from process.env @@ -277,7 +364,7 @@ If upgrading from a previous version, please add the following keys to your conf - Copy/paste moved to server-side - Content plugins preloaded on server boot - Asset records now use relative paths - + ### Removed - iframe previews - Sockets.io (for now...) @@ -287,11 +374,12 @@ If upgrading from a previous version, please add the following keys to your conf - Issue where project settings caused a javascript error - Issue with uploading gifs would fail - Issues with course duplication -- Issues with bowercache file locking +- Issues with `bowercache` file locking - Issues with drag and drop in page editor - Loading screen of death - Session cookie security issues +[0.4.0]: https://github.com/adaptlearning/adapt_authoring/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/adaptlearning/adapt_authoring/compare/v0.2.2...v0.3.0 [0.2.2]: https://github.com/adaptlearning/adapt_authoring/compare/v0.2.1...v0.2.2 [0.2.1]: https://github.com/adaptlearning/adapt_authoring/compare/v0.2.0...v0.2.1 diff --git a/INSTALL.md b/INSTALL.md index 58acfc68d9..51c1f889c7 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,81 +1,3 @@ -## Introduction -* The Adapt authoring tool is a node app that provides a user interface for the Adapt framework. -* This version of instructions contains only the essential details. [A more detailed set of installation instructions is also available.](https://github.com/adaptlearning/adapt_authoring/wiki/Install-on-Server) If you need assistance troubleshooting, consult the Adapt community's Technical Discussion Forum. +# Installation -### Navigation -- [Installing Manually](#to-install-manually) -- [Updating the tool](#updating-the-tool) - -## To Install Manually - -### 1. Install Prerequisites - -Install the following before proceeding: -* [Git](http://git-scm.com/downloads) -* [Node](http://nodejs.org/) We recommend the [current LTS release](https://github.com/nodejs/LTS). Node version managers, such as [nodist (Windows)](https://github.com/marcelklehr/nodist) or [nvm (OS X, Linux)] (https://github.com/creationix/nvm), make it easy to switch between versions of Node. -* [npm](https://www.npmjs.com/) is bundled and installed automatically with Node. -* [Grunt](http://gruntjs.com/) -* [Adapt-CLI](https://github.com/adaptlearning/adapt-cli) -* [MongoDB](http://docs.mongodb.org/manual/) - -The following are optional: -* [FFmpeg](https://www.ffmpeg.org/index.html) is used to provide thumbnails for image and video assets. -* [Robomongo](http://robomongo.org/) is not used by the authoring tool, but you might find it helpful in your work with MongoDB. - -> **Tips:** -> + Windows users should run these commands in Git Bash if Git was installed using default settings. Otherwise, run the command prompt window as Administrator. -> + Mac and Linux users may need to prefix the commands with `sudo` or give yourself elevated permissions on the */usr/local directory* as documented [here](http://foohack.com/2010/08/intro-to-npm/#what_no_sudo). - -### 2. Clone the Adapt_Authoring Project - -`git clone https://github.com/adaptlearning/adapt_authoring.git` - - -### 3. Install Dependencies -Navigate to the folder where you cloned adapt_authoring and run the following command: -`npm install` - -### 4. Run the Install Script - -The final portion of the install script will help you configure the authoring tool. Most configuration questions will appear with a default answer already in place. And most times you can just accept the default values by pressing the Enter key. **The only input you are required to provide are an email address and password for the super user account.** (The questions about the super user account is not the same as the SMTP service or the master tenant.) The super user's email address and password will be used to login to the authoring tool. ->**Notes:** ->* FFmpeg is not used by default. When the question "Will ffmpeg be used?" N for no will appear as the default. If FFmpeg is installed and you want to use it, type Y before pressing the Enter key. ->* In the future the authoring tool will be able to send notifications via e-mail. Configuration questions will ask about SMTP service, SMTP username, SMTP password, and Sender email address. Because this is not yet functioning, your responses have no impact. Accept the default of "none" for the SMTP service and leave the others blank. ->* It is essential that you verify that the MongoDB service has started and is running. Installation will fail if the MongoDB service has stopped. - -Run the following command. -`node install` - -If the script succeeds, you'll receive the following message: -`Done, without errors.` -And you'll be instructed to -`Run the command 'node server' (or 'foreman start' if using heroku toolbelt) to start your instance.` - -### 5. Run the Application -1. Verify MongoDB service is running. - -2. Run the following command. -`node server` -As the server starts, it will report in the terminal: -`Server started listening on port 5000` -If your server is listening on a different port, take note of it. - -3. Open a browser and in the address bar type: -`localhost:5000` (If your server is listening on a different port, substitute your port for 5000.) - -When the login page appears, **enter the super user's e-mail address and password.** - -## Updating the tool - -We've written a Node.js script to allow you to easily update both the authoring tool core and the installed Adapt framework to the latest versions. - -**IMPORTANT**: -- Before upgrading, make sure to first remove the `node_modules` folder and re-install the dependencies to ensure that you get the correct package dependencies for the updated code. -- Also please consult the [CHANGELOG](https://github.com/adaptlearning/adapt_authoring/blob/update-changelog/CHANGELOG.md) for the release you're upgrading to; any extra upgrade instructions will be noted here. - -```javascript -npm install --production -node upgrade -``` - -The upgrade script will ask for confirmation before proceeding. Once you've consented, the process will begin. +You can find detailed instructions for installing the Adapt authoring tool on the [repository's wiki](https://github.com/adaptlearning/adapt_authoring/wiki/Installing-the-Authoring-Tool). From 9512418e540110ae5fa73d9012f72dd3dd0fec53 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Tue, 17 Oct 2017 22:04:36 +0100 Subject: [PATCH 017/111] Remove testing header --- lib/installHelpers.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/installHelpers.js b/lib/installHelpers.js index 2e3a7319e5..7e5287f09c 100644 --- a/lib/installHelpers.js +++ b/lib/installHelpers.js @@ -198,8 +198,7 @@ function checkLatestAdaptRepoVersion(repoName, versionLimit, callback) { var _getReleases = function(done) { request({ headers: { - 'User-Agent': DEFAULT_USER_AGENT , - Authorization: 'token 15e160298d59a7a70ac7895c9766b0802735ac99' + 'User-Agent': DEFAULT_USER_AGENT }, uri: nextPage, method: 'GET' From 2c96f984037354d59e52b7d5c7eb41f875b28021 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Tue, 17 Oct 2017 23:23:45 +0100 Subject: [PATCH 018/111] Fix bug with recursive function call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was called infinitely if value wasn’t undefined --- lib/installHelpers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/installHelpers.js b/lib/installHelpers.js index 7e5287f09c..71e7ef008a 100644 --- a/lib/installHelpers.js +++ b/lib/installHelpers.js @@ -288,7 +288,8 @@ function installFramework(opts, callback) { if(!opts.revision) { return getLatestFrameworkVersion(function(error, version) { if(error) return callback(error); - installFramework(_.extend({ revision: version }, opts), callback); + opts.revision = version; + installFramework(opts, callback); }); } if(!fs.existsSync(opts.directory) || opts.force) { From 53c09cb3e0b41339f53dbfcdc000e1ec8e178fc3 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 25 Oct 2017 10:49:13 +0100 Subject: [PATCH 019/111] Handle response errors better --- lib/installHelpers.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/installHelpers.js b/lib/installHelpers.js index 71e7ef008a..76b504d6d7 100644 --- a/lib/installHelpers.js +++ b/lib/installHelpers.js @@ -205,15 +205,16 @@ function checkLatestAdaptRepoVersion(repoName, versionLimit, callback) { }, done); }; var _requestHandler = function(error, response, body) { - // we've exceeded the API limit - if(response.statusCode === 403 && response.headers['x-ratelimit-remaining'] === '0') { - var reqsReset = new Date(response.headers['x-ratelimit-reset']*1000); - error = `You have exceeded GitHub's request limit of ${response.headers['x-ratelimit-limit']} requests per hour. Please wait until at least ${reqsReset.toTimeString()} before trying again.`; - } - else if (response.statusCode !== 200) { - error = 'GitubAPI did not respond with a 200 status code.'; + if(response) { + // we've exceeded the API limit + if(response.statusCode === 403 && response.headers['x-ratelimit-remaining'] === '0') { + var reqsReset = new Date(response.headers['x-ratelimit-reset']*1000); + error = `You have exceeded GitHub's request limit of ${response.headers['x-ratelimit-limit']} requests per hour. Please wait until at least ${reqsReset.toTimeString()} before trying again.`; + } + else if(response.statusCode !== 200) { + error = 'GitubAPI did not respond with a 200 status code.'; + } } - if (error) { return callback(`Couldn't check latest version of ${repoName}\n${error}`); } From b5f82b1923cab7fa8fa6f6f4c25d89a5880f8fbb Mon Sep 17 00:00:00 2001 From: Tom Greenfield Date: Thu, 26 Oct 2017 10:46:53 +0100 Subject: [PATCH 020/111] Add override for removal of list items Accomodate SweetAlert in Backbone Forms Lists library --- .../scaffold/backboneFormsOverrides.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/frontend/src/modules/scaffold/backboneFormsOverrides.js b/frontend/src/modules/scaffold/backboneFormsOverrides.js index bf18e15b68..522930be80 100644 --- a/frontend/src/modules/scaffold/backboneFormsOverrides.js +++ b/frontend/src/modules/scaffold/backboneFormsOverrides.js @@ -94,6 +94,33 @@ define(function(require) { return parts.join('
      '); }; + Backbone.Form.editors.List.prototype.removeItem = function(item) { + //Confirm delete + var confirmMsg = this.schema.confirmDelete; + + var remove = _.bind(function(isConfirmed) { + if (isConfirmed === false) return; + + var index = _.indexOf(this.items, item); + + this.items[index].remove(); + this.items.splice(index, 1); + + if (item.addEventTriggered) { + this.trigger('remove', this, item.editor); + this.trigger('change', this); + } + + if (!this.items.length && !this.Editor.isAsync) this.addItem(); + }, this); + + if (confirmMsg) { + window.confirm({ title: confirmMsg, type: 'warning', callback: remove }); + } else { + remove(); + } + }; + // Used to setValue with defaults Backbone.Form.editors.Base.prototype.setValue = function(value) { From f0a665d1e4d30ff00e1b646e390fcb207a323b0a Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 30 Oct 2017 12:47:41 +0000 Subject: [PATCH 021/111] Rewrite projectsView.js to fix pagination Fixes #1759 --- .../src/modules/projects/less/qproject.less | 15 - .../modules/projects/views/projectsView.js | 336 +++++++----------- 2 files changed, 119 insertions(+), 232 deletions(-) diff --git a/frontend/src/modules/projects/less/qproject.less b/frontend/src/modules/projects/less/qproject.less index db30d099ab..794b30a448 100644 --- a/frontend/src/modules/projects/less/qproject.less +++ b/frontend/src/modules/projects/less/qproject.less @@ -185,21 +185,6 @@ .project-detail-title { margin-bottom:10px; display: inline-block; - - .project-detail-title-inner { - h4 { - height: 36px; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - - & span.highlighted { - background-color: #ff0; - } - } - } } .project-details-row { diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index e6ca357a4c..47f9f75f52 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -1,7 +1,5 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); var OriginView = require('core/views/originView'); var ProjectView = require('./projectView'); @@ -9,272 +7,176 @@ define(function(require){ var ProjectsView = OriginView.extend({ className: 'projects', - settings: { - autoRender: true, - preferencesKey: 'dashboard' + + preRender: function() { + this.settings.preferencesKey = 'dashboard'; + + this.initEventListeners(); + this.initUserPreferences(); }, - preRender: function(options) { - this.setupFilterSettings(); + postRender: function() { + this.initPaging(_.bind(function() { + this.resetCollection(this.setViewToReady); + }, this)); + }, + initEventListeners: function() { this.listenTo(Origin, { - 'window:resize': this.resizeDashboard, - 'dashboard:layout:grid': this.switchLayoutToGrid, - 'dashboard:layout:list': this.switchLayoutToList, - 'dashboard:dashboardSidebarView:filterBySearch': this.filterBySearchInput, - 'dashboard:dashboardSidebarView:filterByTags': this.filterCoursesByTags, - 'dashboard:sidebarFilter:add': this.addTag, - 'dashboard:sidebarFilter:remove': this.removeTag, - // These need to pass in true to re-render the collections - 'dashboard:sort:asc': function() { this.sortAscending(true); }, - 'dashboard:sort:desc': function() { this.sortDescending(true); }, - 'dashboard:sort:updated': function() { this.sortLastUpdated(true); } + 'window:resize': this.initPaging, + 'dashboard:layout:grid': function() { this.doLayout('grid') }, + 'dashboard:layout:list': function() { this.doLayout('list') }, + 'dashboard:dashboardSidebarView:filterBySearch': function(text) { this.doFilter(text) }, + 'dashboard:dashboardSidebarView:filterByTags': function(tags) { this.doFilter(null, tags) }, + 'dashboard:sort:asc': function() { this.doSort('asc'); }, + 'dashboard:sort:desc': function() { this.doSort('desc'); }, + 'dashboard:sort:updated': function() { this.doSort('updated'); } }); + this.listenTo(this.collection, 'add', this.appendProjectItem); - this.listenTo(this.collection, { - 'add': this.appendProjectItem, - 'sync': this.checkIfCollectionIsEmpty - }); + $('#app > .app-inner > .contentPane').scroll(_.bind(this.doLazyScroll, this)); }, - setupFilterSettings: function() { - // Setup filtering and lazy loading settings - this.sort = {createdAt: -1}; - this.search = {}; - this.courseLimit = -32; - this.courseDenominator = 32; - // Set empty filters - this.filters = []; - this.tags = []; - - this.collectionLength = 0; - this.shouldStopFetches = false; + initUserPreferences: function() { + var prefs = this.getUserPreferences(); + this.doLayout(prefs.layout, false); + this.doSort(prefs.sort, false); + this.doFilter(prefs.search, prefs.tags, false); // set relevant filters as selected $("a[data-callback='dashboard:layout:grid']").addClass('selected'); $("a[data-callback='dashboard:sort:asc']").addClass('selected'); - }, - - resizeDashboard: function() { - var navigationHeight = $('.navigation').outerHeight(); - var locationTitleHeight = $('.location-title').outerHeight(); - var windowHeight = $(window).height(); - var actualHeight = windowHeight - (navigationHeight + locationTitleHeight); - this.$el.css('height', actualHeight); - }, - - checkIfCollectionIsEmpty: function() { - this.$('.no-projects').toggleClass('display-none', this.collection.length > 0); - }, - - postRender: function() { - this.setupUserPreferences(); - - // Fake a scroll trigger - just incase the limit is too low and no scroll bars - this.getProjectsContainer().trigger('scroll'); - this.lazyRenderCollection(); - this.resizeDashboard(); - this.setViewToReady(); - this.setupLazyScrolling(); - }, - switchLayoutToList: function() { - this.getProjectsContainer().removeClass('grid-layout').addClass('list-layout'); - this.setUserPreference('layout','list'); - }, + prefs = this.getUserPreferences(); - switchLayoutToGrid: function() { - this.getProjectsContainer().removeClass('list-layout').addClass('grid-layout'); - this.setUserPreference('layout','grid'); + Origin.trigger('options:update:ui', prefs); + Origin.trigger('sidebar:update:ui', prefs); }, - sortAscending: function(shouldRenderProjects) { - this.sort = { title: 1 }; - this.setUserPreference('sort','asc'); - if(shouldRenderProjects) this.updateCollection(true); - }, + initPaging: function(cb) { + // we need to load one course first to check page size + this.pageSize = 1; + this.resetCollection(_.bind(function(collection) { + var containerHeight = $(window).height()-this.$el.offset().top; + var containerWidth = this.$('.projects-inner').width(); + var itemHeight = $('.project-list-item').outerHeight(true); + var itemWidth = $('.project-list-item').outerWidth(true); + var columns = Math.floor(containerWidth/itemWidth); + var rows = Math.floor(containerHeight/itemHeight); + // columns stack nicely, but need to add extra row if it's not a clean split + if((containerHeight % itemHeight) > 0) rows++; + this.pageSize = columns*rows; - sortDescending: function(shouldRenderProjects) { - this.sort = { title: -1 }; - this.setUserPreference('sort','desc'); - if(shouldRenderProjects) this.updateCollection(true); + if(typeof cb === 'function') { + cb(); + } + }, this)); }, - sortLastUpdated: function(shouldRenderProjects) { - this.sort = { updatedAt: -1 }; - this.setUserPreference('sort','updated'); - if (shouldRenderProjects) this.updateCollection(true); + getProjectsContainer: function() { + return this.$('.projects-list'); }, - setupUserPreferences: function() { - // Preserve the user preferences or display default mode - var userPreferences = this.getUserPreferences(); - // Check if the user preferences are list view - // Else if nothing is set or is grid view default to grid view - if (userPreferences && userPreferences.layout === 'list') { - this.switchLayoutToList(); - } else { - this.switchLayoutToGrid(); - } - // Check if there's any user preferences for search and tags - // then set on this view - if (userPreferences) { - var searchString = (userPreferences.search || ''); - this.search = this.convertFilterTextToPattern(searchString); - this.setUserPreference('search', searchString); - this.tags = (_.pluck(userPreferences.tags, 'id') || []); - this.setUserPreference('tags', userPreferences.tags); - } - // Check if sort is set and sort the collection - if (userPreferences && userPreferences.sort === 'desc') { - this.sortDescending(); - } else if (userPreferences && userPreferences.sort === 'updated') { - this.sortLastUpdated(); - } else { - this.sortAscending(); - } - // Once everything has been setup - // refresh the userPreferences object - userPreferences = this.getUserPreferences(); - // Trigger event to update options UI - Origin.trigger('options:update:ui', userPreferences); - Origin.trigger('sidebar:update:ui', userPreferences); + emptyProjectsContainer: function() { + Origin.trigger('dashboard:dashboardView:removeSubViews'); + this.getProjectsContainer().empty(); }, - lazyRenderCollection: function() { - // Adjust limit based upon the denominator - this.courseLimit += this.courseDenominator; - this.updateCollection(false); + appendProjectItem: function(model) { + var viewClass = model.isEditable() ? ProjectView : SharedProjectView; + this.getProjectsContainer().append(new viewClass({ model: model }).$el); }, - getProjectsContainer: function() { - return this.$('.projects-list'); + convertFilterTextToPattern: function(filterText) { + var pattern = '.*' + filterText.toLowerCase() + '.*'; + return { title: pattern }; }, - emptyProjectsContainer: function() { - // Trigger event to kill zombie views - Origin.trigger('dashboard:dashboardView:removeSubViews'); - // Empty collection container - this.getProjectsContainer().empty(); + resetCollection: function(cb) { + this.emptyProjectsContainer(); + this.fetchCount = 0; + this.shouldStopFetches = false; + this.collection.reset(); + this.fetchCollection(cb); }, - updateCollection: function(reset) { - // If removing items, we need to reset our limits - if (reset) { - // Empty container - this.emptyProjectsContainer(); - // Reset fetches cache - this.shouldStopFetches = false; - this.courseLimit = 0; - this.collectionLength = 0; - this.collection.reset(); - } - this.search = _.extend(this.search, { tags: { $all: this.tags } }); - // This is set when the fetched amount is equal to the collection length - // Stops any further fetches and HTTP requests - if (this.shouldStopFetches) { + fetchCollection: function(cb) { + if(this.shouldStopFetches) { return; } + this.isCollectionFetching = true; this.collection.fetch({ - remove: reset, data: { - search: this.search, + search: _.extend(this.search, { tags: { $all: this.tags } }), operators : { - skip: this.courseLimit, - limit: this.courseDenominator, + skip: this.fetchCount, + limit: this.pageSize, sort: this.sort } }, - success: _.bind(function(data) { - // On successful collection fetching set lazy render to enabled - if (this.collectionLength === this.collection.length) { - this.shouldStopFetches = true; - } else { - this.shouldStopFetches = false; - this.collectionLength = this.collection.length; - } + success: _.bind(function(collection, response) { this.isCollectionFetching = false; + this.fetchCount += response.length; + // stop further fetching if this is the last page + if(response.length < this.pageSize) this.shouldStopFetches = true; + + this.$('.no-projects').toggleClass('display-none', this.fetchCount > 0); + if(typeof cb === 'function') cb(collection); }, this) }); }, - appendProjectItem: function(projectModel) { - projectModel.attributes.title=this.highlight(projectModel.attributes.title) - - if (!projectModel.isEditable()) { - this.getProjectsContainer().append(new SharedProjectView({ model: projectModel }).$el); - } else { - this.getProjectsContainer().append(new ProjectView({ model: projectModel }).$el); + doLazyScroll: function(e) { + if(this.isCollectionFetching) { + return; } + var $el = $(e.currentTarget); + var pxRemaining = this.getProjectsContainer().height() - ($el.scrollTop() + $el.height()); + // we're at the bottom, fetch more + if (pxRemaining <= 0) this.fetchCollection(); }, - highlight: function(text) { - var search = this.getUserPreferences().search || ''; - // replace special characters: .*+?|()[]{}\$^ - search.replace(/[.*+?|()\[\]{}\\$^]/g, "\\$&"); - // add the span - return text.replace(new RegExp(search, "gi"), function(term) { - return '' + term + ''; - }); - }, - - addTag: function(filterType) { - // add filter to this.filters - this.tags.push(filterType); - this.filterCollection(); - }, - - removeTag: function(filterType) { - // remove filter from this.filters - this.tags = _.filter(this.tags, function(item) { return item != filterType; }); - this.filterCollection(); - }, - - filterCollection: function() { - this.search.tags = this.tags.length - ? { $all: this.tags } - : null ; - this.updateCollection(true); - }, - - convertFilterTextToPattern: function(filterText) { - var pattern = '.*' + filterText.toLowerCase() + '.*'; - return { title: pattern}; + doLayout: function(layout) { + var layouts = ["grid", "list"]; + if(_.indexOf(layouts, layout) === -1) { + return; + } + var classSuffix = '-layout'; + this.getProjectsContainer() + .removeClass(layouts.join(classSuffix + ' ') + classSuffix) + .addClass(layout + '-layout'); + this.setUserPreference('layout', layout); + }, + + doSort: function(sort, fetch) { + switch(sort) { + case "desc": + this.sort = { title: -1 }; + break; + case "updated": + this.sort = { updatedAt: -1 }; + break; + case "asc": + default: + sort = "asc"; + this.sort = { title: 1 }; + } + this.setUserPreference('sort', sort); + if(fetch !== false) this.resetCollection(); }, - filterBySearchInput: function (filterText) { - this.filterText = filterText; - this.search = this.convertFilterTextToPattern(filterText); - this.setUserPreference('search', filterText); - this.updateCollection(true); - }, + doFilter: function(text, tags, fetch) { + text = text || ''; + this.filterText = text; + this.search = this.convertFilterTextToPattern(text || ''); + this.setUserPreference('search', text); - filterCoursesByTags: function(tags) { - this.setUserPreference('tags', tags); + tags = tags || []; this.tags = _.pluck(tags, 'id'); - this.updateCollection(true); - }, - - setupLazyScrolling: function() { - var $projectContainer = $('.projects'); - var $projectContainerInner = $('.projects-inner'); - // Remove event before attaching - $projectContainer.off('scroll'); + this.setUserPreference('tags', tags); - $projectContainer.on('scroll', _.bind(function() { - var scrollTop = $projectContainer.scrollTop(); - var scrollableHeight = $projectContainerInner.height(); - var containerHeight = $projectContainer.height(); - // If the scroll position of the assets container is - // near the bottom - if ((scrollableHeight-containerHeight) - scrollTop < 30) { - if (!this.isCollectionFetching) { - this.isCollectionFetching = true; - this.lazyRenderCollection(); - } - } - }, this)); + if(fetch !== false) this.resetCollection(); } }, { template: 'projects' From 2db9f5c86275fa1f13ba6e5ac331d992827c802b Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 30 Oct 2017 13:42:24 +0000 Subject: [PATCH 022/111] Fix version check Fixes #1760 --- lib/bowermanager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bowermanager.js b/lib/bowermanager.js index a96fd1090a..f2bf976ce6 100644 --- a/lib/bowermanager.js +++ b/lib/bowermanager.js @@ -125,7 +125,7 @@ BowerManager.prototype.installPlugin = function(pluginName, pluginVersion, callb if (err) { return callback(err); } - installHelpers.getLatestFrameworkVersion(function(error, frameworkVersion) { + installHelpers.getInstalledFrameworkVersion(function(error, frameworkVersion) { if (error) { return callback(error); } From 4c6a35d26137722859d6debdff4ca3a32ba0ad47 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 30 Oct 2017 13:50:58 +0000 Subject: [PATCH 023/111] Stop throwing error if repo update check failed --- lib/installHelpers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/installHelpers.js b/lib/installHelpers.js index 71e7ef008a..7597575f82 100644 --- a/lib/installHelpers.js +++ b/lib/installHelpers.js @@ -213,9 +213,10 @@ function checkLatestAdaptRepoVersion(repoName, versionLimit, callback) { else if (response.statusCode !== 200) { error = 'GitubAPI did not respond with a 200 status code.'; } - + // exit, but just log the error if (error) { - return callback(`Couldn't check latest version of ${repoName}\n${error}`); + logger.warn(`Couldn't check latest version of ${repoName}\n${error}`); + return callback(); } nextPage = parseLinkHeader(response.headers.link).next; try { @@ -287,8 +288,8 @@ function installFramework(opts, callback) { } if(!opts.revision) { return getLatestFrameworkVersion(function(error, version) { - if(error) return callback(error); - opts.revision = version; + // NOTE we default to the master branch + opts.revision = version || 'master'; installFramework(opts, callback); }); } From f4a9d86443b03178a776eb6fd8ee5ca7fd8678e9 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 30 Oct 2017 18:24:56 +0000 Subject: [PATCH 024/111] Switch to local log func --- lib/installHelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/installHelpers.js b/lib/installHelpers.js index b2ba287ac6..2a6d2ab9b0 100644 --- a/lib/installHelpers.js +++ b/lib/installHelpers.js @@ -217,7 +217,7 @@ function checkLatestAdaptRepoVersion(repoName, versionLimit, callback) { } // exit, but just log the error if (error) { - logger.warn(`Couldn't check latest version of ${repoName}\n${error}`); + log(`Couldn't check latest version of ${repoName}\n${error}`); return callback(); } nextPage = parseLinkHeader(response.headers.link).next; From fc866d7dce5dfa457044301466a6a00ee507f2c8 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Tue, 31 Oct 2017 14:30:08 +0000 Subject: [PATCH 025/111] Amend check to allow undefined data --- lib/installHelpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/installHelpers.js b/lib/installHelpers.js index 2a6d2ab9b0..cdcb9d43b5 100644 --- a/lib/installHelpers.js +++ b/lib/installHelpers.js @@ -166,10 +166,10 @@ function getUpdateData(callback) { return callback(error); } var updateData = {}; - if(semver.lt(results[0].adapt_authoring, results[1].adapt_authoring)) { + if(results[1].adapt_authoring && semver.lt(results[0].adapt_authoring, results[1].adapt_authoring)) { updateData.adapt_authoring = results[1].adapt_authoring; } - if(semver.lt(results[0].adapt_framework, results[1].adapt_framework)) { + if(results[1].adapt_framework && semver.lt(results[0].adapt_framework, results[1].adapt_framework)) { updateData.adapt_framework = results[1].adapt_framework; } if(_.isEmpty(updateData)) { From b770c539f287fbb30a32764d19ec63dcba7c4641 Mon Sep 17 00:00:00 2001 From: Louise McMahon Date: Wed, 1 Nov 2017 11:56:30 +0000 Subject: [PATCH 026/111] article view now generated after article model saved --- .../src/modules/editor/contentObject/views/editorPageView.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/modules/editor/contentObject/views/editorPageView.js b/frontend/src/modules/editor/contentObject/views/editorPageView.js index d247d76588..2af692323c 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageView.js @@ -127,8 +127,6 @@ define(function(require){ _type:'article' }); - var newArticleView = _this.addArticleView(newPageArticleModel); - newPageArticleModel.save(null, { error: function() { Origin.Notify.alert({ @@ -138,6 +136,7 @@ define(function(require){ }, success: function(model, response, options) { Origin.editor.data.articles.add(model); + var newArticleView = _this.addArticleView(newPageArticleModel); newArticleView.$el.removeClass('syncing').addClass('synced'); newArticleView.addBlock(); } From f76bfa70b414eb10e556184950f42f05b8d9a817 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 3 Nov 2017 15:33:01 +0000 Subject: [PATCH 027/111] Fixes #1772 --- frontend/src/modules/scaffold/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/modules/scaffold/index.js b/frontend/src/modules/scaffold/index.js index 3f216bf134..4da27f8c2c 100644 --- a/frontend/src/modules/scaffold/index.js +++ b/frontend/src/modules/scaffold/index.js @@ -74,7 +74,8 @@ define(function(require) { itemType: 'Object', subSchema: field.items.properties, confirmDelete: Origin.l10n.t('app.confirmdelete'), - fieldType: 'List' + fieldType: 'List', + help: field.help } } @@ -96,7 +97,8 @@ define(function(require) { type: 'List', itemType:field.items.inputType, subSchema: field.items, - fieldType: field.items.inputType + fieldType: field.items.inputType, + help: field.help } } } From 56a748c373e8b822729d591c8cd86431c4e65bbc Mon Sep 17 00:00:00 2001 From: Louise McMahon Date: Wed, 1 Nov 2017 11:56:30 +0000 Subject: [PATCH 028/111] article view now generated after article model saved --- .../src/modules/editor/contentObject/views/editorPageView.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/modules/editor/contentObject/views/editorPageView.js b/frontend/src/modules/editor/contentObject/views/editorPageView.js index d247d76588..2af692323c 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageView.js @@ -127,8 +127,6 @@ define(function(require){ _type:'article' }); - var newArticleView = _this.addArticleView(newPageArticleModel); - newPageArticleModel.save(null, { error: function() { Origin.Notify.alert({ @@ -138,6 +136,7 @@ define(function(require){ }, success: function(model, response, options) { Origin.editor.data.articles.add(model); + var newArticleView = _this.addArticleView(newPageArticleModel); newArticleView.$el.removeClass('syncing').addClass('synced'); newArticleView.addBlock(); } From aef981f9e0f27891a2a59cfb487cb36127742354 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 22 Nov 2017 16:11:56 +0000 Subject: [PATCH 029/111] Fix issues highlighted by review --- frontend/src/modules/projects/less/projects.less | 4 ++-- frontend/src/modules/projects/less/qproject.less | 5 ----- frontend/src/modules/projects/views/projectsView.js | 11 ++++------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/frontend/src/modules/projects/less/projects.less b/frontend/src/modules/projects/less/projects.less index d336974225..bdfbd872f4 100644 --- a/frontend/src/modules/projects/less/projects.less +++ b/frontend/src/modules/projects/less/projects.less @@ -38,7 +38,7 @@ ul.projects-options { padding: 30px 30px 0px 0px; } -.grid-layout { +.projects-list[data-layout=grid] { .project-list-item, .shared-project-list-item { margin-left: 25px; @@ -74,7 +74,7 @@ ul.projects-options { } } -.list-layout { +.projects-list[data-layout=list] { margin-left:30px; .project-list-item, diff --git a/frontend/src/modules/projects/less/qproject.less b/frontend/src/modules/projects/less/qproject.less index 794b30a448..e474dfc50e 100644 --- a/frontend/src/modules/projects/less/qproject.less +++ b/frontend/src/modules/projects/less/qproject.less @@ -197,8 +197,3 @@ margin-bottom:4px; } } - -.shared-projects-details-label { - color:#9d9d9d; - font-weight: 600; -} diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 47f9f75f52..465bc2741a 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -34,13 +34,13 @@ define(function(require){ }); this.listenTo(this.collection, 'add', this.appendProjectItem); - $('#app > .app-inner > .contentPane').scroll(_.bind(this.doLazyScroll, this)); + $('.contentPane').scroll(_.bind(this.doLazyScroll, this)); }, initUserPreferences: function() { var prefs = this.getUserPreferences(); - this.doLayout(prefs.layout, false); + this.doLayout(prefs.layout); this.doSort(prefs.sort, false); this.doFilter(prefs.search, prefs.tags, false); // set relevant filters as selected @@ -142,10 +142,7 @@ define(function(require){ if(_.indexOf(layouts, layout) === -1) { return; } - var classSuffix = '-layout'; - this.getProjectsContainer() - .removeClass(layouts.join(classSuffix + ' ') + classSuffix) - .addClass(layout + '-layout'); + this.getProjectsContainer().attr('data-layout', layout); this.setUserPreference('layout', layout); }, @@ -169,7 +166,7 @@ define(function(require){ doFilter: function(text, tags, fetch) { text = text || ''; this.filterText = text; - this.search = this.convertFilterTextToPattern(text || ''); + this.search = this.convertFilterTextToPattern(text); this.setUserPreference('search', text); tags = tags || []; From 1d334feb07f7937c1df93650dab68ea16f79b948 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 22 Nov 2017 16:12:51 +0000 Subject: [PATCH 030/111] Make dashboard layouts a bit less hard-coded --- .../src/modules/projects/views/projectsView.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 465bc2741a..30c3bfb6ab 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -7,6 +7,10 @@ define(function(require){ var ProjectsView = OriginView.extend({ className: 'projects', + supportedLayouts: [ + "grid", + "list" + ], preRender: function() { this.settings.preferencesKey = 'dashboard'; @@ -23,15 +27,18 @@ define(function(require){ initEventListeners: function() { this.listenTo(Origin, { - 'window:resize': this.initPaging, - 'dashboard:layout:grid': function() { this.doLayout('grid') }, - 'dashboard:layout:list': function() { this.doLayout('list') }, + 'window:resize': this.onResize, 'dashboard:dashboardSidebarView:filterBySearch': function(text) { this.doFilter(text) }, 'dashboard:dashboardSidebarView:filterByTags': function(tags) { this.doFilter(null, tags) }, 'dashboard:sort:asc': function() { this.doSort('asc'); }, 'dashboard:sort:desc': function() { this.doSort('desc'); }, 'dashboard:sort:updated': function() { this.doSort('updated'); } }); + + this.supportedLayouts.forEach(function(layout) { + this.listenTo(Origin, 'dashboard:layout:' + layout, function() { this.doLayout(layout); }); + }, this); + this.listenTo(this.collection, 'add', this.appendProjectItem); $('.contentPane').scroll(_.bind(this.doLazyScroll, this)); @@ -138,8 +145,7 @@ define(function(require){ }, doLayout: function(layout) { - var layouts = ["grid", "list"]; - if(_.indexOf(layouts, layout) === -1) { + if(this.supportedLayouts.indexOf(layout) === -1) { return; } this.getProjectsContainer().attr('data-layout', layout); From e1f002ba2b50c33a196fd733318405bfd02863ec Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 22 Nov 2017 16:13:28 +0000 Subject: [PATCH 031/111] Fix issues with window resizing and dashboard paging --- .../modules/projects/views/projectsView.js | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 30c3bfb6ab..962f2da179 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -12,17 +12,11 @@ define(function(require){ "list" ], - preRender: function() { + postRender: function() { this.settings.preferencesKey = 'dashboard'; - - this.initEventListeners(); this.initUserPreferences(); - }, - - postRender: function() { - this.initPaging(_.bind(function() { - this.resetCollection(this.setViewToReady); - }, this)); + this.initEventListeners(); + this.initPaging(); }, initEventListeners: function() { @@ -60,7 +54,21 @@ define(function(require){ Origin.trigger('sidebar:update:ui', prefs); }, - initPaging: function(cb) { + // Set some default preferences + getUserPreferences: function() { + var prefs = OriginView.prototype.getUserPreferences.apply(this, arguments); + + if(!prefs.layout) prefs.layout = 'grid'; + if(!prefs.sort) prefs.sort = 'asc'; + + return prefs; + }, + + initPaging: function() { + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + this.resizeTimer = -1; + } // we need to load one course first to check page size this.pageSize = 1; this.resetCollection(_.bind(function(collection) { @@ -73,10 +81,8 @@ define(function(require){ // columns stack nicely, but need to add extra row if it's not a clean split if((containerHeight % itemHeight) > 0) rows++; this.pageSize = columns*rows; - - if(typeof cb === 'function') { - cb(); - } + // need another reset to get the actual pageSize number of items + this.resetCollection(this.setViewToReady); }, this)); }, @@ -180,6 +186,14 @@ define(function(require){ this.setUserPreference('tags', tags); if(fetch !== false) this.resetCollection(); + }, + + onResize: function() { + // we don't want to re-initialise for _every_ resize event + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + } + this.resizeTimer = setTimeout(_.bind(this.initPaging, this), 250); } }, { template: 'projects' From bc47e8c224250fb2c9b93adb8b0bc14eac1c1007 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Wed, 22 Nov 2017 16:13:47 +0000 Subject: [PATCH 032/111] Fix bug with restoring dashboard preferences --- frontend/src/modules/projects/views/projectsView.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 962f2da179..84e41f5456 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -45,11 +45,10 @@ define(function(require){ this.doSort(prefs.sort, false); this.doFilter(prefs.search, prefs.tags, false); // set relevant filters as selected - $("a[data-callback='dashboard:layout:grid']").addClass('selected'); - $("a[data-callback='dashboard:sort:asc']").addClass('selected'); - + $("a[data-callback='dashboard:layout:" + prefs.layout + "']").addClass('selected'); + $("a[data-callback='dashboard:sort:" + prefs.sort + "']").addClass('selected'); + // need to refresh this to get latest filters prefs = this.getUserPreferences(); - Origin.trigger('options:update:ui', prefs); Origin.trigger('sidebar:update:ui', prefs); }, From ce95d140f7788789628396355defa0b75c7eed14 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 23 Nov 2017 09:36:04 +0000 Subject: [PATCH 033/111] Fix spacing size --- .../views/assetManagementCollectionView.js | 382 +++++++++--------- 1 file changed, 181 insertions(+), 201 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index ce196eb7fe..bfac739fee 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -1,205 +1,185 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); - var Origin = require('core/origin'); - var OriginView = require('core/views/originView'); - var AssetItemView = require('./assetManagementItemView'); - var AssetModel = require('../models/assetModel'); - var AssetManagementPreview = require('./assetManagementPreviewView'); - - var AssetCollectionView = OriginView.extend({ - - tagName: "div", - - className: "asset-management-collection", - - events: {}, - - preRender: function(options) { - this.sort = { createdAt: -1 }; - this.search = (options.search || {}); - // Set to minus so we can have more DRY code - this.assetLimit = -32; - this.assetDenominator = 32; - this.filters = (this.search.assetType) ? options.search.assetType.$in : []; - this.tags = []; - this.collectionLength = 0; - this.shouldStopFetches = false; - this.listenTo(this.collection, 'add', this.appendAssetItem); - this.listenTo(Origin, 'assetManagement:sidebarFilter:add', this.addFilter); - this.listenTo(Origin, 'assetManagement:sidebarFilter:remove', this.removeFilter); - this.listenTo(Origin, 'assetManagement:sidebarView:filter', this.filterBySearchInput); - this.listenTo(Origin, 'assetManagement:assetManagementSidebarView:filterByTags', this.filterByTags); - this.listenTo(this.collection, 'sync', this.onCollectionSynced); - this.listenTo(Origin, 'assetManagement:collection:refresh', this.updateCollection); - }, - - onCollectionSynced: function () { - if (this.collection.length === 0) { - $('.asset-management-no-assets').removeClass('display-none'); - } else { - $('.asset-management-no-assets').addClass('display-none'); - } - - // FIX: Purely and lovingly put in for a rendering issue with chrome. - // For when the items being re-rendering after a search return an - // amount of items that means the container is not scrollable - if (this.assetLimit < this.assetDenominator) { - $('.asset-management-assets-container').hide(); - _.delay(function() { - $('.asset-management-assets-container').show(); - }, 10); - } - - }, - - setupLazyScrolling: function() { - - var $assetContainer = $('.asset-management-assets-container'); - var $assetContainerInner = $('.asset-management-assets-container-inner'); - // Remove event before attaching - $assetContainer.off('scroll'); - - $assetContainer.on('scroll', _.bind(function() { - - var scrollTop = $assetContainer.scrollTop(); - var scrollableHeight = $assetContainerInner.height(); - var containerHeight = $assetContainer.height(); - - // If the scroll position of the assets container is - // near the bottom - if ((scrollableHeight-containerHeight) - scrollTop < 30) { - if (!this.isCollectionFetching) { - this.isCollectionFetching = true; - this.lazyRenderCollection(); - } - } - - }, this)); - }, - - updateCollection: function (reset) { - // If removing items, we need to reset our limits - if (reset) { - // Trigger event to kill zombie views - Origin.trigger('assetManagement:assetViews:remove'); - - // Reset fetches cache - this.shouldStopFetches = false; - - this.assetLimit = 0; - this.collectionLength = 0; - this.collection.reset(); - } - - if (!Origin.permissions.hasPermissions(["*"])) { - this.search = _.extend(this.search, {_isDeleted: false}); - } - - this.search = _.extend(this.search, { - tags: { - $all: this.tags - } - }, { - assetType: { - $in: this.filters - } - }); - - // This is set when the fetched amount is equal to the collection length - // Stops any further fetches and HTTP requests - if (this.shouldStopFetches) { - return; - } - - this.collection.fetch({ - remove: reset, - data: { - search: this.search, - operators : { - skip: this.assetLimit, - limit: this.assetDenominator, - sort: this.sort - } - }, - success: _.bind(function() { - // On successful collection fetching set lazy render to enabled - if (this.collectionLength === this.collection.length) { - this.shouldStopFetches = true; - } else { - this.shouldStopFetches = false; - this.collectionLength = this.collection.length; - } - - this.isCollectionFetching = false; - Origin.trigger('assetManagement:assetManagementCollection:fetched'); - }, this) - }); - }, - - appendAssetItem: function (asset) { - this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); - }, - - lazyRenderCollection: function() { - // Adjust limit based upon the denominator - this.assetLimit += this.assetDenominator; - this.updateCollection(false); - }, - - postRender: function() { - this.setupLazyScrolling(); - - // Fake a scroll trigger - just incase the limit is too low and no scroll bars - $('.asset-management-assets-container').trigger('scroll'); - this.setViewToReady(); - }, - - addFilter: function(filterType) { - // add filter to this.filters - this.filters.push(filterType); - this.filterCollection(); - }, - - removeFilter: function(filterType) { - // remove filter from this.filters - this.filters = _.filter(this.filters, function(item) { - return item != filterType; - }); - - this.filterCollection(); - }, - - filterCollection: function() { - this.search.assetType = this.filters.length - ? { $in: this.filters } - : null ; - this.updateCollection(true); - }, - - filterBySearchInput: function (filterText) { - var pattern = '.*' + filterText.toLowerCase() + '.*'; - this.search = { title: pattern, description: pattern }; - this.updateCollection(true); - - $(".asset-management-modal-filter-search" ).focus(); - }, - - removeLazyScrolling: function() { - $('.asset-management-assets-container').off('scroll'); - }, - - filterByTags: function(tags) { - this.tags = _.pluck(tags, 'id'); - this.updateCollection(true); + var Backbone = require('backbone'); + var Handlebars = require('handlebars'); + var Origin = require('core/origin'); + var OriginView = require('core/views/originView'); + var AssetItemView = require('./assetManagementItemView'); + var AssetModel = require('../models/assetModel'); + var AssetManagementPreview = require('./assetManagementPreviewView'); + + var AssetCollectionView = OriginView.extend({ + tagName: "div", + className: "asset-management-collection", + events: {}, + + preRender: function(options) { + this.sort = { createdAt: -1 }; + this.search = (options.search || {}); + // Set to minus so we can have more DRY code + this.assetLimit = -32; + this.assetDenominator = 32; + this.filters = (this.search.assetType) ? options.search.assetType.$in : []; + this.tags = []; + this.collectionLength = 0; + this.shouldStopFetches = false; + this.listenTo(this.collection, 'add', this.appendAssetItem); + this.listenTo(Origin, 'assetManagement:sidebarFilter:add', this.addFilter); + this.listenTo(Origin, 'assetManagement:sidebarFilter:remove', this.removeFilter); + this.listenTo(Origin, 'assetManagement:sidebarView:filter', this.filterBySearchInput); + this.listenTo(Origin, 'assetManagement:assetManagementSidebarView:filterByTags', this.filterByTags); + this.listenTo(this.collection, 'sync', this.onCollectionSynced); + this.listenTo(Origin, 'assetManagement:collection:refresh', this.updateCollection); + }, + + onCollectionSynced: function () { + if (this.collection.length === 0) { + $('.asset-management-no-assets').removeClass('display-none'); + } else { + $('.asset-management-no-assets').addClass('display-none'); + } + // FIX: Purely and lovingly put in for a rendering issue with chrome. + // For when the items being re-rendering after a search return an + // amount of items that means the container is not scrollable + if (this.assetLimit < this.assetDenominator) { + $('.asset-management-assets-container').hide(); + _.delay(function() { + $('.asset-management-assets-container').show(); + }, 10); + } + }, + + setupLazyScrolling: function() { + var $assetContainer = $('.asset-management-assets-container'); + var $assetContainerInner = $('.asset-management-assets-container-inner'); + // Remove event before attaching + $assetContainer.off('scroll'); + $assetContainer.on('scroll', _.bind(function() { + var scrollTop = $assetContainer.scrollTop(); + var scrollableHeight = $assetContainerInner.height(); + var containerHeight = $assetContainer.height(); + // If the scroll position of the assets container is + // near the bottom + if ((scrollableHeight-containerHeight) - scrollTop < 30) { + if (!this.isCollectionFetching) { + this.isCollectionFetching = true; + this.lazyRenderCollection(); + } } - - }, { - template: 'assetManagementCollection' - }); - - return AssetCollectionView; - + }, this)); + }, + + updateCollection: function (reset) { + // If removing items, we need to reset our limits + if (reset) { + // Trigger event to kill zombie views + Origin.trigger('assetManagement:assetViews:remove'); + + // Reset fetches cache + this.shouldStopFetches = false; + + this.assetLimit = 0; + this.collectionLength = 0; + this.collection.reset(); + } + + if (!Origin.permissions.hasPermissions(["*"])) { + this.search = _.extend(this.search, {_isDeleted: false}); + } + + this.search = _.extend(this.search, { + tags: { + $all: this.tags + } + }, { + assetType: { + $in: this.filters + } + }); + // This is set when the fetched amount is equal to the collection length + // Stops any further fetches and HTTP requests + if (this.shouldStopFetches) { + return; + } + + this.collection.fetch({ + remove: reset, + data: { + search: this.search, + operators : { + skip: this.assetLimit, + limit: this.assetDenominator, + sort: this.sort + } + }, + success: _.bind(function() { + // On successful collection fetching set lazy render to enabled + if (this.collectionLength === this.collection.length) { + this.shouldStopFetches = true; + } else { + this.shouldStopFetches = false; + this.collectionLength = this.collection.length; + } + this.isCollectionFetching = false; + Origin.trigger('assetManagement:assetManagementCollection:fetched'); + }, this) + }); + }, + + appendAssetItem: function (asset) { + this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); + }, + + lazyRenderCollection: function() { + // Adjust limit based upon the denominator + this.assetLimit += this.assetDenominator; + this.updateCollection(false); + }, + + postRender: function() { + this.setupLazyScrolling(); + // Fake a scroll trigger - just incase the limit is too low and no scroll bars + $('.asset-management-assets-container').trigger('scroll'); + this.setViewToReady(); + }, + + addFilter: function(filterType) { + // add filter to this.filters + this.filters.push(filterType); + this.filterCollection(); + }, + + removeFilter: function(filterType) { + // remove filter from this.filters + this.filters = _.filter(this.filters, function(item) { + return item != filterType; + }); + this.filterCollection(); + }, + + filterCollection: function() { + this.search.assetType = this.filters.length ? { $in: this.filters } : null; + this.updateCollection(true); + }, + + filterBySearchInput: function (filterText) { + var pattern = '.*' + filterText.toLowerCase() + '.*'; + this.search = { title: pattern, description: pattern }; + this.updateCollection(true); + + $(".asset-management-modal-filter-search" ).focus(); + }, + + removeLazyScrolling: function() { + $('.asset-management-assets-container').off('scroll'); + }, + + filterByTags: function(tags) { + this.tags = _.pluck(tags, 'id'); + this.updateCollection(true); + } + }, { + template: 'assetManagementCollection' + }); + return AssetCollectionView; }); From 77a461e96d3bcec39ebb30256f7b5c51642c99a4 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 23 Nov 2017 10:18:08 +0000 Subject: [PATCH 034/111] Shift code around to group similar behaviour --- .../views/assetManagementCollectionView.js | 181 +++++++++--------- 1 file changed, 89 insertions(+), 92 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index bfac739fee..e31f36f9e9 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -1,52 +1,56 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); var OriginView = require('core/views/originView'); var AssetItemView = require('./assetManagementItemView'); - var AssetModel = require('../models/assetModel'); - var AssetManagementPreview = require('./assetManagementPreviewView'); var AssetCollectionView = OriginView.extend({ tagName: "div", className: "asset-management-collection", - events: {}, - preRender: function(options) { - this.sort = { createdAt: -1 }; - this.search = (options.search || {}); - // Set to minus so we can have more DRY code - this.assetLimit = -32; - this.assetDenominator = 32; - this.filters = (this.search.assetType) ? options.search.assetType.$in : []; - this.tags = []; - this.collectionLength = 0; - this.shouldStopFetches = false; - this.listenTo(this.collection, 'add', this.appendAssetItem); - this.listenTo(Origin, 'assetManagement:sidebarFilter:add', this.addFilter); - this.listenTo(Origin, 'assetManagement:sidebarFilter:remove', this.removeFilter); - this.listenTo(Origin, 'assetManagement:sidebarView:filter', this.filterBySearchInput); - this.listenTo(Origin, 'assetManagement:assetManagementSidebarView:filterByTags', this.filterByTags); - this.listenTo(this.collection, 'sync', this.onCollectionSynced); - this.listenTo(Origin, 'assetManagement:collection:refresh', this.updateCollection); + sort: { + createdAt: -1 }, + search: {}, + filters: [], + tags: [], + assetLimit: -32, + assetDenominator: 32, + collectionLength: 0, + shouldStopFetches: false, - onCollectionSynced: function () { - if (this.collection.length === 0) { - $('.asset-management-no-assets').removeClass('display-none'); - } else { - $('.asset-management-no-assets').addClass('display-none'); - } - // FIX: Purely and lovingly put in for a rendering issue with chrome. - // For when the items being re-rendering after a search return an - // amount of items that means the container is not scrollable - if (this.assetLimit < this.assetDenominator) { - $('.asset-management-assets-container').hide(); - _.delay(function() { - $('.asset-management-assets-container').show(); - }, 10); + preRender: function(options) { + if(options.search) { + this.search = options.search; + var assetType = this.search.assetType; + if(assetType) this.filters = assetType.$in; } + this.initEventListeners(); + }, + + postRender: function() { + this.setupLazyScrolling(); + // Fake a scroll trigger - just incase the limit is too low and no scroll bars + $('.asset-management-assets-container').trigger('scroll'); + this.setViewToReady(); + }, + + initEventListeners: function() { + this.listenTo(Origin, { + 'assetManagement:sidebarFilter:add': this.addFilter, + 'assetManagement:sidebarFilter:remove': this.removeFilter, + 'assetManagement:sidebarView:filter': this.filterBySearchInput, + 'assetManagement:assetManagementSidebarView:filterByTags': this.filterByTags, + 'assetManagement:collection:refresh': this.updateCollection + }); + this.listenTo(this.collection, { + 'add': this.appendAssetItem, + 'sync': this.onCollectionSynced + }); + }, + + appendAssetItem: function (asset) { + this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); }, setupLazyScrolling: function() { @@ -55,46 +59,34 @@ define(function(require){ // Remove event before attaching $assetContainer.off('scroll'); $assetContainer.on('scroll', _.bind(function() { + var scrollableHeight = $assetContainerInner.height() - $assetContainer.height(); var scrollTop = $assetContainer.scrollTop(); - var scrollableHeight = $assetContainerInner.height(); - var containerHeight = $assetContainer.height(); - // If the scroll position of the assets container is - // near the bottom - if ((scrollableHeight-containerHeight) - scrollTop < 30) { - if (!this.isCollectionFetching) { - this.isCollectionFetching = true; - this.lazyRenderCollection(); - } + var isAtBottom = (scrollableHeight - scrollTop) < 30; + if (isAtBottom && !this.isCollectionFetching) { + this.isCollectionFetching = true; + this.lazyRenderCollection(); } }, this)); }, - updateCollection: function (reset) { - // If removing items, we need to reset our limits - if (reset) { - // Trigger event to kill zombie views - Origin.trigger('assetManagement:assetViews:remove'); + removeLazyScrolling: function() { + $('.asset-management-assets-container').off('scroll'); + }, - // Reset fetches cache - this.shouldStopFetches = false; + /** + * Collection manipulation + */ - this.assetLimit = 0; - this.collectionLength = 0; - this.collection.reset(); - } + updateCollection: function (reset) { + if (reset) this.resetCollection(); if (!Origin.permissions.hasPermissions(["*"])) { - this.search = _.extend(this.search, {_isDeleted: false}); + this.search = _.extend(this.search, { _isDeleted: false }); } this.search = _.extend(this.search, { - tags: { - $all: this.tags - } - }, { - assetType: { - $in: this.filters - } + tags: { $all: this.tags }, + assetType: { $in: this.filters } }); // This is set when the fetched amount is equal to the collection length // Stops any further fetches and HTTP requests @@ -113,21 +105,36 @@ define(function(require){ } }, success: _.bind(function() { - // On successful collection fetching set lazy render to enabled - if (this.collectionLength === this.collection.length) { - this.shouldStopFetches = true; - } else { - this.shouldStopFetches = false; - this.collectionLength = this.collection.length; - } + this.shouldStopFetches = this.collectionLength === this.collection.length; + this.collectionLength = this.collection.length; this.isCollectionFetching = false; Origin.trigger('assetManagement:assetManagementCollection:fetched'); }, this) }); }, - appendAssetItem: function (asset) { - this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); + onCollectionSynced: function () { + $('.asset-management-no-assets').toggleClass('display-none', this.collection.length === 0); + // FIX: Purely and lovingly put in for a rendering issue with chrome. + // For when the items being re-rendering after a search return an + // amount of items that means the container is not scrollable + if (this.assetLimit >= this.assetDenominator) { + return; + } + $('.asset-management-assets-container').hide(); + _.delay(function() { + $('.asset-management-assets-container').show(); + }, 10); + }, + + resetCollection: function() { + // Trigger event to kill zombie views + Origin.trigger('assetManagement:assetViews:remove'); + + this.shouldStopFetches = false; + this.assetLimit = 0; + this.collectionLength = 0; + this.collection.reset(); }, lazyRenderCollection: function() { @@ -136,32 +143,26 @@ define(function(require){ this.updateCollection(false); }, - postRender: function() { - this.setupLazyScrolling(); - // Fake a scroll trigger - just incase the limit is too low and no scroll bars - $('.asset-management-assets-container').trigger('scroll'); - this.setViewToReady(); + /** + * Filtering + */ + + filterCollection: function() { + this.search.assetType = this.filters.length ? { $in: this.filters } : null; + this.updateCollection(true); }, addFilter: function(filterType) { - // add filter to this.filters this.filters.push(filterType); this.filterCollection(); }, removeFilter: function(filterType) { // remove filter from this.filters - this.filters = _.filter(this.filters, function(item) { - return item != filterType; - }); + this.filters = _.filter(this.filters, function(item) { return item !== filterType; }); this.filterCollection(); }, - filterCollection: function() { - this.search.assetType = this.filters.length ? { $in: this.filters } : null; - this.updateCollection(true); - }, - filterBySearchInput: function (filterText) { var pattern = '.*' + filterText.toLowerCase() + '.*'; this.search = { title: pattern, description: pattern }; @@ -170,16 +171,12 @@ define(function(require){ $(".asset-management-modal-filter-search" ).focus(); }, - removeLazyScrolling: function() { - $('.asset-management-assets-container').off('scroll'); - }, - filterByTags: function(tags) { this.tags = _.pluck(tags, 'id'); this.updateCollection(true); } }, { - template: 'assetManagementCollection' + template: 'assetManagementCollection' }); return AssetCollectionView; }); From 9e932b025d6617483137081aa4491dc2e11989fe Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 14:07:40 +0000 Subject: [PATCH 035/111] Fix indentation --- frontend/src/modules/assetManagement/index.js | 49 ++++++++--------- .../views/assetManagementView.js | 53 ++++++++----------- 2 files changed, 44 insertions(+), 58 deletions(-) diff --git a/frontend/src/modules/assetManagement/index.js b/frontend/src/modules/assetManagement/index.js index d9ddcc5a5a..a493bb24ca 100644 --- a/frontend/src/modules/assetManagement/index.js +++ b/frontend/src/modules/assetManagement/index.js @@ -14,35 +14,32 @@ define(function(require) { Origin.assetManagement.filterData = {}; if (!location) { - var tagsCollection = new TagsCollection(); - - tagsCollection.fetch({ - success: function() { - // Load asset collection before so sidebarView has access to it - var assetCollection = new AssetCollection(); - // No need to fetch as the collectionView takes care of this - // Mainly due to serverside filtering - Origin.trigger('location:title:hide'); - Origin.sidebar.addView(new AssetManagementSidebarView({collection: tagsCollection}).$el); - Origin.contentPane.setView(AssetManagementView, {collection: assetCollection}); - Origin.trigger('assetManagement:loaded'); - }, - error: function() { - console.log('Error occured getting the tags collection - try refreshing your page'); - } - }); - } else if (location=== 'new') { - Origin.trigger('location:title:update', {title: 'New Asset'}); - Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); - Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); + (new TagsCollection()).fetch({ + success: function(tagsCollection) { + // Load asset collection before so sidebarView has access to it + var assetCollection = new AssetCollection(); + // No need to fetch as the collectionView takes care of this + // Mainly due to serverside filtering + Origin.trigger('location:title:hide'); + Origin.sidebar.addView(new AssetManagementSidebarView({ collection: tagsCollection }).$el); + Origin.contentPane.setView(AssetManagementView, { collection: assetCollection }); + Origin.trigger('assetManagement:loaded'); + }, + error: function() { + console.log('Error occured getting the tags collection - try refreshing your page'); + } + }); + } else if (location === 'new') { + Origin.trigger('location:title:update', { title: 'New Asset' }); + Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); } else if (subLocation === 'edit') { - var Asset = new AssetModel({ _id: location }); // Fetch existing asset model - Asset.fetch({ - success: function() { - Origin.trigger('location:title:update', {title: 'Edit Asset'}); + (new AssetModel({ _id: location })).fetch({ + success: function(model) { + Origin.trigger('location:title:update', { title: 'Edit Asset' }); Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); - Origin.contentPane.setView(AssetManagementNewAssetView, { model: Asset }); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: model }); } }); } diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index dbc757171c..81f5c18059 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -1,34 +1,27 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); - var OriginView = require('core/views/originView'); var Origin = require('core/origin'); - var AssetModel = require('../models/assetModel'); + var OriginView = require('core/views/originView'); var AssetManagementCollectionView = require('./assetManagementCollectionView'); var AssetManagementPreviewView = require('./assetManagementPreviewView'); var AssetManagementView = OriginView.extend({ - tagName: 'div', - className: 'asset-management', - events: { - }, - preRender: function() { - this.listenTo(Origin, 'window:resize', this.resizeAssetPanels); - this.listenTo(Origin, 'assetManagement:assetItemView:preview', this.onAssetClicked); - this.listenTo(Origin, 'assetManagement:assetPreviewView:delete', this.onAssetDeleted); + this.listenTo(Origin, { + 'window:resize': this.resizeAssetPanels, + 'assetManagement:assetItemView:preview': this.onAssetClicked, + 'assetManagement:assetPreviewView:delete': this.onAssetDeleted + }); }, postRender: function() { - this.setupSubViews(); - this.resizeAssetPanels(); - // Set imageReady - _.defer(_.bind(this.setupImageReady, this)); + this.setupSubViews(); + this.resizeAssetPanels(); + // Set imageReady + _.defer(_.bind(this.setupImageReady, this)); }, setupImageReady: function() { @@ -36,34 +29,30 @@ define(function(require){ }, setupSubViews: function() { - // Push collection through to collection view - this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({collection: this.collection}).$el); + // Push collection through to collection view + this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({ collection: this.collection }).$el); }, resizeAssetPanels: function() { - var navigationHeight = $('.navigation').outerHeight(); - var locationTitleHeight = $('.location-title').outerHeight(); - var windowHeight = $(window).height(); - var actualHeight = windowHeight - (navigationHeight + locationTitleHeight); - this.$('.asset-management-assets-container').height(actualHeight); - this.$('.asset-management-preview-container').height(actualHeight); + var navigationHeight = $('.navigation').outerHeight(); + var locationTitleHeight = $('.location-title').outerHeight(); + var windowHeight = $(window).height(); + var actualHeight = windowHeight - (navigationHeight + locationTitleHeight); + this.$('.asset-management-assets-container').height(actualHeight); + this.$('.asset-management-preview-container').height(actualHeight); }, onAssetClicked: function(model) { - this.$('.asset-management-no-preview').hide(); - this.$('.asset-management-preview-container-inner').html(new AssetManagementPreviewView({ - model: model - }).$el); + this.$('.asset-management-no-preview').hide(); + this.$('.asset-management-preview-container-inner').html(new AssetManagementPreviewView({ model: model }).$el); }, onAssetDeleted: function() { - this.$('.asset-management-no-preview').show(); + this.$('.asset-management-no-preview').show(); } - }, { template: 'assetManagement' }); return AssetManagementView; - }); From 037ade536b9cf764b22bd918279ffab974fc424f Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 15:00:10 +0000 Subject: [PATCH 036/111] Refactor into separate functions --- frontend/src/modules/assetManagement/index.js | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/frontend/src/modules/assetManagement/index.js b/frontend/src/modules/assetManagement/index.js index a493bb24ca..db159f08bf 100644 --- a/frontend/src/modules/assetManagement/index.js +++ b/frontend/src/modules/assetManagement/index.js @@ -10,39 +10,12 @@ define(function(require) { var TagsCollection = require('core/collections/tagsCollection'); Origin.on('router:assetManagement', function(location, subLocation, action) { - Origin.assetManagement = {}; - Origin.assetManagement.filterData = {}; - - if (!location) { - (new TagsCollection()).fetch({ - success: function(tagsCollection) { - // Load asset collection before so sidebarView has access to it - var assetCollection = new AssetCollection(); - // No need to fetch as the collectionView takes care of this - // Mainly due to serverside filtering - Origin.trigger('location:title:hide'); - Origin.sidebar.addView(new AssetManagementSidebarView({ collection: tagsCollection }).$el); - Origin.contentPane.setView(AssetManagementView, { collection: assetCollection }); - Origin.trigger('assetManagement:loaded'); - }, - error: function() { - console.log('Error occured getting the tags collection - try refreshing your page'); - } - }); - } else if (location === 'new') { - Origin.trigger('location:title:update', { title: 'New Asset' }); - Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); - Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); - } else if (subLocation === 'edit') { - // Fetch existing asset model - (new AssetModel({ _id: location })).fetch({ - success: function(model) { - Origin.trigger('location:title:update', { title: 'Edit Asset' }); - Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); - Origin.contentPane.setView(AssetManagementNewAssetView, { model: model }); - } - }); - } + Origin.assetManagement = { + filterData: {} + }; + if(!location) return loadAssetsView(); + if(location === 'new') loadNewAssetView(); + if(subLocation === 'edit') loadEditAssetView(); }); Origin.on('globalMenu:assetManagement:open', function() { @@ -58,4 +31,39 @@ define(function(require) { "sortOrder": 2 }); }); + + function loadAssetsView() { + (new TagsCollection()).fetch({ + success: function(tagsCollection) { + // Load asset collection before so sidebarView has access to it + var assetCollection = new AssetCollection(); + // No need to fetch as the collectionView takes care of this + // Mainly due to serverside filtering + Origin.trigger('location:title:hide'); + Origin.sidebar.addView(new AssetManagementSidebarView({ collection: tagsCollection }).$el); + Origin.contentPane.setView(AssetManagementView, { collection: assetCollection }); + Origin.trigger('assetManagement:loaded'); + }, + error: function() { + console.log('Error occured getting the tags collection - try refreshing your page'); + } + }); + } + + function loadNewAssetView() { + Origin.trigger('location:title:update', { title: 'New Asset' }); + Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); + } + + function loadEditAssetView() { + // Fetch existing asset model + (new AssetModel({ _id: location })).fetch({ + success: function(model) { + Origin.trigger('location:title:update', { title: 'Edit Asset' }); + Origin.sidebar.addView(new AssetManagementNewAssetSidebarView().$el); + Origin.contentPane.setView(AssetManagementNewAssetView, { model: model }); + } + }); + } }); From 37b16bcf85bd5d08b0f8d01bdfa133edde3623fb Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 15:00:53 +0000 Subject: [PATCH 037/111] Add asset._isDeleted check to back-end --- lib/assetmanager.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/assetmanager.js b/lib/assetmanager.js index 6ef5b1e592..58da74d21d 100644 --- a/lib/assetmanager.js +++ b/lib/assetmanager.js @@ -183,13 +183,12 @@ exports = module.exports = { */ retrieveAsset: function (search, options, next) { - + var __this = this; // shuffle params if ('function' === typeof options) { next = options; options = {}; } - // Ensure the tags are populated var pop = { tags: '_id title' }; if (!options.populate) { @@ -197,18 +196,20 @@ exports = module.exports = { } else { options.populate = _.extend(pop, options.populate); } - database.getDatabase(function (error, db) { if (error) { return next(error); } - - db.retrieve('asset', search, options, function (error, records) { + var user = usermanager.getCurrentUser(); + // only return deleted assets if user has correct permissions + __this.hasPermission('delete', user._id, user.tenant._id, '*', function(error, isAllowed) { if (error) { return next(error); } - - return next(null, records); + if(!isAllowed) { + search = _.extend(search, { _isDeleted: false }); + } + db.retrieve('asset', search, options, next); }); }); }, From 375fd2e61ebec36adaf3153c778c2baa9ec1a85b Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 15:02:42 +0000 Subject: [PATCH 038/111] Refactor for readability --- .../assetManagement/views/assetManagementView.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index 81f5c18059..61ca811ee8 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -18,7 +18,9 @@ define(function(require){ }, postRender: function() { - this.setupSubViews(); + var view = new AssetManagementCollectionView({ collection: this.collection }); + this.$('.asset-management-assets-container-inner').append(view.$el); + this.resizeAssetPanels(); // Set imageReady _.defer(_.bind(this.setupImageReady, this)); @@ -28,11 +30,6 @@ define(function(require){ this.$el.imageready(this.setViewToReady); }, - setupSubViews: function() { - // Push collection through to collection view - this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({ collection: this.collection }).$el); - }, - resizeAssetPanels: function() { var navigationHeight = $('.navigation').outerHeight(); var locationTitleHeight = $('.location-title').outerHeight(); @@ -44,7 +41,9 @@ define(function(require){ onAssetClicked: function(model) { this.$('.asset-management-no-preview').hide(); - this.$('.asset-management-preview-container-inner').html(new AssetManagementPreviewView({ model: model }).$el); + + var view = new AssetManagementPreviewView({ model: model }); + this.$('.asset-management-preview-container-inner').html(view.$el); }, onAssetDeleted: function() { From 3e724bebbf378138737320a7f86ca899b155ba06 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 15:33:05 +0000 Subject: [PATCH 039/111] Refactor for brevity --- .../assetManagement/views/assetManagementView.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index 61ca811ee8..992de99696 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -22,12 +22,10 @@ define(function(require){ this.$('.asset-management-assets-container-inner').append(view.$el); this.resizeAssetPanels(); - // Set imageReady - _.defer(_.bind(this.setupImageReady, this)); - }, - - setupImageReady: function() { - this.$el.imageready(this.setViewToReady); + // defer setting ready status until images are ready + _.defer(function() { + view.$el.imageready(this.setViewToReady); + }); }, resizeAssetPanels: function() { @@ -41,7 +39,7 @@ define(function(require){ onAssetClicked: function(model) { this.$('.asset-management-no-preview').hide(); - + var view = new AssetManagementPreviewView({ model: model }); this.$('.asset-management-preview-container-inner').html(view.$el); }, From a4a60bab38ff310ae779a4f5954055d32626aed2 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 16:06:05 +0000 Subject: [PATCH 040/111] Fix scrollbar position --- .../assetManagement/views/assetManagementView.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementView.js b/frontend/src/modules/assetManagement/views/assetManagementView.js index 992de99696..fcc1d462b4 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementView.js @@ -11,7 +11,7 @@ define(function(require){ preRender: function() { this.listenTo(Origin, { - 'window:resize': this.resizeAssetPanels, + 'window:resize': this.resizePanels, 'assetManagement:assetItemView:preview': this.onAssetClicked, 'assetManagement:assetPreviewView:delete': this.onAssetDeleted }); @@ -21,20 +21,25 @@ define(function(require){ var view = new AssetManagementCollectionView({ collection: this.collection }); this.$('.asset-management-assets-container-inner').append(view.$el); - this.resizeAssetPanels(); + this.resizePanels(); // defer setting ready status until images are ready _.defer(function() { view.$el.imageready(this.setViewToReady); }); }, - resizeAssetPanels: function() { + resizePanels: function() { var navigationHeight = $('.navigation').outerHeight(); var locationTitleHeight = $('.location-title').outerHeight(); var windowHeight = $(window).height(); var actualHeight = windowHeight - (navigationHeight + locationTitleHeight); - this.$('.asset-management-assets-container').height(actualHeight); - this.$('.asset-management-preview-container').height(actualHeight); + var paneWidth = $('.asset-management').outerWidth(); + this.$('.asset-management-assets-container') + .height(actualHeight) + .width(paneWidth*0.75); + this.$('.asset-management-preview-container') + .height(actualHeight) + .width(paneWidth*0.25); }, onAssetClicked: function(model) { From 206747832ea0ac9dd026b3b831166df0e96f2fb0 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Fri, 24 Nov 2017 16:06:43 +0000 Subject: [PATCH 041/111] Fix asset management paging Fixes #1790 --- .../modules/assetManagement/less/asset.less | 3 +- .../views/assetManagementCollectionView.js | 163 +++++++++--------- 2 files changed, 81 insertions(+), 85 deletions(-) diff --git a/frontend/src/modules/assetManagement/less/asset.less b/frontend/src/modules/assetManagement/less/asset.less index a421973117..21da7efc46 100644 --- a/frontend/src/modules/assetManagement/less/asset.less +++ b/frontend/src/modules/assetManagement/less/asset.less @@ -16,8 +16,8 @@ } .asset-management-assets-container { - width:75%; float:left; + overflow-y: auto; .asset-management-no-assets { text-align:center; position:relative; @@ -31,7 +31,6 @@ } .asset-management-preview-container { - width:25%; float:right; position: fixed; right: 0; diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index e31f36f9e9..3d82b5ddc6 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -8,16 +8,13 @@ define(function(require){ tagName: "div", className: "asset-management-collection", - sort: { - createdAt: -1 - }, + sort: { createdAt: -1 }, search: {}, filters: [], tags: [], - assetLimit: -32, - assetDenominator: 32, - collectionLength: 0, + fetchCount: 0, shouldStopFetches: false, + pageSize: 1, preRender: function(options) { if(options.search) { @@ -29,10 +26,9 @@ define(function(require){ }, postRender: function() { - this.setupLazyScrolling(); - // Fake a scroll trigger - just incase the limit is too low and no scroll bars - $('.asset-management-assets-container').trigger('scroll'); - this.setViewToReady(); + this.initPaging(); + // init lazy scrolling + $('.asset-management-assets-container').scroll(_.bind(this.doLazyScroll, this)); }, initEventListeners: function() { @@ -41,106 +37,83 @@ define(function(require){ 'assetManagement:sidebarFilter:remove': this.removeFilter, 'assetManagement:sidebarView:filter': this.filterBySearchInput, 'assetManagement:assetManagementSidebarView:filterByTags': this.filterByTags, - 'assetManagement:collection:refresh': this.updateCollection - }); - this.listenTo(this.collection, { - 'add': this.appendAssetItem, - 'sync': this.onCollectionSynced + 'assetManagement:collection:refresh': this.fetchCollection }); + this.listenTo(this.collection, 'add', this.appendAssetItem); }, - appendAssetItem: function (asset) { - this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); - }, - - setupLazyScrolling: function() { - var $assetContainer = $('.asset-management-assets-container'); - var $assetContainerInner = $('.asset-management-assets-container-inner'); - // Remove event before attaching - $assetContainer.off('scroll'); - $assetContainer.on('scroll', _.bind(function() { - var scrollableHeight = $assetContainerInner.height() - $assetContainer.height(); - var scrollTop = $assetContainer.scrollTop(); - var isAtBottom = (scrollableHeight - scrollTop) < 30; - if (isAtBottom && !this.isCollectionFetching) { - this.isCollectionFetching = true; - this.lazyRenderCollection(); - } + initPaging: function() { + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + this.resizeTimer = -1; + } + this.resetCollection(_.bind(function(collection) { + var containerHeight = $(window).height()-this.$el.offset().top; + var containerWidth = this.$el.width(); + var itemHeight = $('.asset-management-list-item').outerHeight(true); + var itemWidth = $('.asset-management-list-item').outerWidth(true); + var columns = Math.floor(containerWidth/itemWidth); + var rows = Math.floor(containerHeight/itemHeight); + // columns stack nicely, but need to add extra row if it's not a clean split + if((containerHeight % itemHeight) > 0) rows++; + this.pageSize = columns*rows; + // need another reset to get the actual pageSize number of items + this.resetCollection(this.setViewToReady); }, this)); }, - removeLazyScrolling: function() { - $('.asset-management-assets-container').off('scroll'); + appendAssetItem: function (asset) { + this.$('.asset-management-collection-inner').append(new AssetItemView({ model: asset }).$el); }, /** * Collection manipulation */ - updateCollection: function (reset) { - if (reset) this.resetCollection(); - - if (!Origin.permissions.hasPermissions(["*"])) { - this.search = _.extend(this.search, { _isDeleted: false }); - } - - this.search = _.extend(this.search, { - tags: { $all: this.tags }, - assetType: { $in: this.filters } - }); - // This is set when the fetched amount is equal to the collection length - // Stops any further fetches and HTTP requests - if (this.shouldStopFetches) { + fetchCollection: function(cb) { + if(this.shouldStopFetches) { return; } + this.isCollectionFetching = true; this.collection.fetch({ - remove: reset, data: { - search: this.search, + search: _.extend(this.search, { + tags: { $all: this.tags }, + assetType: { $in: this.filters } + }), operators : { - skip: this.assetLimit, - limit: this.assetDenominator, + skip: this.fetchCount, + limit: this.pageSize, sort: this.sort } }, - success: _.bind(function() { - this.shouldStopFetches = this.collectionLength === this.collection.length; - this.collectionLength = this.collection.length; + success: _.bind(function(collection, response) { this.isCollectionFetching = false; + this.fetchCount += response.length; + // stop further fetching if this is the last page + if(response.length < this.pageSize) this.shouldStopFetches = true; + + $('.asset-management-no-assets').toggleClass('display-none', this.fetchCount > 0); + Origin.trigger('assetManagement:assetManagementCollection:fetched'); - }, this) + if(typeof cb === 'function') cb(collection); + }, this), + error: function(error) { + console.log(error); + this.isCollectionFetching = false; + } }); }, - onCollectionSynced: function () { - $('.asset-management-no-assets').toggleClass('display-none', this.collection.length === 0); - // FIX: Purely and lovingly put in for a rendering issue with chrome. - // For when the items being re-rendering after a search return an - // amount of items that means the container is not scrollable - if (this.assetLimit >= this.assetDenominator) { - return; - } - $('.asset-management-assets-container').hide(); - _.delay(function() { - $('.asset-management-assets-container').show(); - }, 10); - }, - - resetCollection: function() { - // Trigger event to kill zombie views + resetCollection: function(cb) { + // to remove old views Origin.trigger('assetManagement:assetViews:remove'); this.shouldStopFetches = false; - this.assetLimit = 0; - this.collectionLength = 0; + this.fetchCount = 0; this.collection.reset(); - }, - - lazyRenderCollection: function() { - // Adjust limit based upon the denominator - this.assetLimit += this.assetDenominator; - this.updateCollection(false); + this.fetchCollection(cb); }, /** @@ -149,7 +122,7 @@ define(function(require){ filterCollection: function() { this.search.assetType = this.filters.length ? { $in: this.filters } : null; - this.updateCollection(true); + this.fetchCollection(); }, addFilter: function(filterType) { @@ -166,14 +139,38 @@ define(function(require){ filterBySearchInput: function (filterText) { var pattern = '.*' + filterText.toLowerCase() + '.*'; this.search = { title: pattern, description: pattern }; - this.updateCollection(true); + this.fetchCollection(); $(".asset-management-modal-filter-search" ).focus(); }, filterByTags: function(tags) { this.tags = _.pluck(tags, 'id'); - this.updateCollection(true); + this.fetchCollection(); + }, + + /** + * Event handling + */ + + onResize: function() { + // we don't want to re-initialise for _every_ resize event + if(this.resizeTimer) { + clearTimeout(this.resizeTimer); + } + this.resizeTimer = setTimeout(_.bind(this.initPaging, this), 250); + }, + + doLazyScroll: function(e) { + if(this.isCollectionFetching) { + return; + } + var $el = $(e.currentTarget); + var scrollableHeight = this.$el.height() - this.$el.height(); + var pxRemaining = this.$el.height() - ($el.scrollTop() + $el.height()); + var scrollTriggerAmmount = $('.asset-management-list-item').first().outerHeight()/2; + // we're at the bottom, fetch more + if (pxRemaining <= scrollTriggerAmmount) this.fetchCollection(); } }, { template: 'assetManagementCollection' From 6af5ac25e833a6efe94a6928991051d8bcafeb30 Mon Sep 17 00:00:00 2001 From: thomas Date: Tue, 5 Dec 2017 13:20:28 +0100 Subject: [PATCH 042/111] store scroll position in origin.editor --- .../contentObject/views/editorPageView.js | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/frontend/src/modules/editor/contentObject/views/editorPageView.js b/frontend/src/modules/editor/contentObject/views/editorPageView.js index 2af692323c..01e9cb8688 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageView.js @@ -25,13 +25,14 @@ define(function(require){ preRender: function() { this.setupChildCount(); - this.listenTo(Origin, { 'editorView:removeSubViews': this.remove, 'pageView:itemRendered': this.evaluateChildStatus }); this.listenTo(Origin, 'editorView:moveArticle:' + this.model.get('_id'), this.render); this.listenTo(Origin, 'editorView:cutArticle:' + this.model.get('_id'), this.onCutArticle); + + this._onScroll = _.bind(_.throttle(this.onScroll, 400), this); }, resize: function() { @@ -66,10 +67,14 @@ define(function(require){ evaluateChildStatus: function() { this.childrenRenderedCount++; + + if (this.childrenRenderedCount < this.childrenCount) return; + this.allChildrenRendered(); }, postRender: function() { this.addArticleViews(); + this.setupScrollListener(); _.defer(_.bind(function(){ this.resize(); @@ -168,7 +173,31 @@ define(function(require){ onCutArticle: function(view) { this.once('pageView:postRender', view.showPasteZones); this.render(); + }, + + setupScrollListener: function() { + $('.contentPane').on('scroll', this._onScroll); + }, + + onScroll: function(event) { + var scrollPos = event.currentTarget.scrollTop; + Origin.editor.scrollTo = scrollPos; + }, + + removeScrollListener: function() { + $('.contentPane').off('scroll', this._onScroll); + }, + + allChildrenRendered: function() { + $('.contentPane').scrollTop(Origin.editor.scrollTo); + }, + + remove: function() { + this.removeScrollListener(); + + EditorOriginView.prototype.remove.apply(this, arguments); } + }, { template: 'editorPage' }); From 0c2a92a2c2a23e2ff7aa17a0e3d5061dc46ee65f Mon Sep 17 00:00:00 2001 From: thomas Date: Wed, 6 Dec 2017 15:08:26 +0100 Subject: [PATCH 043/111] throttle callbacks use on to properly remove scroll listener in remove function --- .../modules/projects/views/projectsView.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 84e41f5456..1dca27ded4 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -20,8 +20,11 @@ define(function(require){ }, initEventListeners: function() { + this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this); + this._onResize = _.bind(_.throttle(this.onResize, 250), this); + this.listenTo(Origin, { - 'window:resize': this.onResize, + 'window:resize': this._onResize, 'dashboard:dashboardSidebarView:filterBySearch': function(text) { this.doFilter(text) }, 'dashboard:dashboardSidebarView:filterByTags': function(tags) { this.doFilter(null, tags) }, 'dashboard:sort:asc': function() { this.doSort('asc'); }, @@ -35,7 +38,7 @@ define(function(require){ this.listenTo(this.collection, 'add', this.appendProjectItem); - $('.contentPane').scroll(_.bind(this.doLazyScroll, this)); + $('.contentPane').on('scroll', this._doLazyScroll); }, initUserPreferences: function() { @@ -188,12 +191,15 @@ define(function(require){ }, onResize: function() { - // we don't want to re-initialise for _every_ resize event - if(this.resizeTimer) { - clearTimeout(this.resizeTimer); - } - this.resizeTimer = setTimeout(_.bind(this.initPaging, this), 250); + this.initPaging(); + }, + + remove: function() { + $('.contentPane').off('scroll', this._doLazyScroll); + + OriginView.prototype.remove.apply(this, arguments); } + }, { template: 'projects' }); From 8c2f25d361b0e5f757550d01a34a9e823ae9ab23 Mon Sep 17 00:00:00 2001 From: thomas Date: Wed, 6 Dec 2017 16:03:10 +0100 Subject: [PATCH 044/111] use debounce instead of throttle --- frontend/src/modules/projects/views/projectsView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 1dca27ded4..a9d0bd622e 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -21,7 +21,7 @@ define(function(require){ initEventListeners: function() { this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this); - this._onResize = _.bind(_.throttle(this.onResize, 250), this); + this._onResize = _.bind(_.debounce(this.onResize, 250), this); this.listenTo(Origin, { 'window:resize': this._onResize, From 6ea27c893b623d7eeb25fe04483870cfb9cc6fdb Mon Sep 17 00:00:00 2001 From: lc-thomasberger Date: Thu, 7 Dec 2017 14:03:12 +0100 Subject: [PATCH 045/111] Fix asset filters (#1810) * update object syntax * pass in location into funciton, otherwise window.location is used This fixes asset edit mode * remove scroll handler * fixed filter * throttle scroll, debounce resize --- frontend/src/modules/assetManagement/index.js | 4 +- .../views/assetManagementCollectionView.js | 40 ++++++++++++------- .../views/assetManagementNewAssetView.js | 2 +- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/frontend/src/modules/assetManagement/index.js b/frontend/src/modules/assetManagement/index.js index db159f08bf..9f6171331c 100644 --- a/frontend/src/modules/assetManagement/index.js +++ b/frontend/src/modules/assetManagement/index.js @@ -15,7 +15,7 @@ define(function(require) { }; if(!location) return loadAssetsView(); if(location === 'new') loadNewAssetView(); - if(subLocation === 'edit') loadEditAssetView(); + if(subLocation === 'edit') loadEditAssetView(location); }); Origin.on('globalMenu:assetManagement:open', function() { @@ -56,7 +56,7 @@ define(function(require) { Origin.contentPane.setView(AssetManagementNewAssetView, { model: new AssetModel }); } - function loadEditAssetView() { + function loadEditAssetView(location) { // Fetch existing asset model (new AssetModel({ _id: location })).fetch({ success: function(model) { diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index 3d82b5ddc6..a0a15ca5e1 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -5,7 +5,6 @@ define(function(require){ var AssetItemView = require('./assetManagementItemView'); var AssetCollectionView = OriginView.extend({ - tagName: "div", className: "asset-management-collection", sort: { createdAt: -1 }, @@ -23,12 +22,16 @@ define(function(require){ if(assetType) this.filters = assetType.$in; } this.initEventListeners(); + + this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this); + this._onResize = _.bind(_.debounce(this.onResize, 400), this); }, postRender: function() { this.initPaging(); // init lazy scrolling - $('.asset-management-assets-container').scroll(_.bind(this.doLazyScroll, this)); + $('.asset-management-assets-container').on('scroll', this._doLazyScroll); + $(window).on('resize', this._onResize); }, initEventListeners: function() { @@ -43,12 +46,8 @@ define(function(require){ }, initPaging: function() { - if(this.resizeTimer) { - clearTimeout(this.resizeTimer); - this.resizeTimer = -1; - } this.resetCollection(_.bind(function(collection) { - var containerHeight = $(window).height()-this.$el.offset().top; + var containerHeight = $(window).height() - this.$el.offset().top; var containerWidth = this.$el.width(); var itemHeight = $('.asset-management-list-item').outerHeight(true); var itemWidth = $('.asset-management-list-item').outerWidth(true); @@ -57,6 +56,7 @@ define(function(require){ // columns stack nicely, but need to add extra row if it's not a clean split if((containerHeight % itemHeight) > 0) rows++; this.pageSize = columns*rows; + // need another reset to get the actual pageSize number of items this.resetCollection(this.setViewToReady); }, this)); @@ -71,7 +71,7 @@ define(function(require){ */ fetchCollection: function(cb) { - if(this.shouldStopFetches) { + if(this.shouldStopFetches || this.isCollectionFetching) { return; } this.isCollectionFetching = true; @@ -106,14 +106,17 @@ define(function(require){ }); }, - resetCollection: function(cb) { + resetCollection: function(cb, shouldFetch) { // to remove old views Origin.trigger('assetManagement:assetViews:remove'); this.shouldStopFetches = false; this.fetchCount = 0; this.collection.reset(); - this.fetchCollection(cb); + + if (shouldFetch === undefined || shouldFetch === true) { + this.fetchCollection(cb); + } }, /** @@ -121,6 +124,7 @@ define(function(require){ */ filterCollection: function() { + this.resetCollection(null, false); this.search.assetType = this.filters.length ? { $in: this.filters } : null; this.fetchCollection(); }, @@ -137,6 +141,7 @@ define(function(require){ }, filterBySearchInput: function (filterText) { + this.resetCollection(null, false); var pattern = '.*' + filterText.toLowerCase() + '.*'; this.search = { title: pattern, description: pattern }; this.fetchCollection(); @@ -145,6 +150,7 @@ define(function(require){ }, filterByTags: function(tags) { + this.resetCollection(null, false); this.tags = _.pluck(tags, 'id'); this.fetchCollection(); }, @@ -154,11 +160,7 @@ define(function(require){ */ onResize: function() { - // we don't want to re-initialise for _every_ resize event - if(this.resizeTimer) { - clearTimeout(this.resizeTimer); - } - this.resizeTimer = setTimeout(_.bind(this.initPaging, this), 250); + this.initPaging(); }, doLazyScroll: function(e) { @@ -171,7 +173,15 @@ define(function(require){ var scrollTriggerAmmount = $('.asset-management-list-item').first().outerHeight()/2; // we're at the bottom, fetch more if (pxRemaining <= scrollTriggerAmmount) this.fetchCollection(); + }, + + remove: function() { + $('.asset-management-assets-container').off('scroll', this._doLazyScroll); + $(window).on('resize', this._onResize); + + OriginView.prototype.remove.apply(this, arguments); } + }, { template: 'assetManagementCollection' }); diff --git a/frontend/src/modules/assetManagement/views/assetManagementNewAssetView.js b/frontend/src/modules/assetManagement/views/assetManagementNewAssetView.js index 73ad9e0520..6f4cd9a3bf 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementNewAssetView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementNewAssetView.js @@ -10,7 +10,7 @@ define(function(require){ className: 'asset-management-new-asset', events: { - 'change .asset-file' : 'onChangeFile', + 'change .asset-file': 'onChangeFile', }, preRender: function() { From db34b77e29bd649d7237e558608f236ed8b6a9a2 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 16:42:53 +0000 Subject: [PATCH 046/111] Rename/fix function --- .../views/assetManagementModalView.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalView.js b/frontend/src/modules/assetManagement/views/assetManagementModalView.js index 6ab909531c..3c043f03e2 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalView.js @@ -23,7 +23,7 @@ define(function(require) { if (this.options.assetType === "Asset:image" && Origin.scaffold.getCurrentModel().get('_component') === 'graphic') { this.setupImageAutofillButton(); } - this.resizeAssetPanels(); + this.resizePanels(); }, setupSubViews: function() { @@ -48,12 +48,10 @@ define(function(require) { new AssetManagementModelAutofillView({modalView: this}); }, - resizeAssetPanels: function() { - var navigationHeight = $('.navigation').outerHeight(); - var windowHeight = $(window).height(); - var actualHeight = windowHeight - (navigationHeight); - this.$('.asset-management-assets-container').height(actualHeight); - this.$('.asset-management-preview-container').height(actualHeight); + resizePanels: function() { + var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); + this.$('.asset-management-assets-container').height(actualHeight); + this.$('.asset-management-preview-container').height(actualHeight); }, onAssetClicked: function(model) { From efdecfc8dbc5940577e4dfc561ce91ea8beff32b Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 16:43:22 +0000 Subject: [PATCH 047/111] Fix issues with whitespace --- .../views/assetManagementModalView.js | 151 ++++++++---------- 1 file changed, 71 insertions(+), 80 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalView.js b/frontend/src/modules/assetManagement/views/assetManagementModalView.js index 3c043f03e2..6239cbffd1 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalView.js @@ -1,83 +1,74 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require) { - - var Backbone = require('backbone'); - var Origin = require('core/origin'); - var AssetModel = require('../models/assetModel'); - var AssetManagementCollectionView = require('./assetManagementCollectionView'); - var AssetManagementPreviewView = require('./assetManagementPreviewView'); - var AssetManagementView = require('./assetManagementView'); - var AssetManagementModalFiltersView = require('./assetManagementModalFiltersView'); - var AssetManagementModelAutofillView = require('./assetManagementModalAutofillView'); - - var AssetManagementModalView = AssetManagementView.extend({ - - preRender: function(options) { - this.options = options; - AssetManagementView.prototype.preRender.apply(this, arguments); - }, - - postRender: function() { - this.setupSubViews(); - this.setupFilterAndSearchView(); - if (this.options.assetType === "Asset:image" && Origin.scaffold.getCurrentModel().get('_component') === 'graphic') { - this.setupImageAutofillButton(); - } - this.resizePanels(); - }, - - setupSubViews: function() { - this.search = {}; - // Replace Asset and : so we can have both filtered and all asset types - var assetType = this.options.assetType.replace('Asset', '').replace(':', ''); - - if (assetType) { - var filters = [assetType]; - this.search.assetType = { $in: filters }; - } - - // Push collection through to collection view - this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({collection: this.collection, search: this.search}).$el); - }, - - setupFilterAndSearchView: function() { - new AssetManagementModalFiltersView(this.options); - }, - - setupImageAutofillButton: function() { - new AssetManagementModelAutofillView({modalView: this}); - }, - - resizePanels: function() { - var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); - this.$('.asset-management-assets-container').height(actualHeight); - this.$('.asset-management-preview-container').height(actualHeight); - }, - - onAssetClicked: function(model) { - this.$('.asset-management-no-preview').hide(); - this.$('.asset-management-preview-container-inner').html(new AssetManagementPreviewView({ - model: model - }).$el); - - var filename = model.get('filename'); - var selectedFileAlias = 'course/assets/' + filename; - var assetId = model.get('_id'); - var assetObject = { - assetLink: selectedFileAlias, - assetId: assetId, - assetFilename: filename - } - this.data = assetObject; - Origin.trigger('modal:assetSelected', assetObject); - }, - - getData: function() { - return this.data; - } - - }); - - return AssetManagementModalView; - + var Origin = require('core/origin'); + var AssetManagementCollectionView = require('./assetManagementCollectionView'); + var AssetManagementPreviewView = require('./assetManagementPreviewView'); + var AssetManagementView = require('./assetManagementView'); + var AssetManagementModalFiltersView = require('./assetManagementModalFiltersView'); + var AssetManagementModelAutofillView = require('./assetManagementModalAutofillView'); + + var AssetManagementModalView = AssetManagementView.extend({ + preRender: function(options) { + this.options = options; + AssetManagementView.prototype.preRender.apply(this, arguments); + }, + + postRender: function() { + this.setupSubViews(); + this.setupFilterAndSearchView(); + if (this.options.assetType === "Asset:image" && Origin.scaffold.getCurrentModel().get('_component') === 'graphic') { + this.setupImageAutofillButton(); + } + this.resizePanels(); + }, + + setupSubViews: function() { + this.search = {}; + // Replace Asset and : so we can have both filtered and all asset types + var assetType = this.options.assetType.replace('Asset', '').replace(':', ''); + + if (assetType) { + var filters = [assetType]; + this.search.assetType = { $in: filters }; + } + // Push collection through to collection view + this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({collection: this.collection, search: this.search}).$el); + }, + + setupFilterAndSearchView: function() { + new AssetManagementModalFiltersView(this.options); + }, + + setupImageAutofillButton: function() { + new AssetManagementModelAutofillView({ modalView: this }); + }, + + resizePanels: function() { + var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); + this.$('.asset-management-assets-container').height(actualHeight); + this.$('.asset-management-preview-container').height(actualHeight); + }, + + onAssetClicked: function(model) { + this.$('.asset-management-no-preview').hide(); + + var previewView = new AssetManagementPreviewView({ model: model }); + this.$('.asset-management-preview-container-inner').html(previewView.$el); + + var filename = model.get('filename'); + var assetObject = { + assetLink: 'course/assets/' + filename, + assetId: model.get('_id'), + assetFilename: filename + }; + this.data = assetObject; + Origin.trigger('modal:assetSelected', assetObject); + }, + + getData: function() { + return this.data; + } + }); + + return AssetManagementModalView; }); From 2f3a0d838210d69637bc55a4c40c037f0d986f90 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 16:56:54 +0000 Subject: [PATCH 048/111] Fix asset collection page size calculations --- .../views/assetManagementCollectionView.js | 6 +++--- .../views/assetManagementModalView.js | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js index a0a15ca5e1..1bc5533734 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementCollectionView.js @@ -47,8 +47,8 @@ define(function(require){ initPaging: function() { this.resetCollection(_.bind(function(collection) { - var containerHeight = $(window).height() - this.$el.offset().top; - var containerWidth = this.$el.width(); + var containerHeight = $('.asset-management-assets-container').outerHeight(); + var containerWidth = $('.asset-management-assets-container').outerWidth(); var itemHeight = $('.asset-management-list-item').outerHeight(true); var itemWidth = $('.asset-management-list-item').outerWidth(true); var columns = Math.floor(containerWidth/itemWidth); @@ -178,7 +178,7 @@ define(function(require){ remove: function() { $('.asset-management-assets-container').off('scroll', this._doLazyScroll); $(window).on('resize', this._onResize); - + OriginView.prototype.remove.apply(this, arguments); } diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalView.js b/frontend/src/modules/assetManagement/views/assetManagementModalView.js index 6239cbffd1..6885f17775 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalView.js @@ -32,7 +32,11 @@ define(function(require) { this.search.assetType = { $in: filters }; } // Push collection through to collection view - this.$('.asset-management-assets-container-inner').append(new AssetManagementCollectionView({collection: this.collection, search: this.search}).$el); + var view = new AssetManagementCollectionView({ + collection: this.collection, + search: this.search + }); + this.$('.asset-management-assets-container-inner').append(view.$el); }, setupFilterAndSearchView: function() { @@ -45,8 +49,13 @@ define(function(require) { resizePanels: function() { var actualHeight = $(window).height() - $('.modal-popup-toolbar').outerHeight(); - this.$('.asset-management-assets-container').height(actualHeight); - this.$('.asset-management-preview-container').height(actualHeight); + var windowWidth = $(window).width(); + this.$('.asset-management-assets-container') + .width(windowWidth*0.75) + .height(actualHeight); + this.$('.asset-management-preview-container') + .width(windowWidth*0.25) + .height(actualHeight); }, onAssetClicked: function(model) { From d9210f7b0825188659a16d985a66671686102469 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 17:47:44 +0000 Subject: [PATCH 049/111] Hide modal scrollbar for asset management --- frontend/src/modules/modal/less/modal.less | 2 +- frontend/src/modules/modal/views/modalView.js | 4 ++++ frontend/src/modules/scaffold/views/scaffoldAssetView.js | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/modules/modal/less/modal.less b/frontend/src/modules/modal/less/modal.less index 0e485c3a72..18c1dd0afe 100644 --- a/frontend/src/modules/modal/less/modal.less +++ b/frontend/src/modules/modal/less/modal.less @@ -31,10 +31,10 @@ width:100%; height:100%; z-index:4000; + overflow-y: scroll; .modal-popup { background-color: @ui-content-color; height: 100%; - overflow-y: scroll; } i { diff --git a/frontend/src/modules/modal/views/modalView.js b/frontend/src/modules/modal/views/modalView.js index 0d8683dca8..0684ec4288 100644 --- a/frontend/src/modules/modal/views/modalView.js +++ b/frontend/src/modules/modal/views/modalView.js @@ -16,6 +16,7 @@ define(function(require) { var defaults = { _shouldShowCancelButton: true, _shouldShowDoneButton: true, + _shouldShowScrollbar: true, _shouldDisableCancelButton: false, _shouldDisableDoneButton: false } @@ -48,6 +49,9 @@ define(function(require) { if (this.options._shouldDisableDoneButton) { this.onDoneButtonDisabled(); } + if(this.options._shouldShowScrollbar === false) { + this.$el.css('overflow-y', 'hidden'); + } this.modalView = new this.view(this.options); this.$('.modal-popup-content-inner').append(this.modalView.$el); $('html').addClass('no-scroll'); diff --git a/frontend/src/modules/scaffold/views/scaffoldAssetView.js b/frontend/src/modules/scaffold/views/scaffoldAssetView.js index 79aa971020..be0adaeee6 100644 --- a/frontend/src/modules/scaffold/views/scaffoldAssetView.js +++ b/frontend/src/modules/scaffold/views/scaffoldAssetView.js @@ -143,6 +143,7 @@ define(function(require) { Origin.trigger('modal:open', AssetManagementModalView, { collection: new AssetCollection, assetType: this.schema.fieldType, + _shouldShowScrollbar: false, onUpdate: function(data) { if (data) { From db14fd90900f4609caf822eb0f2279aa4d4e5e10 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 17:48:11 +0000 Subject: [PATCH 050/111] Remove duplicate listenTo calls --- frontend/src/modules/modal/views/modalView.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/modal/views/modalView.js b/frontend/src/modules/modal/views/modalView.js index 0684ec4288..ed6a83054e 100644 --- a/frontend/src/modules/modal/views/modalView.js +++ b/frontend/src/modules/modal/views/modalView.js @@ -23,13 +23,17 @@ define(function(require) { this.view = options.view; this.options = _.extend(defaults, options.options); this.context = options.context; - this.listenTo(Origin, 'remove:views', this.remove); - this.listenTo(Origin, 'modal:onCancel', this.onCloseButtonClicked); - this.listenTo(Origin, 'modal:onUpdate', this.onDoneButtonClicked); - this.listenTo(Origin, 'modal:disableCancelButton', this.onCloseButtonDisabled); - this.listenTo(Origin, 'modal:disableDoneButton', this.onDoneButtonDisabled); - this.listenTo(Origin, 'modal:enableCancelButton', this.onCloseButtonEnabled); - this.listenTo(Origin, 'modal:enableDoneButton', this.onDoneButtonEnabled); + + this.listenTo(Origin, { + 'remove:views': this.remove, + 'modal:onCancel': this.onCloseButtonClicked, + 'modal:onUpdate': this.onDoneButtonClicked, + 'modal:disableCancelButton': this.onCloseButtonDisabled, + 'modal:disableDoneButton': this.onDoneButtonDisabled, + 'modal:enableCancelButton': this.onCloseButtonEnabled, + 'modal:enableDoneButton': this.onDoneButtonEnabled + }); + this.render(); }, From 4be4b3a6cf54be626c1a4997e7f3cec187e13f13 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 11 Dec 2017 17:48:23 +0000 Subject: [PATCH 051/111] Fix whitespace --- frontend/src/modules/modal/views/modalView.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/src/modules/modal/views/modalView.js b/frontend/src/modules/modal/views/modalView.js index ed6a83054e..a5106b1cc5 100644 --- a/frontend/src/modules/modal/views/modalView.js +++ b/frontend/src/modules/modal/views/modalView.js @@ -1,10 +1,8 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require) { - var Origin = require('core/origin'); var ModalView = Backbone.View.extend({ - className: 'modal', events: { @@ -13,15 +11,14 @@ define(function(require) { }, initialize: function(options) { - var defaults = { + this.view = options.view; + this.options = _.extend({ _shouldShowCancelButton: true, _shouldShowDoneButton: true, _shouldShowScrollbar: true, _shouldDisableCancelButton: false, _shouldDisableDoneButton: false - } - this.view = options.view; - this.options = _.extend(defaults, options.options); + }, options.options); this.context = options.context; this.listenTo(Origin, { @@ -42,7 +39,7 @@ define(function(require) { var template = Handlebars.templates['modal']; this.$el.html(template(data)).appendTo('body'); _.defer(_.bind(this.postRender, this)); - + return this; }, From b7d8f786113d96560a858f22a0bbba8f3fb3c5bc Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Wed, 13 Dec 2017 14:21:37 +0000 Subject: [PATCH 052/111] Editor performance improvements (#1798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../src/core/collections/contentCollection.js | 35 + frontend/src/core/helpers.js | 204 +++--- frontend/src/core/models/articleModel.js | 6 +- frontend/src/core/models/blockModel.js | 6 +- frontend/src/core/models/componentModel.js | 4 +- .../src/core/models/componentTypeModel.js | 2 +- frontend/src/core/models/contentModel.js | 145 ++-- .../src/core/models/contentObjectModel.js | 6 +- frontend/src/core/models/courseModel.js | 2 +- .../views/assetManagementModalNewAssetView.js | 1 - .../article/views/editorArticleEditView.js | 1 - .../editor/block/views/editorBlockEditView.js | 1 - .../views/editorComponentEditSidebarView.js | 11 +- .../views/editorComponentEditView.js | 1 - .../src/modules/editor/contentObject/index.js | 45 +- .../contentObject/views/editorMenuItemView.js | 117 +--- .../views/editorMenuLayerView.js | 102 ++- .../contentObject/views/editorMenuView.js | 196 ++++-- .../views/editorPageArticleView.js | 114 ++-- .../views/editorPageBlockView.js | 182 +++-- .../views/editorPageComponentListItemView.js | 37 +- .../views/editorPageComponentPasteZoneView.js | 3 - .../views/editorPageComponentView.js | 165 ++--- .../contentObject/views/editorPageView.js | 129 ++-- frontend/src/modules/editor/course/index.js | 19 +- .../course/views/editorCourseEditView.js | 28 +- .../views/editorExtensionsEditView.js | 2 - .../modules/editor/global/editorDataLoader.js | 30 +- .../modules/editor/global/less/editor.less | 22 +- .../editor/global/views/editorOriginView.js | 13 +- .../global/views/editorPasteZoneView.js | 18 +- .../modules/editor/global/views/editorView.js | 316 ++++----- .../scaffold/templates/scaffoldAsset.hbs | 130 ++-- .../scaffold/views/scaffoldAssetView.js | 641 +++++++++--------- lib/contentmanager.js | 119 ++-- plugins/content/clipboard/model.schema | 8 +- routes/lang/en-application.json | 6 +- 37 files changed, 1353 insertions(+), 1514 deletions(-) create mode 100644 frontend/src/core/collections/contentCollection.js diff --git a/frontend/src/core/collections/contentCollection.js b/frontend/src/core/collections/contentCollection.js new file mode 100644 index 0000000000..4aec220c22 --- /dev/null +++ b/frontend/src/core/collections/contentCollection.js @@ -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; +}); diff --git a/frontend/src/core/helpers.js b/frontend/src/core/helpers.js index b41b7bad8b..ec882aafb3 100644 --- a/frontend/src/core/helpers.js +++ b/frontend/src/core/helpers.js @@ -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); @@ -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); @@ -297,68 +267,38 @@ 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 += "
    • " + alerts[i] + "
    • "; + if(alerts.length > 0) { + for(var i = 0, len = alerts.length; i < len; i++) { + errorMessage += "
    • " + alerts[i] + "
    • "; + } + 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)) { @@ -366,6 +306,100 @@ define(function(require){ } 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); + }); } }; diff --git a/frontend/src/core/models/articleModel.js b/frontend/src/core/models/articleModel.js index e3ec105ccd..854d5d62aa 100644 --- a/frontend/src/core/models/articleModel.js +++ b/frontend/src/core/models/articleModel.js @@ -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; diff --git a/frontend/src/core/models/blockModel.js b/frontend/src/core/models/blockModel.js index a3d9425b9b..8e20ddc836 100644 --- a/frontend/src/core/models/blockModel.js +++ b/frontend/src/core/models/blockModel.js @@ -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, diff --git a/frontend/src/core/models/componentModel.js b/frontend/src/core/models/componentModel.js index 8ab83cedf9..679b8479ca 100644 --- a/frontend/src/core/models/componentModel.js +++ b/frontend/src/core/models/componentModel.js @@ -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: [ diff --git a/frontend/src/core/models/componentTypeModel.js b/frontend/src/core/models/componentTypeModel.js index 4621605a66..29ffdf6af7 100644 --- a/frontend/src/core/models/componentTypeModel.js +++ b/frontend/src/core/models/componentTypeModel.js @@ -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'); diff --git a/frontend/src/core/models/contentModel.js b/frontend/src/core/models/contentModel.js index 5eee95b80b..59f81fe23e 100644 --- a/frontend/src/core/models/contentModel.js +++ b/frontend/src/core/models/contentModel.js @@ -2,6 +2,8 @@ define(function(require) { var Backbone = require('backbone'); var Origin = require('core/origin'); + var Helpers = require('core/helpers'); + var ContentCollection = require('core/collections/contentCollection'); var ContentModel = Backbone.Model.extend({ idAttribute: '_id', @@ -10,91 +12,77 @@ define(function(require) { initialize: function(options) { this.on('sync', this.loadedData, this); this.on('change', this.loadedData, this); - this.fetch(); }, loadedData: function() { - if (this._siblings) { - this._type = this._siblings; - } - Origin.trigger('editorModel:dataLoaded', this._type, this.get('_id')); + if(this._siblingTypes) this._type = this._siblingTypes; }, - getChildren: function() { - var self = this; - var getChildrenDelegate = function(type) { - if (Origin.editor.data[type]) { - var children = Origin.editor.data[type].where({ _parentId: self.get('_id') }); - var childrenCollection = new Backbone.Collection(children); - return childrenCollection; - } - return null; - }; - if(_.isArray(this._children)) { - var allChildren; - for(var i = 0, count = this._children.length; i < count; i++) { - var children = getChildrenDelegate(this._children[i]); - if(children) { - if(!allChildren) allChildren = children; - else allChildren.add(children.models); + fetchChildren: function(callback) { + var childTypes = _.isArray(this._childTypes) ? this._childTypes : [this._childTypes]; + // has to be a plain old array because we may have multiple model types + var children = []; + Helpers.forParallelAsync(childTypes, _.bind(function(childType, index, done) { + (new ContentCollection(null, { + _type: childType, + _parentId: this.get('_id') + })).fetch({ + success: function(collection) { + children = children.concat(collection.models); + done(); + }, + error: function(collecion, response) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); } - } - return allChildren; - } else { - return getChildrenDelegate(this._children); - } + }); + }, this), function() { + callback(children); + }); }, - getParent: function() { - var currentType = this.get('_type'); - var parent; - var currentParentId = this.get('_parentId'); - - if (currentType === 'menu' || currentType === 'page') { - if (currentParentId === Origin.editor.data.course.get('_id')) { - parent = Origin.editor.data.course; - } else { - parent = Origin.editor.data.contentObjects.findWhere({ _id: currentParentId }); - } - } else if (currentType != 'course'){ - parent = Origin.editor.data[this._parent].findWhere({ _id: currentParentId }); + fetchParent: function(callback) { + if(!this._parentType || !this.get('_parentId')) { + return callback(); } - - return parent; - }, - - getSiblings: function(returnMyself) { - if (returnMyself) { - var siblings = Origin.editor.data[this._siblings].where({ _parentId: this.get('_parentId') }); - return new Backbone.Collection(siblings); + if(this.get('_parentId') === Origin.editor.data.course.get('_id')) { + return callback(Origin.editor.data.course); } - var siblings = _.reject(Origin.editor.data[this._siblings].where({ - _parentId: this.get('_parentId') - }), _.bind(function(model){ - return model.get('_id') == this.get('_id'); - }, this)); - - return new Backbone.Collection(siblings); - }, - - setOnChildren: function(key, value, options) { - var args = arguments; - - if(!this._children) return; - - this.getChildren().each(function(child){ - child.setOnChildren.apply(child, args); + // create model instance using _parentType and _parentId + var modelClass = Helpers.contentModelMap(this._parentType); + var model = new modelClass({ _id: this.get('_parentId') }); + model.fetch({ + success: _.bind(callback, this), + error: _.bind(function(jqXHR) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + callback.call(this); + }, this) }); }, - getPossibleAncestors: function() { - var map = { - 'contentObjects': { 'ancestorType': 'page' }, - 'articles': { 'ancestorType': 'article' }, - 'blocks': { 'ancestorType': 'block' } - }; - ancestors = Origin.editor.data[this._parent].where({ _type: map[this._parent].ancestorType }); - return new Backbone.Collection(ancestors); + fetchSiblings: function(callback) { + var siblings = new ContentCollection(null, { + _type: this._siblingTypes, + _parentId: this.get('_parentId') + }); + siblings.fetch({ + success: _.bind(function(collection) { + collection.remove(this); + callback(collection); + }, this), + error: _.bind(function(jqXHR) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + callback.call(this); + }, this) + }); }, serialize: function() { @@ -112,19 +100,6 @@ define(function(require) { } }); } - }, - - serializeChildren: function() { - var children = this.getChildren(); - var serializedJson = ''; - - if (children) { - _.each(children.models, function(child) { - serializedJson += child.serialize(); - }); - } - - return serializedJson; } }); diff --git a/frontend/src/core/models/contentObjectModel.js b/frontend/src/core/models/contentObjectModel.js index 1ba306df62..6d15570995 100644 --- a/frontend/src/core/models/contentObjectModel.js +++ b/frontend/src/core/models/contentObjectModel.js @@ -5,9 +5,9 @@ define(function(require) { var ContentObjectModel = ContentModel.extend({ urlRoot: '/api/content/contentobject', - _parent: 'contentObjects', - _siblings: 'contentObjects', - _children: ['contentObjects', 'articles'], + _parentType: 'contentobject', + _siblingTypes: 'contentobject', + _childTypes: ['contentobject', 'article'], defaults: { _isSelected: false, diff --git a/frontend/src/core/models/courseModel.js b/frontend/src/core/models/courseModel.js index 9da53ed0e0..5544639c65 100644 --- a/frontend/src/core/models/courseModel.js +++ b/frontend/src/core/models/courseModel.js @@ -7,7 +7,7 @@ define(function(require) { var CourseModel = ContentModel.extend({ urlRoot: '/api/content/course', _type: 'course', - _children: 'contentObjects', + _childTypes: 'contentobject', getHeroImageURI: function () { if(Helpers.isAssetExternal(this.get('heroImage'))) { diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js b/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js index dac2364e8a..29b3b0d043 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalNewAssetView.js @@ -54,7 +54,6 @@ define(function(require){ var $uploadFileErrormsg = $uploadFile.prev('label').find('span.error'); $.each(this.$('.required'), function (index, el) { - console.log(el.val, el); var $errormsg = $(el).prev('label').find('span.error'); if (!$.trim($(el).val())) { validated = false; diff --git a/frontend/src/modules/editor/article/views/editorArticleEditView.js b/frontend/src/modules/editor/article/views/editorArticleEditView.js index c28cd1738e..9d05bbd3af 100644 --- a/frontend/src/modules/editor/article/views/editorArticleEditView.js +++ b/frontend/src/modules/editor/article/views/editorArticleEditView.js @@ -10,7 +10,6 @@ define(function(require) { preRender: function() { this.listenTo(Origin, 'editorArticleEditSidebar:views:save', this.save); - this.model.set('ancestors', this.model.getPossibleAncestors().toJSON()); } }, { template: 'editorArticleEdit' diff --git a/frontend/src/modules/editor/block/views/editorBlockEditView.js b/frontend/src/modules/editor/block/views/editorBlockEditView.js index 422071b3a7..cb089ff51a 100644 --- a/frontend/src/modules/editor/block/views/editorBlockEditView.js +++ b/frontend/src/modules/editor/block/views/editorBlockEditView.js @@ -10,7 +10,6 @@ define(function(require) { preRender: function() { this.listenTo(Origin, 'editorBlockEditSidebar:views:save', this.save); - this.model.set('ancestors', this.model.getPossibleAncestors().toJSON()); } }, { template: 'editorBlockEdit' diff --git a/frontend/src/modules/editor/component/views/editorComponentEditSidebarView.js b/frontend/src/modules/editor/component/views/editorComponentEditSidebarView.js index f2f4478cc9..2c9deb7e70 100644 --- a/frontend/src/modules/editor/component/views/editorComponentEditSidebarView.js +++ b/frontend/src/modules/editor/component/views/editorComponentEditSidebarView.js @@ -18,9 +18,14 @@ define(function(require) { cancelEditing: function(event) { event.preventDefault(); - var currentCourseId = Origin.editor.data.course.get('_id'); - var currentPageId = this.model.getParent().getParent().getParent().get('_id'); - Origin.router.navigateTo('editor/' + currentCourseId + '/page/' + currentPageId); + // FIXME got to be a better way to do this + this.model.fetchParent(function(parentBlock) { + parentBlock.fetchParent(function(parentArticle) { + parentArticle.fetchParent(function(parentPage) { + Origin.router.navigateTo('editor/' + Origin.editor.data.course.get('_id') + '/page/' + parentPage.get('_id')); + }); + }); + }); } }, { template: 'editorComponentEditSidebar' diff --git a/frontend/src/modules/editor/component/views/editorComponentEditView.js b/frontend/src/modules/editor/component/views/editorComponentEditView.js index 42c6954d1c..c25be986a4 100644 --- a/frontend/src/modules/editor/component/views/editorComponentEditView.js +++ b/frontend/src/modules/editor/component/views/editorComponentEditView.js @@ -10,7 +10,6 @@ define(function(require) { preRender: function() { this.listenTo(Origin, 'editorComponentEditSidebar:views:save', this.save); - this.model.set('ancestors', this.model.getPossibleAncestors().toJSON()); }, cancel: function (event) { diff --git a/frontend/src/modules/editor/contentObject/index.js b/frontend/src/modules/editor/contentObject/index.js index 3777705d02..64f4c9c7dd 100644 --- a/frontend/src/modules/editor/contentObject/index.js +++ b/frontend/src/modules/editor/contentObject/index.js @@ -4,10 +4,8 @@ define(function(require) { * This module handles both sections/menus and pages. */ var Origin = require('core/origin'); - var ContentObjectModel = require('core/models/contentObjectModel'); var EditorMenuSidebarView = require('./views/editorMenuSidebarView'); - var EditorPageComponentListView = require('./views/editorPageComponentListView'); var EditorPageEditView = require('./views/editorPageEditView'); var EditorPageEditSidebarView = require('./views/editorPageEditSidebarView'); var EditorPageSidebarView = require('./views/editorPageSidebarView'); @@ -15,37 +13,27 @@ define(function(require) { var Helpers = require('../global/helpers'); Origin.on('editor:contentObject', function(data) { - if(data.action === 'edit') renderContentObjectEdit(data); - else if(data.id) renderPageStructure(data); - else renderMenuStructure(data); - }); - - // component add is just a page overlay view, so handling it here - Origin.on('editor:block', function(data) { - if(data.action !== 'add') { - return; + var route = function() { + if(data.action === 'edit') renderContentObjectEdit(data); + else if(data.id) renderPageStructure(data); + else renderMenuStructure(data); + } + if(!data.id) { + return route(); } - var containingBlock = Origin.editor.data.blocks.findWhere({ _id: Origin.location.route3 }); - var layoutOptions = containingBlock.get('layoutOptions'); - var componentsModel = new Backbone.Model({ - title: Origin.l10n.t('app.addcomponent'), - body: Origin.l10n.t('app.pleaseselectcomponent'), - _parentId: Origin.location.route3, - componentTypes: Origin.editor.data.componenttypes.toJSON(), - layoutOptions: layoutOptions - }); - Origin.contentPane.setView(EditorPageComponentListView, { model: componentsModel }); - }); - - function renderContentObjectEdit(data) { (new ContentObjectModel({ _id: data.id })).fetch({ success: function(model) { - Helpers.setPageTitle(model, true); - var form = Origin.scaffold.buildForm({ model: model }); - Origin.sidebar.addView(new EditorPageEditSidebarView({ form: form }).$el); - Origin.contentPane.setView(EditorPageEditView, { model: model, form: form }); + data.model = model; + route(); } }); + }); + + function renderContentObjectEdit(data) { + Helpers.setPageTitle(data.model, true); + var form = Origin.scaffold.buildForm({ model: data.model }); + Origin.sidebar.addView(new EditorPageEditSidebarView({ form: form }).$el); + Origin.contentPane.setView(EditorPageEditView, { model: data.model, form: form }); } function renderPageStructure(data) { @@ -65,7 +53,6 @@ define(function(require) { function renderMenuStructure(data) { Origin.trigger('location:title:update', { title: 'Menu editor' }); - Origin.editor.currentContentObjectId = data.id; Origin.editor.scrollTo = 0; Origin.sidebar.addView(new EditorMenuSidebarView().$el, { diff --git a/frontend/src/modules/editor/contentObject/views/editorMenuItemView.js b/frontend/src/modules/editor/contentObject/views/editorMenuItemView.js index adeafc4414..f5336d90ab 100644 --- a/frontend/src/modules/editor/contentObject/views/editorMenuItemView.js +++ b/frontend/src/modules/editor/contentObject/views/editorMenuItemView.js @@ -24,11 +24,6 @@ define(function(require){ postRender: function() { this.setupEvents(); - // Check if the current item is expanded and update the next menuLayerView - // This can end up being recursive if an item is selected inside a few menu items - if(this.model.get('_isExpanded')) { - Origin.trigger('editorView:menuView:updateSelectedItem', this); - } }, remove: function() { @@ -39,31 +34,23 @@ define(function(require){ setupEvents: function() { this.listenTo(Origin, 'editorView:removeSubViews', this.remove); - this.listenTo(this.model, { - 'change:_isExpanded': this.onExpandedChange, - 'change:_isSelected': this.onSelectedChange - }); - // Handle the context menu clicks - this.on('contextMenu:' + this.model.get('_type') + ':edit', this.editMenuItem); - this.on('contextMenu:' + this.model.get('_type') + ':copy', this.copyMenuItem); - this.on('contextMenu:' + this.model.get('_type') + ':copyID', this.copyID); - this.on('contextMenu:' + this.model.get('_type') + ':delete', this.deleteItemPrompt); + var type = this.model.get('_type'); + + this.on('contextMenu:' + type + ':edit', this.editMenuItem); + this.on('contextMenu:' + type + ':copy', this.copyMenuItem); + this.on('contextMenu:' + type + ':copyID', this.copyID); + this.on('contextMenu:' + type + ':delete', this.deleteItemPrompt); this.$el.closest('.editor-menu').on('mousemove', _.bind(this.handleDrag, this)); }, setupClasses: function() { - var classString = ''; - if (this.model.get('_isSelected')) classString += 'selected '; - if(this.model.get('_isExpanded')) classString += 'expanded '; - classString += ('content-type-'+this.model.get('_type')); - this.$el.addClass(classString); + this.$el.addClass('content-type-' + this.model.get('_type')); }, onMenuItemClicked: function(event) { event && event.preventDefault(); - // select item regardless of single/double click - this.setItemAsSelected(); + this.trigger('click', this); // handle double-click if(this.clickTimerActive) { return this.onMenuItemDoubleClicked(event); @@ -78,75 +65,7 @@ define(function(require){ onMenuItemDoubleClicked: function(event) { event && event.preventDefault(); - var type = this.model.get('_type'); - if(type === 'page') { - this.gotoPageEditor(); - } - else if(type === 'menu') { - this.gotoSubMenuEditor(); - } - }, - - gotoPageEditor: function() { - Origin.router.navigateTo('editor/' + Origin.editor.data.course.get('_id') + '/page/' + this.model.get('_id')); - }, - - gotoSubMenuEditor: function() { - Origin.router.navigateTo('editor/' + Origin.editor.data.course.get('_id') + '/menu/' + this.model.get('_id') + '/edit'); - }, - - setItemAsSelected: function() { - if(this.model.get('_isSelected')) { - return; - } - if(this.model.get('_isExpanded')) { - // bit odd, but we need to remove and child views before we continue - this.model.set('_isExpanded', false); - } - else { - this.setSiblingsSelectedState(); - this.setParentSelectedState(); - } - this.model.set({ - _isExpanded: this.model.get('_type') === 'menu', - _isSelected: true - }); - // This event passes out the view to the editorMenuView to add - // a editorMenuLayerView and setup this.subView - Origin.trigger('editorView:menuView:updateSelectedItem', this); - }, - - setParentSelectedState: function() { - this.model.getParent().set('_isSelected', false); - }, - - setSiblingsSelectedState: function() { - this.model.getSiblings().each(function(sibling) { - sibling.set({ _isSelected: false, _isExpanded: false }); - }); - }, - - setChildrenSelectedState: function() { - this.model.getChildren().each(function(child) { - child.set({ _isSelected: false, _isExpanded: false }); - }) - }, - - onSelectedChange: function(model, isSelected) { - this.$el.toggleClass('selected', isSelected); - }, - - onExpandedChange: function(model, isExpanded) { - var isMenuType = (this.model.get('_type') === 'menu'); - if(isExpanded) { - this.$el.addClass('expanded'); - return; - } - if(isMenuType) { - this.setChildrenSelectedState(); - if (this.subView) this.subView.remove(); - } - this.$el.removeClass('expanded'); + this.trigger('dblclick', this); }, editMenuItem: function() { @@ -193,10 +112,6 @@ define(function(require){ deleteItem: function(event) { this.stopListening(Origin, 'editorView:cancelRemoveItem:'+ this.model.get('_id'), this.cancelDeleteItem); - this.model.set({ _isExpanded: false, _isSelected: false }); - // When deleting an item - the parent needs to be selected - this.model.getParent().set({ _isSelected: true, _isExpanded: true }); - // We also need to navigate to the parent element - but if it's the courseId let's // navigate up to the menu var type = this.model.get('_type'); @@ -204,12 +119,22 @@ define(function(require){ var parentId = isTopLevel ? '' : '/' + this.model.get('_parentId'); Origin.router.navigateTo('editor/' + Origin.editor.data.course.id + '/menu' + parentId); - if(this.model.destroy()) this.remove(); + this.model.destroy({ + success: _.bind(function(model) { + Origin.trigger('editorView:itemDeleted', model); + this.remove() + }, this), + error: function() { + Origin.Notify.alert({ + type: 'error', + text: 'app.errordelete' + }); + } + }); }, cancelDeleteItem: function() { this.stopListening(Origin, 'editorView:removeItem:'+ this.model.get('_id'), this.deleteItem); - this.model.set({ _isSelected: true }); }, enableDrag: function(event) { diff --git a/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js b/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js index e4d3cf7413..fa1e1aff84 100644 --- a/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js +++ b/frontend/src/modules/editor/contentObject/views/editorMenuLayerView.js @@ -9,37 +9,55 @@ define(function(require) { var EditorMenuLayerView = EditorOriginView.extend({ className: 'editor-menu-layer', + models: undefined, events: { - 'click button.editor-menu-layer-add-page': 'addPage', - 'click button.editor-menu-layer-add-menu': 'addMenu', + 'click button.editor-menu-layer-add-page': 'addNewPage', + 'click button.editor-menu-layer-add-menu': 'addNewMenu', 'click .editor-menu-layer-paste': 'pasteMenuItem', 'click .editor-menu-layer-paste-cancel': 'cancelPasteMenuItem' }, - preRender: function(options) { - if(options._parentId) this._parentId = options._parentId; + initialize: function(options) { + this.models = options.models; + EditorOriginView.prototype.initialize.apply(this, arguments); + }, - this.listenTo(Origin, { + preRender: function(options) { + if(options._parentId) { + this._parentId = options._parentId; + } + var events = { 'editorView:removeSubViews': this.remove, 'editorMenuView:removeMenuViews': this.remove - }); - }, - - postRender: function() { - // Append the parentId value to the container to allow us to move pages, etc. - if(this._parentId) this.$el.attr('data-parentId', this._parentId); - this.setHeight(); + }; + events['editorView:pasted:' + this._parentId] = this.onPaste; + this.listenTo(Origin, events); }, render: function() { var data = this.data ? this.data : false; var template = Handlebars.templates[this.constructor.template]; + this.$el.html(template(data)); + this.renderMenuItems(); + _.defer(_.bind(this.postRender, this)); return this; }, + renderMenuItems: function() { + for(var i = 0, count = this.models.length; i < count; i++) { + this.addMenuItemView(this.models[i]); + } + }, + + postRender: function() { + // Append the parentId value to the container to allow us to move pages, etc. + if(this._parentId) this.$el.attr('data-parentid', this._parentId); + this.setHeight(); + }, + setHeight: function() { var windowHeight = $(window).height(); var offsetTop = $('.editor-menu-inner').offset().top; @@ -48,19 +66,19 @@ define(function(require) { this.$('.editor-menu-layer-inner').height(windowHeight-(offsetTop+controlsHeight)); }, - addMenu: function(event) { - this.addMenuItem(event, 'menu'); + addNewMenu: function(event) { + this.addNewMenuItem(event, 'menu'); }, - addPage: function(event) { - this.addMenuItem(event, 'page'); + addNewPage: function(event) { + this.addNewMenuItem(event, 'page'); }, /** * Adds a new contentObject of a given type * @param {String} type Given contentObject type, i.e. 'menu' or 'page' */ - addMenuItem: function(event, type) { + addNewMenuItem: function(event, type) { event && event.preventDefault(); var newMenuItemModel = new ContentObjectModel({ @@ -75,6 +93,8 @@ define(function(require) { }); // Instantly add the view for UI purposes var newMenuItemView = this.addMenuItemView(newMenuItemModel); + newMenuItemView.$el.addClass('syncing'); + newMenuItemModel.save(null, { error: function(error) { // fade out menu item and alert @@ -86,7 +106,7 @@ define(function(require) { _.delay(newMenuItemView.remove, 3000); }, success: _.bind(function(model) { - Origin.editor.data.contentObjects.add(model); + Origin.trigger('editorView:menuView:addItem', model); // Force setting the data-id attribute as this is required for drag-drop sorting newMenuItemView.$el.children('.editor-menu-item-inner').attr('data-id', model.get('_id')); if (type === 'page') { @@ -94,7 +114,7 @@ define(function(require) { this.addNewPageArticleAndBlock(model, newMenuItemView); return; } - newMenuItemView.$el.removeClass('syncing').addClass('synced'); + newMenuItemView.$el.removeClass('syncing'); this.setHeight(); }, this) }); @@ -135,13 +155,10 @@ define(function(require) { _.delay(newMenuItemView.remove, 3000); }, success: _.bind(function(model, response, options) { - // Add this new element to the collect - Origin.editor.data[model.get('_type') + 's'].add(model); - if (typeToAdd === 'article') { this.addNewPageArticleAndBlock(model, newMenuItemView); } else { - newMenuItemView.$el.removeClass('syncing').addClass('synced'); + newMenuItemView.$el.removeClass('syncing'); } }, this) }); @@ -149,7 +166,13 @@ define(function(require) { addMenuItemView: function(model) { var newMenuItemView = new EditorMenuItemView({ model: model }); - this.$('.editor-menu-layer-inner').append(newMenuItemView.$el.addClass('syncing')); + this.$('.editor-menu-layer-inner').append(newMenuItemView.$el); + + newMenuItemView.on({ + 'click': _.bind(this.onMenuItemClicked, this), + 'dblclick': _.bind(this.onMenuItemDblclicked, this) + }); + return newMenuItemView; }, @@ -167,6 +190,37 @@ define(function(require) { _courseId: Origin.editor.data.course.get('_id') }); Origin.trigger('editorView:pasteCancel', target); + }, + + onMenuItemClicked: function(menuItem) { + // if item's already selected, don't bother continuing + if(menuItem.$el.hasClass('selected')) { + return; + } + Origin.trigger('editorView:menuView:updateSelectedItem', menuItem.model); + }, + + onMenuItemDblclicked: function(menuItem) { + var courseId = Origin.editor.data.course.get('_id'); + var id = menuItem.model.get('_id'); + var type = menuItem.model.get('_type'); + + var route = 'editor/' + courseId + '/' + type + '/' + id; + if(type === 'menu') route += '/edit'; + + Origin.router.navigateTo(route); + }, + + // called after a successful paste + onPaste: function(data) { + (new ContentObjectModel({ _id: data._id})).fetch({ + success: _.bind(function(model) { + this.addMenuItemView(model); + }, this), + error: function() { + + } + }); } }, { template: 'editorMenuLayer' diff --git a/frontend/src/modules/editor/contentObject/views/editorMenuView.js b/frontend/src/modules/editor/contentObject/views/editorMenuView.js index 2e790093e0..215026fc9e 100644 --- a/frontend/src/modules/editor/contentObject/views/editorMenuView.js +++ b/frontend/src/modules/editor/contentObject/views/editorMenuView.js @@ -1,6 +1,8 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ var Origin = require('core/origin'); + var Helpers = require('core/helpers'); + var ContentCollection = require('core/collections/contentCollection'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorMenuLayerView = require('./editorMenuLayerView'); var EditorMenuItemView = require('./editorMenuItemView'); @@ -11,102 +13,131 @@ define(function(require){ preRender: function() { this.listenTo(Origin, { - 'editorView:menuView:updateSelectedItem': this.updateSelectedItem, + 'editorView:menuView:updateSelectedItem': this.onSelectedItemChanged, + 'editorView:menuView:addItem': this.onItemAdded, + 'editorView:itemDeleted': this.onItemDeleted, 'window:resize': this.setupHorizontalScroll }); }, postRender: function() { - this.setupMenuViews(); - _.defer(this.setViewToReady); + this.contentobjects = new ContentCollection(null, { + _type: 'contentobject', + _courseId: Origin.editor.data.course.get('_id') + }); + this.contentobjects.fetch({ + success: _.bind(function(children) { + this.contentobjects = children; + this.renderLayers(); + _.defer(this.setViewToReady); + }, this), + error: console.error + }); }, - setupMenuViews: function() { - this.addMenuLayerView(this); - if (!Origin.editor.currentContentObjectId) { + /** + * Renders all menu layers from the current course to the Origin.editor.currentContentObject + */ + renderLayers: function() { + var selectedModel = Origin.editor.currentContentObject; + // no previous state, so should only render the first level + if(!selectedModel) { + this.renderLayer(Origin.editor.data.course); return; } - this.restoreCurrentMenuState(); + // check if we can reuse any existing layers, and only render the new ones + this.getItemHeirarchy(selectedModel, function(hierarchy) { + var index; + var renderedLayers = this.$('.editor-menu-layer'); + for(var i = 0, count = hierarchy.length; i < count; i++) { + if($(renderedLayers[i]).attr('data-parentid') === hierarchy[i].get('_id')) { + index = i+1; + } + } + // we can reuse layers up to 'index', remove the rest + if(index !== undefined) { + hierarchy = hierarchy.slice(index); + var layersToRemove = renderedLayers.slice(index); + for(var i = 0, count = layersToRemove.length; i < count; i++) { + layersToRemove[i].remove(); + } + } + // all items left in hierarchy are new, render these + Helpers.forSeriesAsync(hierarchy, _.bind(function(model, index, callback) { + this.renderLayer(model, callback); + }, this), _.defer(_.bind(function() { + // called after all layers rendered + this.removeSelectedItemStyling(); + this.addSelectedItemStyling(selectedModel.get('_id')); + this.setUpInteraction(); + }, this))); + }); }, /** - * Recursive function which shows the expanded children for a given context model - * @param {Model} A given contextObject model + * Renders a single menu layer */ - addMenuLayerView: function(view) { - var menuLayer = this.renderMenuLayerView(view); - // Add children views of current model - view.model.getChildren().each(function(contentObject) { - menuLayer.append(new EditorMenuItemView({ model: contentObject }).$el); - }, this); - - _.defer(_.bind(function() { - this.setupDragDrop(); - var $window = $(window); - this.setupHorizontalScroll($window.width(), $window.height()); - this.scrollToElement(); - }, this)); + renderLayer: function(model, callback) { + var menuLayerView = new EditorMenuLayerView({ + _parentId: model.get('_id'), + models: this.contentobjects.where({ _parentId: model.get('_id') }) + }); + $('.editor-menu-inner').append(menuLayerView.$el); + if(typeof callback === 'function') callback(); }, - /** - * Appemds a menu item layer for a given ID to the editor - * @param {String} parentId Unique identifier of the parent - */ - renderMenuLayerView: function(view) { - // Get the current views _id to store as the _parentId - var parentId = view.model.get('_id'); - // Create MenuLayerView - var menuLayerView = new EditorMenuLayerView({ _parentId: parentId }); - // Set subview on layerView so this can be removed - view.subView = menuLayerView; - // Render and append the view - $('.editor-menu-inner').append(menuLayerView.$el); - // Return the container ready to render menuItemView's - return menuLayerView.$('.editor-menu-layer-inner'); + setUpInteraction: function() { + this.setupDragDrop(); + var $window = $(window); + this.setupHorizontalScroll($window.width(), $window.height()); + this.scrollToElement(); }, - /** - * Restores the current menu state by finding the current element - * then setting it's parent recursively to _isExpanded - */ - restoreCurrentMenuState: function() { - // Find current menu item - var currentSelectedMenuItem = Origin.editor.data.contentObjects.findWhere({ - _id: Origin.editor.currentContentObjectId - }); - currentSelectedMenuItem.set({ _isSelected: true, _isExpanded: true }); - this.setParentElementToSelected(currentSelectedMenuItem); + addSelectedItemStyling: function(id) { + this.$('.editor-menu-item[data-id="' + id + '"]').addClass('selected'); + var model = this.contentobjects.findWhere({ _id: id }); + var parentId = model && model.get('_parentId'); + if(parentId) { + // recurse + this.addSelectedItemStyling(parentId); + } + }, + + removeSelectedItemStyling: function() { + this.$('.editor-menu-item').removeClass('selected'); }, /** - * This is triggered when an item is clicked + * Generates an array with the inheritence line from a given contentobject to the current course + * @param {Model} contentModel + * @return {Array} */ - updateSelectedItem: function(view) { - // store the ID of the currently selected contentObject - Origin.editor.currentContentObjectId = view.model.get('_id'); - - if(view.model.get('_type') === 'menu') { - this.addMenuLayerView(view); - return; + getItemHeirarchy: function(model, done) { + var hierarchy = []; + if(model.get('_type') === 'menu') { + hierarchy.push(model); } - this.scrollToElement(); + var __this = this; + var _getParent = function(model, callback) { + var parent = __this.contentobjects.findWhere({ _id: model.get('_parentId') }); + if(parent) { + hierarchy.push(parent); + return _getParent(parent, callback); + } + hierarchy.push(Origin.editor.data.course); + callback(); + }; + _getParent(model, function() { + if(typeof done === 'function') done.call(__this, hierarchy.reverse()); + }); }, - /** - * Recursive function which shows any children for a given contentObject and sets - * the UI element to 'expanded' - * @param {Model} selectedItem A given contextObject model - */ - setParentElementToSelected: function(selectedItem) { - var parentId = selectedItem.get('_parentId'); - - if(parentId === Origin.editor.data.course.get('_id')) { + onSelectedItemChanged: function(model) { + if(model.get('_id') === Origin.editor.currentContentObject && Origin.editor.currentContentObject.get('_id')) { return; } - var parentModel = Origin.editor.data.contentObjects.findWhere({ _id: parentId }); - parentModel.set('_isExpanded', true); - - this.setParentElementToSelected(parentModel); + Origin.editor.currentContentObject = model; + this.renderLayers(); }, setupHorizontalScroll: function(windowWidth, windowHeight) { @@ -138,15 +169,15 @@ define(function(require){ connectWith: ".editor-menu-layer-inner", scroll: true, helper: 'clone', - stop: function(event,ui) { + stop: _.bind(function(event,ui) { var $draggedElement = ui.item; var id = $('.editor-menu-item-inner', $draggedElement).attr('data-id'); var sortOrder = $draggedElement.index() + 1; var parentId = $draggedElement.closest('.editor-menu-layer').attr('data-parentId'); - var currentModel = Origin.editor.data.contentObjects.findWhere({ _id: id }); + var currentModel = this.contentobjects.findWhere({ _id: id }); currentModel.save({ _sortOrder: sortOrder, _parentId: parentId }, { patch: true }); currentModel.set('_isDragging', false); - }, + }, this), over: function(event, ui) { $(event.target).closest('.editor-menu-layer').attr('data-over', true); }, @@ -158,6 +189,25 @@ define(function(require){ if (ui.item.hasClass('content-type-menu')) ui.sender.sortable("cancel"); } }); + }, + + onItemAdded: function(newModel) { + this.contentobjects.add(newModel); + }, + + onItemDeleted: function(oldModel) { + this.contentobjects.fetch({ + success: _.bind(function() { + // select the parent of the deleted item + Origin.trigger('editorView:menuView:updateSelectedItem', this.contentobjects.findWhere({ _id: oldModel.get('_parentId') })); + }, this), + error: function() { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } + }); } }, { template: 'editorMenu' diff --git a/frontend/src/modules/editor/contentObject/views/editorPageArticleView.js b/frontend/src/modules/editor/contentObject/views/editorPageArticleView.js index a7d79a4edf..d6a3c8bdaf 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageArticleView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageArticleView.js @@ -1,9 +1,6 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); - var BlockModel = require('core/models/blockModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorPageBlockView = require('./editorPageBlockView'); @@ -27,10 +24,6 @@ define(function(require){ postRender: function() { this.addBlockViews(); this.setupDragDrop(); - _.defer(_.bind(function(){ - this.trigger('articleView:postRender'); - Origin.trigger('pageView:itemRendered'); - }, this)); }, listenToEvents: function() { @@ -40,8 +33,8 @@ define(function(require){ var id = this.model.get('_id'); var events = {}; events['editorView:moveBlock:' + id] = this.render; - events['editorView:cutBlock:' + id] = this.onCutBlock; events['editorView:deleteArticle:' + id] = this.deletePageArticle; + events['editorView:pasted:' + id] = this.onPaste; this.listenTo(Origin, events); } @@ -49,18 +42,10 @@ define(function(require){ 'contextMenu:article:edit': this.loadArticleEdit, 'contextMenu:article:copy': this.onCopy, 'contextMenu:article:copyID': this.onCopyID, - 'contextMenu:article:cut': this.onCut, 'contextMenu:article:delete': this.deleteArticlePrompt }); }, - onCutBlock: function(view) { - this.once('articleView:postRender', function() { - view.showPasteZones(); - }); - this.render(); - }, - addBlockViews: function() { this.$('.article-blocks').empty(); // Insert the 'pre' paste zone for blocks @@ -73,72 +58,63 @@ define(function(require){ }); this.$('.article-blocks').append(view.$el); // Iterate over each block and add it to the article - this.model.getChildren().each(this.addBlockView, this); + this.model.fetchChildren(_.bind(function(children) { + for(var i = 0, count = children.length; i < count; i++) { + this.addBlockView(children[i]); + } + }, this)); }, addBlockView: function(blockModel, scrollIntoView) { - var newBlockView = new EditorPageBlockView({model: blockModel}); - var sortOrder = blockModel.get('_sortOrder'); - - // Add syncing class - if (blockModel.isNew()) { - newBlockView.$el.addClass('syncing'); - } - scrollIntoView = scrollIntoView || false; - this.$('.article-blocks').append(newBlockView.$el); + var newBlockView = new EditorPageBlockView({ model: blockModel }); + var $blocks = this.$('.article-blocks .block'); + var sortOrder = blockModel.get('_sortOrder'); + var index = sortOrder > 0 ? sortOrder-1 : undefined; + var shouldAppend = index === undefined || index >= $blocks.length || $blocks.length === 0; - if (scrollIntoView) { - $.scrollTo(newBlockView.$el, 200); + if(shouldAppend) { // add to the end of the article + this.$('.article-blocks').append(newBlockView.$el); + } else { // 'splice' block into the new position + $($blocks[index]).before(newBlockView.$el); } - + if (scrollIntoView) $.scrollTo(newBlockView.$el, 200); // Increment the sortOrder property - blockModel.set('_pasteZoneSortOrder', ++sortOrder); - + blockModel.set('_pasteZoneSortOrder', (blockModel.get('_sortOrder')+1)); // Post-block paste zone - sort order of placeholder will be one greater - this.$('.article-blocks').append(new EditorPasteZoneView({model: blockModel}).$el); - // Return the block view so syncing can be shown - return newBlockView; + this.$('.article-blocks').append(new EditorPasteZoneView({ model: blockModel }).$el); }, addBlock: function(event) { event && event.preventDefault(); - - var self = this; - var layoutOptions = [{ - type: 'left', - name: 'app.layoutleft', - pasteZoneRenderOrder: 2 - }, { - type: 'full', - name: 'app.layoutfull', - pasteZoneRenderOrder: 1 - }, { - type: 'right', - name: 'app.layoutright', - pasteZoneRenderOrder: 3 - }]; - - var newPageBlockModel = new BlockModel({ + var model = new BlockModel(); + model.save({ title: Origin.l10n.t('app.placeholdernewblock'), displayTitle: Origin.l10n.t('app.placeholdernewblock'), body: '', - _parentId: self.model.get('_id'), + _parentId: this.model.get('_id'), _courseId: Origin.editor.data.course.get('_id'), - layoutOptions: layoutOptions, + layoutOptions: [{ + type: 'left', + name: 'app.layoutleft', + pasteZoneRenderOrder: 2 + }, { + type: 'full', + name: 'app.layoutfull', + pasteZoneRenderOrder: 1 + }, { + type: 'right', + name: 'app.layoutright', + pasteZoneRenderOrder: 3 + }], _type: 'block' - }); - - newPageBlockModel.save(null, { + }, { + success: _.bind(function(model, response, options) { + this.addBlockView(model, true); + }, this), error: function() { Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.erroraddingblock') }); - }, - success: function(model, response, options) { - var newBlockView = self.addBlockView(model, true); - Origin.editor.data.blocks.add(model); - newBlockView.$el.removeClass('syncing').addClass('synced'); - newBlockView.reRender(); } }); }, @@ -241,6 +217,20 @@ define(function(require){ $container.scrollTop($(this).offset().top*-1); } }); + }, + + onPaste: function(data) { + (new BlockModel({ _id: data._id })).fetch({ + success: _.bind(function(model) { + this.addBlockView(model); + }, this), + error: function(data) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } + }); } }, { template: 'editorPageArticle' diff --git a/frontend/src/modules/editor/contentObject/views/editorPageBlockView.js b/frontend/src/modules/editor/contentObject/views/editorPageBlockView.js index 148b4b5e1c..00f78622ab 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageBlockView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageBlockView.js @@ -1,9 +1,7 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); - var ComponentModel = require('core/models/componentModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorPageComponentView = require('./editorPageComponentView'); @@ -11,12 +9,13 @@ define(function(require){ var EditorPageComponentListView = require('./editorPageComponentListView'); var EditorPageBlockView = EditorOriginView.extend({ - className: 'block editable block-draggable', + className: 'block editable block-draggable page-content-syncing', tagName: 'div', - settings: { + settings: _.extend({}, EditorOriginView.prototype.settings, { + hasAsyncPostRender: true, autoRender: false - }, + }), events: _.extend({}, EditorOriginView.prototype.events, { 'click a.block-delete': 'deleteBlockPrompt', @@ -28,42 +27,70 @@ define(function(require){ preRender: function() { this.listenToEvents(); this.model.set('componentTypes', Origin.editor.data.componenttypes.toJSON()); - // seems odd calling re-render here, but it does what we want - this.reRender(); + this.render(); + }, + + render: function() { + this.model.fetchChildren(_.bind(function(components) { + this.children = components; + var layouts = this.getAvailableLayouts(); + // FIXME why do we have two attributes with the same value? + this.model.set({ layoutOptions: layouts, dragLayoutOptions: layouts }); + + EditorOriginView.prototype.render.apply(this); + + this.addComponentViews(); + this.setupDragDrop(); + + this.handleAsyncPostRender(); + }, this)); + }, + + animateIn: function() { + this.$el.removeClass('page-content-syncing'); + }, + + handleAsyncPostRender: function() { + var renderedChildren = []; + if(this.children.length === 0) { + return this.animateIn(); + } + this.listenTo(Origin, 'editorPageComponent:postRender', function(view) { + var id = view.model.get('_id'); + if(this.children.indexOf(view.model) !== -1 && renderedChildren.indexOf(id) === -1) { + renderedChildren.push(id); + } + if(renderedChildren.length === this.children.length) { + this.stopListening(Origin, 'editorPageComponent:postRender'); + this.animateIn(); + } + }); }, listenToEvents: function() { var id = this.model.get('_id'); - var events = {}; - events['editorView:removeSubViews editorPageView:removePageSubViews'] = this.remove; - events['editorView:removeComponent:' + id] = this.handleRemovedComponent; - events['editorView:moveComponent:' + id] = this.reRender; - events['editorView:cutComponent:' + id] = this.onCutComponent; - events['editorView:addComponent:' + id] = this.addComponent; - events['editorView:deleteBlock:' + id] = this.deleteBlock; + var events = { + 'editorView:removeSubViews editorPageView:removePageSubViews': this.remove + }; + events[ + 'editorView:addComponent:' + id + ' ' + + 'editorView:removeComponent:' + id + ' ' + + 'editorView:moveComponent:' + id + ] = this.render; + events['editorView:pasted:' + id] = this.onPaste; this.listenTo(Origin, events); this.listenTo(this, { 'contextMenu:block:edit': this.loadBlockEdit, 'contextMenu:block:copy': this.onCopy, 'contextMenu:block:copyID': this.onCopyID, - 'contextMenu:block:cut': this.onCut, 'contextMenu:block:delete': this.deleteBlockPrompt }); }, postRender: function() { - this.addComponentViews(); - this.setupDragDrop(); - - _.defer(_.bind(function(){ - this.trigger('blockView:postRender'); - Origin.trigger('pageView:itemRendered'); - }, this)); - }, - - reRender: function() { - this.evaluateComponents(this.render); + this.trigger('blockView:postRender'); + Origin.trigger('pageView:itemRendered', this); }, getAvailableLayouts: function() { @@ -72,26 +99,17 @@ define(function(require){ left: { type: 'left', name: 'app.layoutleft', pasteZoneRenderOrder: 2 }, right: { type: 'right', name: 'app.layoutright', pasteZoneRenderOrder: 3 } }; - var components = this.model.getChildren(); - if (components.length === 0) { + if (this.children.length === 0) { return [layoutOptions.full,layoutOptions.left,layoutOptions.right]; } - if (components.length === 1) { - var layout = components.at(0).get('_layout'); - if(layout === 'left') return [layoutOptions.right]; - if(layout === 'right') return [layoutOptions.left]; + if (this.children.length === 1) { + var layout = this.children[0].get('_layout'); + if(layout === layoutOptions.left.type) return [layoutOptions.right]; + if(layout === layoutOptions.right.type) return [layoutOptions.left]; } return []; }, - evaluateComponents: function(callback) { - this.model.set({ - layoutOptions: this.getAvailableLayouts(), - dragLayoutOptions: this.getAvailableLayouts() - }); - if(callback) callback.apply(this); - }, - deleteBlockPrompt: function(event) { event && event.preventDefault(); @@ -99,16 +117,12 @@ define(function(require){ type: 'warning', title: Origin.l10n.t('app.deleteblock'), text: Origin.l10n.t('app.confirmdeleteblock') + '
      ' + '
      ' + Origin.l10n.t('app.confirmdeleteblockwarning'), - callback: _.bind(this.deleteBlockConfirm, this) + callback: _.bind(function(confirmed) { + if (confirmed) this.deleteBlock(); + }, this) }); }, - deleteBlockConfirm: function(confirmed) { - if (confirmed) { - Origin.trigger('editorView:deleteBlock:' + this.model.get('_id')); - } - }, - deleteBlock: function(event) { this.model.destroy({ success: _.bind(this.remove, this), @@ -118,18 +132,6 @@ define(function(require){ }); }, - handleRemovedComponent: function() { - this.reRender(); - }, - - onCutComponent: function(view) { - this.once('blockView:postRender', function() { - view.showPasteZones(); - }); - - this.reRender(); - }, - setupDragDrop: function() { var view = this; var autoScrollTimer = false; @@ -194,26 +196,20 @@ define(function(require){ addComponentViews: function() { this.$('.page-components').empty(); - var components = this.model.getChildren(); - var addPasteZonesFirst = components.length && components.at(0).get('_layout') != 'full'; - this.addComponentButtonLayout(components); - - if (addPasteZonesFirst) { - this.setupPasteZones(); - } + var addPasteZonesFirst = this.children.length && this.children[0].get('_layout') !== 'full'; + this.addComponentButtonLayout(this.children); + if (addPasteZonesFirst) this.setupPasteZones(); // Add component elements - this.model.getChildren().each(function(component) { - this.$('.page-components').append(new EditorPageComponentView({ model: component }).$el); - }, this); - - if (!addPasteZonesFirst) { - this.setupPasteZones(); + for(var i = 0, count = this.children.length; i < count; i++) { + var view = new EditorPageComponentView({ model: this.children[i] }); + this.$('.page-components').append(view.$el); } + if (!addPasteZonesFirst) this.setupPasteZones(); }, - addComponentButtonLayout: function(components){ + addComponentButtonLayout: function(components) { if(components.length === 2) { return; } @@ -221,7 +217,10 @@ define(function(require){ this.$('.add-component').addClass('full'); return; } - var className = (components.models[0].attributes._layout === 'left') ? 'right' : 'left'; + var layout = components[0].get('_layout'); + var className = ''; + if(layout === 'left') className = 'right'; + if(layout === 'right') className = 'left'; this.$('.add-component').addClass(className); }, @@ -255,15 +254,8 @@ define(function(require){ setupPasteZones: function() { // Add available paste zones - var layouts = []; - var dragLayouts = []; - - _.each(this.model.get('dragLayoutOptions'), function (dragLayout) { - dragLayouts.push(dragLayout); - }); - _.each(this.model.get('layoutOptions'), function (layout) { - layouts.push(layout); - }); + var layouts = this.model.get('layoutOptions').slice(); + var dragLayouts = this.model.get('dragLayoutOptions').slice(); _.each(this.sortArrayByKey(dragLayouts, 'pasteZoneRenderOrder'), function(layout) { var pasteComponent = new ComponentModel(); @@ -284,17 +276,19 @@ define(function(require){ }, this); }, - swapLayout: function (layout) { - if (layout === 'full') { - return layout; - } - return (layout == 'left') ? 'right' : 'left'; - }, - - toggleAddComponentsButton: function() { - var layoutOptions = this.model.get('layoutOptions') || []; - // display-none if we've no layout options - this.$('.add-control').toggleClass('display-none', layoutOptions.length === 0); + onPaste: function(data) { + (new ComponentModel({ _id: data._id })).fetch({ + success: _.bind(function(model) { + this.children.push(model); + this.render(); + }, this), + error: function(data) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } + }); } }, { template: 'editorPageBlock' diff --git a/frontend/src/modules/editor/contentObject/views/editorPageComponentListItemView.js b/frontend/src/modules/editor/contentObject/views/editorPageComponentListItemView.js index 511ec06c78..056b933ab7 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageComponentListItemView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageComponentListItemView.js @@ -1,8 +1,6 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require) { - var Backbone = require('backbone'); var Origin = require('core/origin'); - var ComponentModel = require('core/models/componentModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorPageComponentView = require('./editorPageComponentView'); @@ -64,12 +62,10 @@ define(function(require) { addComponent: function(layout) { Origin.trigger('editorComponentListView:remove'); - var componentName = this.model.get('name'); - var componentType = _.find(Origin.editor.data.componenttypes.models, function(type){ - return type.get('name') == componentName; - }); + var componentType = Origin.editor.data.componenttypes.findWhere({ name: this.model.get('name') }); + var model = new ComponentModel(); - var newComponentModel = new ComponentModel({ + model.save({ title: Origin.l10n.t('app.placeholdernewcomponent'), displayTitle: Origin.l10n.t('app.placeholdernewcomponent'), body: '', @@ -81,28 +77,17 @@ define(function(require) { _component: componentType.get('component'), _layout: layout, version: componentType.get('version') - }); - - var newComponentView = new EditorPageComponentView({ model: newComponentModel }).$el.addClass('syncing'); - - this.$parentElement - .find('.page-components') - .append(newComponentView); - - newComponentModel.save(null, { + }, { + success: _.bind(function(model) { + var parentId = model.get('_parentId'); + Origin.trigger('editorView:addComponent:' + parentId); + $('html').css('overflow-y', ''); + $.scrollTo('.block[data-id=' + parentId + ']'); + }, this), error: function() { $('html').css('overflow-y', ''); Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.erroraddingcomponent') }); - }, - success: _.bind(function() { - Origin.editor.data.components.add(newComponentModel); - this.parentView.evaluateComponents(this.parentView.toggleAddComponentsButton); - // Re-render the block - this.parentView.reRender(); - newComponentView.addClass('synced'); - $('html').css('overflow-y', ''); - $.scrollTo(newComponentView.$el); - }, this) + } }); } }, { diff --git a/frontend/src/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js b/frontend/src/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js index 669ea1dd9f..89440642ac 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageComponentPasteZoneView.js @@ -43,9 +43,6 @@ define(function(require){ url:'/api/content/component/' + componentId, data: newData, success: function(jqXHR, textStatus, errorThrown) { - var componentModel = Origin.editor.data.components.get(componentId); - componentModel.set(newData); - // Re-render the move-from block Origin.trigger('editorView:moveComponent:' + blockId); if (blockId !== parentId) { diff --git a/frontend/src/modules/editor/contentObject/views/editorPageComponentView.js b/frontend/src/modules/editor/contentObject/views/editorPageComponentView.js index 103ef4883c..3a501c2b57 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageComponentView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageComponentView.js @@ -1,7 +1,5 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ - var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); var EditorOriginView = require('../../global/views/editorOriginView'); @@ -9,6 +7,10 @@ define(function(require){ className: 'component editable component-draggable', tagName: 'div', + settings: _.extend({}, EditorOriginView.prototype.settings, { + autoRender: false, + }), + events: _.extend({}, EditorOriginView.prototype.events, { 'click a.component-delete': 'deleteComponentPrompt', 'click a.component-move': 'evaluateMove', @@ -18,23 +20,24 @@ define(function(require){ preRender: function() { this.$el.addClass('component-' + this.model.get('_layout')); - this.listenTo(Origin, 'editorView:removeSubViews', this.remove); - this.listenTo(Origin, 'editorPageView:removePageSubViews', this.remove); - - this.evaluateLayout(); - - this.on('contextMenu:component:edit', this.loadComponentEdit); - this.on('contextMenu:component:copy', this.onCopy); - this.on('contextMenu:component:copyID', this.onCopyID); - this.on('contextMenu:component:cut', this.onCut); - this.on('contextMenu:component:delete', this.deleteComponentPrompt); + this.listenTo(Origin, 'editorView:removeSubViews editorPageView:removePageSubViews', this.remove); + this.on({ + 'contextMenu:component:edit': this.loadComponentEdit, + 'contextMenu:component:copy': this.onCopy, + 'contextMenu:component:copyID': this.onCopyID, + 'contextMenu:component:delete': this.deleteComponentPrompt + }); + this.evaluateLayout(_.bind(function(layouts) { + this.model.set('_movePositions', layouts); + this.render(); + }, this)); }, postRender: function () { this.setupDragDrop(); _.defer(_.bind(function(){ this.trigger('componentView:postRender'); - Origin.trigger('pageView:itemRendered'); + Origin.trigger('pageView:itemRendered', this); }, this)); }, @@ -45,23 +48,22 @@ define(function(require){ type: 'warning', title: Origin.l10n.t('app.deletecomponent'), text: Origin.l10n.t('app.confirmdeletecomponent') + '
      ' + '
      ' + Origin.l10n.t('app.confirmdeletecomponentwarning'), - callback: _.bind(this.deleteComponentConfirm, this) + callback: _.bind(function(confirmed) { + if(confirmed) this.deleteComponent(); + }, this) }); }, - deleteComponentConfirm: function(confirmed) { - if(confirmed) { - this.deleteComponent(); - } - }, - deleteComponent: function() { - var parentId = this.model.get('_parentId'); - - if (this.model.destroy()) { - this.remove(); - Origin.trigger('editorView:removeComponent:' + parentId); - } + this.model.destroy({ + success: _.bind(function(model) { + this.remove(); + Origin.trigger('editorView:removeComponent:' + model.get('_parentId')); + }, this), + error: function(response) { + console.error(response); + } + }) }, loadComponentEdit: function(event) { @@ -137,11 +139,10 @@ define(function(require){ }, getSupportedLayout: function() { - var componentType = _.find(Origin.editor.data.componenttypes.models, function(type){ - return type.get('component') === this.model.get('_component'); - }, this); - + var componentType = Origin.editor.data.componenttypes.findWhere({ component: this.model.get('_component') }); var supportedLayout = componentType.get('properties')._supportedLayout; + // allow all layouts by default + if(!supportedLayout) return { full: true, half: true }; return { full: _.indexOf(supportedLayout.enum, 'full-width') > -1, @@ -149,114 +150,58 @@ define(function(require){ } }, - evaluateLayout: function() { + evaluateLayout: function(cb) { var supportedLayout = this.getSupportedLayout(); - var isFullWidthSupported = supportedLayout.full; - var isHalfWidthSupported = supportedLayout.half; - var movePositions = { left: false, right: false, full: false }; - - if (isHalfWidthSupported) { - var siblings = this.model.getSiblings(); - var showFull = !siblings.length && isFullWidthSupported; - var type = this.model.get('_layout'); - - switch (type) { + this.model.fetchSiblings(_.bind(function(siblings) { + var showFull = supportedLayout.full && siblings.length < 1; + switch(this.model.get('_layout')) { case 'left': - movePositions.right = true; + movePositions.right = supportedLayout.half; movePositions.full = showFull; break; case 'right': - movePositions.left = true; + movePositions.left = supportedLayout.half; movePositions.full = showFull; break; case 'full': - movePositions.left = true; - movePositions.right = true; + movePositions.left = supportedLayout.half; + movePositions.right = supportedLayout.half; break } - } - - this.model.set('_movePositions', movePositions); - + cb(movePositions); + }, this)); }, evaluateMove: function(event) { event && event.preventDefault(); - var left = $(event.currentTarget).hasClass('component-move-left'); - var right = $(event.currentTarget).hasClass('component-move-right'); - var newComponentLayout = (!left && !right) ? 'full' : (left ? 'left' : 'right'); - var siblings = this.model.getSiblings(); - - if (siblings && siblings.length > 0) { - var siblingId = siblings.models[0].get('_id'); - } - - if (siblingId) { - this.moveSiblings(newComponentLayout, siblingId); - } else { - this.moveComponent(newComponentLayout); - } + var $btn = $(event.currentTarget); + this.model.fetchSiblings(_.bind(function(siblings) { + var isLeft = $btn.hasClass('component-move-left'); + var isRight = $btn.hasClass('component-move-right'); + var isFull = $btn.hasClass('component-move-full'); + // move self to layout of clicked button + this.moveComponent(this.model.get('_id'), (isLeft ? 'left' : isRight ? 'right' : 'full')); + // move sibling to inverse of self + var siblingId = siblings && siblings.length > 0 && siblings.models[0].get('_id'); + if (siblingId) this.moveComponent(siblingId, (isLeft ? 'right' : 'left')); + }, this)); }, - moveComponent: function (layout) { - var componentId = this.model.get('_id'); + moveComponent: function (id, layout) { var parentId = this.model.get('_parentId'); - var layoutData = { - _layout: layout, - _parentId: parentId - }; - $.ajax({ type: 'PUT', - url:'/api/content/component/' + componentId, - data: layoutData, - success: function(jqXHR, textStatus, errorThrown) { - var componentModel = Origin.editor.data.components.get(componentId); - componentModel.set(layoutData); - - // Re-render the block - Origin.trigger('editorView:moveComponent:' + parentId); - }, - error: function(jqXHR, textStatus, errorThrown) { - Origin.Notify.alert({ - type: 'error', - text: jqXHR.responseJSON.message - }); - } - }); - }, - - moveSiblings: function (layout, siblingId) { - var componentId = this.model.get('_id'); - var parentId = this.model.get('_parentId'); - var newSiblingLayout = (layout == 'left') ? 'right' : 'left'; - var layoutData = { - newLayout: { + url:'/api/content/component/' + id, + data: { _layout: layout, _parentId: parentId }, - siblingLayout: { - _layout: newSiblingLayout, - _parentId: parentId - } - }; - $.ajax({ - type: 'PUT', - url:'/api/content/component/switch/' + componentId +'/'+ siblingId, - data: layoutData, success: function(jqXHR, textStatus, errorThrown) { - var componentModel = Origin.editor.data.components.get(componentId); - componentModel.set(layoutData.newLayout); - - var siblingModel = Origin.editor.data.components.get(siblingId); - siblingModel.set(layoutData.siblingLayout); - - // Re-render the block Origin.trigger('editorView:moveComponent:' + parentId); }, error: function(jqXHR, textStatus, errorThrown) { diff --git a/frontend/src/modules/editor/contentObject/views/editorPageView.js b/frontend/src/modules/editor/contentObject/views/editorPageView.js index 2af692323c..08ecfe2e21 100644 --- a/frontend/src/modules/editor/contentObject/views/editorPageView.js +++ b/frontend/src/modules/editor/contentObject/views/editorPageView.js @@ -1,11 +1,8 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require){ var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); - var ArticleModel = require('core/models/articleModel'); - var ContentModel = require('core/models/contentModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorPageArticleView = require('./editorPageArticleView'); var EditorPasteZoneView = require('../../global/views/editorPasteZoneView'); @@ -17,21 +14,33 @@ define(function(require){ childrenRenderedCount: 0, events: _.extend({}, EditorOriginView.prototype.events, { - 'click a.add-article': 'addArticle', + 'click a.add-article': 'addNewArticle', 'click a.page-edit-button': 'openContextMenu', 'dblclick .page-detail': 'loadPageEdit', 'click .paste-cancel': 'onPasteCancel' }), preRender: function() { - this.setupChildCount(); - - this.listenTo(Origin, { + var id = this.model.get('_id'); + var originEvents = { 'editorView:removeSubViews': this.remove, 'pageView:itemRendered': this.evaluateChildStatus - }); - this.listenTo(Origin, 'editorView:moveArticle:' + this.model.get('_id'), this.render); - this.listenTo(Origin, 'editorView:cutArticle:' + this.model.get('_id'), this.onCutArticle); + }; + originEvents['editorView:moveArticle:' + id] = this.render; + originEvents['editorView:pasted:' + id] = this.onPaste; + this.listenTo(Origin, originEvents); + }, + + render: function() { + var returnVal = EditorOriginView.prototype.render.apply(this, arguments); + + this.addArticleViews(); + + return returnVal; + }, + + postRender: function() { + this.resize(); }, resize: function() { @@ -41,43 +50,10 @@ define(function(require){ }, this)); }, - setupChildCount: function() { - var articles = Origin.editor.data.articles.where({_parentId: this.model.get('_id')}); - var articleList = [], blockList = []; - - _.each(articles, function(article) { - articleList.push(article.get('_id')); - }); - - var blocks = _.filter(Origin.editor.data.blocks.models, function (block) { - return _.contains(articleList, block.get('_parentId')); - }); - - _.each(blocks, function(block) { - blockList.push(block.get('_id')); - }); - - var components = _.filter(Origin.editor.data.components.models, function(component) { - return _.contains(blockList, component.get('_parentId')); - }); - - this.childrenCount = articles.length + blocks.length + components.length; - }, - evaluateChildStatus: function() { this.childrenRenderedCount++; }, - postRender: function() { - this.addArticleViews(); - - _.defer(_.bind(function(){ - this.resize(); - this.trigger('pageView:postRender'); - this.setViewToReady(); - }, this)); - }, - addArticleViews: function() { this.$('.page-articles').empty(); Origin.trigger('editorPageView:removePageSubViews'); @@ -89,19 +65,30 @@ define(function(require){ }); this.$('.page-articles').append(new EditorPasteZoneView({ model: prePasteArticle }).$el); // Iterate over each article and add it to the page - this.model.getChildren().each(this.addArticleView, this); + this.model.fetchChildren(_.bind(function(children) { + for(var i = 0, count = children.length; i < count; i++) { + if(children[i].get('_type') !== 'article') { + continue; + } + this.addArticleView(children[i]); + } + }, this)); }, - addArticleView: function(articleModel, scrollIntoView, addNewBlock) { + addArticleView: function(articleModel, scrollIntoView) { + scrollIntoView = scrollIntoView || false; + var newArticleView = new EditorPageArticleView({ model: articleModel }); var sortOrder = articleModel.get('_sortOrder'); - // Add syncing class - if (articleModel.isNew()) { - newArticleView.$el.addClass('syncing'); + var $articles = this.$('.page-articles .article'); + var index = sortOrder > 0 ? sortOrder-1 : undefined; + var shouldAppend = index === undefined || index >= $articles.length || $articles.length === 0; + + if(shouldAppend) { // add to the end of the article + this.$('.page-articles').append(newArticleView.$el); + } else { // 'splice' block into the new position + $($articles[index]).before(newArticleView.$el); } - scrollIntoView = scrollIntoView || false; - - this.$('.page-articles').append(newArticleView.$el); if (scrollIntoView) { $.scrollTo(newArticleView.$el, 200); @@ -109,36 +96,29 @@ define(function(require){ // Increment the 'sortOrder' property articleModel.set('_pasteZoneSortOrder', sortOrder++); // Post-article paste zone - sort order of placeholder will be one greater - this.$('.page-articles').append(new EditorPasteZoneView({model: articleModel}).$el); - // Return the article view so syncing can be shown + this.$('.page-articles').append(new EditorPasteZoneView({ model: articleModel }).$el); return newArticleView; }, - addArticle: function(event) { + addNewArticle: function(event) { event && event.preventDefault(); - - var _this = this; - var newPageArticleModel = new ArticleModel({ + (new ArticleModel()).save({ title: Origin.l10n.t('app.placeholdernewarticle'), displayTitle: Origin.l10n.t('app.placeholdernewarticle'), body: '', - _parentId: _this.model.get('_id'), + _parentId: this.model.get('_id'), _courseId: Origin.editor.data.course.get('_id'), _type:'article' - }); - - newPageArticleModel.save(null, { + }, { + success: _.bind(function(model, response, options) { + var articleView = this.addArticleView(model); + articleView.addBlock(); + }, this), error: function() { Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.erroraddingarticle') }); - }, - success: function(model, response, options) { - Origin.editor.data.articles.add(model); - var newArticleView = _this.addArticleView(newPageArticleModel); - newArticleView.$el.removeClass('syncing').addClass('synced'); - newArticleView.addBlock(); } }); }, @@ -165,9 +145,18 @@ define(function(require){ Origin.trigger('contextMenu:open', fakeView, event); }, - onCutArticle: function(view) { - this.once('pageView:postRender', view.showPasteZones); - this.render(); + onPaste: function(data) { + (new ArticleModel({ _id: data._id })).fetch({ + success: _.bind(function(model) { + this.addArticleView(model); + }, this), + error: function(data) { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } + }); } }, { template: 'editorPage' diff --git a/frontend/src/modules/editor/course/index.js b/frontend/src/modules/editor/course/index.js index d256091721..3a54ea890d 100644 --- a/frontend/src/modules/editor/course/index.js +++ b/frontend/src/modules/editor/course/index.js @@ -5,7 +5,8 @@ define(function(require) { var CourseModel = require('core/models/courseModel'); var EditorCourseEditView = require('./views/editorCourseEditView'); var EditorCourseEditSidebarView = require('./views/editorCourseEditSidebarView'); - var Helpers = require('../global/helpers'); + var CoreHelpers = require('core/helpers'); + var EditorHelpers = require('../global/helpers'); Origin.on('router:project', function(route1, route2, route3, route4) { if(route1 === 'new') createNewCourse(); @@ -13,13 +14,13 @@ define(function(require) { Origin.on('editor:course', renderCourseEdit); function renderCourseEdit() { - (new CourseModel({ _id: Origin.location.route1 })).fetch({ - success: function(model) { - Helpers.setPageTitle({ title: Origin.l10n.t('app.editorsettings') }); - var form = Origin.scaffold.buildForm({ model: model }); - Origin.contentPane.setView(EditorCourseEditView, { model: model, form: form }); - Origin.sidebar.addView(new EditorCourseEditSidebarView({ form: form }).$el); - } + var courseModel = new CourseModel({ _id: Origin.location.route1 }); + // FIXME need to fetch config to ensure scaffold has the latest extensions data + CoreHelpers.multiModelFetch([ courseModel, Origin.editor.data.config ], function(data) { + EditorHelpers.setPageTitle({ title: Origin.l10n.t('app.editorsettings') }); + var form = Origin.scaffold.buildForm({ model: courseModel }); + Origin.contentPane.setView(EditorCourseEditView, { model: courseModel, form: form }); + Origin.sidebar.addView(new EditorCourseEditSidebarView({ form: form }).$el); }); } @@ -28,7 +29,7 @@ define(function(require) { title: Origin.l10n.t('app.placeholdernewcourse'), displayTitle: Origin.l10n.t('app.placeholdernewcourse') }); - Helpers.setPageTitle({ title: Origin.l10n.t('app.editornew') }); + EditorHelpers.setPageTitle({ title: Origin.l10n.t('app.editornew') }); var form = Origin.scaffold.buildForm({ model: model }); Origin.contentPane.setView(EditorCourseEditView, { model: model, form: form }); Origin.sidebar.addView(new EditorCourseEditSidebarView({ form: form }).$el); diff --git a/frontend/src/modules/editor/course/views/editorCourseEditView.js b/frontend/src/modules/editor/course/views/editorCourseEditView.js index d215aef577..172e6c01bf 100644 --- a/frontend/src/modules/editor/course/views/editorCourseEditView.js +++ b/frontend/src/modules/editor/course/views/editorCourseEditView.js @@ -1,8 +1,6 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE define(function(require) { var Origin = require('core/origin'); - - var ConfigModel = require('core/models/configModel'); var ContentObjectModel = require('core/models/contentObjectModel'); var ArticleModel = require('core/models/articleModel'); var BlockModel = require('core/models/blockModel'); @@ -23,44 +21,35 @@ define(function(require) { this.$el.addClass('project-detail-hide-hero'); // Initialise the 'tags' property for a new course this.model.set('tags', []); - } else { - // Ensure that the latest config model is always up-to-date when entering this screen - Origin.editor.data.config = new ConfigModel({_courseId: this.model.get('_id')}); } // This next line is important for a proper PATCH request on saveProject() this.originalAttributes = _.clone(this.model.attributes); }, getAttributesToSave: function() { - // set tags - var tags = []; - _.each(this.model.get('tags'), function(item) { - item._id && tags.push(item._id); - }); - this.model.set('tags', tags); + this.model.set('tags', _.pluck(this.model.get('tags'), '_id')); var changedAttributes = this.model.changedAttributes(this.originalAttributes); if(changedAttributes) { return _.pick(this.model.attributes, _.keys(changedAttributes)); } - return null; }, onSaveSuccess: function(model, response, options) { - if (this.isNew) { - this.populateNewCourse(model); - } else { + if(!this.isNew) { EditorOriginView.prototype.onSaveSuccess.apply(this, arguments); + return; } + this.populateNewCourse(model); }, - // TODO not really good enough to handle model save errors and other errors here + // FIXME not really good enough to handle model save errors and other errors here onSaveError: function(model, response, options) { - if(arguments.length == 2) { - return EditorOriginView.prototype.onSaveError.apply(this, arguments); + if(arguments.length === 2) { + EditorOriginView.prototype.onSaveError.apply(this, arguments); + return; } - var messageText = typeof response.responseJSON == 'object' && response.responseJSON.message; EditorOriginView.prototype.onSaveError.call(this, null, messageText); }, @@ -145,7 +134,6 @@ define(function(require) { }, this) }); } - }, { template: 'editorCourseEdit' }); diff --git a/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js b/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js index 5730ab0965..44c37d598d 100644 --- a/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js +++ b/frontend/src/modules/editor/extensions/views/editorExtensionsEditView.js @@ -2,8 +2,6 @@ define(function(require) { var Backbone = require('backbone'); var Origin = require('core/origin'); - - var ConfigModel = require('core/models/configModel'); var EditorOriginView = require('../../global/views/editorOriginView'); var EditorExtensionsEditView = EditorOriginView.extend({ diff --git a/frontend/src/modules/editor/global/editorDataLoader.js b/frontend/src/modules/editor/global/editorDataLoader.js index 9a9635d032..6a1fdaf64e 100644 --- a/frontend/src/modules/editor/global/editorDataLoader.js +++ b/frontend/src/modules/editor/global/editorDataLoader.js @@ -3,12 +3,8 @@ define(function(require) { var _ = require('underscore'); var Origin = require('core/origin'); - var ArticleModel = require('core/models/articleModel'); - var BlockModel = require('core/models/blockModel'); var ClipboardModel = require('core/models/clipboardModel'); - var ComponentModel = require('core/models/componentModel'); var ComponentTypeModel = require('core/models/componentTypeModel'); - var ContentObjectModel = require('core/models/contentObjectModel'); var ConfigModel = require('core/models/configModel'); var CourseAssetModel = require('core/models/courseAssetModel'); var CourseModel = require('core/models/courseModel'); @@ -20,20 +16,13 @@ define(function(require) { // used to check what's preloaded var globalData = { - courses: false, extensiontypes: false, componenttypes: false }; // used to check what's loaded var courseData = { - clipboards: false, course: false, - config: false, - contentObjects: false, - articles: false, - blocks: false, - components: false, - courseassets: false + config: false }; // Public API @@ -49,9 +38,6 @@ define(function(require) { ensureEditorData(); resetLoadStatus(globalData); // create the global collections - if(!Origin.editor.data.courses) { - Origin.editor.data.courses = createCollection(CourseModel); - } if(!Origin.editor.data.extensiontypes) { Origin.editor.data.extensiontypes = createCollection(ExtensionModel); } @@ -90,13 +76,7 @@ define(function(require) { if(!isAlreadyLoaded) { _.extend(Origin.editor.data, { course: new CourseModel({ _id: courseId }), - config: new ConfigModel({ _courseId: courseId }), - contentObjects: createCollection(ContentObjectModel), - articles: createCollection(ArticleModel), - blocks: createCollection(BlockModel), - components: createCollection(ComponentModel), - clipboards: createCollection(ClipboardModel, '&createdBy=' + Origin.sessionModel.get('id')), - courseassets: createCollection(CourseAssetModel) + config: new ConfigModel({ _courseId: courseId }) }); } // fetch all collections @@ -199,14 +179,14 @@ define(function(require) { function createCollection(Model, query) { var courseId = Origin.location.route1; var url = Model.prototype.urlRoot; - var siblings = Model.prototype._siblings; + var siblingTypes = Model.prototype._siblingTypes; /** * FIXME for non course-specific data without a model._type. * Adding siblings will break the below check... */ var inferredType = url.split('/').slice(-1) + 's'; // FIXME not the best check for course-specific collections - if(siblings !== undefined) { + if(siblingTypes !== undefined) { if(!courseId) throw new Error('No Editor.data.course specified, cannot load ' + url); url += '?_courseId=' + courseId + (query || ''); } @@ -214,7 +194,7 @@ define(function(require) { autoFetch: false, model: Model, url: url, - _type: siblings || inferredType + _type: siblingTypes || inferredType }); } diff --git a/frontend/src/modules/editor/global/less/editor.less b/frontend/src/modules/editor/global/less/editor.less index f3f2872496..93670892dc 100644 --- a/frontend/src/modules/editor/global/less/editor.less +++ b/frontend/src/modules/editor/global/less/editor.less @@ -149,17 +149,27 @@ i.asset-clear { font-weight: @font-weight-bold; } +.module-editor .block { + transition: transform 0.3s cubic-bezier(0.8, 0.0, 0.2, 1), opacity 0.3s cubic-bezier(0.8, 0.0, 0.2, 1) !important; +} +.page-content-syncing { + &.block { + min-height: 150px; + transform: scale(0.95, 0.95); + opacity: 0.6; + .component { + &-inner { + display: none; + } + } + } +} + .syncing { transform: scale(0.8, 0.8); opacity:0.6; } -.synced { - transition:transform 0.3s cubic-bezier(0.8, 0.0, 0.2, 1), opacity 0.3s cubic-bezier(0.8, 0.0, 0.2, 1)!important; - transform: scale(1, 1); - opacity:1; -} - .not-synced { transition:transform 0.3s cubic-bezier(0.8, 0.0, 0.2, 1), opacity 0.3s cubic-bezier(0.8, 0.0, 0.2, 1)!important; transform: scale(0.4, 0.4); diff --git a/frontend/src/modules/editor/global/views/editorOriginView.js b/frontend/src/modules/editor/global/views/editorOriginView.js index 19ecbc31fd..2c7a6e965c 100644 --- a/frontend/src/modules/editor/global/views/editorOriginView.js +++ b/frontend/src/modules/editor/global/views/editorOriginView.js @@ -24,6 +24,14 @@ define(function(require){ }); }, + render: function() { + OriginView.prototype.render.apply(this, arguments); + if(this.model) { + this.$el.attr('data-id', this.model.get('_id')); + } + return this; + }, + postRender: function() { if (!this.form) { return this.setViewToReady(); @@ -186,11 +194,6 @@ define(function(require){ Origin.trigger('editorView:copyID', this.model); }, - onCut: function(e) { - e && e.preventDefault(); - Origin.trigger('editorView:cut', this); - }, - onPaste: function(e) { if(e) { e.stopPropagation(); diff --git a/frontend/src/modules/editor/global/views/editorPasteZoneView.js b/frontend/src/modules/editor/global/views/editorPasteZoneView.js index c74417f002..2ad025997c 100644 --- a/frontend/src/modules/editor/global/views/editorPasteZoneView.js +++ b/frontend/src/modules/editor/global/views/editorPasteZoneView.js @@ -55,18 +55,12 @@ define(function(require){ _parentId: parentId, _sortOrder: $('.paste-' + type, this.$el).attr('data-sort-order') }, - success: _.bind(function() { - // fetch collection for the pasted type, and send motification - Origin.editor.data[this.model._siblings].fetch().done(function() { - var eventPrefix = 'editorView:move' + Helpers.capitalise(type) + ':'; - var itemId = (droppedOnId === parentId) ? droppedOnId : parentId; - // notify the old parent that the child's gone - if(itemId !== droppedOnId) { - Origin.trigger(eventPrefix + droppedOnId); - } - Origin.trigger(eventPrefix + itemId); - }); - }, this), + success: function() { + var eventPrefix = 'editorView:move' + Helpers.capitalise(type) + ':'; + Origin.trigger(eventPrefix + droppedOnId); + // notify the old parent that the child's gone + if(droppedOnId !== parentId) Origin.trigger(eventPrefix + parentId); + }, error: function(jqXHR) { Origin.Notify.alert({ type: 'error', text: jqXHR.responseJSON.message }); } diff --git a/frontend/src/modules/editor/global/views/editorView.js b/frontend/src/modules/editor/global/views/editorView.js index 9af67089cc..15bc44395f 100644 --- a/frontend/src/modules/editor/global/views/editorView.js +++ b/frontend/src/modules/editor/global/views/editorView.js @@ -4,7 +4,6 @@ */ define(function(require){ var Backbone = require('backbone'); - var Handlebars = require('handlebars'); var Origin = require('core/origin'); var helpers = require('core/helpers'); @@ -35,7 +34,6 @@ define(function(require){ preRender: function(options) { this.currentView = options.currentView; - Origin.editor.pasteParentModel = false; Origin.editor.isPreviewPending = false; this.currentCourseId = Origin.editor.data.course.get('_id'); this.currentCourse = Origin.editor.data.course; @@ -45,13 +43,23 @@ define(function(require){ 'editorView:refreshView': this.setupEditor, 'editorView:copy': this.addToClipboard, 'editorView:copyID': this.copyIdToClipboard, - 'editorView:cut': this.cutContent, 'editorView:paste': this.pasteFromClipboard, - 'editorCommon:download': this.downloadProject, - 'editorCommon:preview': this.previewProject, - 'editorCommon:export': this.exportProject + 'editorCommon:download': function(event) { + this.validateProject(event, this.downloadProject); + }, + 'editorCommon:preview': function(event) { + var previewWindow = window.open('/loading', 'preview'); + this.validateProject(event, function(error) { + if(error) { + return previewWindow.close(); + } + this.previewProject(previewWindow); + }); + }, + 'editorCommon:export': function(event) { + this.validateProject(event, this.exportProject); + } }); - this.render(); this.setupEditor(); }, @@ -64,157 +72,112 @@ define(function(require){ this.renderCurrentEditorView(); }, - downloadProject: function(e) { + validateProject: function(e, next) { e && e.preventDefault(); + helpers.validateCourseContent(this.currentCourse, _.bind(function(error) { + if(error) { + Origin.Notify.alert({ type: 'error', text: "There's something wrong with your course:

      " + error }); + } + next.call(this, error); + }, this)); + }, - var self = this; - - if (helpers.validateCourseContent(this.currentCourse) && !Origin.editor.isDownloadPending) { - $('.editor-common-sidebar-download-inner').addClass('display-none'); - $('.editor-common-sidebar-downloading').removeClass('display-none'); - - var courseId = Origin.editor.data.course.get('_id'); - var tenantId = Origin.sessionModel.get('tenantId'); - - $.ajax({ - method: 'get', - url: '/api/output/' + Origin.constants.outputPlugin + '/publish/' + this.currentCourseId, - success: function (jqXHR, textStatus, errorThrown) { - if (jqXHR.success) { - if (jqXHR.payload && typeof(jqXHR.payload.pollUrl) != 'undefined' && jqXHR.payload.pollUrl != '') { - // Ping the remote URL to check if the job has been completed - self.updateDownloadProgress(jqXHR.payload.pollUrl); - } else { - self.resetDownloadProgress(); - - var $downloadForm = $('#downloadForm'); - - $downloadForm.attr('action', '/download/' + tenantId + '/' + courseId + '/' + jqXHR.payload.zipName + '/download.zip'); - $downloadForm.submit(); - } - } else { - self.resetDownloadProgress(); - - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorgeneric') - }); - } - }, - error: function (jqXHR, textStatus, errorThrown) { - self.resetDownloadProgress(); - - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorgeneric') - }); - } - }); - } else { - return false; + previewProject: function(previewWindow) { + if(Origin.editor.isPreviewPending) { + return; } + Origin.editor.isPreviewPending = true; + $('.navigation-loading-indicator').removeClass('display-none'); + $('.editor-common-sidebar-preview-inner').addClass('display-none'); + $('.editor-common-sidebar-previewing').removeClass('display-none'); + + $.get('/api/output/' + Origin.constants.outputPlugin + '/preview/' + this.currentCourseId, _.bind(function(jqXHR, textStatus, errorThrown) { + if(!jqXHR.success) { + this.resetPreviewProgress(); + Origin.Notify.alert({ + type: 'error', + text: Origin.l10n.t('app.errorgeneratingpreview') + }); + previewWindow.close(); + return; + } + if (jqXHR.payload && typeof(jqXHR.payload.pollUrl) !== undefined && jqXHR.payload.pollUrl) { + // Ping the remote URL to check if the job has been completed + this.updatePreviewProgress(jqXHR.payload.pollUrl, previewWindow); + return; + } + this.updateCoursePreview(previewWindow); + this.resetPreviewProgress(); + }, this)).fail(_.bind(function(jqXHR, textStatus, errorThrown) { + this.resetPreviewProgress(); + Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.errorgeneric') }); + previewWindow.close(); + }, this)); }, - exportProject: function(e) { - e && e.preventDefault(); + downloadProject: function() { + if(Origin.editor.isDownloadPending) { + return; + } + $('.editor-common-sidebar-download-inner').addClass('display-none'); + $('.editor-common-sidebar-downloading').removeClass('display-none'); - // aleady processing, don't try again - if(this.exporting) return; + $.get('/api/output/' + Origin.constants.outputPlugin + '/publish/' + this.currentCourseId, _.bind(function(jqXHR, textStatus, errorThrown) { - var courseId = Origin.editor.data.course.get('_id'); - var tenantId = Origin.sessionModel.get('tenantId'); - - this.showExportAnimation(); - this.exporting = true; + if (!jqXHR.success) { + Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.errorgeneric') }); + this.resetDownloadProgress(); + return; + } + if (jqXHR.payload && typeof(jqXHR.payload.pollUrl) !== undefined && jqXHR.payload.pollUrl) { + // Ping the remote URL to check if the job has been completed + this.updateDownloadProgress(jqXHR.payload.pollUrl); + return; + } + this.resetDownloadProgress(); + + var $downloadForm = $('#downloadForm'); + $downloadForm.attr('action', '/download/' + Origin.sessionModel.get('tenantId') + '/' + Origin.editor.data.course.get('_id') + '/' + jqXHR.payload.zipName + '/download.zip'); + $downloadForm.submit(); - var self = this; - $.ajax({ - url: '/export/' + tenantId + '/' + courseId, - success: function(data, textStatus, jqXHR) { - self.showExportAnimation(false); - self.exporting = false; - - // get the zip - var form = document.createElement('form'); - self.$el.append(form); - form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/' + data.zipName + '/download.zip'); - form.submit(); - }, - error: function(jqXHR, textStatus, errorThrown) { - var messageText = errorThrown; - if(jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.message) messageText += ':
      ' + jqXHR.responseJSON.message; - - self.showExportAnimation(false); - self.exporting = false; - - Origin.Notify.alert({ - type: 'error', - title: Origin.l10n.t('app.exporterrortitle'), - text: messageText - }); - } - }); + }, this)).fail(_.bind(function (jqXHR, textStatus, errorThrown) { + this.resetDownloadProgress(); + Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.errorgeneric') }); + }, this)); }, - showExportAnimation: function(show) { - if(show !== false) { - $('.editor-common-sidebar-export-inner').addClass('display-none'); - $('.editor-common-sidebar-exporting').removeClass('display-none'); - } else { - $('.editor-common-sidebar-export-inner').removeClass('display-none'); - $('.editor-common-sidebar-exporting').addClass('display-none'); + exportProject: function() { + if(this.exporting) { + return; } - }, + this.showExportAnimation(); + this.exporting = true; - updateCoursePreview: function(previewWindow) { var courseId = Origin.editor.data.course.get('_id'); var tenantId = Origin.sessionModel.get('tenantId'); - previewWindow.location.href = '/preview/' + tenantId + '/' + courseId + '/'; - }, - - previewProject: function(e) { - e && e.preventDefault(); - - var self = this; - if (helpers.validateCourseContent(this.currentCourse) && !Origin.editor.isPreviewPending) { - var previewWindow = window.open('/loading', 'preview'); - Origin.editor.isPreviewPending = true; - $('.navigation-loading-indicator').removeClass('display-none'); - $('.editor-common-sidebar-preview-inner').addClass('display-none'); - $('.editor-common-sidebar-previewing').removeClass('display-none'); - - $.ajax({ - method: 'get', - url: '/api/output/' + Origin.constants.outputPlugin + '/preview/' + this.currentCourseId, - success: function (jqXHR, textStatus, errorThrown) { - if (jqXHR.success) { - if (jqXHR.payload && typeof(jqXHR.payload.pollUrl) != 'undefined' && jqXHR.payload.pollUrl != '') { - // Ping the remote URL to check if the job has been completed - self.updatePreviewProgress(jqXHR.payload.pollUrl, previewWindow); - } else { - self.updateCoursePreview(previewWindow); - self.resetPreviewProgress(); - } - } else { - self.resetPreviewProgress(); - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorgeneratingpreview') - }); - previewWindow.close(); - } - }, - error: function (jqXHR, textStatus, errorThrown) { - self.resetPreviewProgress(); - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorgeneric') - }); - previewWindow.close(); - } + $.get('/export/' + tenantId + '/' + courseId, _.bind(function(data, textStatus, jqXHR) { + // success + var form = document.createElement('form'); + this.$el.append(form); + form.setAttribute('action', '/export/' + tenantId + '/' + courseId + '/' + data.zipName + '/download.zip'); + form.submit(); + }, this)).fail(_.bind(function(jqXHR, textStatus, errorThrown) { + // failure + var messageText = errorThrown; + if(jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.message) { + messageText += ':
      ' + jqXHR.responseJSON.message; + } + Origin.Notify.alert({ + type: 'error', + title: Origin.l10n.t('app.exporterrortitle'), + text: messageText }); - } + }, this)).always(_.bind(function() { + // always + this.showExportAnimation(false); + this.exporting = false; + }, this)); }, updatePreviewProgress: function(url, previewWindow) { @@ -269,17 +232,30 @@ define(function(require){ Origin.editor.isDownloadPending = false; }, - addToClipboard: function(model) { - _.defer(_.bind(function() { _.invoke(Origin.editor.data.clipboards.models, 'destroy') }, this)); + showExportAnimation: function(show) { + if(show !== false) { + $('.editor-common-sidebar-export-inner').addClass('display-none'); + $('.editor-common-sidebar-exporting').removeClass('display-none'); + } else { + $('.editor-common-sidebar-export-inner').removeClass('display-none'); + $('.editor-common-sidebar-exporting').addClass('display-none'); + } + }, + updateCoursePreview: function(previewWindow) { + var courseId = Origin.editor.data.course.get('_id'); + var tenantId = Origin.sessionModel.get('tenantId'); + previewWindow.location.href = '/preview/' + tenantId + '/' + courseId + '/'; + }, + + addToClipboard: function(model) { var postData = { objectId: model.get('_id'), courseId: Origin.editor.data.course.get('_id'), - referenceType: model._siblings + referenceType: model._siblingTypes }; $.post('/api/content/clipboard/copy', postData, _.bind(function(jqXHR) { Origin.editor.clipboardId = jqXHR.clipboardId; - Origin.editor.pasteParentModel = model.getParent(); this.showPasteZones(model.get('_type')); }, this)).fail(_.bind(function (jqXHR, textStatus, errorThrown) { Origin.Notify.alert({ @@ -307,23 +283,20 @@ define(function(require){ }, pasteFromClipboard: function(parentId, sortOrder, layout) { - var data = { + Origin.trigger('editorView:pasteCancel'); + var postData = { id: Origin.editor.clipboardId, parentId: parentId, layout: layout, sortOrder: sortOrder, courseId: Origin.editor.data.course.get('_id') }; - $.post('/api/content/clipboard/paste', data, function(jqXHR) { + $.post('/api/content/clipboard/paste', postData, function(data) { Origin.editor.clipboardId = null; - Origin.editor.pasteParentModel = null; - Origin.trigger('editor:refreshData', function() { - /** - * FIXME views should handle rendering the new data, - * we shouldn't need to refresh the whole page - */ - Backbone.history.loadUrl(); - }, this); + Origin.trigger('editorView:pasted:' + postData.parentId, { + _id: data._id, + sortOrder: postData.sortOrder + }); }).fail(function(jqXHR, textStatus, errorThrown) { Origin.Notify.alert({ type: 'error', @@ -369,24 +342,19 @@ define(function(require){ }, renderEditorPage: function() { - var view = new EditorPageView({ - model: Origin.editor.data.contentObjects.findWhere({ _id: this.currentPageId }) - }); - this.$('.editor-inner').html(view.$el); - }, - - cutContent: function(view) { - var type = helpers.capitalise(view.model.get('_type')); - var collectionType = view.model._siblings; - - this.addToClipboard(view.model); - - // Remove model from collection (to save fetching) and destroy it - Origin.editor.data[collectionType].remove(view.model); - view.model.destroy(); - - _.defer(function () { - Origin.trigger('editorView:cut' + type + ':' + view.model.get('_parentId'), view); + (new ContentObjectModel({ + _id: this.currentPageId + })).fetch({ + success: function(model) { + var view = new EditorPageView({ model: model }); + this.$('.editor-inner').html(view.$el); + }, + error: function() { + Origin.Notify.alert({ + type: 'error', + text: 'app.errorfetchingdata' + }); + } }); }, diff --git a/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs b/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs index 73e5b4e089..bf40b12dc8 100644 --- a/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs +++ b/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs @@ -1,89 +1,65 @@ {{#if value}} + {{! Work out which type it is and display the correct html markup}} -{{! Work out which type it is and display the correct html markup}} -{{#ifValueEquals type 'image'}} - {{#ifAssetIsExternal value}} - - {{else}} - {{#ifAssetIsHeroImage value}} - + {{#ifValueEquals type 'image'}} + {{#if thumbUrl}} + {{else}} - {{#if thumbnailPath}} - - {{else}} - {{#ifImageIsCourseAsset value}} - - {{else}} - - - - {{/ifImageIsCourseAsset}} - {{/if}} - {{/ifAssetIsHeroImage}} - {{/ifAssetIsExternal}} -{{/ifValueEquals}} + + + + {{/if}} + {{/ifValueEquals}} -{{#ifValueEquals type 'other'}} - {{#ifAssetIsExternal value}} - - -
      {{value}}
      -
      - {{else}} - - -
      {{value}}
      -
      - {{/ifAssetIsExternal}} + {{#ifValueEquals type 'other'}} + + +
      {{value}}
      +
      + {{/ifValueEquals}} -{{/ifValueEquals}} - -{{#ifValueEquals type 'video'}} - {{#ifAssetIsExternal value}} - - -
      {{value}}
      -
      - {{else}} - - {{/ifAssetIsExternal}} -{{/ifValueEquals}} - -{{#ifValueEquals type 'audio'}} - {{#ifAssetIsExternal value}} - - -
      {{value}}
      -
      - {{else}} - - -
      {{value}}
      -
      - {{/ifAssetIsExternal}} + {{#ifValueEquals type 'video'}} + {{#ifAssetIsExternal value}} + + +
      {{value}}
      +
      + {{else}} + + {{/ifAssetIsExternal}} + {{/ifValueEquals}} -{{/ifValueEquals}} + {{#ifValueEquals type 'audio'}} + + +
      {{value}}
      +
      + {{/ifValueEquals}} -
      - {{#ifAssetIsExternal value}} - - {{else}} - - {{/ifAssetIsExternal}} -
      +
      + +
      {{else}} -