From cd1ac0943734d580d6dcd59d9ce8e48fe7cb900f Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 17 Sep 2018 16:50:26 +0100 Subject: [PATCH 01/30] Add front-end --- frontend/src/modules/scaffold/index.js | 3 +- frontend/src/modules/scaffold/less/users.less | 8 ++ .../scaffold/views/scaffoldUsersView.js | 92 +++++++++++++++++++ plugins/content/course/model.schema | 15 ++- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 frontend/src/modules/scaffold/less/users.less create mode 100644 frontend/src/modules/scaffold/views/scaffoldUsersView.js diff --git a/frontend/src/modules/scaffold/index.js b/frontend/src/modules/scaffold/index.js index 38326dc466..2033e4a822 100644 --- a/frontend/src/modules/scaffold/index.js +++ b/frontend/src/modules/scaffold/index.js @@ -11,7 +11,8 @@ define([ './views/scaffoldDisplayTitleView', './views/scaffoldItemsModalView', './views/scaffoldListView', - './views/scaffoldTagsView' + './views/scaffoldTagsView', + './views/scaffoldUsersView' ], function(Origin, Helpers, Schemas, BackboneForms, BackboneFormsLists, Overrides, ScaffoldAssetView, ScaffoldCodeEditorView, ScaffoldColourPickerView, ScaffoldDisplayTitleView, ScaffoldItemsModalView, ScaffoldListView, ScaffoldTagsView) { var Scaffold = {}; diff --git a/frontend/src/modules/scaffold/less/users.less b/frontend/src/modules/scaffold/less/users.less new file mode 100644 index 0000000000..10d1f3ccc9 --- /dev/null +++ b/frontend/src/modules/scaffold/less/users.less @@ -0,0 +1,8 @@ +.scaffold-users .selectize-input { + box-sizing: inherit; + width: 90%; + padding: 11px 10px; + .selectize-control.multi&.has-items { + padding: 9px 10px 6px; + } +} diff --git a/frontend/src/modules/scaffold/views/scaffoldUsersView.js b/frontend/src/modules/scaffold/views/scaffoldUsersView.js new file mode 100644 index 0000000000..19ff2180ce --- /dev/null +++ b/frontend/src/modules/scaffold/views/scaffoldUsersView.js @@ -0,0 +1,92 @@ +define([ 'core/origin', 'backbone-forms' ], function(Origin, BackboneForms) { + var ScaffoldUsersView = Backbone.Form.editors.Base.extend({ + tagName: 'input', + className: 'scaffold-users', + idField: 'email', + events: { + 'change': function() { this.trigger('change', this); }, + 'focus': function() { this.trigger('focus', this); }, + 'blur': function() { this.trigger('blur', this); } + }, + + render: function() { + this.setValue(this.value); + _.defer(this.postRender.bind(this)); + return this; + }, + + renderItem: function(item, escape) { + var name = item.firstName && item.lastName ? escape(item.firstName + ' ' + item.lastName) : ''; + var email = escape(item.email); + if(name) { + return '
' + name + '
' + } + return '
' + email + '
'; + }, + + postRender: function() { + this.model.set('users', []); + this.fetchUsers(this.initSelectize); + }, + + fetchUsers: function(callback) { + $.get('/api/user') + .done(callback.bind(this)) + .fail(function(error) { + Origin.Notify.alert({ type: 'error', text: error }); + }); + }, + + initSelectize: function(users) { + this.$el.selectize({ + labelField: 'email', + valueField: '_id', + options: users, + onItemAdd: this.onAddUser.bind(this), + onItemRemove: this.onRemoveUser.bind(this), + searchField: [ 'email', 'firstName', 'lastName' ], + render: { + item: this.renderItem, + option: this.renderItem + } + }); + }, + + getValue: function() { + return this.model.get('users'); + }, + + setValue: function(value) { + this.$el.val(value); + }, + + focus: function() { + if(!this.hasFocus) this.$el.focus(); + }, + + blur: function() { + if(this.hasFocus) this.$el.blur(); + }, + + onAddUser: function(value) { + var users = this.model.get('users'); + users.push(value); + this.model.set('users', users); + console.log(value, users); + }, + + onRemoveUser: function(value) { + var users = this.model.get('users').filter(function(item) { + return item[this.idField] !== value; + }); + this.model.set('users', users); + } + }); + + Origin.on('origin:dataReady', function() { + Origin.scaffold.addCustomField('Users', ScaffoldUsersView); + }); + + return ScaffoldUsersView; + +}); diff --git a/plugins/content/course/model.schema b/plugins/content/course/model.schema index dd135bfa15..b0f31116d0 100644 --- a/plugins/content/course/model.schema +++ b/plugins/content/course/model.schema @@ -584,9 +584,22 @@ "editorOnly": true, "inputType": "Checkbox", "validators": [], - "title": "Share with other users", + "title": "Share with all other users", "help": "Controls whether or not your colleagues will be able to see this course from the 'Shared courses' option" }, + "_shareWithUsers": { + "type": "array", + "inputType": "Users", + "items": { + "type": "objectid", + "inputType": "Text", + "required": false, + "editorOnly": true, + "ref": "user" + }, + "title": "Share with specific users", + "help": "Specifies which colleagues will be able to see this course from the 'Shared courses' option" + }, "themeSettings": { "type": "object" }, From a058fadbf98b7dd854966a77d7e49659b3c3c83c Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 17 Sep 2018 21:58:15 +0100 Subject: [PATCH 02/30] Fix helper --- lib/helpers.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/helpers.js b/lib/helpers.js index 76ddb2c73c..c044ecee9f 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -137,20 +137,16 @@ function isMasterPreviewAccessible(courseId, userId, next) { function hasCoursePermission (action, userId, tenantId, contentItem, next) { // Check that the contentItem has something resembling a courseId - if (contentItem && typeof contentItem._id === 'undefined' && typeof contentItem._courseId === 'undefined') { + if (contentItem && !contentItem._id && !contentItem._courseId) { // Course permission cannot be verified return next(null, false); } - - var courseId = contentItem && contentItem._courseId - ? contentItem._courseId.toString() - : contentItem._id.toString(); - database.getDatabase(function(err, db) { if (err) { logger.log('error', err); return err; } + const courseId = contentItem && contentItem._courseId ? contentItem._courseId.toString() : contentItem._id.toString(); db.retrieve('course', { _id: courseId }, { jsonOnly: true }, function (err, results) { if (err) { @@ -158,16 +154,23 @@ function hasCoursePermission (action, userId, tenantId, contentItem, next) { return next(err); } if (!results || results.length !== 1) { - return next(new Error('Course ' + courseId + ' not found')); + return next(new Error(`Course ${courseId} not found`)); } + const course = results[0]; - var course = results[0]; - - if ((course._isShared || course.createdBy == userId) && course._tenantId.toString() == tenantId) { - // Shared courses on the same tenant are open to users on the same tenant + if(course._tenantId.toString() !== tenantId) { // no sharing across tenants + return next(null, false); + } + if(course.createdBy.toString() === userId) { // user's own course + return next(null, true); + } + if(course._shareWithUsers) { // check if userId is in _shareWithUsers + return next(null, course._shareWithUsers.map(user => user.toString()).includes(userId)); + } + if (course._isShared) { // shared universally return next(null, true); } - return next(null, false); + next(null, false); }); }); } From 0554a22018c2389910f59162a4cae7d083ecfbc8 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 17 Sep 2018 22:00:43 +0100 Subject: [PATCH 03/30] Refactor middleware --- lib/permissions.js | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/lib/permissions.js b/lib/permissions.js index 2826b081ac..8f413f8510 100644 --- a/lib/permissions.js +++ b/lib/permissions.js @@ -597,40 +597,28 @@ exports = module.exports = { */ policyChecker: function () { - var that = this; - return function (req, res, next) { - // first check if the route should be ignored - if (that.shouldIgnore(req.url)) { - next(); - return; + return (req, res, next) => { + if (this.shouldIgnore(req.url)) { // first check if the route should be ignored + return next(); } + const user = app.usermanager.getCurrentUser(); - var user = app.usermanager.getCurrentUser(); if (!user || !user._id) { - res.statusCode = 403; - return res.json({statusCode: STATUS.NOT_AUTHENTICATED}); + return res.status(403).json({ statusCode: STATUS.NOT_AUTHENTICATED }); } - // build path from req.url - var resource = that.buildResourceString(user.tenant._id, req.url); - var action = that.getActionType(req.method.toLowerCase()); // at this point, we can only enforce CRUD methods - that.hasPermission(user._id, action, resource, function (error, allowed) { + const resource = this.buildResourceString(user.tenant._id, req.url); + const action = this.getActionType(req.method.toLowerCase()); // at this point, we can only enforce CRUD methods + this.hasPermission(user._id, action, resource, function (error, allowed) { if (error) { - // summat went wrong - res.statusCode = 500; - return res.json(error); + return res.status(500).json(error); } - - if (allowed) { - // if we have permission, just continue + if (allowed) { // if we have permission, just continue return next(); } - - // denied! - res.statusCode = 403; - return res.json({statusCode: STATUS.ACCESS_DENIED}); + res.status(403).json({ statusCode: STATUS.ACCESS_DENIED }); }); - }; + } }, getUserPermissions: function(userId, callback) { From c50e0fab948cec3fbbd0ccb931e5b349595e1616 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Tue, 18 Sep 2018 10:44:29 +0100 Subject: [PATCH 04/30] Update role permissions --- lib/role/CourseCreator.json | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/role/CourseCreator.json b/lib/role/CourseCreator.json index 097d72c082..a8ba789aaa 100644 --- a/lib/role/CourseCreator.json +++ b/lib/role/CourseCreator.json @@ -6,6 +6,7 @@ { "condition":{}, "resource":"urn:x-adapt:{{tenantid}}/api/shared/*", "action":["create","read"], "effect":"allow" }, { "condition":{}, "resource":"urn:x-adapt:{{tenantid}}/api/subscribed/*", "action":["create","read"], "effect":"allow" }, { "condition":{}, "resource":"urn:x-adapt:{{tenantid}}/api/asset/*", "action":["create", "read", "update"], "effect":"allow" }, + { "condition":{}, "resource":"urn:x-adapt:{{tenantid}}/api/user/*", "action":["read"], "effect":"allow" }, { "condition":{}, "resource":"urn:x-adapt:{{tenantid}}/api/user/me", "action":["read", "update"], "effect":"allow" }, { "condition":{}, "resource":"urn:x-adapt:{{tenantid}}/api/output/*", "action":["read"], "effect":"allow" }, { "condition":{}, "resource":"urn:x-adapt:{{tenantid}}/api/extensiontype/*", "action":["read"], "effect":"allow" }, From 4f94533eb35dec2e72c9a1363778899fcb4d01f6 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Tue, 18 Sep 2018 10:45:54 +0100 Subject: [PATCH 05/30] Refactor API endpoints to work with new sharing --- plugins/content/course/index.js | 114 ++++++++------------------------ 1 file changed, 29 insertions(+), 85 deletions(-) diff --git a/plugins/content/course/index.js b/plugins/content/course/index.js index e86e89cbb2..efd59d0a2d 100644 --- a/plugins/content/course/index.js +++ b/plugins/content/course/index.js @@ -40,102 +40,46 @@ function initialize () { var self = this; var app = origin(); app.once('serverStarted', function (server) { - // My Courses - rest.get('/my/course', function (req, res, next) { - var options = _.keys(req.body).length - ? req.body - : req.query; + // common function used by API endpoints + function retrieveCourses(req, res, addQuery) { + var options = _.keys(req.body).length ? req.body : req.query; var search = options.search || {}; - var self = this; var orList = []; - var andList = []; - + var andList = addQuery ? addQuery : []; // convert searches to regex - async.each( - Object.keys(search), - function (key, nextKey) { - var exp = {}; - // convert strings to regex for likey goodness - if ('string' === typeof search[key]) { - exp[key] = new RegExp(search[key], 'i'); - orList.push(exp); - } else { - exp[key] = search[key]; - andList.push(exp); - } - nextKey(); - }, function () { - var query = {}; - if (orList.length) { - query.$or = orList; + async.each(Object.keys(search), function (key, nextKey) { + var exp = {}; + // convert strings to regex for likey goodness + if ('string' === typeof search[key]) { + exp[key] = new RegExp(search[key], 'i'); + orList.push(exp); + } else { + exp[key] = search[key]; + andList.push(exp); } - - query.$and = andList; - - // force search to use only courses created by current user - var user = usermanager.getCurrentUser(); - query.$and.push({ createdBy : user._id }); + nextKey(); + }, () => { + var query = {}; + if (orList.length) query.$or = orList; + if (andList.length) query.$and = andList; - options.jsonOnly = true; - options.fields = DASHBOARD_COURSE_FIELDS.join(' '); - + Object.assign(options, { + jsonOnly: true, + fields: DASHBOARD_COURSE_FIELDS.join(' ') + }); new CourseContent().retrieve(query, options, function (err, results) { - if (err) { - res.statusCode = 500; - return res.json(err); - } - return res.json(results); + if (err) return res.status(500).json(err); + res.json(results); }); }); + } + + rest.get('/my/course', function (req, res, next) { + retrieveCourses(req, res, [{ createdBy: usermanager.getCurrentUser()._id }]); }); - // Shared Courses rest.get('/shared/course', function (req, res, next) { - var options = _.keys(req.body).length - ? req.body - : req.query; - var search = options.search || {}; - var self = this; - var orList = []; - var andList = []; - - // convert searches to regex - async.each( - Object.keys(search), - function (key, nextKey) { - var exp = {}; - // convert strings to regex for likey goodness - if ('string' === typeof search[key]) { - exp[key] = new RegExp(search[key], 'i'); - orList.push(exp); - } else { - exp[key] = search[key]; - andList.push(exp); - } - nextKey(); - }, function () { - var query = {}; - if (orList.length) { - query.$or = orList; - } - - query.$and = andList; - - // Only return courses which have been shared - query.$and.push({ _isShared: true }); - - options.jsonOnly = true; - options.fields = DASHBOARD_COURSE_FIELDS.join(' '); - - new CourseContent().retrieve(query, options, function (err, results) { - if (err) { - res.statusCode = 500; - return res.json(err); - } - - return res.json(results); - }); - }); + retrieveCourses(req, res, [{ $or: [{ _shareWithUsers: usermanager.getCurrentUser()._id }, { _isShared: true }] }]); }); /** From 92bce352e47afe9b3f79fe2f6e22af870e80b500 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Tue, 18 Sep 2018 12:50:10 +0100 Subject: [PATCH 06/30] Remove sharedProjectView --- .../contextMenu/views/contextMenuView.js | 5 +- .../src/modules/projects/less/projects.less | 6 +- .../src/modules/projects/less/qproject.less | 56 ------------ .../projects/templates/sharedProject.hbs | 51 ----------- .../modules/projects/views/projectsView.js | 4 +- .../projects/views/sharedProjectView.js | 91 ------------------- 6 files changed, 4 insertions(+), 209 deletions(-) delete mode 100644 frontend/src/modules/projects/templates/sharedProject.hbs delete mode 100644 frontend/src/modules/projects/views/sharedProjectView.js diff --git a/frontend/src/modules/contextMenu/views/contextMenuView.js b/frontend/src/modules/contextMenu/views/contextMenuView.js index 2de7b71a72..7101aeadc6 100644 --- a/frontend/src/modules/contextMenu/views/contextMenuView.js +++ b/frontend/src/modules/contextMenu/views/contextMenuView.js @@ -48,10 +48,7 @@ define(function(require) { setMenu: function(view, $parent) { this.contextView = view; - - var type = view.model.get('_type'); - if (type === 'course' && !view.model.isEditable()) type = 'sharedcourse'; - this.type = type; + this.type = view.model.get('_type'); this.renderItems(); diff --git a/frontend/src/modules/projects/less/projects.less b/frontend/src/modules/projects/less/projects.less index bdfbd872f4..929637cc52 100644 --- a/frontend/src/modules/projects/less/projects.less +++ b/frontend/src/modules/projects/less/projects.less @@ -39,8 +39,7 @@ ul.projects-options { } .projects-list[data-layout=grid] { - .project-list-item, - .shared-project-list-item { + .project-list-item { margin-left: 25px; min-width: 200px; min-height: 341px; @@ -77,8 +76,7 @@ ul.projects-options { .projects-list[data-layout=list] { margin-left:30px; - .project-list-item, - .shared-project-list-item { + .project-list-item { margin-left:0px; width:100%; .project-settings { diff --git a/frontend/src/modules/projects/less/qproject.less b/frontend/src/modules/projects/less/qproject.less index e474dfc50e..7d7acb0c03 100644 --- a/frontend/src/modules/projects/less/qproject.less +++ b/frontend/src/modules/projects/less/qproject.less @@ -102,62 +102,6 @@ } } -.shared-project-list-item { - overflow:hidden; - border-radius:3px; - background-color:#fff; - box-shadow: 0px 0px 3pt 2pt transparent; - - &:hover { - box-shadow: 0px 0px 3px 3px #e0e0e0; - cursor: pointer; - .project-settings { - top:0%; - opacity:1; - } - } - - &.selected { - color:#fff; - background-color:#333c4e; - .project-settings { - top:0%; - opacity:1; - } - - .tags li.tag { - background-color:#293141; - span { - color:#E0E8F7; - background-color:#293141; - } - } - } - - &.listing { - width: 700px; - } - - img { - display:block; - } - - .tags li.tag { - margin:2px 5px 2px 0px; - padding:4px; - display: inline-block; - background-color:#9CABC4; - span { - color:#fff; - } - } - - div.tag-container { - height: 34px; - overflow-y: auto; - } -} - .project-header-inner { position:relative; } diff --git a/frontend/src/modules/projects/templates/sharedProject.hbs b/frontend/src/modules/projects/templates/sharedProject.hbs deleted file mode 100644 index b4bc6e2825..0000000000 --- a/frontend/src/modules/projects/templates/sharedProject.hbs +++ /dev/null @@ -1,51 +0,0 @@ -
- -
-
- -
-
- -
-
- -
-
- - {{#if heroImage}} -
-
- {{else}} -
-
- {{/if}} - -
-
-
-

{{{title}}}

-
- -
-
{{t 'app.recent'}}
-
{{momentFormat updatedAt "Do MMMM YYYY"}}
-
- {{#if tags.length}} -
-
{{t 'app.tags'}}
-
-
    - {{#each tags}} -
  • - {{title}} -
  • - {{/each}} -
-
-
- {{/if}} -
-
- - -
diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index a6ea6a645b..b3136714e4 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -3,7 +3,6 @@ define(function(require){ var Origin = require('core/origin'); var OriginView = require('core/views/originView'); var ProjectView = require('./projectView'); - var SharedProjectView = require('./sharedProjectView'); var ProjectsView = OriginView.extend({ className: 'projects', @@ -98,8 +97,7 @@ define(function(require){ }, appendProjectItem: function(model) { - var viewClass = model.isEditable() ? ProjectView : SharedProjectView; - this.getProjectsContainer().append(new viewClass({ model: model }).$el); + this.getProjectsContainer().append(new ProjectView({ model: model }).$el); }, convertFilterTextToPattern: function(filterText) { diff --git a/frontend/src/modules/projects/views/sharedProjectView.js b/frontend/src/modules/projects/views/sharedProjectView.js deleted file mode 100644 index 055eab7510..0000000000 --- a/frontend/src/modules/projects/views/sharedProjectView.js +++ /dev/null @@ -1,91 +0,0 @@ -// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE -define(function(require){ - var OriginView = require('core/views/originView'); - var Origin = require('core/origin'); - - var SharedProjectView = OriginView.extend({ - tagName: 'li', - className: 'shared-project-list-item', - - events: { - 'dblclick': 'promptDuplicateProject', - 'click': 'selectProject', - 'click a.open-context-course': 'openContextMenu' - }, - - preRender: function() { - this.listenTo(this, 'remove', this.remove); - this.listenTo(Origin, 'editorView:deleteProject:' + this.model.get('_id'), this.deleteProject); - this.listenTo(Origin, 'dashboard:projectView:itemSelected', this.deselectItem); - this.listenTo(Origin, 'dashboard:dashboardView:deselectItem', this.deselectItem); - - this.on('contextMenu:sharedcourse:duplicate', this.promptDuplicateProject); - this.on('contextMenu:sharedcourse:preview', this.preview); - - this.model.set('heroImageURI', this.model.getHeroImageURI()); - }, - - openContextMenu: function(e) { - if(e) { - e.stopPropagation(); - e.preventDefault(); - } - Origin.trigger('contextMenu:open', this, e); - }, - - selectProject: function(e) { - e && e.preventDefault(); - this.selectItem(); - }, - - selectItem: function() { - Origin.trigger('dashboard:projectView:itemSelected'); - this.$el.addClass('selected'); - this.model.set({ _isSelected: true }); - }, - - deselectItem: function() { - this.$el.removeClass('selected'); - this.model.set({ _isSelected: false }); - }, - - preview: function() { - var tenantId = this.model.get('_tenantId'); - var courseId = this.model.get('_id'); - - window.open('/preview/' + tenantId + '/' + courseId + '/'); - }, - - promptDuplicateProject: function() { - var self = this; - Origin.Notify.confirm({ - text: Origin.l10n.t('app.confirmduplicate'), - callback: function(confirmed) { - if (confirmed) { - self.duplicateProject(); - } - } - }); - }, - - duplicateProject: function() { - $.ajax({ - url: this.model.getDuplicateURI(), - type: 'GET', - success: function (data) { - Origin.router.navigateTo('editor/' + data.newCourseId + '/settings'); - }, - error: function() { - Origin.Notify.alert({ - type: 'error', - text: Origin.l10n.t('app.errorduplication') - }); - } - }); - } - }, { - template: 'sharedProject' - }); - - return SharedProjectView; -}); From 4bcd32b9e3a658c80e5871e6092e82c39525370d Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Tue, 18 Sep 2018 17:42:08 +0100 Subject: [PATCH 07/30] Remove model attribute --- .../scaffold/views/scaffoldUsersView.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/frontend/src/modules/scaffold/views/scaffoldUsersView.js b/frontend/src/modules/scaffold/views/scaffoldUsersView.js index 19ff2180ce..5d38e7ff9e 100644 --- a/frontend/src/modules/scaffold/views/scaffoldUsersView.js +++ b/frontend/src/modules/scaffold/views/scaffoldUsersView.js @@ -25,7 +25,6 @@ define([ 'core/origin', 'backbone-forms' ], function(Origin, BackboneForms) { }, postRender: function() { - this.model.set('users', []); this.fetchUsers(this.initSelectize); }, @@ -42,8 +41,6 @@ define([ 'core/origin', 'backbone-forms' ], function(Origin, BackboneForms) { labelField: 'email', valueField: '_id', options: users, - onItemAdd: this.onAddUser.bind(this), - onItemRemove: this.onRemoveUser.bind(this), searchField: [ 'email', 'firstName', 'lastName' ], render: { item: this.renderItem, @@ -53,7 +50,7 @@ define([ 'core/origin', 'backbone-forms' ], function(Origin, BackboneForms) { }, getValue: function() { - return this.model.get('users'); + return this.$el.val(); }, setValue: function(value) { @@ -66,20 +63,6 @@ define([ 'core/origin', 'backbone-forms' ], function(Origin, BackboneForms) { blur: function() { if(this.hasFocus) this.$el.blur(); - }, - - onAddUser: function(value) { - var users = this.model.get('users'); - users.push(value); - this.model.set('users', users); - console.log(value, users); - }, - - onRemoveUser: function(value) { - var users = this.model.get('users').filter(function(item) { - return item[this.idField] !== value; - }); - this.model.set('users', users); } }); From 4d49532366333a720a478d116e284aabceb58f23 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Tue, 18 Sep 2018 17:42:14 +0100 Subject: [PATCH 08/30] Filter out me --- frontend/src/modules/scaffold/views/scaffoldUsersView.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/modules/scaffold/views/scaffoldUsersView.js b/frontend/src/modules/scaffold/views/scaffoldUsersView.js index 5d38e7ff9e..89671b8c0d 100644 --- a/frontend/src/modules/scaffold/views/scaffoldUsersView.js +++ b/frontend/src/modules/scaffold/views/scaffoldUsersView.js @@ -40,7 +40,9 @@ define([ 'core/origin', 'backbone-forms' ], function(Origin, BackboneForms) { this.$el.selectize({ labelField: 'email', valueField: '_id', - options: users, + options: users.filter(function(user) { // filter out me + return user._id !== Origin.sessionModel.get('id'); + }), searchField: [ 'email', 'firstName', 'lastName' ], render: { item: this.renderItem, From e62406e9ddeb81ba48a99999637045a788227c26 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 24 Sep 2018 12:14:48 +0100 Subject: [PATCH 09/30] Fix query --- plugins/content/course/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/content/course/index.js b/plugins/content/course/index.js index efd59d0a2d..4638ec3de4 100644 --- a/plugins/content/course/index.js +++ b/plugins/content/course/index.js @@ -79,7 +79,10 @@ function initialize () { }); rest.get('/shared/course', function (req, res, next) { - retrieveCourses(req, res, [{ $or: [{ _shareWithUsers: usermanager.getCurrentUser()._id }, { _isShared: true }] }]); + retrieveCourses(req, res, [ + { createdBy: { $ne: usermanager.getCurrentUser()._id } }, + { $or: [{ _shareWithUsers: usermanager.getCurrentUser()._id }, { _isShared: true }] } + ]); }); /** From 8b9360a4db151139eb57ff5a504e0deaabf2949f Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 24 Sep 2018 12:15:15 +0100 Subject: [PATCH 10/30] Update lang strings --- routes/lang/en-application.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/routes/lang/en-application.json b/routes/lang/en-application.json index 11e1005baa..dd6e854026 100644 --- a/routes/lang/en-application.json +++ b/routes/lang/en-application.json @@ -150,7 +150,8 @@ "app.configurationsettings": "Configuration settings", "app.themepicker": "Theme picker", "app.themepreviewalt": "Preview image", - "app.lastupdated": "UPDATED", + "app.lastupdated": "updated", + "app.createdBy": "author", "app.recent": "Recent", "app.assettitle": "Asset title", "app.assetdescription": "Asset description", From c53a3aed5d42f519fbbad6abc1cab7c65749ea69 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 24 Sep 2018 14:52:54 +0100 Subject: [PATCH 11/30] Add population to user route --- plugins/content/course/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/content/course/index.js b/plugins/content/course/index.js index 4638ec3de4..f569cd67f1 100644 --- a/plugins/content/course/index.js +++ b/plugins/content/course/index.js @@ -67,6 +67,9 @@ function initialize () { jsonOnly: true, fields: DASHBOARD_COURSE_FIELDS.join(' ') }); + if(!options.populate) { + options.populate = { 'createdBy': 'email firstName lastName' }; + } new CourseContent().retrieve(query, options, function (err, results) { if (err) return res.status(500).json(err); res.json(results); From 2811e6294d26ce428dd2d5c29918d8118c06393d Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 24 Sep 2018 14:53:28 +0100 Subject: [PATCH 12/30] Refactor route to allow querying --- lib/usermanager.js | 95 ++++++++-------------------------------------- 1 file changed, 16 insertions(+), 79 deletions(-) diff --git a/lib/usermanager.js b/lib/usermanager.js index bfb7b4a59a..96ad5e4dd2 100644 --- a/lib/usermanager.js +++ b/lib/usermanager.js @@ -663,89 +663,26 @@ exports = module.exports = { // yep app.usermanager = this; - // Get all users - rest.get('/user', function(req, res, next) { - var search = req.query.search || {}; - var orList = []; - var exp = {}; - if (search.email) { - exp['email'] = new RegExp(search['email'], 'i'); - orList.push(exp); - } - - var query = {}; - if (orList.length) { - query.$or = orList; - } - - self.retrieveUsers(query, { populate: { 'roles' : 'name', '_tenantId' : 'name' } }, function(err, users) { - if (err) { - return next(err); + // Get users + rest.get('/user', (req, res, next) => { + const search = req.query.search || {}; + let andList = []; + let orList = []; + // convert searches to regex + Object.keys(search).forEach(key => { + if('string' !== typeof search[key]) { + return andList.push({ [key]: search[key] }); } - - res.statusCode = 200; - return res.json(users); + orList.push({ [key]: new RegExp(search[key], 'i') }); }); - }); - - // Get all users - rest.get('/user/query', function(req, res, next) { - - - var options = _.keys(req.body).length - ? req.body - : req.query; - var search = options.search || {}; - var orList = []; - var andList = []; - - // convert searches to regex - async.each(Object.keys(search), - function (key, nextKey) { - var exp = {}; - // convert strings to regex for likey goodness - if ('string' === typeof search[key]) { - exp[key] = new RegExp(search[key], 'i'); - orList.push(exp); - } else { - exp[key] = search[key]; - andList.push(exp); - } - nextKey(); - }, function () { - var query = {}; - if (orList.length) { - query.$or = orList; - } - - if (andList.length) { - query.$and = andList; - } - - options.populate = { - 'roles' : 'name', - '_tenantId' : 'name' - }; - - self.retrieveUsers(query, options, function(err, users) { - - if (err) { - return next(err); - } - - // user was not found - if (!users) { - res.statusCode = 404; - return res.json({ success: false, message: 'users not found' }); - } - - res.statusCode = 200; - return res.json(users); - }); + let query = {}; + if (orList.length) query.$or = orList; + if (andList.length) query.$and = andList; + this.retrieveUsers(query, { populate: { 'roles': 'name', '_tenantId': 'name' } }, function(err, users) { + if (err) return next(err); + res.status(200).json(users); }); - - }); rest.get('/user/me', function (req, res, next) { From 7032d8b4d507be10b74ccd4ba75b6b7ff2c0290c Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 24 Sep 2018 14:53:40 +0100 Subject: [PATCH 13/30] Fix function --- frontend/src/modules/scaffold/views/scaffoldUsersView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/modules/scaffold/views/scaffoldUsersView.js b/frontend/src/modules/scaffold/views/scaffoldUsersView.js index 89671b8c0d..01a0203d82 100644 --- a/frontend/src/modules/scaffold/views/scaffoldUsersView.js +++ b/frontend/src/modules/scaffold/views/scaffoldUsersView.js @@ -52,7 +52,7 @@ define([ 'core/origin', 'backbone-forms' ], function(Origin, BackboneForms) { }, getValue: function() { - return this.$el.val(); + return this.$el.val().split(','); }, setValue: function(value) { From 457b84f16bd370a6127e1ea2ec159daea430a351 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 24 Sep 2018 15:04:10 +0100 Subject: [PATCH 14/30] Add creator to projectView --- frontend/src/modules/projects/index.js | 2 +- frontend/src/modules/projects/less/projects.less | 7 +++++++ frontend/src/modules/projects/templates/project.hbs | 9 ++++++++- frontend/src/modules/projects/views/projectsView.js | 8 ++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/frontend/src/modules/projects/index.js b/frontend/src/modules/projects/index.js index 16a8615d4b..aefc5227c9 100644 --- a/frontend/src/modules/projects/index.js +++ b/frontend/src/modules/projects/index.js @@ -66,7 +66,7 @@ define(function(require) { switch (options.type) { case 'shared': Origin.trigger('location:title:update', {title: 'Dashboard - viewing shared courses'}); - Origin.contentPane.setView(ProjectsView, { collection: new SharedProjectCollection }); + Origin.contentPane.setView(ProjectsView, { collection: new SharedProjectCollection, _isShared: true }); break; case 'all': Origin.trigger('location:title:update', {title: 'Dashboard - viewing my courses'}); diff --git a/frontend/src/modules/projects/less/projects.less b/frontend/src/modules/projects/less/projects.less index 929637cc52..58cb9af7b1 100644 --- a/frontend/src/modules/projects/less/projects.less +++ b/frontend/src/modules/projects/less/projects.less @@ -57,6 +57,13 @@ ul.projects-options { border-left: none; border-right: none; } + .project-details-last-updated, + .project-details-createdBy { + .projects-details-label { + text-transform: uppercase; + } + } + .project-details-createdBy, .project-details-tags { margin-top:8px; } diff --git a/frontend/src/modules/projects/templates/project.hbs b/frontend/src/modules/projects/templates/project.hbs index dffa5b8f68..8a33877102 100644 --- a/frontend/src/modules/projects/templates/project.hbs +++ b/frontend/src/modules/projects/templates/project.hbs @@ -1,5 +1,4 @@
-
@@ -32,6 +31,14 @@
{{t 'app.lastupdated'}}
{{momentFormat updatedAt "Do MMMM YYYY"}}
+ + {{#if creatorName}} +
+
{{t 'app.createdBy'}}
+
{{creatorName}}
+
+ {{/if}} + {{#if tags.length}}
diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index b3136714e4..e0f6834ed9 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -11,6 +11,11 @@ define(function(require){ "list" ], + preRender: function(options) { + OriginView.prototype.preRender.apply(this, arguments); + this._isShared = options._isShared; + }, + postRender: function() { this.settings.preferencesKey = 'dashboard'; this.initUserPreferences(); @@ -97,6 +102,9 @@ define(function(require){ }, appendProjectItem: function(model) { + var creator = model.get('createdBy'); + var name = creator.firstName ? creator.firstName + ' ' + creator.lastName : creator.email; + if(this._isShared && name) model.set('creatorName', name); this.getProjectsContainer().append(new ProjectView({ model: model }).$el); }, From fedccf308e2d1baa37ea7e6d010f9952599276fe Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 24 Sep 2018 15:04:25 +0100 Subject: [PATCH 15/30] Refactor permissions --- lib/permissions.js | 55 +++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/lib/permissions.js b/lib/permissions.js index 8f413f8510..a573183e4b 100644 --- a/lib/permissions.js +++ b/lib/permissions.js @@ -18,6 +18,7 @@ var ignoreRoutes = [ /^\/api\/createtoken\/?$/, /^\/api\/userpasswordreset\/?.*$/, /^\/api\/my\/course\/?.*$/, + /^\/api\/shared\/course\/?.*$/, /^\/api\/duplicatecourse\/?.*$/, /^\/api\/subscribed\/?.*$/, /^\/api\/theme\/?.*$/, @@ -130,59 +131,39 @@ function PolicyStatementParser(action, resource, statements, callback) { */ parse: function (cb) { - var cachedPermissions = app.usermanager.getSessionVariable('cachedPermissions') || {}; - if ('undefined' !== typeof cachedPermissions[resource]) { + let allowed = false; + let cachedPermissions = app.usermanager.getSessionVariable('cachedPermissions') || {}; + // return early using cached permissions (if production) + if (configuration.getConfig('isProduction') && cachedPermissions[resource]) { return cb(null, cachedPermissions[resource]); } + const resourceToken = this.tokens(resource); - var index, el; - var allowed = false; - - var resourceTok = this.tokens(resource); - if (!resourceTok) { - callback(new Error('given resource is not a valid format')); + if (!resourceToken) { + return callback(new Error('given resource is not a valid format')); } - - for (index = 0; index < statements.length; ++index) { - el = statements[index]; - if (!el) { // save us! - continue; - } - - // check if action matches - if (-1 === el.action.indexOf(action)) { - // no point in further parsing of the statement + for(let i = 0, statement = statements[i]; i < statements.length; i++) { + if (!statement.action.includes(action)) { // check if action matches continue; } + const statementToken = this.tokens(statement.resource) || {}; + const sharesNamespace = statementToken.namespace === resourceToken.namespace; + const matchesResource = this.captures(statementToken.identifier, resourceToken.identifier); - // must share a namespace - var toks = this.tokens(el.resource); - if (!toks || toks.namespace !== resourceTok.namespace) { - // early return + if(!sharesNamespace || !matchesResource) { continue; } - - // check if the identifier captures the supplied identifier - if (!this.captures(toks.identifier, resourceTok.identifier)) { - continue; - } - - // we have a match, now we need to check the effect - an explicit deny has precedence - el.effect = el.effect.toLowerCase(); - if (el.effect === 'deny') { + const effect = statement.effect.toLowerCase(); + if(effect === 'deny') { // explicit deny has precedence allowed = false; break; - } else if (el.effect === 'allow') { // explicitly require 'allow' - allowed = true; // may be overridden by further statements } + if(effect === 'allow') allowed = true; // this may be overridden by further statements } - // save the result in the session - cachedPermissions[resource] = allowed; - app.usermanager.setSessionVariable('cachedPermissions', cachedPermissions); + app.usermanager.setSessionVariable('cachedPermissions', Object.assign(cachedPermissions, { [resource]: allowed })); cb(null, allowed); } - }); } From c5eaf9a8095215478a65e5e127d70eff2f489c89 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Mon, 24 Sep 2018 18:33:19 +0100 Subject: [PATCH 16/30] Remove comment --- lib/usermanager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/usermanager.js b/lib/usermanager.js index 96ad5e4dd2..333702477d 100644 --- a/lib/usermanager.js +++ b/lib/usermanager.js @@ -660,7 +660,6 @@ exports = module.exports = { var auth = require('./auth'); var permissions = require('./permissions'); - // yep app.usermanager = this; // Get users From e5a6b551df70f8a03fa76d833b99de72c4635f9e Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Thu, 1 Nov 2018 18:43:58 +0000 Subject: [PATCH 17/30] Update title text --- plugins/content/course/model.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/content/course/model.schema b/plugins/content/course/model.schema index b0f31116d0..9dcdb71015 100644 --- a/plugins/content/course/model.schema +++ b/plugins/content/course/model.schema @@ -584,7 +584,7 @@ "editorOnly": true, "inputType": "Checkbox", "validators": [], - "title": "Share with all other users", + "title": "Share with all users", "help": "Controls whether or not your colleagues will be able to see this course from the 'Shared courses' option" }, "_shareWithUsers": { From d93666e37e7c54dab8631c72182de97a0237dd31 Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Thu, 1 Nov 2018 18:48:01 +0000 Subject: [PATCH 18/30] Update selectize --- .../libraries/selectize/css/selectize.less | 129 ++++++++++++++---- .../src/libraries/selectize/js/selectize.js | 128 ++++++++++++----- 2 files changed, 199 insertions(+), 58 deletions(-) mode change 100644 => 100755 frontend/src/libraries/selectize/css/selectize.less mode change 100644 => 100755 frontend/src/libraries/selectize/js/selectize.js diff --git a/frontend/src/libraries/selectize/css/selectize.less b/frontend/src/libraries/selectize/css/selectize.less old mode 100644 new mode 100755 index 698c0559f4..2cdb1c24fc --- a/frontend/src/libraries/selectize/css/selectize.less +++ b/frontend/src/libraries/selectize/css/selectize.less @@ -1,5 +1,5 @@ /** - * selectize.css (v0.12.4) + * selectize.default.css (v0.12.6) - Default Theme * Copyright (c) 2013–2015 Brian Reavis & contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this @@ -13,14 +13,13 @@ * * @author Brian Reavis */ - .selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder { visibility: visible !important; background: #f2f2f2 !important; background: rgba(0, 0, 0, 0.06) !important; border: 0 none !important; - -webkit-box-shadow: inset 0 0 12px 4px #ffffff; - box-shadow: inset 0 0 12px 4px #ffffff; + -webkit-box-shadow: inset 0 0 12px 4px #fff; + box-shadow: inset 0 0 12px 4px #fff; } .selectize-control.plugin-drag_drop .ui-sortable-placeholder::after { content: '!'; @@ -89,7 +88,7 @@ vertical-align: middle; display: inline-block; padding: 2px 0 0 0; - border-left: 1px solid #d0d0d0; + border-left: 1px solid #0073bb; -webkit-border-radius: 0 2px 2px 0; -moz-border-radius: 0 2px 2px 0; border-radius: 0 2px 2px 0; @@ -101,18 +100,18 @@ background: rgba(0, 0, 0, 0.05); } .selectize-control.plugin-remove_button [data-value].active .remove { - border-left-color: #cacaca; + border-left-color: #00578d; } .selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { background: none; } .selectize-control.plugin-remove_button .disabled [data-value] .remove { - border-left-color: #ffffff; + border-left-color: #aaaaaa; } .selectize-control.plugin-remove_button .remove-single { position: absolute; - right: 28px; - top: 6px; + right: 0; + top: 0; font-size: 23px; } .selectize-control { @@ -129,7 +128,7 @@ } .selectize-input, .selectize-control.single .selectize-input.input-active { - background: #ffffff; + background: #fff; cursor: text; display: inline-block; } @@ -151,10 +150,10 @@ border-radius: 3px; } .selectize-control.multi .selectize-input.has-items { - padding: 6px 8px 3px; + padding: 5px 8px 2px; } .selectize-input.full { - background-color: #ffffff; + background-color: #fff; } .selectize-input.disabled, .selectize-input.disabled * { @@ -180,20 +179,20 @@ cursor: pointer; margin: 0 3px 3px 0; padding: 2px 6px; - background: #f2f2f2; - color: #303030; - border: 0 solid #d0d0d0; + background: #1da7ee; + color: #fff; + border: 1px solid #0073bb; } .selectize-control.multi .selectize-input > div.active { - background: #e8e8e8; - color: #303030; - border: 0 solid #cacaca; + background: #92c836; + color: #fff; + border: 1px solid #00578d; } .selectize-control.multi .selectize-input.disabled > div, .selectize-control.multi .selectize-input.disabled > div.active { - color: #7d7d7d; - background: #ffffff; - border: 0 solid #ffffff; + color: #ffffff; + background: #d2d2d2; + border: 1px solid #aaaaaa; } .selectize-input > input { display: inline-block !important; @@ -201,7 +200,7 @@ min-height: 0 !important; max-height: none !important; max-width: 100% !important; - margin: 0 2px 0 0 !important; + margin: 0 1px !important; text-indent: 0 !important; border: 0 none !important; background: none !important; @@ -235,7 +234,7 @@ position: absolute; z-index: 10; border: 1px solid #d0d0d0; - background: #ffffff; + background: #fff; margin: -1px 0 0 0; border-top: 0 none; -webkit-box-sizing: border-box; @@ -257,16 +256,25 @@ -moz-border-radius: 1px; border-radius: 1px; } -.selectize-dropdown [data-selectable], +.selectize-dropdown .option, .selectize-dropdown .optgroup-header { padding: 5px 8px; } +.selectize-dropdown .option, +.selectize-dropdown [data-disabled], +.selectize-dropdown [data-disabled] [data-selectable].option { + cursor: inherit; + opacity: 0.5; +} +.selectize-dropdown [data-selectable].option { + opacity: 1; +} .selectize-dropdown .optgroup:first-child .optgroup-header { border-top: 0 none; } .selectize-dropdown .optgroup-header { color: #303030; - background: #ffffff; + background: #fff; cursor: default; } .selectize-dropdown .active { @@ -322,3 +330,74 @@ opacity: 0.5; background-color: #fafafa; } +.selectize-control.multi .selectize-input.has-items { + padding-left: 5px; + padding-right: 5px; +} +.selectize-control.multi .selectize-input.disabled [data-value] { + color: #999; + text-shadow: none; + background: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.selectize-control.multi .selectize-input.disabled [data-value], +.selectize-control.multi .selectize-input.disabled [data-value] .remove { + border-color: #e6e6e6; +} +.selectize-control.multi .selectize-input.disabled [data-value] .remove { + background: none; +} +.selectize-control.multi .selectize-input [data-value] { + text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3); + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background-color: #1b9dec; + background-image: -moz-linear-gradient(top, #1da7ee, #178ee9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#1da7ee), to(#178ee9)); + background-image: -webkit-linear-gradient(top, #1da7ee, #178ee9); + background-image: -o-linear-gradient(top, #1da7ee, #178ee9); + background-image: linear-gradient(to bottom, #1da7ee, #178ee9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1da7ee', endColorstr='#ff178ee9', GradientType=0); + -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03); + box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03); +} +.selectize-control.multi .selectize-input [data-value].active { + background-color: #0085d4; + background-image: -moz-linear-gradient(top, #008fd8, #0075cf); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008fd8), to(#0075cf)); + background-image: -webkit-linear-gradient(top, #008fd8, #0075cf); + background-image: -o-linear-gradient(top, #008fd8, #0075cf); + background-image: linear-gradient(to bottom, #008fd8, #0075cf); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff008fd8', endColorstr='#ff0075cf', GradientType=0); +} +.selectize-control.single .selectize-input { + -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8); + box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8); + background-color: #f9f9f9; + background-image: -moz-linear-gradient(top, #fefefe, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2); + background-image: -o-linear-gradient(top, #fefefe, #f2f2f2); + background-image: linear-gradient(to bottom, #fefefe, #f2f2f2); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0); +} +.selectize-control.single .selectize-input, +.selectize-dropdown.single { + border-color: #b8b8b8; +} +.selectize-dropdown .optgroup-header { + padding-top: 7px; + font-weight: bold; + font-size: 0.85em; +} +.selectize-dropdown .optgroup { + border-top: 1px solid #f0f0f0; +} +.selectize-dropdown .optgroup:first-child { + border-top: 0 none; +} diff --git a/frontend/src/libraries/selectize/js/selectize.js b/frontend/src/libraries/selectize/js/selectize.js old mode 100644 new mode 100755 index 1774599b7d..e064981cf5 --- a/frontend/src/libraries/selectize/js/selectize.js +++ b/frontend/src/libraries/selectize/js/selectize.js @@ -635,7 +635,7 @@ })); /** - * selectize.js (v0.12.4) + * selectize.js (v0.12.6) * Copyright (c) 2013–2015 Brian Reavis & contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this @@ -670,6 +670,8 @@ var highlight = function(node) { var skip = 0; + // Wrap matching part of text node with highlighting , e.g. + // Soccer -> Soccer for regex = /soc/i if (node.nodeType === 3) { var pos = node.data.search(regex); if (pos >= 0 && node.data.length > 0) { @@ -683,7 +685,10 @@ middlebit.parentNode.replaceChild(spannode, middlebit); skip = 1; } - } else if (node.nodeType === 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { + } + // Recurse element node, looking for child text nodes to highlight, unless element + // is childless,