Skip to content

Commit

Permalink
feat(accounts): debounce read request
Browse files Browse the repository at this point in the history
This commit adds a debounce() call to the read request.  Since it uses
promises and Angular's $timeout(), it will require flushing the
$timeouts in tests.

Closes Third-Culture-Software#2892.
  • Loading branch information
jniles committed Sep 5, 2018
1 parent 510934b commit bc16b30
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 88 deletions.
41 changes: 41 additions & 0 deletions client/src/js/services/debounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
angular.module('bhima.services')
.factory('debounce', ['$timeout', '$q', ($timeout, $q) => {

// The service is actually this function, which we call with the func
// that should be debounced and how long to wait in between calls
return function debounce(func, wait, immediate) {
let timeout;
// Create a deferred object that will be resolved when we need to
// actually call the func
let deferred = $q.defer();

return function fn() {
const context = this;

// eslint-disable-next-line prefer-rest-params
const args = arguments;

const later = function () {
timeout = null;
if (!immediate) {
deferred.resolve(func.apply(context, args));
deferred = $q.defer();
}
};

const callNow = immediate && !timeout;
if (timeout) {
$timeout.cancel(timeout);
}

timeout = $timeout(later, wait);

if (callNow) {
deferred.resolve(func.apply(context, args));
deferred = $q.defer();
}

return deferred.promise;
};
};
}]);
8 changes: 5 additions & 3 deletions client/src/modules/accounts/accounts.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ angular.module('bhima.services')
.service('AccountService', AccountService);

AccountService.$inject = [
'PrototypeApiService', 'bhConstants',
'PrototypeApiService', 'bhConstants', 'debounce',
];

