React March Meetup Recap

Last night we hosted the largest-ever React Melbourne Meetup!

Talks included:

  • Short intro to React & Flux - Nick Farrell
  • React migration methods, made easy - Me
  • Converting Stateful components to Redux - Cam Jackson
  • React + Typescript - Basarat Ali Syded

There was a great range of content and complexity across the talks, with some good discussions during the breaks.


I gave an overview of how we have iteratively converted Kogan.com to React, in the most non-breaking way possible.

Slides are embedded below, or available here.

Interested?

If you'd like to learn more about this we are hiring!

Or if you are interested in speaking at the next Meetup, get in touch. You may even get a free hat.

Template Illogic: from Django and Handlebars to React

When the current incarnation of Kogan.com was developed, most pages were rendered server-side with Django templates; client-side rendering used Handlebars.js templates.

As well as the effort (and inevitable mismatching content bugs!) caused by maintaining two templates for some parts of the site, the syntax often became awkward and cumbersome. As Kogan.com expanded with new features and into new markets, both templates grew more complex.

For example, if you wanted a page to use "Zip code" in one country and "Postcode" in the others, one could do this:

     {% if country.name == 'United States' %}  Zip code
     {% else %} Postcode
     {% endif %}

This is just the "obvious" way to choose which word to use; there are more appropriate ways of doing this (using the translation features) as well as less appropriate methods:

     {{ country.name == 'United States'|yesno:"Zip code,Postcode" }}

Unfortunately this isn't so straightforward in Handlebars, as there are no operators to allow comparison of variables - control flow structures such as if/else will only operate directly on the boolean value of a variable.

This is a deliberate design decision by Handlebars to enforce the separation between logic and presentation. Mustache (the templating language Handlebars is based on) is even stricter - there are no explicit control flow statements other than loops.

