From 1e0e5066fd68670eb4e11dfa81fa5d95c0953c12 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 3 Jan 2018 10:39:33 +1000 Subject: [PATCH] Added ability for admins to copy rules from one user to another --- src/backend/internal/rule.js | 63 +++++++++++++++++++++---- src/backend/lib/access/rules-copy.json | 7 +++ src/backend/routes/api/rules.js | 51 +++++++++++++++----- src/backend/schema/endpoints/rules.json | 30 ++++++++++++ src/frontend/js/app/api.js | 13 +++++ src/frontend/js/app/controller.js | 14 ++++++ src/frontend/js/app/user/copy_rules.ejs | 36 ++++++++++++++ src/frontend/js/app/user/copy_rules.js | 47 ++++++++++++++++++ src/frontend/js/app/users/list-item.ejs | 3 +- src/frontend/js/app/users/list-item.js | 28 +++++++++-- 10 files changed, 265 insertions(+), 27 deletions(-) create mode 100644 src/backend/lib/access/rules-copy.json create mode 100644 src/frontend/js/app/user/copy_rules.ejs create mode 100644 src/frontend/js/app/user/copy_rules.js diff --git a/src/backend/internal/rule.js b/src/backend/internal/rule.js index b84161f..f03147d 100644 --- a/src/backend/internal/rule.js +++ b/src/backend/internal/rule.js @@ -1,7 +1,6 @@ 'use strict'; const _ = require('lodash'); -const debug = require('debug')('juxtapose:internal:rule'); const error = require('../lib/error'); const ruleModel = require('../models/rule'); const batchflow = require('batchflow'); @@ -18,10 +17,6 @@ const internalRule = { * @returns {Promise} */ create: (access, data) => { - if (typeof data.is_disabled !== 'undefined') { - data.is_disabled = data.is_disabled ? 1 : 0; - } - // Define some defaults if they were not set if (typeof data.user_id === 'undefined' || !data.user_id) { data.user_id = access.token.get('attrs').id; @@ -112,8 +107,6 @@ const internalRule = { * @return {Promise} */ get: (access, data) => { - //debug('Getting rule record', data); - return access.can('rules:get', data.id) .then(() => { let query = ruleModel @@ -206,7 +199,7 @@ const internalRule = { }, /** - * Set Rule Order + * Set Rule Order - not currently in use * * @param {Access} access * @param {Array} orders @@ -226,10 +219,10 @@ const internalRule = { next(_.assign({}, obj, {updated: res})); }); }) - .error((err) => { + .error(err => { reject(err); }) - .end((results) => { + .end(results => { resolve(results); }); }); @@ -237,6 +230,56 @@ const internalRule = { .then(() => { return true; }); + }, + + /** + * Copy rules from one account to another + * + * @param {Access} access + * @param {Object} data + * @param {Integer} data.from + * @param {Integer} data.to + */ + copy: (access, data) => { + return access.can('rules:copy') + .then(() => { + if (data.from === data.to) { + throw new error.ValidationError('Cannot copy rules to the same person'); + } + + // 1. Select rules from user + return ruleModel + .query() + .where('is_deleted', 0) + .andWhere('user_id', data.from); + }) + .then(rules => { + // 2. Insert modified rules for a user + return new Promise((resolve, reject) => { + batchflow(rules).sequential() + .each((i, rule, next) => { + let new_rule = _.omit(rule, ['fired_count', 'user_id', 'id']); + new_rule.user_id = data.to; + + ruleModel + .query() + .insert(new_rule) + .then(() => { + next(); + }) + .catch(err => { + console.error(err); + next(err); + }); + }) + .error(err => { + reject(err); + }) + .end((/*results*/) => { + resolve(true); + }); + }); + }); } }; diff --git a/src/backend/lib/access/rules-copy.json b/src/backend/lib/access/rules-copy.json new file mode 100644 index 0000000..d2709fd --- /dev/null +++ b/src/backend/lib/access/rules-copy.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/routes/api/rules.js b/src/backend/routes/api/rules.js index 1de5c25..ac61994 100644 --- a/src/backend/routes/api/rules.js +++ b/src/backend/routes/api/rules.js @@ -44,19 +44,19 @@ router sort: req.query.sort, expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) }) - .then((data) => { + .then(data => { return Promise.all([ internalRule.getCount(res.locals.access), internalRule.getAll(res.locals.access, req.query.offset, req.query.limit, data.sort, data.expand) ]); }) - .then((data) => { + .then(data => { res.setHeader('X-Dataset-Total', data.shift()); res.setHeader('X-Dataset-Offset', req.query.offset); res.setHeader('X-Dataset-Limit', req.query.limit); return data.shift(); }) - .then((rules) => { + .then(rules => { res.status(200) .send(rules); }) @@ -70,10 +70,10 @@ router */ .post((req, res, next) => { apiValidator({$ref: 'endpoints/rules#/links/1/schema'}, req.body) - .then((payload) => { + .then(payload => { return internalRule.create(res.locals.access, payload); }) - .then((result) => { + .then(result => { res.status(201) .send(result); }) @@ -99,10 +99,37 @@ router */ .post((req, res, next) => { apiValidator({$ref: 'endpoints/rules#/links/4/schema'}, req.body) - .then((payload) => { + .then(payload => { return internalRule.setOrder(res.locals.access, payload); }) - .then((result) => { + .then(result => { + res.status(200) + .send(result); + }) + .catch(next); + }); + +/** + * Copy rules from one user to another + * + * /api/rules/copy + */ +router + .route('/copy') + .options((req, res) => { + res.sendStatus(204); + }) + .all(jwtdecode()) // preferred so it doesn't apply to nonexistent routes + + /** + * POST /api/rules/copy + */ + .post((req, res, next) => { + apiValidator({$ref: 'endpoints/rules#/links/5/schema'}, req.body) + .then(payload => { + return internalRule.copy(res.locals.access, payload); + }) + .then(result => { res.status(200) .send(result); }) @@ -142,13 +169,13 @@ router rule_id: req.params.rule_id, expand: (typeof req.query.expand === 'string' ? req.query.expand.split(',') : null) }) - .then((data) => { + .then(data => { return internalRule.get(res.locals.access, { id: data.rule_id, expand: data.expand }); }) - .then((rule) => { + .then(rule => { res.status(200) .send(rule); }) @@ -162,11 +189,11 @@ router */ .put((req, res, next) => { apiValidator({$ref: 'endpoints/rules#/links/2/schema'}, req.body) - .then((payload) => { + .then(payload => { payload.id = parseInt(req.params.rule_id, 10); return internalRule.update(res.locals.access, payload); }) - .then((result) => { + .then(result => { res.status(200) .send(result); }) @@ -180,7 +207,7 @@ router */ .delete((req, res, next) => { internalRule.delete(res.locals.access, {id: req.params.rule_id}) - .then((result) => { + .then(result => { res.status(200) .send(result); }) diff --git a/src/backend/schema/endpoints/rules.json b/src/backend/schema/endpoints/rules.json index 0ed59d4..35c82f9 100644 --- a/src/backend/schema/endpoints/rules.json +++ b/src/backend/schema/endpoints/rules.json @@ -227,6 +227,36 @@ "targetSchema": { "type": "boolean" } + }, + { + "title": "Copy", + "description": "Copies rules from one user to another", + "href": "/rules/copy", + "access": "private", + "method": "POST", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "from": { + "type": "integer", + "minimum": 1 + }, + "to": { + "type": "integer", + "minimum": 1 + } + } + }, + "targetSchema": { + "type": "boolean" + } } ], "properties": { diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index 16e43e5..1a3bd50 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -339,6 +339,19 @@ module.exports = { */ setOrder: function (order) { return fetch('post', 'rules/order', order); + }, + + /** + * + * @param {Integer} from_user_id + * @param {Integer} to_user_id + * @returns {Promise} + */ + copy: function (from_user_id, to_user_id) { + return fetch('post', 'rules/copy', { + from: from_user_id, + to: to_user_id + }); } }, diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 786244a..2de5a83 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -360,6 +360,20 @@ module.exports = { } }, + /** + * User Copy Rules Form + * + * @param {Object} model + * @param {Array} users + */ + showUserCopyRules: function (model, users) { + if (Cache.User.isAdmin() || model.get('id') === Cache.User.get('id')) { + require(['./main', './user/copy_rules'], function (App, View) { + App.UI.showModalDialog(new View({model: model, users: users})); + }); + } + }, + /** * User Service Settings Form * diff --git a/src/frontend/js/app/user/copy_rules.ejs b/src/frontend/js/app/user/copy_rules.ejs new file mode 100644 index 0000000..8e26da3 --- /dev/null +++ b/src/frontend/js/app/user/copy_rules.ejs @@ -0,0 +1,36 @@ + diff --git a/src/frontend/js/app/user/copy_rules.js b/src/frontend/js/app/user/copy_rules.js new file mode 100644 index 0000000..4143c8b --- /dev/null +++ b/src/frontend/js/app/user/copy_rules.js @@ -0,0 +1,47 @@ +'use strict'; + +import Mn from 'backbone.marionette'; + +const template = require('./copy_rules.ejs'); +const Controller = require('../controller'); +const Api = require('../api'); +const App = require('../main'); + +module.exports = Mn.View.extend({ + template: template, + + ui: { + form: 'form', + buttons: 'form button', + cancel: 'button.cancel', + from_user_id: 'select[name="from_user_id"]' + }, + + events: { + 'submit @ui.form': function (e) { + e.preventDefault(); + let from_user_id = parseInt(this.ui.from_user_id.val(), 10); + + this.ui.buttons.prop('disabled', true).addClass('btn-disabled'); + Api.Rules.copy(from_user_id, this.model.get('id')) + .then(() => { + App.UI.closeModal(); + Controller.showUsers(); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + }, + + templateContext: function () { + let view = this; + + return { + getUsers: function () { + return view.getOption('users'); + } + }; + } +}); diff --git a/src/frontend/js/app/users/list-item.ejs b/src/frontend/js/app/users/list-item.ejs index 6a1e18d..c6af624 100644 --- a/src/frontend/js/app/users/list-item.ejs +++ b/src/frontend/js/app/users/list-item.ejs @@ -3,8 +3,9 @@ <%- email %> <%- roles.join(', ') %> <%- niceNumber(rule_count) %> - + + diff --git a/src/frontend/js/app/users/list-item.js b/src/frontend/js/app/users/list-item.js index 5bcefd3..9c7e695 100644 --- a/src/frontend/js/app/users/list-item.js +++ b/src/frontend/js/app/users/list-item.js @@ -14,7 +14,8 @@ module.exports = Mn.View.extend({ user: 'a.user-link', edit: '.btn-edit', password: '.btn-password', - service_settings: '.btn-service-settings' + service_settings: '.btn-service-settings', + copy_rules: '.btn-copy-rules' }, events: { @@ -40,17 +41,36 @@ module.exports = Mn.View.extend({ this.ui.service_settings.prop('disabled', true).addClass('btn-disabled'); Api.Services.getAvailable() - .then((services) => { + .then(services => { if (!view.isDestroyed()) { Controller.showUserServiceSettingsForm(this.model, services); this.ui.service_settings.prop('disabled', false).removeClass('btn-disabled'); } }) - .catch((err) => { + .catch(err => { Controller.showError(err, 'Could not fetch available services'); this.ui.service_settings.prop('disabled', false).removeClass('btn-disabled'); }); - } + }, + + 'click @ui.copy_rules': function (e) { + e.preventDefault(); + let view = this; + + this.ui.copy_rules.prop('disabled', true).addClass('btn-disabled'); + + Api.Users.getAll() + .then(users => { + if (!view.isDestroyed()) { + Controller.showUserCopyRules(this.model, users.data); + this.ui.copy_rules.prop('disabled', false).removeClass('btn-disabled'); + } + }) + .catch(err => { + Controller.showError(err, 'Could not fetch Users'); + this.ui.copy_rules.prop('disabled', false).removeClass('btn-disabled'); + }); + }, }, templateContext: function () {