/**
* Account Service
*
* A service wrapper for the /accounts HTTP endpoint.
*/
function AccountService(Api, bhConstants) {
function AccountService(Api, bhConstants, debounce) {
const baseUrl = '/accounts/';
const service = new Api(baseUrl);

service.read = read;
// debounce the read() method by 250 milliseconds to avoid needless GET requests
service.read = debounce(read, 150, false);
service.label = label;

service.getBalance = getBalance;
Expand All @@ -41,6 +42,7 @@ function AccountService(Api, bhConstants) {
.then(service.util.unwrapHttpResponse);
}


/**
* The read() method loads data from the api endpoint. If an id is provided,
* the $http promise is resolved with a single JSON object, otherwise an array
Expand Down
45 changes: 24 additions & 21 deletions client/src/modules/cash/modals/transfer-modal.ctrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ angular.module('bhima.controllers')

CashTransferModalController.$inject = [
'CurrencyService', 'VoucherService', 'CashboxService', 'AccountService', 'SessionService',
'CashService', '$state', 'NotifyService', 'ReceiptModal', 'bhConstants', 'VoucherForm'
'CashService', '$state', 'NotifyService', 'ReceiptModal', 'bhConstants', 'VoucherForm',
];

/**
Expand All @@ -12,32 +12,35 @@ CashTransferModalController.$inject = [
* @description
* This controller is responsible transferring money between a cashbox and a transfer account.
*/
function CashTransferModalController(Currencies, Vouchers, Cashboxes, Accounts, Session, Cash, $state, Notify, Receipts, bhConstants, VoucherForm) {
var vm = this;
function CashTransferModalController(
Currencies, Vouchers, Cashboxes, Accounts, Session, Cash, $state, Notify,
Receipts, bhConstants, VoucherForm
) {
const vm = this;

vm.voucher = new VoucherForm('CashTransferForm');

var TRANSFER_TYPE_ID = bhConstants.transactionType.TRANSFER;
const TRANSFER_TYPE_ID = bhConstants.transactionType.TRANSFER;

vm.loadAccountDetails = loadAccountDetails;
vm.submit = submit;

// submit and close the modal
function submit(form) {
if (form.$invalid) { return; }
if (form.$invalid) { return 0; }

var record = prepareVoucherRecord();
const record = prepareVoucherRecord();

// validate
var validation = vm.voucher.validate();
if (!validation) { return; }
const validation = vm.voucher.validate();
if (!validation) { return 0; }

return Vouchers.create(record)
.then(function (response) {
.then((response) => {
Notify.success('CASH.TRANSFER.SUCCESS');
return Receipts.voucher(response.uuid, true);
})
.then(function () {
.then(() => {
return $state.go('^.window', { id : $state.params.id });
})
.catch(Notify.handleError);
Expand All @@ -46,35 +49,35 @@ function CashTransferModalController(Currencies, Vouchers, Cashboxes, Accounts,
function prepareVoucherRecord() {

// extract the voucher from the VoucherForm
var record = vm.voucher.details;
const record = vm.voucher.details;
record.items = vm.voucher.store.data;

// configure the debits/credits appropriately

var debit = record.items[0];
const debit = record.items[0];
debit.configure({ debit : vm.amount, account_id : vm.transferAccount.id });

var credit = record.items[1];
const credit = record.items[1];
credit.configure({ credit : vm.amount, account_id : vm.cashAccount.id });

// format voucher description as needed
vm.voucher.description('CASH.TRANSFER.DESCRIPTION', {
amount : vm.amount,
fromLabel : vm.cashAccount.label,
toLabel : vm.transferAccount.label,
userName : Session.user.display_name
userName : Session.user.display_name,
});

return record;
}

// this object retains a mapping of the currency ids to their respective accounts.
var cashCurrencyMap = {};
let cashCurrencyMap = {};

// this function maps the accounts to their respective currencies.
// { currency_id : { currency_id, account_id, transfer_account_id } }
function mapCurrenciesToAccounts(currencies) {
return currencies.reduce(function (map, currency) {
return currencies.reduce((map, currency) => {
map[currency.currency_id] = currency;
return map;
}, {});
Expand All @@ -88,11 +91,11 @@ function CashTransferModalController(Currencies, Vouchers, Cashboxes, Accounts,

// load needed modules
Currencies.read()
.then(function (currencies) {
.then((currencies) => {
vm.currencies = currencies;
return Cashboxes.read($state.params.id);
})
.then(function (cashbox) {
.then((cashbox) => {
vm.cashbox = cashbox;
vm.disabledCurrencyIds = Cash.calculateDisabledIds(cashbox, vm.currencies);

Expand All @@ -108,19 +111,19 @@ function CashTransferModalController(Currencies, Vouchers, Cashboxes, Accounts,
cashCurrencyMap = mapCurrenciesToAccounts(vm.cashbox.currencies);

// pull the accounts from the cashCurrencyMap
var accounts = cashCurrencyMap[selectedCurrencyId];
const accounts = cashCurrencyMap[selectedCurrencyId];

// look up the transfer account
Accounts.read(accounts.transfer_account_id)
.then(function (account) {
.then((account) => {
account.hrlabel = Accounts.label(account);
vm.transferAccount = account;
})
.catch(Notify.handleError);

// look up the cash account
Accounts.read(accounts.account_id)
.then(function (account) {
.then((account) => {
account.hrlabel = Accounts.label(account);
vm.cashAccount = account;
})
Expand Down
108 changes: 49 additions & 59 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,47 +195,34 @@ gulp.task('client-compile-typescript', (cb) => {
});

// minify the vendor JS code and compact into a vendor.min.js file.
gulp.task('client-compile-vendor', () =>
gulp.src(paths.client.vendorJs)
.pipe(gulpif(isProduction, uglify({ mangle : true })))
.pipe(concat('js/vendor.min.js'))
.pipe(gulp.dest(CLIENT_FOLDER)));
gulp.task('client-compile-vendor', () => gulp.src(paths.client.vendorJs)
.pipe(gulpif(isProduction, uglify({ mangle : true })))
.pipe(concat('js/vendor.min.js'))
.pipe(gulp.dest(CLIENT_FOLDER)));


// minify the client css styles via cssnano
// writes output to style.min.css
gulp.task('client-compile-css', () =>
gulp.src(paths.client.css)
.pipe(cssnano({ zindex : false }))
.pipe(concat('css/style.min.css'))
.pipe(gulp.dest(CLIENT_FOLDER)));
gulp.task('client-compile-css', () => gulp.src(paths.client.css)
.pipe(cssnano({ zindex : false }))
.pipe(concat('css/style.min.css'))
.pipe(gulp.dest(CLIENT_FOLDER)));

// move vendor files over to the /vendor directory
// TODO - separate movement of fonts from the movement of styles
gulp.task('client-mv-vendor-style', () =>
gulp.src(paths.client.vendorStyle)
.pipe(gulp.dest(`${CLIENT_FOLDER}vendor/`)));

gulp.task('client-vendor-build-bootstrap', () =>
/**
* For now this just moves the latest version of bootstrap into the repository
* This build process should compile the latest bhima less definition with the
* latest vendor files
* - move less file to bootstrap vendor folder
* - compile with less
* - copy CSS into static file folder
*/
gulp.src(BHIMA_LESS)
.pipe(gulp.dest('client/vendor/bootstrap/less/'))
.pipe(less({
paths : ['./client/vendor/bootstrap/less/'],
}))
.pipe(gulp.dest(`${CLIENT_FOLDER}css`)));
gulp.task('client-mv-vendor-style', () => gulp.src(paths.client.vendorStyle)
.pipe(gulp.dest(`${CLIENT_FOLDER}vendor/`)));

gulp.task('client-vendor-build-bootstrap', () => gulp.src(BHIMA_LESS)
.pipe(gulp.dest('client/vendor/bootstrap/less/'))
.pipe(less({
paths : ['./client/vendor/bootstrap/less/'],
}))
.pipe(gulp.dest(`${CLIENT_FOLDER}css`)));

// move static files to the public directory
gulp.task('client-mv-static', ['lint-i18n'], () =>
gulp.src(paths.client.static)
.pipe(gulp.dest(CLIENT_FOLDER)));
gulp.task('client-mv-static', ['lint-i18n'], () => gulp.src(paths.client.static)
.pipe(gulp.dest(CLIENT_FOLDER)));

// custom task: compare the English and French for missing tokens
gulp.task('lint-i18n', (cb) => {
Expand All @@ -251,16 +238,14 @@ gulp.task('lint-i18n', (cb) => {
});

// compile i18n files english
gulp.task('client-compile-i18n-en', () =>
gulp.src(paths.client.translate_en)
.pipe(merge({ fileName : 'en.json' }))
.pipe(gulp.dest(CLIENT_FOLDER + 'i18n/')));
gulp.task('client-compile-i18n-en', () => gulp.src(paths.client.translate_en)
.pipe(merge({ fileName : 'en.json' }))
.pipe(gulp.dest(`${CLIENT_FOLDER}i18n/`)));

// compile i18n files french
gulp.task('client-compile-i18n-fr', () =>
gulp.src(paths.client.translate_fr)
.pipe(merge({ fileName : 'fr.json' }))
.pipe(gulp.dest(CLIENT_FOLDER + 'i18n/')));
gulp.task('client-compile-i18n-fr', () => gulp.src(paths.client.translate_fr)
.pipe(merge({ fileName : 'fr.json' }))
.pipe(gulp.dest(`${CLIENT_FOLDER}i18n/`)));

// watches for any change and builds the appropriate route
gulp.task('watch-client', () => {
Expand All @@ -277,25 +262,33 @@ gulp.task('watch-client', () => {
// gather a list of files to rewrite revisions for
const toHash = ['**/*.min.js', '**/*.css'].map(file => `${CLIENT_FOLDER}${file}`);

gulp.task('client-compute-hashes', ['client-compile-typescript', 'client-compile-vendor', 'client-compile-css'], () =>
gulp.src(toHash)
.pipe(rev())
.pipe(gulp.dest(CLIENT_FOLDER))
.pipe(rev.manifest(MANIFEST_PATH))
.pipe(gulp.dest(CLIENT_FOLDER)));

gulp.task('client-compile-assets', ['client-mv-static', 'client-compute-hashes'], () =>
gulp.src(paths.client.index)
.pipe(template({ isProduction, isDevelopment }))
.pipe(revReplace({ manifest : gulp.src(`${CLIENT_FOLDER}${MANIFEST_PATH}`) }))
.pipe(gulp.dest(CLIENT_FOLDER)));
gulp.task('client-compute-hashes', [
'client-compile-typescript',
'client-compile-vendor',
'client-compile-css',
], () => gulp.src(toHash)
.pipe(rev())
.pipe(gulp.dest(CLIENT_FOLDER))
.pipe(rev.manifest(MANIFEST_PATH))
.pipe(gulp.dest(CLIENT_FOLDER)));

gulp.task('client-compile-assets', ['client-mv-static', 'client-compute-hashes'], () => gulp.src(paths.client.index)
.pipe(template({ isProduction, isDevelopment }))
.pipe(revReplace({ manifest : gulp.src(`${CLIENT_FOLDER}${MANIFEST_PATH}`) }))
.pipe(gulp.dest(CLIENT_FOLDER)));

// TODO - streamline piping so that all the assets - CSS, javascript
// are built with rev() and then written with rev.manifest({ merge : true });
// Then the last thing will be to rewrite index.html, replacing the values
// with gulp-replace and then replacing the manifest itself.
gulp.task('build-client', () => {
gulp.start('client-compile-assets', 'client-vendor-build-bootstrap', 'client-mv-vendor-style', 'client-compile-i18n-en', 'client-compile-i18n-fr');
gulp.start(
'client-compile-assets',
'client-vendor-build-bootstrap',
'client-mv-vendor-style',
'client-compile-i18n-en',
'client-compile-i18n-fr'
);
});

/* -------------------------------------------------------------------------- */
Expand All @@ -314,13 +307,11 @@ gulp.task('build-client', () => {
*/

// move the server files into /bin/server
gulp.task('server-mv-files', () =>
gulp.src(paths.server.files)
.pipe(gulp.dest(SERVER_FOLDER)));
gulp.task('server-mv-files', () => gulp.src(paths.server.files)
.pipe(gulp.dest(SERVER_FOLDER)));

// build the server
gulp.task('build-server', () =>
gulp.start('server-mv-files'));
gulp.task('build-server', () => gulp.start('server-mv-files'));

/* -------------------------------------------------------------------------- */

Expand All @@ -339,4 +330,3 @@ gulp.task('build', ['clean'], () => {
gulp.task('default', ['clean'], () => {
gulp.start('build-client', 'build-server');
});

1 change: 0 additions & 1 deletion server/config/express.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ exports.configure = function configure(app) {

// Only allow routes to use /login, /projects, /logout, and /languages if a
// user session does not exists

app.use((req, res, next) => {
if (_.isUndefined(req.session.user) && !within(req.path, publicRoutes)) {
debug(`Rejecting unauthorized access to ${req.path} from ${req.ip}`);
Expand Down
Loading

0 comments on commit bc16b30

Please sign in to comment.