Skip to content

Commit

Permalink
Added ability for admins to copy rules from one user to another
Browse files Browse the repository at this point in the history
  • Loading branch information
jc21 committed Jan 3, 2018
1 parent e45ce48 commit 1e0e506
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 27 deletions.
63 changes: 53 additions & 10 deletions src/backend/internal/rule.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -206,7 +199,7 @@ const internalRule = {
},

/**
* Set Rule Order
* Set Rule Order - not currently in use
*
* @param {Access} access
* @param {Array} orders
Expand All @@ -226,17 +219,67 @@ const internalRule = {
next(_.assign({}, obj, {updated: res}));
});
})
.error((err) => {
.error(err => {
reject(err);
})
.end((results) => {
.end(results => {
resolve(results);
});
});
})
.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);
});
});
});
}
};

Expand Down
7 changes: 7 additions & 0 deletions src/backend/lib/access/rules-copy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"anyOf": [
{
"$ref": "roles#/definitions/admin"
}
]
}
51 changes: 39 additions & 12 deletions src/backend/routes/api/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
Expand All @@ -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);
})
Expand All @@ -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);
})
Expand Down Expand Up @@ -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);
})
Expand All @@ -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);
})
Expand All @@ -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);
})
Expand Down
30 changes: 30 additions & 0 deletions src/backend/schema/endpoints/rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
13 changes: 13 additions & 0 deletions src/frontend/js/app/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
},

Expand Down
14 changes: 14 additions & 0 deletions src/frontend/js/app/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
36 changes: 36 additions & 0 deletions src/frontend/js/app/user/copy_rules.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<div class="modal-dialog">
<div class="modal-content">
<form class="form-horizontal">
<div class="modal-header text-left">
<h4 class="modal-title">Copy Rules to <%- name %></h4>
</div>
<div class="modal-body">
<p>This procedure will copy <strong>ALL</strong> of the defined rules from the selected user below in to <%- name %>'s account.</p>
<p>This will NOT overwrite or remove any existing rules for the user, who currently has <strong><%- rule_count %></strong> rules.</p>

<div class="form-group">
<label class="col-sm-3 control-label">Copy rules from</label>
<div class="col-sm-9">
<select name="from_user_id" class="form-control" required>
<option value="">Select...</option>
<%
var users = getUsers();
_.each(users, function (user) {
if (user.id !== id && user.rule_count > 0) {
%>
<option value="<%- user.id %>"><%- user.name %> (<%- user.rule_count %> rules)</option>
<%
}
});
%>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-accent save">Copy</button>
</div>
</form>
</div>
</div>
47 changes: 47 additions & 0 deletions src/frontend/js/app/user/copy_rules.js
Original file line number Diff line number Diff line change
@@ -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');
}
};
}
});
Loading

0 comments on commit 1e0e506

Please sign in to comment.