Fast atomic content switching, and more
For a financial service provider, we created a way to switch the financial data in the site in an all-or-nothing manner. We also highlight some other nice features.
Recently we released a financial portal, where customers can see information about their investments, and place orders.
There are a couple of nice things we did in this site that we'd like to share here.
Datasets
When a customer logs in, they see information about their account (in the financial sense). That data is supplied by our client's back-end system. That system doesn't spit it out on a per-customer basis, but it provides a whole package. This has something to do with the market value at a specific point in time, and with the need to keep it all consistent. The data in this package should not change, only the whole package should be replaced, and they call this a Dataset.
In order to be able to switch all this information instantaneously, and ensuring integrity, we create not one Account object, but we create one for each Dataset it belongs to:
class Account(models.Model):
"""Account, in the financial sense."""
dataset = models.ForeignKey(Dataset, on_delete=models.CASCADE)
number = models.CharField(verbose_name=_("Account number"))
class Meta:
unique_together = [("dataset", "number")]
class Dataset(models.Model):
"""Cluster of data that should only be changed as a whole, not partially"""
active = models.BooleanField(default=False)
Only one Dataset is active at the same time. Whenever new data arrives, we load it into another Dataset at our leisure. When the loading is done, that Dataset is marked active
[^1], and tadaa: the new data is active. At no point in time has the data in the portal been inconsistent.
Obviously, having no direct link between Django objects complicates coding a bit, but that's a small price we pay for a feature our client has been using to their great relief.
Frontend tests
Usually frontend just works ™, and whenever it doesn't, people will notice. Sometimes, that's just not enough.
For example, to assure our four character pincode field will always respond as nicely to keyboard input as it does, ensuring a smooth user experience, we test this:
describe('focusMethods get called when keydown event occurs', () => {
const focusNextFieldMock = jest.spyOn(PincodeField.prototype, 'focusNextField');
const focusPreviousFieldMock = jest.spyOn(PincodeField.prototype, 'focusPreviousField');
let firstInput;
beforeEach(() => {
const field = document.querySelector('.field');
const handler = new PincodeField(field);
firstInput = field.querySelectorAll('input')[0]; // eslint-disable-line
firstInput.value = '';
});
test('When pressing the letter A', () => {
th.fire('keydown', firstInput, { key: 'a' });
expect(focusNextFieldMock).toHaveBeenCalledWith(0, 20);
});
test('When pressing Backspace', () => {
th.fire('keydown', firstInput, { key: 'Backspace' });
expect(focusPreviousFieldMock).toHaveBeenCalledWith(0, 20);
});
test('When pressing Left arrow', () => {
th.fire('keydown', firstInput, { key: 'ArrowLeft' });
expect(focusPreviousFieldMock).toHaveBeenCalledWith(0);
});
test('When pressing Right arrow', () => {
th.fire('keydown', firstInput, { key: 'ArrowRight' });
expect(focusNextFieldMock).toHaveBeenCalledWith(0);
});
Maintaining documentation with the code
Famously, two things in programming are hard: Naming things, and caching. Another thing that's nearly as hard is documenting.
When this project started out, our client supplied us with a nice [^2] long document where they stated their requirements. As we progressed, a lot of things changed. Sound familiar? Halfway through the project, there was no single place to find the result of all that was discussed and agreed on.
At this point we decided to keep documentation alongside the code. We started out with our clients requirements document, changing and adding things [^3] as we moved on. Having documentation inside the repository makes it easier to keep it up-to-date, as it's easy to include in the review of the pull request. Having it outside of the actual code (docstrings and such) allows us to write it so that our client can also read it.
As a bonus: When we started features by writing this documentation first and discussing it with the client, it saved us a lot of work.
Webtest for user interaction
We used Webtest (via django-webtest) to test user interaction, which has some nice advantages over Django's default test client.
Logging in as a user is simple:
page = django_app.get(url, user=username)
It makes form submission easier:
form = page.form
form["some-field"] = ""
page_after_submit = form.submit()
This is more comprehensive than just posting certain parameters to a certain URL, it also tests the form on the page.
It's easy to find things in the HTML of the response:
# Use BeautifulSoup to find back-link
back_link = page.html.find("a", text="Back")
assert back_link.attrs["href"] == expected_reverse_url
[^1]: Yes, we also mark the previously active one as inactive
, thanks for paying attention!
[^2]: Well, not always nice.
[^3]: Things like "where does the back-end data come from", "how should it should this data be delivered to the website" [^4], "what data powers the My Accounts page", "how does the website send an order to the backend".
[^4]: In a bunch of CSV files packed in a .rar, in case you're curious.