diff --git a/frontend/src/modules/projects/views/projectView.js b/frontend/src/modules/projects/views/projectView.js
index 9ab4a47f30..406bacd2e5 100644
--- a/frontend/src/modules/projects/views/projectView.js
+++ b/frontend/src/modules/projects/views/projectView.js
@@ -76,14 +76,22 @@ define(function(require) {
deleteProjectPrompt: function(event) {
event && event.preventDefault();
if(this.model.get('_isShared') === true) {
- Origin.Notify.confirm({
- type: 'warning',
- title: Origin.l10n.t('app.deletesharedproject'),
- text: Origin.l10n.t('app.confirmdeleteproject') + '
' + Origin.l10n.t('app.confirmdeletesharedprojectwarning'),
- destructive: true,
- callback: _.bind(this.deleteProjectConfirm, this)
- });
+ if(this.model.get('createdBy') === Origin.sessionModel.id){
+ Origin.Notify.confirm({
+ type: 'warning',
+ title: Origin.l10n.t('app.deletesharedproject'),
+ text: Origin.l10n.t('app.confirmdeleteproject') + '
' + Origin.l10n.t('app.confirmdeletesharedprojectwarning'),
+ destructive: true,
+ callback: _.bind(this.deleteProjectConfirm, this)
+ });
+ } else {
+ Origin.Notify.alert({
+ type: 'error',
+ text: Origin.l10n.t('app.errorpermission')
+ });
+ }
return;
+
}
Origin.Notify.confirm({
type: 'warning',
diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js
index e6ca357a4c..a6ea6a645b 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,142 +7,85 @@ define(function(require){
var ProjectsView = OriginView.extend({
className: 'projects',
- settings: {
- autoRender: true,
- preferencesKey: 'dashboard'
+ supportedLayouts: [
+ "grid",
+ "list"
+ ],
+
+ postRender: function() {
+ this.settings.preferencesKey = 'dashboard';
+ this.initUserPreferences();
+ this.initEventListeners();
+ this.initPaging();
},
- preRender: function(options) {
- this.setupFilterSettings();
+ initEventListeners: function() {
+ this._doLazyScroll = _.bind(_.throttle(this.doLazyScroll, 250), this);
+ this._onResize = _.bind(_.debounce(this.onResize, 250), this);
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._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.listenTo(this.collection, {
- 'add': this.appendProjectItem,
- 'sync': this.checkIfCollectionIsEmpty
- });
- },
+ this.supportedLayouts.forEach(function(layout) {
+ this.listenTo(Origin, 'dashboard:layout:' + layout, function() { this.doLayout(layout); });
+ }, 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;
+ this.listenTo(this.collection, 'add', this.appendProjectItem);
- // 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();
+ $('.contentPane').on('scroll', this._doLazyScroll);
},
- switchLayoutToList: function() {
- this.getProjectsContainer().removeClass('grid-layout').addClass('list-layout');
- this.setUserPreference('layout','list');
- },
+ initUserPreferences: function() {
+ var prefs = this.getUserPreferences();
- switchLayoutToGrid: function() {
- this.getProjectsContainer().removeClass('list-layout').addClass('grid-layout');
- this.setUserPreference('layout','grid');
+ this.doLayout(prefs.layout);
+ this.doSort(prefs.sort, false);
+ this.doFilter(prefs.search, prefs.tags, false);
+ // set relevant filters as 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);
},
- sortAscending: function(shouldRenderProjects) {
- this.sort = { title: 1 };
- this.setUserPreference('sort','asc');
- if(shouldRenderProjects) this.updateCollection(true);
- },
+ // Set some default preferences
+ getUserPreferences: function() {
+ var prefs = OriginView.prototype.getUserPreferences.apply(this, arguments);
- sortDescending: function(shouldRenderProjects) {
- this.sort = { title: -1 };
- this.setUserPreference('sort','desc');
- if(shouldRenderProjects) this.updateCollection(true);
- },
+ if(!prefs.layout) prefs.layout = 'grid';
+ if(!prefs.sort) prefs.sort = 'asc';
- sortLastUpdated: function(shouldRenderProjects) {
- this.sort = { updatedAt: -1 };
- this.setUserPreference('sort','updated');
- if (shouldRenderProjects) this.updateCollection(true);
+ return prefs;
},
- 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();
+ initPaging: function() {
+ if(this.resizeTimer) {
+ clearTimeout(this.resizeTimer);
+ this.resizeTimer = -1;
}
- // 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);
- },
-
- lazyRenderCollection: function() {
- // Adjust limit based upon the denominator
- this.courseLimit += this.courseDenominator;
- this.updateCollection(false);
+ // 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;
+ // need another reset to get the actual pageSize number of items
+ this.resetCollection(this.setViewToReady);
+ }, this));
},
getProjectsContainer: function() {
@@ -152,130 +93,113 @@ define(function(require){
},
emptyProjectsContainer: function() {
- // Trigger event to kill zombie views
Origin.trigger('dashboard:dashboardView:removeSubViews');
- // Empty collection container
this.getProjectsContainer().empty();
},
- 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) {
+ appendProjectItem: function(model) {
+ var viewClass = model.isEditable() ? ProjectView : SharedProjectView;
+ this.getProjectsContainer().append(new viewClass({ model: model }).$el);
+ },
+
+ convertFilterTextToPattern: function(filterText) {
+ var pattern = '.*' + filterText.toLowerCase() + '.*';
+ return { title: pattern };
+ },
+
+ resetCollection: function(cb) {
+ this.emptyProjectsContainer();
+ this.fetchCount = 0;
+ this.shouldStopFetches = false;
+ this.collection.reset();
+ this.fetchCollection(cb);
+ },
+
+ 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 '
';
- });
- },
-
- addTag: function(filterType) {
- // add filter to this.filters
- this.tags.push(filterType);
- this.filterCollection();
+ doLayout: function(layout) {
+ if(this.supportedLayouts.indexOf(layout) === -1) {
+ return;
+ }
+ this.getProjectsContainer().attr('data-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();
},
- removeTag: function(filterType) {
- // remove filter from this.filters
- this.tags = _.filter(this.tags, function(item) { return item != filterType; });
- this.filterCollection();
- },
+ doFilter: function(text, tags, fetch) {
+ text = text || '';
+ this.filterText = text;
+ this.search = this.convertFilterTextToPattern(text);
+ this.setUserPreference('search', text, true);
- filterCollection: function() {
- this.search.tags = this.tags.length
- ? { $all: this.tags }
- : null ;
- this.updateCollection(true);
- },
+ tags = tags || [];
+ this.tags = _.pluck(tags, 'id');
+ this.setUserPreference('tags', tags, true);
- convertFilterTextToPattern: function(filterText) {
- var pattern = '.*' + filterText.toLowerCase() + '.*';
- return { title: pattern};
+ if(fetch !== false) this.resetCollection();
},
- filterBySearchInput: function (filterText) {
- this.filterText = filterText;
- this.search = this.convertFilterTextToPattern(filterText);
- this.setUserPreference('search', filterText);
- this.updateCollection(true);
+ onResize: function() {
+ this.initPaging();
},
- filterCoursesByTags: function(tags) {
- this.setUserPreference('tags', tags);
- this.tags = _.pluck(tags, 'id');
- this.updateCollection(true);
- },
+ remove: function() {
+ $('.contentPane').off('scroll', this._doLazyScroll);
- setupLazyScrolling: function() {
- var $projectContainer = $('.projects');
- var $projectContainerInner = $('.projects-inner');
- // Remove event before attaching
- $projectContainer.off('scroll');
-
- $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));
+ OriginView.prototype.remove.apply(this, arguments);
}
+
}, {
template: 'projects'
});
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) {
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
}
}
}
diff --git a/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs b/frontend/src/modules/scaffold/templates/scaffoldAsset.hbs
index 73e5b4e089..2f7da0df5d 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}}
-
- {{/ifImageIsCourseAsset}}
- {{/if}}
- {{/ifAssetIsHeroImage}}
- {{/ifAssetIsExternal}}
-{{/ifValueEquals}}
+
+ {{/if}}
+ {{/ifValueEquals}}
-{{#ifValueEquals type 'other'}}
- {{#ifAssetIsExternal value}}
-
+ {{/ifValueEquals}}
-{{/ifValueEquals}}
-
-{{#ifValueEquals type 'video'}}
- {{#ifAssetIsExternal value}}
-
- {{/ifAssetIsExternal}}
-{{/ifValueEquals}}
-
-{{#ifValueEquals type 'audio'}}
- {{#ifAssetIsExternal value}}
-
- {{/ifAssetIsExternal}}
+ {{#ifValueEquals type 'video'}}
+ {{#ifAssetIsExternal value}}
+
+ {{/ifAssetIsExternal}}
+ {{/ifValueEquals}}
-{{/ifValueEquals}}
+ {{#ifValueEquals type 'audio'}}
+
-
-
-
-
-
-
-{{/if}}
\ No newline at end of file
+{{/if}}
diff --git a/frontend/src/modules/scaffold/views/scaffoldAssetView.js b/frontend/src/modules/scaffold/views/scaffoldAssetView.js
index 79aa971020..bd57e32132 100644
--- a/frontend/src/modules/scaffold/views/scaffoldAssetView.js
+++ b/frontend/src/modules/scaffold/views/scaffoldAssetView.js
@@ -1,342 +1,310 @@
// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE
define(function(require) {
-
- var Backbone = require('backbone');
- var BackboneForms = require('backbone-forms');
- var Origin = require('core/origin');
- var AssetManagementModalView = require('modules/assetManagement/views/assetManagementModalView');
- var AssetCollection = require('modules/assetManagement/collections/assetCollection');
- var CourseAssetModel = require('core/models/courseAssetModel');
-
- var ScaffoldAssetView = Backbone.Form.editors.Base.extend({
-
- tagName: 'div',
-
- events: {
- 'change input': function() {
- // The 'change' event should be triggered whenever something happens
- // that affects the result of `this.getValue()`.
- this.toggleFieldAvailibility();
- //this.checkValueHasChanged();
- this.trigger('change', this);
- },
- 'focus input': function() {
- // The 'focus' event should be triggered whenever an input within
- // this editor becomes the `document.activeElement`.
- this.trigger('focus', this);
- // This call automatically sets `this.hasFocus` to `true`.
- },
- 'blur input': function() {
- // The 'blur' event should be triggered whenever an input within
- // this editor stops being the `document.activeElement`.
- this.trigger('blur', this);
- // This call automatically sets `this.hasFocus` to `false`.
- },
- 'click .scaffold-asset-picker': 'onAssetButtonClicked',
- 'click .scaffold-asset-external': 'onExternalAssetButtonClicked',
- 'click .scaffold-asset-clear': 'onClearButtonClicked',
- 'click .scaffold-asset-external-input-save': 'onExternalAssetSaveClicked',
- 'click .scaffold-asset-external-input-cancel': 'onExternalAssetCancelClicked',
- 'click .scaffold-asset-clear-external': 'onExternalClearButtonClicked'
-
- },
-
- initialize: function(options) {
- this.listenTo(Origin, 'scaffold:assets:autofill', this.onAutofill);
- // Call parent constructor
- Backbone.Form.editors.Base.prototype.initialize.call(this, options);
-
- },
-
- onAutofill: function(courseAssetObject, value) {
- this.value = value;
- this.createCourseAsset(courseAssetObject);
- },
-
- render: function() {
- var assetType = this.schema.fieldType.replace('Asset:', '');
- this.$el.html(Handlebars.templates[this.constructor.template]({value: this.value, type: assetType}));
- this.setValue(this.value);
- // Should see if the field contains anything on render
- // if so disable it
- this.toggleFieldAvailibility();
-
- return this;
- },
-
- getValue: function() {
- return this.value || '';
- //return this.$('input').val();
- },
-
- setValue: function(value) {
- this.value = value;
- //this.$('input').val(value);
- },
-
- focus: function() {
- if (this.hasFocus) return;
-
- // This method call should result in an input within this edior
- // becoming the `document.activeElement`.
- // This, in turn, should result in this editor's `focus` event
- // being triggered, setting `this.hasFocus` to `true`.
- // See above for more detail.
- this.$('input').focus();
- },
-
- blur: function() {
- if (!this.hasFocus) return;
-
- this.$('input').blur();
- },
-
- toggleFieldAvailibility: function() {
- if (this.getValue().length === 0) {
- this.$('input').attr('disabled', false);
- //this.$('.scaffold-asset-clear').addClass('display-none');
- } else {
- //this.$('.scaffold-asset-clear').removeClass('display-none');
- this.$('input').attr('disabled', true);
- }
- },
-
- checkValueHasChanged: function() {
- if ('heroImage' === this.key){
- this.saveModel(false, {heroImage: this.getValue()});
- return;
- }
- var contentTypeId = Origin.scaffold.getCurrentModel().get('_id');
- var contentType = Origin.scaffold.getCurrentModel().get('_type');
- var fieldname = this.getValue() ? this.getValue().replace('course/assets/', '') : '';
- this.removeCourseAsset(contentTypeId, contentType, fieldname);
- },
-
- onExternalAssetButtonClicked: function(event) {
- event.preventDefault();
- this.$('.scaffold-asset-external-input').removeClass('display-none');
- this.$('.scaffold-asset-buttons').addClass('display-none');
- },
-
- onExternalAssetSaveClicked: function(event) {
- event.preventDefault();
- var inputValue = this.$('.scaffold-asset-external-input-field').val();
- // Check that there's actually some value
- if (inputValue.length > 0) {
- this.value = inputValue;
- this.saveModel(false);
- } else {
- // If nothing don't bother saving - instead revert to showing the buttons again
- this.$('.scaffold-asset-external-input').addClass('display-none');
- this.$('.scaffold-asset-buttons').removeClass('display-none');
- }
- },
-
- onExternalAssetCancelClicked: function(event) {
- event.preventDefault();
- this.$('.scaffold-asset-external-input').addClass('display-none');
- this.$('.scaffold-asset-buttons').removeClass('display-none');
- },
-
- onAssetButtonClicked: function(event) {
- event.preventDefault();
- Origin.trigger('modal:open', AssetManagementModalView, {
- collection: new AssetCollection,
- assetType: this.schema.fieldType,
- onUpdate: function(data) {
- if (data) {
-
- if ('heroImage' === this.key){
- this.setValue(data.assetId);
- this.saveModel(false, {heroImage: data.assetId});
- return;
- }
- // Setup courseasset
- var contentTypeId = Origin.scaffold.getCurrentModel().get('_id') || '';
- var contentType = Origin.scaffold.getCurrentModel().get('_type');
- var contentTypeParentId = Origin.scaffold.getCurrentModel().get('_parentId') || Origin.editor.data.course.get('_id');
- var fieldname = data.assetFilename;
- var assetId = data.assetId;
-
-
- var courseAssetObject = {
- contentTypeId: contentTypeId,
- contentType: contentType,
- contentTypeParentId: contentTypeParentId,
- fieldname: fieldname,
- assetId: assetId
- }
-
- // If the data is meant to autofill the rest of the graphic sizes
- // pass out an event instead - this is currently only used for the graphic component
- if (data._shouldAutofill) {
- Origin.trigger('scaffold:assets:autofill', courseAssetObject, data.assetLink);
- return;
- }
-
- this.value = data.assetLink;
-
- this.createCourseAsset(courseAssetObject);
-
- }
- },
- onCancel: function(data) {}
- }, this);
- },
-
- onClearButtonClicked: function(event) {
- event.preventDefault();
- this.checkValueHasChanged();
- this.setValue('');
- this.toggleFieldAvailibility();
- },
-
- onExternalClearButtonClicked: function(event) {
- event.preventDefault();
- this.setValue('');
- this.saveModel(false);
- this.toggleFieldAvailibility();
- },
-
- findAsset: function (contentTypeId, contentType, fieldname) {
- var searchCriteria = {
- _contentType: contentType,
- _fieldName: fieldname
- };
-
- if (contentTypeId) {
- searchCriteria._contentTypeId = contentTypeId;
- } else {
- searchCriteria._contentTypeParentId = Origin.editor.data.course.get('_id');
- }
- var asset = Origin.editor.data.courseassets.findWhere(searchCriteria);
-
- if (!asset) {
- // HACK - Try relaxing the search criteria for historic data
- asset = Origin.editor.data.courseassets.findWhere({_contentType: contentType, _fieldName: fieldname});
- }
-
- return asset ? asset : false;
- },
-
- createCourseAsset: function (courseAssetObject) {
- var self = this;
-
- var courseAsset = new CourseAssetModel();
- courseAsset.save({
- _courseId : Origin.editor.data.course.get('_id'),
- _contentType : courseAssetObject.contentType,
- _contentTypeId : courseAssetObject.contentTypeId,
- _fieldName : courseAssetObject.fieldname,
- _assetId : courseAssetObject.assetId,
- _contentTypeParentId: courseAssetObject.contentTypeParentId
- },{
- error: function(error) {
- Origin.Notify.alert({
- type: 'error',
- text: Origin.l10n.t('app.errorsaveasset')
- });
- },
- success: function() {
- self.saveModel(true);
- }
- });
-
- },
-
- removeCourseAsset: function (contentTypeId, contentType, fieldname) {
- var that = this;
- var courseAsset = this.findAsset(contentTypeId, contentType, fieldname);
- if (courseAsset) {
- courseAsset.destroy({
- success: function(success) {
- that.saveModel(true);
- },
- error: function(error) {
- }
- });
- } else {
- this.setValue('');
- this.saveModel(true);
- }
+ var Backbone = require('backbone');
+ var BackboneForms = require('backbone-forms');
+ var Origin = require('core/origin');
+ var Helpers = require('core/helpers');
+ var AssetManagementModalView = require('modules/assetManagement/views/assetManagementModalView');
+ var AssetCollection = require('modules/assetManagement/collections/assetCollection');
+ var ContentCollection = require('core/collections/contentCollection');
+ var CourseAssetModel = require('core/models/courseAssetModel');
+
+ var ScaffoldAssetView = Backbone.Form.editors.Base.extend({
+ tagName: 'div',
+ events: {
+ // triggered whenever something happens that affects the result of `this.getValue()`
+ 'change input': function() {
+ this.toggleFieldAvailibility();
+ this.trigger('change', this);
+ },
+ // triggered whenever an input in this editor becomes the `document.activeElement`
+ 'focus input': function() {
+ this.trigger('focus', this);
+ },
+ // triggered whenever an input in this editor stops being the `document.activeElement`
+ 'blur input': function() {
+ this.trigger('blur', this);
+ },
+ 'click .scaffold-asset-picker': 'onAssetButtonClicked',
+ 'click .scaffold-asset-external': 'onExternalAssetButtonClicked',
+ 'click .scaffold-asset-clear': 'onClearButtonClicked',
+ 'click .scaffold-asset-external-input-save': 'onExternalAssetSaveClicked',
+ 'click .scaffold-asset-external-input-cancel': 'onExternalAssetCancelClicked',
+ },
+
+ initialize: function(options) {
+ this.listenTo(Origin, 'scaffold:assets:autofill', this.onAutofill);
+ Backbone.Form.editors.Base.prototype.initialize.call(this, options);
+ },
+
+ render: function() {
+ var template = Handlebars.templates[this.constructor.template];
+ var templateData = {
+ value: this.value,
+ type: this.schema.fieldType.replace('Asset:', '')
+ };
+ // this delgate function is async, so does a re-render once the data is loaded
+ var _renderDelegate = _.bind(function(assetId) {
+ if(assetId) {
+ templateData.url = '/api/asset/serve/' + assetId;
+ templateData.thumbUrl = '/api/asset/thumb/' + assetId;
+ this.$el.html(template(templateData));
+ }
+ }, this);
+ if(Helpers.isAssetExternal(this.value)) {
+ // we know there won't be a courseasset record, so don't bother fetching
+ templateData.url = this.value;
+ templateData.thumbUrl = this.value;
+ } else {
+ // don't have asset ID, so query courseassets for matching URL && content ID
+ this.fetchCourseAsset({
+ _fieldName: this.value.split('/').pop(),
+ _contentTypeId: Origin.scaffold.getCurrentModel().get('_id')
+ }, function(error, collection) {
+ if(error) {
+ console.error(error);
+ return _renderDelegate();
+ }
+ if(collection.length === 0) {
+ return _renderDelegate();
+ }
+ _renderDelegate(collection.at(0).get('_assetId'));
+ });
+ }
+ // we do a first pass render here to satisfy code expecting us to return 'this'
+ this.setValue(this.value);
+ this.toggleFieldAvailibility();
+ this.$el.html(template(templateData));
+ return this;
+ },
+
+ getValue: function() {
+ return this.value || '';
+ },
+
+ setValue: function(value) {
+ this.value = value;
+ },
+
+ toggleFieldAvailibility: function() {
+ this.$('input').attr('disabled', this.getValue().length === 0);
+ },
+
+ checkValueHasChanged: function() {
+ var val = this.getValue();
+ if ('heroImage' === this.key) {
+ this.saveModel({ heroImage: val });
+ return;
+ }
+ if(Helpers.isAssetExternal(val)) {
+ this.saveModel();
+ return;
+ }
+ var contentTypeId = Origin.scaffold.getCurrentModel().get('_id');
+ var contentType = Origin.scaffold.getCurrentModel().get('_type');
+ var fieldname = val.replace('course/assets/', '');
+ this.removeCourseAsset(contentTypeId, contentType, fieldname);
+ },
+
+ createCourseAsset: function(courseAssetObject) {
+ (new CourseAssetModel()).save({
+ _courseId : Origin.editor.data.course.get('_id'),
+ _contentType : courseAssetObject.contentType,
+ _contentTypeId : courseAssetObject.contentTypeId,
+ _fieldName : courseAssetObject.fieldname,
+ _assetId : courseAssetObject.assetId,
+ _contentTypeParentId: courseAssetObject.contentTypeParentId
+ }, {
+ success: _.bind(function() {
+ this.saveModel();
+ }, this),
+ error: function(error) {
+ Origin.Notify.alert({
+ type: 'error',
+ text: Origin.l10n.t('app.errorsaveasset')
+ });
+ }
+ });
+ },
+
+ fetchCourseAsset: function(searchCriteria, cb) {
+ if(!searchCriteria._contentTypeId) {
+ searchCriteria._contentTypeParentId = Origin.editor.data.course.get('_id');
+ }
+ (new ContentCollection(null, { _type: 'courseasset' })).fetch({
+ data: searchCriteria,
+ success: function(collection) {
+ cb(null, collection);
},
-
- saveModel: function(shouldResetAssetCollection, attributesToSave) {
- var that = this;
- var isUsingAlternativeModel = false;
- var currentModel = Origin.scaffold.getCurrentModel()
- var alternativeModel = Origin.scaffold.getAlternativeModel();
- var alternativeAttribute = Origin.scaffold.getAlternativeAttribute();
- var isPatch = false;
-
- attributesToSave = typeof attributesToSave == 'undefined'
- ? []
- : attributesToSave;
-
- // Check if alternative model should be used
- if (alternativeModel) {
- currentModel = alternativeModel;
- isUsingAlternativeModel = true;
- }
-
- var currentForm = Origin.scaffold.getCurrentForm();
- var errors = currentForm.commit({validate: false});
-
- // Check if alternative attribute should be used
- if (alternativeAttribute) {
- attributesToSave[alternativeAttribute] = Origin.scaffold.getCurrentModel().attributes;
- }
-
- if (!attributesToSave && !attributesToSave.length) {
- currentModel.pruneAttributes();
- currentModel.unset('tags');
- } else {
- isPatch = true;
+ error: function(model, response) {
+ cb('Failed to fetch data for', model.get('filename') + ':', response.statusText);
+ }
+ });
+ },
+
+ removeCourseAsset: function(contentTypeId, contentType, fieldname) {
+ this.fetchCourseAsset({
+ _contentTypeId: contentTypeId,
+ _contentType: contentType,
+ _fieldName: fieldname
+ }, _.bind(function(error, courseassets) {
+ if(error) {
+ return console.error(error);
+ }
+ if(courseassets.length === 0) {
+ this.setValue('');
+ this.saveModel();
+ return;
+ }
+ // delete all matching courseassets and then saveModel
+ Helpers.forParallelAsync(courseassets, function(model, index, cb) {
+ model.destroy({
+ success: cb,
+ error: function(error) {
+ console.error('Failed to destroy courseasset record', courseasset.get('_id'));
+ cb();
}
-
- currentModel.save(attributesToSave, {
- patch: isPatch,
- error: function() {
- Origin.Notify.alert({
- type: 'error',
- text: Origin.l10n.t('app.errorsaveasset')
- });
- },
- success: function() {
-
- // Sometimes we don't need to reset the courseassets
- if (shouldResetAssetCollection) {
-
- Origin.editor.data.courseassets.fetch({
- reset:true,
- success: function() {
- that.render();
- that.trigger('change', that);
- }
- });
-
- } else {
- that.render();
- that.trigger('change', that);
- }
- }
- })
+ });
+ }, _.bind(this.saveModel, this));
+ }, this));
+ },
+
+ saveModel: function(attributesToSave) {
+ var isUsingAlternativeModel = false;
+ var currentModel = Origin.scaffold.getCurrentModel();
+ var alternativeModel = Origin.scaffold.getAlternativeModel();
+ var alternativeAttribute = Origin.scaffold.getAlternativeAttribute();
+ // Check if alternative model should be used
+ if (alternativeModel) {
+ currentModel = alternativeModel;
+ isUsingAlternativeModel = true;
+ }
+ // run schema validation
+ Origin.scaffold.getCurrentForm().commit({ validate: false });
+ // Check if alternative attribute should be used
+ if (alternativeAttribute) {
+ attributesToSave[alternativeAttribute] = Origin.scaffold.getCurrentModel().attributes;
+ }
+ if (!attributesToSave) {
+ currentModel.pruneAttributes();
+ currentModel.unset('tags');
+ }
+ currentModel.save(attributesToSave, {
+ patch: attributesToSave !== undefined,
+ success: _.bind(function() {
+ this.render();
+ this.trigger('change', this);
+ }, this),
+ error: function() {
+ Origin.Notify.alert({
+ type: 'error',
+ text: Origin.l10n.t('app.errorsaveasset')
+ });
}
-
- }, {
- template: "scaffoldAsset"
- });
-
- Origin.on('origin:dataReady', function() {
- // Add Image editor to the list of editors
- Origin.scaffold.addCustomField('Asset:image', ScaffoldAssetView);
- Origin.scaffold.addCustomField('Asset:audio', ScaffoldAssetView);
- Origin.scaffold.addCustomField('Asset:video', ScaffoldAssetView);
- Origin.scaffold.addCustomField('Asset:other', ScaffoldAssetView);
- Origin.scaffold.addCustomField('Asset', ScaffoldAssetView);
- })
-
-
- return ScaffoldAssetView;
-
-})
+ });
+ },
+
+ /**
+ * Event handling
+ */
+
+ focus: function() {
+ if (this.hasFocus) return;
+ /**
+ * makes input the `document.activeElement`, triggering this editor's
+ * focus` event, and setting `this.hasFocus` to `true`.
+ * See this.events above for more detail
+ */
+ this.$('input').focus();
+ },
+
+ blur: function() {
+ if(this.hasFocus) this.$('input').blur();
+ },
+
+ onAssetButtonClicked: function(event) {
+ event.preventDefault();
+ Origin.trigger('modal:open', AssetManagementModalView, {
+ collection: new AssetCollection,
+ assetType: this.schema.fieldType,
+ _shouldShowScrollbar: false,
+ onUpdate: function(data) {
+ if (!data) {
+ return;
+ }
+ if ('heroImage' === this.key) {
+ this.setValue(data.assetId);
+ this.saveModel({ heroImage: data.assetId });
+ return;
+ }
+ var courseAssetObject = {
+ contentTypeId: Origin.scaffold.getCurrentModel().get('_id') || '',
+ contentType: Origin.scaffold.getCurrentModel().get('_type'),
+ contentTypeParentId: Origin.scaffold.getCurrentModel().get('_parentId') || Origin.editor.data.course.get('_id'),
+ fieldname: data.assetFilename,
+ assetId: data.assetId
+ };
+ // all ScaffoldAssetViews listen to the autofill event, so we trigger
+ // that rather than call code directly
+ // FIXME only works with graphic components
+ if (data._shouldAutofill) {
+ Origin.trigger('scaffold:assets:autofill', courseAssetObject, data.assetLink);
+ return;
+ }
+ this.value = data.assetLink;
+ this.createCourseAsset(courseAssetObject);
+ }
+ }, this);
+ },
+
+ onClearButtonClicked: function(event) {
+ event.preventDefault();
+ this.checkValueHasChanged();
+ this.setValue('');
+ this.toggleFieldAvailibility();
+ },
+
+ onAutofill: function(courseAssetObject, value) {
+ this.value = value;
+ this.createCourseAsset(courseAssetObject);
+ },
+
+ onExternalAssetButtonClicked: function(event) {
+ event.preventDefault();
+ this.$('.scaffold-asset-external-input').removeClass('display-none');
+ this.$('.scaffold-asset-buttons').addClass('display-none');
+ },
+
+ onExternalAssetSaveClicked: function(event) {
+ event.preventDefault();
+ var inputValue = this.$('.scaffold-asset-external-input-field').val();
+
+ if (inputValue.length === 0) { // nothing to save
+ this.$('.scaffold-asset-external-input').addClass('display-none');
+ this.$('.scaffold-asset-buttons').removeClass('display-none');
+ return;
+ }
+ this.setValue(inputValue);
+ this.saveModel();
+ },
+
+ onExternalAssetCancelClicked: function(event) {
+ event.preventDefault();
+ this.$('.scaffold-asset-external-input').addClass('display-none');
+ this.$('.scaffold-asset-buttons').removeClass('display-none');
+ },
+ }, {
+ template: "scaffoldAsset"
+ });
+
+ Origin.on('origin:dataReady', function() {
+ // Add Image editor to the list of editors
+ Origin.scaffold.addCustomField('Asset:image', ScaffoldAssetView);
+ Origin.scaffold.addCustomField('Asset:audio', ScaffoldAssetView);
+ Origin.scaffold.addCustomField('Asset:video', ScaffoldAssetView);
+ Origin.scaffold.addCustomField('Asset:other', ScaffoldAssetView);
+ Origin.scaffold.addCustomField('Asset', ScaffoldAssetView);
+ });
+
+ return ScaffoldAssetView;
+});
diff --git a/frontend/src/modules/scaffold/views/scaffoldCodeEditorView.js b/frontend/src/modules/scaffold/views/scaffoldCodeEditorView.js
index 6d19ab9190..4398780b03 100644
--- a/frontend/src/modules/scaffold/views/scaffoldCodeEditorView.js
+++ b/frontend/src/modules/scaffold/views/scaffoldCodeEditorView.js
@@ -39,7 +39,7 @@ define(function(require) {
this.setValue(this.value);
_.defer(_.bind(function() {
- window.ace.config.set("basePath", "./build/js/ace");
+ window.ace.config.set("basePath", "./js/ace");
this.editor = window.ace.edit(this.$el[0]);
var session = this.editor.getSession();
diff --git a/install.js b/install.js
index 281f23a94d..6506c95d49 100644
--- a/install.js
+++ b/install.js
@@ -79,6 +79,27 @@ installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) {
pattern: installHelpers.inputHelpers.numberValidator,
default: 27017
},
+ {
+ name: 'dbUser',
+ type: 'string',
+ description: 'Database server user',
+ pattern: installHelpers.inputHelpers.alphanumValidator,
+ default: ''
+ },
+ {
+ name: 'dbPass',
+ type: 'string',
+ description: 'Database server password',
+ pattern: installHelpers.inputHelpers.alphanumValidator,
+ default: ''
+ },
+ {
+ name: 'dbAuthSource',
+ type: 'string',
+ description: 'Database server authentication database',
+ pattern: installHelpers.inputHelpers.alphanumValidator,
+ default: 'admin'
+ },
{
name: 'dataRoot',
type: 'string',
@@ -128,7 +149,28 @@ installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) {
before: installHelpers.inputHelpers.toBoolean,
default: 'N'
},
+ confirmConnectionUrl: {
+ name: 'useSmtpConnectionUrl',
+ type: 'string',
+ description: "Will you use a URL to connect to your smtp Server y/N",
+ before: installHelpers.inputHelpers.toBoolean,
+ default: 'N'
+ },
configure: [
+ {
+ 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
+ }
+ ],
+ configureService: [
{
name: 'smtpService',
type: 'string',
@@ -149,18 +191,14 @@ installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) {
replace: installHelpers.inputHelpers.passwordReplace,
default: '',
before: installHelpers.inputHelpers.passwordBefore
- },
- {
- name: 'fromAddress',
- type: 'string',
- description: "Sender email address",
- default: '',
- },
+ }
+ ],
+ configureConnectionUrl: [
{
- name: 'rootUrl',
+ name: 'smtpConnectionUrl',
type: 'string',
- description: "The url this install will be accessible from",
- default: '' // set using default server options
+ description: "Custom connection URL: smtps://user%40gmail.com:pass@smtp.gmail.com/?pool=true",
+ default: 'none',
}
]
}
@@ -218,6 +256,7 @@ installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) {
}
console.log('');
if(!fs.existsSync('conf/config.json')) {
+ fs.ensureDirSync('conf');
return start();
}
console.log('Found an existing config.json file. Do you want to use the values in this file during install?');
@@ -231,7 +270,7 @@ installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) {
function generatePromptOverrides() {
if(USE_CONFIG) {
var configJson = require('./conf/config.json');
- var configData = JSON.parse(JSON.stringify(configJson).replace('true', '"y"').replace('false', '"n"'));
+ var configData = JSON.parse(JSON.stringify(configJson).replace(/true/g, '"y"').replace(/false/g, '"n"'));
configData.install = 'y';
}
// NOTE config.json < cmd args
@@ -290,23 +329,34 @@ function configureFeatures(callback) {
async.series([
function ffmpeg(cb) {
installHelpers.getInput(inputData.features.ffmpeg, function(result) {
- addConfig(configResults);
+ addConfig(result);
cb();
});
},
function smtp(cb) {
installHelpers.getInput(inputData.features.smtp.confirm, function(result) {
+ addConfig(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}`;
+ // prompt user if custom connection url or well-known-service should be used
+ installHelpers.getInput(inputData.features.smtp.confirmConnectionUrl, function(result) {
+ addConfig(result);
+ var smtpConfig;
+ if (result.useSmtpConnectionUrl === true) {
+ smtpConfig = inputData.features.smtp.configure.concat(inputData.features.smtp.configureConnectionUrl);
+ } else {
+ smtpConfig = inputData.features.smtp.configure.concat(inputData.features.smtp.configureService);
}
- }
- installHelpers.getInput(inputData.features.smtp.configure, function(result) {
- addConfig(configResults);
- cb();
+ for(var i = 0, count = smtpConfig.length; i < count; i++) {
+ if(smtpConfig[i].name === 'rootUrl') {
+ smtpConfig[i].default = `http://${configResults.serverName}:${configResults.serverPort}`;
+ }
+ }
+ installHelpers.getInput(smtpConfig, function(result) {
+ addConfig(result);
+ cb();
+ });
});
});
}
@@ -336,6 +386,15 @@ function configureMasterTenant(callback) {
if(error) {
return callback(error);
}
+ if(USE_CONFIG && prompt.override.masterTenantName) {
+ /**
+ * remove the masterTenantDisplayName, as we can use the existing value
+ * (which isn't in config.json so can't be used as an auto override)
+ */
+ inputData.tenant = _.filter(inputData.tenant, function(item) {
+ return item.name !== 'masterTenantDisplayName';
+ });
+ }
installHelpers.getInput(inputData.tenant, function(result) {
console.log('');
// add the input to our cached config
@@ -356,6 +415,9 @@ function configureMasterTenant(callback) {
if(!IS_INTERACTIVE) {
return exit(1, `Tenant '${tenant.name}' already exists, automatic install cannot continue.`);
}
+ if(!configResults.masterTenant.displayName) {
+ configResults.masterTenant.displayName = tenant.displayName;
+ }
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('');
@@ -391,7 +453,9 @@ function createMasterTenant(callback) {
}
console.log('Master tenant created successfully.');
masterTenant = tenant;
- saveConfig(app.configuration.getConfig(), callback);
+ delete configResults.masterTenant;
+ addConfig(app.configuration.getConfig());
+ saveConfig(configResults, callback);
});
}
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);
});
});
},
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);
}
diff --git a/lib/contentmanager.js b/lib/contentmanager.js
index 2c45fcad2a..8bedc960bc 100644
--- a/lib/contentmanager.js
+++ b/lib/contentmanager.js
@@ -1065,14 +1065,6 @@ ContentManager.prototype.setupRoutes = function () {
rest.post('/content/clipboard/copy', function(req, res, next) {
var user = app.usermanager.getCurrentUser();
var tenantId = user.tenant && user.tenant._id;
-
- var hierarchy = {
- 'contentObjects': 'contentobject',
- 'articles': 'article',
- 'blocks': 'block',
- 'components':'component'
- };
-
var id = req.body.objectId;
var referenceType = req.body.referenceType;
var courseId = req.body.courseId;
@@ -1100,9 +1092,7 @@ ContentManager.prototype.setupRoutes = function () {
},
// Retrieve the current object
function(callback) {
- var type = hierarchy[referenceType];
-
- db.retrieve(type, { _id: id }, function (error, results) {
+ db.retrieve(referenceType, { _id: id }, function (error, results) {
if (error) {
return callback(error);
}
@@ -1112,20 +1102,17 @@ ContentManager.prototype.setupRoutes = function () {
var item = results[0]._doc;
switch (referenceType) {
- case 'contentObjects':
+ case 'contentobject':
isMenu = (item._type == 'menu') ? true : false;
contentObjects.push(item);
break;
-
- case 'articles':
+ case 'article':
articles.push(item);
break;
-
- case 'blocks':
+ case 'block':
blocks.push(item);
break;
-
- case 'components':
+ case 'component':
components.push(item);
break;
}
@@ -1188,10 +1175,10 @@ ContentManager.prototype.setupRoutes = function () {
_tenantId: tenantId,
createdBy: user._id,
referenceType: referenceType,
- contentObjects: contentObjects,
- articles: articles,
- blocks: blocks,
- components: components
+ contentobject: contentObjects,
+ article: articles,
+ block: blocks,
+ component: components
};
db.create('clipboard', clipboard, function(error, newRecord) {
if (error) {
@@ -1221,22 +1208,18 @@ ContentManager.prototype.setupRoutes = function () {
var courseId = req.body.courseId;
var clipboard;
var parentObject;
-
+ var keyMap = {
+ ContentObject: 'contentobject',
+ Article: 'article',
+ Block: 'block',
+ Component: 'component'
+ };
var parentRelationship = {
- 'contentObjects': 'contentobject', // In case
- 'articles': 'contentobject',
- 'blocks': 'article',
- 'components': 'block'
+ 'contentobject': 'contentobject',
+ 'article': 'contentobject',
+ 'block': 'article',
+ 'component': 'block'
};
-
- var typeToCollection = {
- 'page': 'contentObjects',
- 'menu': 'contentObjects',
- 'article': 'articles',
- 'block': 'blocks',
- 'component': 'components'
- }
-
var map = {};
database.getDatabase(function (error, db) {
@@ -1256,9 +1239,9 @@ ContentManager.prototype.setupRoutes = function () {
clipboard = results[0]._doc;
- if (layout && clipboard.components.length == 1) {
+ if (layout && clipboard[keyMap.Component].length == 1) {
// Persist the component layout when there is only one
- clipboard.components[0]._layout = layout;
+ clipboard[keyMap.Component][0]._layout = layout;
}
// OK to proceed
async.series([
@@ -1272,8 +1255,8 @@ ContentManager.prototype.setupRoutes = function () {
parentObject = results[0]._doc;
return callback();
}
- if (clipboard.referenceType === 'contentObjects'
- && (clipboard.contentObjects[0]._courseId.toString() === clipboard._courseId.toString())) {
+ if (clipboard.referenceType === keyMap.ContentObject
+ && (clipboard[keyMap.ContentObject][0]._courseId.toString() === clipboard._courseId.toString())) {
// Handle if this is a root-level page
parentObject = { _id: clipboard._courseId };
callback();
@@ -1290,7 +1273,12 @@ ContentManager.prototype.setupRoutes = function () {
callback();
},
function(callback) {
- async.eachSeries(['contentObjects', 'articles', 'blocks', 'components'], function(level, cb) {
+ async.eachSeries([
+ keyMap.ContentObject,
+ keyMap.Article,
+ keyMap.Block,
+ keyMap.Component
+ ], function(level, cb) {
map[level] = {};
async.eachSeries(clipboard[level], function(item, cb2) {
map[level][item._id.toString()] = null;
@@ -1301,24 +1289,23 @@ ContentManager.prototype.setupRoutes = function () {
},
// Pasting contentObjects
function(callback) {
- if (clipboard['contentObjects'].length === 0) {
+ if (clipboard[keyMap.ContentObject].length === 0) {
return callback();
}
- async.eachSeries(clipboard['contentObjects'], function(item, cb) {
+ async.eachSeries(clipboard[keyMap.ContentObject], function(item, cb) {
var previousId = item._id.toString();
var previousParentId = item._parentId.toString();
- var newParentId = typeof(map['contentObjects'][previousParentId]) !== 'undefined' && map['contentObjects'][previousParentId] != null
- ? map['contentObjects'][previousParentId]
- : parentObject._id;
+ var mappedId = map[keyMap.ContentObject][previousParentId];
+ var newParentId = mappedId ? mappedId : parentObject._id;
delete item._id;
item._parentId = newParentId;
- that.create('contentobject', item, function (error, newItem) {
+ that.create(keyMap.ContentObject, item, function (error, newItem) {
if (error) {
return cb(error);
}
- map['contentObjects'][previousId] = newItem._id;
+ map[keyMap.ContentObject][previousId] = newItem._id;
// For each object being copied we should find out if there's any course assets
// associated with the object. Then create new ones based upon there new ids
copyCourseAssets(that, {
@@ -1332,14 +1319,14 @@ ContentManager.prototype.setupRoutes = function () {
},
// Pasting articles
function(callback) {
- if (clipboard['articles'].length === 0) {
+ if (clipboard[keyMap.Article].length === 0) {
return callback();
}
- async.eachSeries(clipboard['articles'], function(item, cb) {
+ async.eachSeries(clipboard[keyMap.Article], function(item, cb) {
var previousId = item._id.toString();
var previousParentId = item._parentId.toString();
- var newParentId = typeof(map['contentObjects'][previousParentId]) !== 'undefined' && map['contentObjects'][previousParentId] != null
- ? map['contentObjects'][previousParentId]
+ var newParentId = typeof(map[keyMap.ContentObject][previousParentId]) !== 'undefined' && map[keyMap.ContentObject][previousParentId] != null
+ ? map[keyMap.ContentObject][previousParentId]
: parentObject._id;
delete item._id;
@@ -1349,7 +1336,7 @@ ContentManager.prototype.setupRoutes = function () {
if (error) {
return cb(error);
}
- map['articles'][previousId] = newItem._id;
+ map[keyMap.Article][previousId] = newItem._id;
// For each object being copied we should find out if there's any course assets
// associated with the object. Then create new ones based upon there new ids
copyCourseAssets(that, {
@@ -1363,14 +1350,14 @@ ContentManager.prototype.setupRoutes = function () {
},
// Pasting blocks
function(callback) {
- if (clipboard['blocks'].length === 0) {
+ if (clipboard[keyMap.Block].length === 0) {
return callback();
}
- async.eachSeries(clipboard['blocks'], function(item, cb) {
+ async.eachSeries(clipboard[keyMap.Block], function(item, cb) {
var previousId = item._id.toString();
var previousParentId = item._parentId.toString();
- var newParentId = typeof(map['articles'][previousParentId]) !== 'undefined'
- ? map['articles'][previousParentId]
+ var newParentId = typeof(map[keyMap.Article][previousParentId]) !== 'undefined'
+ ? map[keyMap.Article][previousParentId]
: parentObject._id;
delete item._id;
@@ -1380,7 +1367,7 @@ ContentManager.prototype.setupRoutes = function () {
if (error) {
return cb(error);
}
- map['blocks'][previousId] = newItem._id;
+ map[keyMap.Block][previousId] = newItem._id;
// For each object being copied we should find out if there's any course assets
// associated with the object. Then create new ones based upon there new ids
copyCourseAssets(that, {
@@ -1394,14 +1381,14 @@ ContentManager.prototype.setupRoutes = function () {
},
// Pasting components
function(callback) {
- if (clipboard['components'].length === 0) {
+ if (clipboard[keyMap.Component].length === 0) {
return callback();
}
- async.eachSeries(clipboard['components'], function(item, cb) {
+ async.eachSeries(clipboard[keyMap.Component], function(item, cb) {
var previousId = item._id.toString();
var previousParentId = item._parentId.toString();
- var newParentId = typeof(map['blocks'][previousParentId]) !== 'undefined'
- ? map['blocks'][previousParentId]
+ var newParentId = typeof(map[keyMap.Block][previousParentId]) !== 'undefined'
+ ? map[keyMap.Block][previousParentId]
: parentObject._id;
delete item._id;
@@ -1411,7 +1398,7 @@ ContentManager.prototype.setupRoutes = function () {
if (error) {
return cb(error);
}
- map['components'][previousId] = newItem._id;
+ map[keyMap.Component][previousId] = newItem._id;
// For each object being copied we should find out if there's any course assets
// associated with the object. Then create new ones based upon there new ids
copyCourseAssets(that, {
@@ -1428,7 +1415,11 @@ ContentManager.prototype.setupRoutes = function () {
logger.log('error', err);
return res.status(500).json({ success: false, message: 'Error pasting clipboard data' });
}
- return res.status(200).json({ success: true, message: 'ok' });
+ // successful paste, remove entry
+ db.destroy('clipboard', { id: id }, function(error) {
+ var newId = _.values(map[_.find(Object.keys(map), function(key) { return !_.isEmpty(map[key]); })])[0];
+ return res.status(200).json({ _id: newId });
+ });
});
}, tenantId);
});
diff --git a/lib/database.js b/lib/database.js
index f4a8c6c1c1..c6463fc1e1 100644
--- a/lib/database.js
+++ b/lib/database.js
@@ -140,9 +140,11 @@ Database.prototype.addModel = function (modelName, schema, next) {
if (err) {
return next(err);
}
-
- // all models use lowercase
- self.addSchema(modelName, importedSchema);
+ try {
+ self.addSchema(modelName, importedSchema);
+ } catch(e) {
+ return next(e);
+ }
next(null, importedSchema);
});
};
diff --git a/lib/frameworkhelper.js b/lib/frameworkhelper.js
index 3482356b78..83f260276b 100644
--- a/lib/frameworkhelper.js
+++ b/lib/frameworkhelper.js
@@ -53,7 +53,7 @@ function installFramework (next, frameworkRevision) {
console.log('Running \'npm install\' for the Adapt Framework...');
- var child = exec('git checkout --quiet ' + frameworkRevision + ' && npm install', {
+ var child = exec('git checkout --quiet ' + frameworkRevision + ' && npm install --loglevel error', {
cwd: FRAMEWORK_DIR,
stdio: [0, 'pipe', 'pipe']
});
diff --git a/lib/helpers.js b/lib/helpers.js
index 793c2b1e9b..9cd0163c0c 100644
--- a/lib/helpers.js
+++ b/lib/helpers.js
@@ -182,6 +182,24 @@ 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;
+}
+
function cloneObject(obj) {
var clone = {};
if(!obj) return clone;
@@ -227,5 +245,6 @@ exports = module.exports = {
isMasterPreviewAccessible: isMasterPreviewAccessible,
isValidEmail: isValidEmail,
replaceAll: replaceAll,
- cloneObject: cloneObject
+ cloneObject: cloneObject,
+ slugify: slugify
};
diff --git a/lib/installHelpers.js b/lib/installHelpers.js
index 71e7ef008a..cdd3262e34 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)) {
@@ -205,17 +205,20 @@ 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.';
+ }
}
-
+ // exit, but just log the error
if (error) {
- return callback(`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;
try {
@@ -224,16 +227,16 @@ function checkLatestAdaptRepoVersion(repoName, versionLimit, callback) {
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);
+ var satisfiesVersion = !versionLimit || semver.satisfies(release.tag_name, versionLimit);
+
+ if (!isFullRelease || !satisfiesVersion) {
+ return cb(null, false);
}
- cb(null, false);
+
+ compatibleRelease = release;
+ return cb(null, true);
}, function(error, satisfied) {
if(!satisfied) {
if(nextPage) {
@@ -287,8 +290,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);
});
}
@@ -337,7 +340,7 @@ function cloneRepo(opts, callback) {
hideSpinner();
return callback(error);
}
- execCommand(`git clone ${opts.repository} --origin ${REMOTE_NAME} ${opts.directory}`, function(error) {
+ execCommand(`git clone ${opts.repository} --origin ${REMOTE_NAME} "${opts.directory}"`, function(error) {
hideSpinner();
if(error) {
return callback(error);
@@ -493,7 +496,7 @@ function installDependencies(opts, callback) {
if(error) {
return callback(error);
}
- execCommand('npm install --production', { cwd: cwd }, function(error) {
+ execCommand('npm install --loglevel error --production', { cwd: cwd }, function(error) {
hideSpinner();
if(error) {
return callback(error);
diff --git a/lib/mailer.js b/lib/mailer.js
index 6b3634eb05..29eb4e1967 100644
--- a/lib/mailer.js
+++ b/lib/mailer.js
@@ -9,34 +9,52 @@ var configuration = require('./configuration'),
var Mailer = function () {
// Read the mail settings from the config.json
this.isEnabled = configuration.getConfig('useSmtp');
+ this.useSmtpConnectionUrl = configuration.getConfig('useSmtpConnectionUrl');
+
this.service = configuration.getConfig('smtpService');
this.user = configuration.getConfig('smtpUsername');
this.pass = configuration.getConfig('smtpPassword');
this.from = configuration.getConfig('fromAddress');
+ this.connectionUrl = configuration.getConfig('smtpConnectionUrl');
// check that required settings exist
this.validateSettings = function() {
var errors = [];
- if(this.service === '') errors.push('smtpService');
- if(this.user === '') errors.push('smtpUsername');
- if(this.pass === '') errors.push('smtpPassword');
- if(this.from === '') errors.push('fromAddress');
+ // validate based on selected connection
+ if (this.useSmtpConnectionUrl === true) {
+ if (this.connectionUrl === '') errors.push('smtpConnectionUrl');
+ } else {
+ if (this.service === '') errors.push('smtpService');
+ if (this.user === '') errors.push('smtpUsername');
+ if (this.pass === '') errors.push('smtpPassword');
+ }
+
+ // generic configuration options
+ if (this.from === '') errors.push('fromAddress');
- if(errors.length > 0) {
+ if (errors.length > 0) {
throw new Error('Mailer requires the following settings to function: ' + errors.toString());
}
};
// Configure the credentials for the specified sevice
// See http://www.nodemailer.com/ for options
- this.transporter = nodemailer.createTransport({
- service: this.service,
- auth: {
- user: this.user,
- pass: this.pass
- }
- });
+
+ var smtpConfig;
+
+ if (this.useSmtpConnectionUrl) {
+ smtpConfig = this.connectionUrl;
+ } else {
+ smtpConfig = {
+ service: this.service,
+ auth: {
+ user: this.user,
+ pass: this.pass
+ }
+ };
+ }
+ this.transporter = nodemailer.createTransport(smtpConfig);
};
Mailer.prototype.send = function (toAddress, subject, text, templateData, callback) {
@@ -62,7 +80,7 @@ Mailer.prototype.send = function (toAddress, subject, text, templateData, callba
return callback(e);
}
configure(mailOptions, _.bind(function(error, options) {
- if(error) {
+ if (error) {
return callback(error);
}
// Send mail with defined transport object
@@ -82,13 +100,13 @@ Mailer.prototype.send = function (toAddress, subject, text, templateData, callba
var configure = function (options, callback) {
// TODO localise these errors
- if(!helpers.isValidEmail(options.to)) {
+ if (!helpers.isValidEmail(options.to)) {
return callback(new Error("Can't send email, '" + options.to + "' is not a valid email address"));
}
- if(!helpers.isValidEmail(options.from)) {
+ if (!helpers.isValidEmail(options.from)) {
return callback(new Error("Can't send email, '" + options.from + "' is not a valid email address"));
}
- if(options.templateData !== null) {
+ if (options.templateData !== null) {
var template = new EmailTemplate(path.join(__dirname, 'templates', options.templateData.name));
template.render(options, function (error, results) {
if (error) {
diff --git a/lib/outputmanager.js b/lib/outputmanager.js
index 254ede6738..489b1407ff 100644
--- a/lib/outputmanager.js
+++ b/lib/outputmanager.js
@@ -900,21 +900,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
*
diff --git a/lib/router.js b/lib/router.js
index c7bc8f2213..134455f94d 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -40,8 +40,9 @@ Router.prototype.init = function (app) {
var route = require(itemPath);
server.use(route);
} catch(e) {
- var detail = (e.code === 'MODULE_NOT_FOUND') ? ', no index.js found' : ` (${e})`;
- logger.log('error', `Cannot load routes/${item}${detail}`);
+ // if the top-level module can't be loaded, it's the index.js
+ var detail = (e.message.indexOf(`'${itemPath}'`) === -1) ? e.message : 'no index.js found';
+ logger.log('error', `Cannot load routes/${item}, ${detail}`);
}
});
});
diff --git a/package.json b/package.json
index 930d7c3809..b9c1e13ae7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "adapt_authoring",
- "version": "0.4.0",
+ "version": "0.4.1",
"license": "GPL-3.0",
"description": "A server-based user interface for authoring eLearning courses using the Adapt Framework.",
"keywords": [
@@ -20,77 +20,26 @@
"test": "grunt test"
},
"contributors": [
- {
- "name": "Ryan Adams",
- "email": "ryana@learningpool.com"
- },
- {
- "name": "Thomas Berger",
- "email": "thomas.berger@learnchamp.com"
- },
- {
- "name": "Kevin Corry",
- "email": "kevinc@learningpool.com"
- },
- {
- "name": "Simon Date",
- "email": "simondate@yahoo.co.uk"
- },
- {
- "name": "Thomas Eitler",
- "email": "thomas.eitler@learnchamp.com"
- },
- {
- "name": "Dan Gray",
- "email": "dan@sinensis.co.uk"
- },
- {
- "name": "Tom Greenfield",
- "email": "tom.greenfield@kineo.com"
- },
- {
- "name": "Dennis Heaney",
- "email": "dennis@learningpool.com"
- },
- {
- "name": "Daryl Hedley",
- "email": "darylhedley@hotmail.com"
- },
- {
- "name": "Ryan Lafferty",
- "email": "ryanl@learningpool.com"
- },
- {
- "name": "Sven Laux",
- "email": "sven.laux@kineo.com"
- },
- {
- "name": "Louise McMahon",
- "email": "louise.mcmahon@canstudios.com"
- },
- {
- "name": "Rob Moore",
- "email": "rob@learningpool.com"
- },
- {
- "name": "Petra Nussdorfer",
- "email": "petra.nussdorfer@learnchamp.com"
- },
- {
- "name": "Brian Quinn",
- "email": "brian@learningpool.com"
- },
- {
- "name": "Thomas Taylor",
- "email": "hello@tomtaylor.name"
- },
- {
- "name": "Nicola Willis",
- "email": "nicola.willis@canstudios.com"
- }
+ "Ryan Adams
",
+ "Thomas Berger ",
+ "Kevin Corry ",
+ "Simon Date ",
+ "Thomas Eitler ",
+ "Dan Gray ",
+ "Tom Greenfield ",
+ "Dennis Heaney ",
+ "Daryl Hedley ",
+ "Ryan Lafferty ",
+ "Sven Laux ",
+ "Louise McMahon ",
+ "Rob Moore ",
+ "Petra Nussdorfer ",
+ "Brian Quinn ",
+ "Thomas Taylor ",
+ "Nicola Willis "
],
"dependencies": {
- "archiver": "~0.16.0",
+ "archiver": "2.1.1",
"async": "2.5.0",
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.13.3",
@@ -150,7 +99,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 3811d84bee..9e62961ab8 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,
installHelpers = require('../../../lib/installHelpers');
diff --git a/plugins/content/clipboard/model.schema b/plugins/content/clipboard/model.schema
index 981d8b4bd1..d6667a7e75 100644
--- a/plugins/content/clipboard/model.schema
+++ b/plugins/content/clipboard/model.schema
@@ -19,9 +19,9 @@
"type" : "string",
"required" : true
},
- "contentObjects" : [{}],
- "articles" : [{}],
- "blocks" : [{}],
- "components" : [{}]
+ "contentobject" : [{}],
+ "article" : [{}],
+ "block" : [{}],
+ "component" : [{}]
}
}
diff --git a/plugins/content/component/model.schema b/plugins/content/component/model.schema
index 620971e149..c2d9c2bfc1 100644
--- a/plugins/content/component/model.schema
+++ b/plugins/content/component/model.schema
@@ -99,6 +99,12 @@
"validators": [],
"translatable": true
},
+ "instruction": {
+ "type": "string",
+ "default" : "",
+ "inputType": "Text",
+ "translatable": true
+ },
"_onScreen": {
"type": "object",
"title": "On-screen classes",
diff --git a/plugins/output/adapt/index.js b/plugins/output/adapt/index.js
index bc8c1a1c50..28f2e76618 100644
--- a/plugins/output/adapt/index.js
+++ b/plugins/output/adapt/index.js
@@ -23,6 +23,7 @@ var origin = require('../../../'),
assetmanager = require('../../../lib/assetmanager'),
exec = require('child_process').exec,
semver = require('semver'),
+ helpers = require('../../../lib/helpers'),
installHelpers = require('../../../lib/installHelpers'),
logger = require('../../../lib/logger');
@@ -223,8 +224,8 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next
function(callback) {
if (mode === Constants.Modes.publish) {
// Now zip the build package
- var filename = path.join(COURSE_FOLDER, Constants.Filenames.Download);
- var zipName = self.slugify(outputJson['course'].title);
+ var filename = path.join(FRAMEWORK_ROOT_FOLDER, Constants.Folders.AllCourses, tenantId, courseId, Constants.Filenames.Download);
+ var zipName = helpers.slugify(outputJson['course'].title);
var output = fs.createWriteStream(filename),
archive = archiver('zip');
@@ -243,11 +244,7 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next
});
archive.pipe(output);
-
- archive.bulk([
- { expand: true, cwd: path.join(BUILD_FOLDER), src: ['**/*'] },
- ]).finalize();
-
+ archive.glob('**/*', {cwd: path.join(BUILD_FOLDER)}).finalize();
} else {
// No download required -- skip this step
callback();
@@ -267,14 +264,11 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next
AdaptOutput.prototype.export = function (courseId, request, response, next) {
var self = this;
var tenantId = usermanager.getCurrentUser().tenant._id;
- var timestamp = new Date().toISOString().replace('T', '-').replace(/:/g, '').substr(0,17);
+ var userId = usermanager.getCurrentUser()._id;
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);
var mode = Constants.Modes.export;
@@ -282,27 +276,7 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) {
function publishCourse(callback) {
self.publish(courseId, mode, 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);
@@ -348,20 +322,21 @@ AdaptOutput.prototype.export = function (courseId, request, response, next) {
},
function zipFiles(callback) {
var archive = archiver('zip');
- var output = fs.createWriteStream(exportDir + '.zip');
-
+ var zipPath = exportDir + '.zip';
+ 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 cleanUp(callback) {
- fse.remove(exportDir, function (error) {
- callback(error, { zipName: exportName + '.zip' });
- });
+ archive.glob('**/*', {cwd: exportDir}).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);
- }
+ });
});
});
diff --git a/routes/lang/en-application.json b/routes/lang/en-application.json
index 5730633337..44ee3f2bdb 100644
--- a/routes/lang/en-application.json
+++ b/routes/lang/en-application.json
@@ -253,6 +253,7 @@
"app.errorsessionexpired": "Your session has expired, click OK to log on again",
"app.errorassetupdate": "Something went wrong while updating the asset.
Please try again.",
"app.errordeleteasset": "Couldn't delete this asset, %{message}",
+ "app.errordelete": "An error occurred while deleting, please try again",
"app.errorsave": "Something went wrong while saving your data.
Please try again.",
"app.errorgeneric": "Oops, something went wrong!",
"app.errorpreview": "Something went wrong while generating your preview.
Please contact an administrator for assistance.",
@@ -265,11 +266,12 @@
"app.errorgettingschemas": "An error occurred while getting schemas.",
"app.errorgeneratingpreview": "Error generating preview, please contact an administrator.",
"app.errorcopy": "Error during copy.",
- "app.errorpaste": "Error during paste",
+ "app.errorpaste": "An error occurred during paste. Please try again.",
"app.errorsaveasset": "An error occurred doing the save",
"app.errorusernoaccess": "Sorry, the current user doesn't have access to this course",
"app.errorpagenoaccesstitle": "Access Denied",
"app.errorpagenoaccess": "Sorry, you don't have the correct permissions to view this page. Click the button below to go to the dashboard.",
+ "app.errorpermission": "You are not permitted to do that.",
"app.copyidtoclipboardsuccess": "Copied %{id} to clipboard",
"app.copyidtoclipboarderror": "Error copying %{id} to clipboard",
"app.grid": "Grid",
@@ -308,5 +310,6 @@
"app.page": "page",
"app.article": "article",
"app.block": "block",
- "app.component": "component"
+ "app.component": "component",
+ "app.errorloadconfig": "Failed to load configuration settings for %{course}"
}
diff --git a/upgrade.js b/upgrade.js
index 2a4f4d8432..b5faeacc04 100644
--- a/upgrade.js
+++ b/upgrade.js
@@ -25,7 +25,19 @@ function start() {
logger.level('console','error');
// start the server first
app.run({ skipVersionCheck: true, skipStartLog: true });
- app.on('serverStarted', getUserInput);
+ app.on('serverStarted', function() {
+ ensureRepoValues();
+ getUserInput();
+ });
+}
+
+function ensureRepoValues() {
+ if(configuration.getConfig('frameworkRepository') === '') {
+ configuration.setConfig('frameworkRepository', installHelpers.DEFAULT_FRAMEWORK_REPO);
+ }
+ if(configuration.getConfig('authoringToolRepository') === '') {
+ configuration.setConfig('authoringToolRepository', installHelpers.DEFAULT_SERVER_REPO);
+ }
}
function getUserInput() {