Skip to content

Development guide

Atul Varma edited this page Aug 9, 2021 · 15 revisions

This development guide is intended to provide a high-level overview of developing on the JustFix.nyc Tenant Platform.

Before reading this, please make sure you've read Core principles and Architecture.

Note: This guide also doesn't cover how to get the Tenant Platform up and running on your system; for details on that, see the project's README.

Introduction

The Tenant Platform's codebase consists primarily of its Django-based Python back-end, its React-based TypeScript front-end, and various layers of "glue" that tie the two together.

At the time of this writing, the Tenant Platform serves the following sites:

The back-end

The back-end attempts to be largely idiomatic Django code. You'll want to make sure you have gone through, at the very least, the official Django tutorial (part of the Django documentation) before embarking on any adventures there.

You can also check out the Plain Language Django Guide for some more tenants2-specific tips and tricks.

Additionally, the back-end uses Python's optional type annotations and checks them via Mypy.

Server-side rendering

By and large, our codebase use Django's template engine very little. Instead, as mentioned in Architecture, most user-facing view logic is actually done via a JSX rendering service: a separate NodeJS-based process that receives requests for pages to render, and returns the results.

On the Django side, this rendering service is invoked a view called react_rendered_view in frontend/views.py. This is used in project/urls.py to handle almost any kind of path that the Django server doesn't handle natively.

The JSX rendering service itself is implemented in frontend/lambda/lambda.tsx.

When invoked, the JSX rendering service is passed a blob of contextual information about the kind of view it's being asked to render, such as what site and URL path needs to be rendered, who is currently logged-in, how the server is configured, and so on. For more details on this information, see the type definitions for AppProps defined in frontend/lib/app.tsx.

The results returned by the JSX rendering service include details such as the HTTP status code that should be returned, the HTML that was rendered, any extra HTTP headers to be included in the response, and so forth. The full specification is documented in the LambdaResponse type defined in frontend/lambda/lambda.tsx.

Non-web content