Therefore, it is necessary to pass a value which can be treated as a boolean to Handlebars:

    {{#if country.isUSA}} Zip code
    {{else}} Postcode
    {{/if}}

Or just take all the logic out of the template and pass the content itself:

    {{ country.prefixOfTermUsedInThisCountryForPostalCodesIncludingSpaceCharacterIfSeparateWord }}code

Handlebars can be extended with arbitrary functions, so some such as #ifEquals have been written to allow equality comparison. Other helpers such as #ifCond can work with any conditional operators, but they have no matching else statement, so it is necessary to use two opposite operators:

    {{#ifCond country.name '==' 'United States'}}
        Zip code
    {{/ifCond}}
    {{#ifCond country.name '!=' 'United States'}}
        Postcode
    {{/ifCond}}

One of the benefits of our migration to React.js is the melding of Javascript with the template system into the creature known as JSX.

Although at first glance this tramples all over the separation of logic and presentation, the logic is restricted to only presentation-related decisions due to the way React enforces rendering from a meticulously defined state.

Keeping the front end code and the template within the same file also means it is easier to figure out what is going on - and much easier for programmers who have less front-end experience to use. Compared to our previous hierarchy of .js and .handlebars files, we have found React is a more readable and maintainable solution for the whole team - most of the time:

    ${storeCode === 'us' ? 'Zip ' : 'Post'}code

How to Transform your Team's Communication with Stakeholders

How to Transform your Team's Communication with Stakeholders

As tech people - we quickly flock to digital tools to manage our workflow and processes.  Although an Agile digital task tracker is a great start - at Kogan.com our Physical wall has had huge impact in communicating work in process and priority to our stakeholders - find out why and how you can transform your stakeholder communication.

eClaire - printing our trello cards for our physical wall

There is nothing that creates visibility, collaboration and flexibility of process in a team like a physical wall.

It allows you to show what is currently being worked on and whois owning it, where the bottlenecks are in your process, how much and what is in the backlog and the list goes on.

It provides an awesome mechanism to trigger and facilitate conversations around and provides a visual que that often speaks a thousand words.

But as much I advocated for physical walls to manage a team they are not without their limitations.

They lack the detail and the reporting aspect that you really need an online tool to fulfill and that's why...

Going too deep with Django Sessions

The other day I was battling with some weird behaviour where a key in a session was updated, but sometimes it would revert after a while.

The key in question was a flag to say that a customer had been sent an email about abandoning their cart, and when the key reverted they ended up getting duplicate emails.  To achieve this, we have an offline Celery task that looks over all sessions in the DB and checks a flag on the cart to know if the email had already been sent.

When I dug into the cases where duplicate emails were sent, I noticed that all of them came back to the site after the first email and started browsing again. But why? Why would browsing the site cause the flag to change state? And why wasn't everyone effected? ...

A hidden gem in Django 1.7: ManifestStaticFilesStorage

The biggest change in Django 1.7 was the built-in schema migration support which everyone is aware of, however 1.7 also shipped with lots of other great additions, ManifestStaticFilesStorage - the new static files storage backend was one of them.

Static file caching is everywhere

Before explaining what ManifestStaticFilesStorage is and how it works, this is the overview of why we need it at Kogan.com:

cache busting static

In order to deliver the content to our customers as fast as possible, we cache the downloaded static files by using max-age request headers. This allows our customers to download the content once and the subsequent requests to static files will be served from a cache. As shown on the diagram, if we were to use normal static file names like base.css, the content of the file would be cached in the CDN as well as on the browser and we would have a hard time trying to invalidate these caches. We cache-bust the content by appending a md5 hash of the content of the file to the file name. When we deploy a new base.css, {% static %} template tag will turn base.css into base.d1833e.css and the browser will then request a new file. {% static %} template tag is able to translate base.css into base.d1833e.css thanks to static files storage backend. This setting is named STATICFILES_STORAGE in Django.

Before ManifestStaticFilesStorage

Our Django app was previously configured to use CachedStaticFilesStorage which resulted in placing file mappings in the CACHES backend, for us it was Redis. Django adds these mappings during collectstatic when it gathers all statics and puts them in one place.

collectstatic

This solution has coupled static assets deployment with code deployment resulting in a number of issues:

  • Running collectstatic as part of code deployment --> slow deploys
  • Extra load on Redis
  • App servers were sometimes out of sync as we deploy them in batch. When we start the deployment, Redis would be updated with the new keys, the first batch of App servers would get the new code, but the other half still had old code.

Out of sync app servers

ManifestStaticFilesStorage to the rescue

ManifestStaticFilesStorage has helped us to decouple the static compilation stage from deployments by allowing Django to read static file mappings from staticfiles.json on a filesystem. staticfiles.json is an artifact file produced by collectstatic with ManifestStaticFilesStorage as a backend. We can now include this staticfiles.json into our code package and deploy it to a single app server without affecting the others.

New ManifestStaticFilesStorage

Where is staticfiles.json located?

By default staticfiles.json will reside in STATIC_ROOT which is the directory where all static files are collected in. We host all our static assets on an S3 bucket which means staticfiles.json by default would end up being synced to S3. However, we wanted it to live in the code directory so we could package it and ship it to each app server. As a result of this, ManifestStaticFilesStorage will look for staticfiles.json in STATIC_ROOT in order to read the mappings. We had to overwrite this behaviour, so we subclassed ManifestStaticFilesStorage:


from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
from django.conf import settings

class KoganManifestStaticFilesStorage(ManifestStaticFilesStorage):

    def read_manifest(self):
        """
        Looks up staticfiles.json in Project directory
        """
        manifest_location = os.path.abspath(
            os.path.join(settings.PROJECT_ROOT, self.manifest_name)
        )
        try:
            with open(manifest_location) as manifest:
                return manifest.read().decode('utf-8')
        except IOError:
            return None

With the above change, Django static template tag will now read the mappings from staticfiles.json that resides in project root directory.

Thanks Django

Thanks to Django 1.7, we've not only gotten a better schema migration system but also improved our deployment process. And not to mention ManifestStaticFilesStorage addition was only 40-50 lines of code (as of the day this blog post was published).

Catches when Expecting Exceptions in Django Unit Tests

To cover all bases when writing a suite of unit tests, you need to test for the exceptional cases. However, handling exceptions can break the usual flow of the test case and confuse Django.

Example scenario: unique_together

For example, we have an ecommerce site with many products serving multiple countries, which may have different national languages. Our products may have a description written in different languages, but only one description per (product, language) pair.

We can set up a unique_together constraint to enforce that unique pairing:

class Description(models.Model):
    product = models.ForeignKey("Product")
    language = models.ForeignKey("countries.Language")

    class Meta:
        unique_together = ("product", "language")

    subtitle = models.CharField(...)
    body = models.CharField(...)
    ...

Developer chooses AssertRaises()

If the unique_together rule is violated, Django will raise an IntegrityError. A unit test can verify that this occurs using assertRaises() on a lambda function:

def test_unique_product_description(self):
   desc1 = DescriptionFactory(self.prod1, self.lang1)
   self.assertRaises(IntegrityError, lambda:
      desc2 = DescriptionFactory(self.prod1, self.lang1)

The assertion passes, but the test will fail with a new exception.

A wild TransactionManagementError appears!

Raising the exception when creating a new object will break the current database transaction, causing further queries to be invalid. The next code that accesses the DB - probably the test teardown - will cause a TransactionManagementError to be thrown:

Traceback (most recent call last):
File ".../test_....py", line 29, in tearDown
   ...
File ...
   ...
File ".../django/db/backends/__init.py, line 386, in validate_no_broken_transaction
An error occurred in the current transaction.
TransactionManagementError: An error occurred in the current transaction.
You can't execute queries until the end of the 'atomic' block.

Developer used transaction.atomic. It's super effective!

Wrapping the test (or just the assertion) in its own transaction will prevent the TransactionManagementError from occurring, as only the inner transaction will be affected by the IntegrityError:

def test_unique_product_description(self):
   desc1 = DescriptionFactory(self.prod1, self.lang1)
   with transaction.atomic():
       self.assertRaises(IntegrityError, lambda:
          desc2 = DescriptionFactory(self.prod1, self.lang1)

You don't have to catch 'em all: Another solution

Another way to fix this issue is to subclass your test from TransactionTestCase instead of the usual TestCase. Despite the name, TransactionTestCase doesn't use DB transactions to reset between tests; instead it truncates the tables. This may make the test slower for some cases, but will be more convenient if you are dealing with many IntegrityErrors in the one test. See the Django Documentation for more details on the difference between the two classes.