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 authored and Jonathan Niles committed Sep 5, 2018
1 parent 34a9e47 commit eb0a871
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 29 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
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
15 changes: 12 additions & 3 deletions test/client-unit/services/AccountService.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ describe('AccountService', () => {

let Accounts;
let $httpBackend;
let $timeout;
let $verifyNoPendingTasks;

beforeEach(module('bhima.services', 'bhima.mocks', 'angularMoment', 'bhima.constants'));

beforeEach(inject((_$httpBackend_, AccountService, MockDataService) => {
beforeEach(inject((_$httpBackend_, AccountService, MockDataService, _$timeout_, _$verifyNoPendingTasks_) => {
$httpBackend = _$httpBackend_;
$timeout = _$timeout_;
$verifyNoPendingTasks = _$verifyNoPendingTasks_;

Accounts = AccountService;

$httpBackend.when('GET', '/accounts/')
Expand All @@ -19,17 +24,21 @@ describe('AccountService', () => {
afterEach(() => {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
$verifyNoPendingTasks('$timeout');
});


it('#read() will fire an HTTP GET request each time it is called', () => {
it('#read() will fire only a single HTTP GET request when called mutliple times in a row', () => {
const NUM_REQUESTS = 10;
let count = NUM_REQUESTS;
while (count--) {
Accounts.read();
}

$timeout.flush();

// this would throw if too many requests were called.
expect(() => $httpBackend.flush(NUM_REQUESTS)).not.to.throw();
expect(() => $httpBackend.flush(1)).not.to.throw();
expect(() => $httpBackend.flush(1)).to.throw();
});
});
5 changes: 4 additions & 1 deletion test/client-unit/services/VoucherForm.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe('VoucherForm', () => {
let Session;
let form;
let Mocks;
let $timeout;

beforeEach(module(
'bhima.services',
Expand All @@ -19,10 +20,11 @@ describe('VoucherForm', () => {
'bhima.mocks'
));

beforeEach(inject((_VoucherForm_, $httpBackend, _SessionService_, _MockDataService_) => {
beforeEach(inject((_VoucherForm_, $httpBackend, _SessionService_, _MockDataService_, _$timeout_) => {
VoucherForm = _VoucherForm_;
Session = _SessionService_;
Mocks = _MockDataService_;
$timeout = _$timeout_;

// set up the required properties for the session
Session.create(Mocks.user(), Mocks.enterprise(), Mocks.project());
Expand All @@ -37,6 +39,7 @@ describe('VoucherForm', () => {

// make sure $http is clean after tests
afterEach(() => {
$timeout.flush();
httpBackend.flush();
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
Expand Down

0 comments on commit eb0a871

Please sign in to comment.