The JSX rendering service doesn't solely render HTML pages that are intended to be viewed on the web; as it has evolved, it's been used to render other kinds of content intended for other media, such as:

  • HTML and plain-text emails (e.g. #1616)
  • PDFs (e.g. #1536)

GraphQL

The main place our codebase diverges from idiomatic Django is with respect to the way the front-end communicates with the back-end.

Instead of a traditional REST API, the front-end communicates with the back-end via GraphQL. We use Graphene-Django to accomplish this. However, we've also added a few abstractions to make things more DRY.

Before continuing, please skim the GraphQL section of the README, as the rest of this section builds upon it.

The session field

Of special consideration is the server's session GraphQL field: all of this object's fields are automatically resolved and passed to the front-end on every React-driven server-side render, which can be convenient because it relieves the front-end of having to request such frequently-accessed information asynchronously. On the front-end, the contents of this session object can be retrieved via the AppContext React context.

Fields can be added to the session object via the schema registry's register_session_info decorator.

Note, however, that the session field is only automatically retrieved as part of the initial page load. If you create a mutation that modifies any of the fields on session, you may want to have your mutation subclass SessionFormMutation, and have your front-end code use SessionUpdatingFormSubmitter.

For additional optimization, you can also configure autogen-config.toml to only retrieve the particular parts of the session that have been changed by the mutation--but beware that if you accidentally leave out any fields, the client-side session will be out-of-sync with the server!

Finally note that the GraphQL session field should not be confused with Django's session framework, which is used to store data on the server-side and abstract the sending and receiving of cookies. That said, the GraphQL session field is generally intended to contain data that is ultimately derived from the Django session, such as information about the currently logged-in user, hence the reuse of the name.

Multi-pass server-side rendering

If a page that's rendered on the server-side needs to make a GraphQL query, the JSX renderer will return a partial response with the content of the GraphQL query that needs to be executed in order for the full response to be generated. The Django server will then see the GraphQL query, execute it, and pass the results to the JSX renderer again, hopefully receiving a full response this time.

This "multi-pass rendering" allows the full page to be rendered on the server and delivered to the browser without any spinners, throbbers, or skeletons in it.

All this is usually done automatically via the QueryLoader component on the client-side.

Form mutations

We have our own version of Graphene-Django's DjangoFormMutation class that is adapted to our particular needs (such as, for example, adding support for Django Formsets). This class makes it easier to reuse existing Django Form classes in the context of GraphQL mutations; all form validation errors are automatically converted into a valid GraphQL response as needed, and displayed as errors by our front-end form classes.

Using DjangoFormMutation, or some subclass thereof, is the preferred way of creating mutations in the tenant platform, since they will automatically work in the context of progressive enhancement (see below for details on how this is done).

For details on our version of DjangoFormMutation, see project/util/django_graphql_forms.py.

The best way to see form mutations in action is by looking at existing code that uses them; in particular, see the contents of any schema.py file in the repository for the back-end implementations, and search for the use of the SessionUpdatingFormSubmitter React component for their front-end analogues.

Legacy form submission

Because progressive enhancement is a core principle of the platform's design, we've devised a way to convert GraphQL mutations into traditional form-based HTTP POST requests that can be made by browsers without JavaScript enabled.

This is essentially done by including the raw text of a form's GraphQL mutation as an <input type="hidden"> element in the form. When the server receives a POST request with this hidden input, it will execute the mutation wrapped within it, translating all the other form inputs into arguments for the GraphQL mutation (for example, an <input type="checkbox"> is converted into a GraphQL boolean argument). The server will then pass the results of this mutation along to the JSX rendering service, which can process it however it likes. Typically this is all done automatically via the LegacyFormSubmitter component on the front-end.

It should be noted, however, that because every user-facing form that is backed by a GraphQL mutation must also be accessible via this legacy mechanism, the arguments for the GraphQL mutations themselves can become very constrained, since every argument ultimately needs to be able to be converted from a standard HTML form element.

Celery

Any long-running tasks that need to be executed by the server should use the Celery task queue. For more details, see the Celery section of the README.

The front-end

The front-end is a bespoke combination of React and a variety of third-party libraries. Prominent among them are:

  • React Router, used to manage the different URL routes of the site.

  • react-helmet-async, a fork of React Helmet that is used to manage changes to the document head.

  • Loadable Components, used to lazy-load different parts of the site on-demand while ensuring that pages generated on the server-side are fully rendered.

  • LinguiJS, used for internationalization.

Most source code for the front-end is located in the frontend directory.

The entrypoint for the front-end is in frontend/lib/main.ts.

Static assets

With few exceptions, most static assets are ultimately managed via Django's static file system, and depending on configuration are hosted by the server or a CDN upon deployment. The URL to the root of these files is passed to the front-end by the server, allowing the front-end to generate URLs to assets.

Images

SVG files are usually inlined into our main code bundle via ES module imports.

Other images are directly referenced via URLs to static assets. See frontend/lib/ui/static-image.tsx for an example of this.

Note that this distinction between inlined and referenced images can be confusing. For a more detailed explanation, see #1065.

TypeScript and JavaScript

With a few minor exceptions, TypeScript/JavaScript is automatically transpiled by Babel and bundled by Webpack, including code splitting.

SCSS

SCSS is in frontend/sass and compiled directly to a single CSS file on a per-site basis. At present it uses Bulma as its foundation.

While we don't currently adhere to a particular philosophy such as BEM or Atomic CSS, we do have one convention: we try to prefix any new CSS classes we add with jf- (for JustFix). This makes it easier for us to figure out, at a glance, what CSS classes are "first-party" ones that we control, versus "third-party" ones provided by Bulma.

Message catalogs

To complement our code-splitting, we have created a bespoke system to split our internationalization message catalogs into multiple files, allowing page weight to be minimized. See #1407 for more details.

Routing

React Router doesn't provide any out-of-the-box functionality for defining the high level routing structure of a site. This makes it difficult to, for example, link to different parts of the site in a DRY way, or create a list of all the routes on a site. It also makes it difficult to tell whether a URL path is valid without doing a full render.

To ameliorate this, we created our own bespoke concept of "route information", called RouteInfo, a vanilla JS structure which has no third-party dependencies. This lightweight structure allows us to have strong, type-safe references to site routes while minimizing page weight. This structure is defined and documented in frontend/lib/util/route-util.ts.

Different parts of the site are contained in different directories; in general, if a part of the site defines routes, it will contain a route-info.ts file that exports route information, and a routes.tsx file alongside it that defines the actual React Router component(s) that provide the implementation for those routes.