En kodare


Anders Hovmöller
GitHub twitter email

Introducing iommi

2020-05-13

I am very excited to reveal something me and my colleague Johan Lübcke have been working on for quite some time now: iommi. This is a high level framework built on top of Django.

This is primarily a library for developing traditional web apps, but we believe it’s also a solid foundation to build SPAs and APIs going forward. Watch this space. But for now, let’s get started!

I will build a discography app (you can find the code on github with separate commits for each stage of the apps evolution). To make this simpler for demo purposes I will put all my views inside urls.py so we don’t need to jump around between different files too much.

At the end of this 2 part blog series we’ll end up with 4 pages (plus the iommi admin), with custom table, row, cell rendering, filtering, pagination, the advanced query language, and a menu. The index page will look like this:

This library is a union of our previous libraries tri.form, tri.query and tri.table but we’ve collected all our grievances and architectural problems and dealt with them the way you can only do if you are free to let go of backwards compatibility. We’ve also added some new features built on this new foundation that we’ve been thinking about for a few years.

Django setup

Let’s create the venv:

mkdir Supernaut
cd Supernaut
virtualenv -p $(which python3) venv
source venv/bin/activate

Then create a Django project, and the main app:

django-admin startproject Supernaut
cd Supernaut
django-admin startapp Supernaut
cd ..

Django models

We create these models:

from django.db import models


class Artist(models.Model):
    name = models.CharField(max_length=255, db_index=True)

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('name',)


class Album(models.Model):
    name = models.CharField(max_length=255, db_index=True)
    artist = models.ForeignKey(
        Artist, 
        on_delete=models.CASCADE, 
        related_name='albums'
    )
    year = models.IntegerField()

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('name',)


class Track(models.Model):
    name = models.CharField(max_length=255, db_index=True)
    index = models.IntegerField()
    album = models.ForeignKey(
        Album, 
        on_delete=models.CASCADE, 
        related_name='tracks'
    )
    duration = models.CharField(
        max_length=255,
        db_index=False, 
        null=True, 
        blank=True
    )

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('index',)

Create migrations and run them to create our database tables:

./manage.py makemigrations
./manage.py migrate

The real project also has a little database that it creates with pre filled data.

The most basic app

We’ll start with the simplest app we can think of, creating a base.html, index.html and a very simple index view:

base.html:

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}{{ title }}{% endblock %}</title>

    {# jQuery #}
    <script src="https://code.jquery.com/jquery-3.4.1.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="   crossorigin="anonymous"></script>

    {# Bootstrap #}
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">

    {# jQuery first, then Popper.js, then Bootstrap JS #}
    {# Note that bootstrap tells you to use the slim version of jQuery but this does NOT work with select2! #}
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>

    {# Select2 #}
    <link href="https://cdn.jsdelivr.net/npm/select2@4.0.12/dist/css/select2.min.css" rel="stylesheet" />
    <script src="https://cdn.jsdelivr.net/npm/select2@4.0.12/dist/js/select2.min.js"></script>
</head>
<body>
    <div class="container">
        {% block content %}{% endblock %}
    </div>
</body>
</html>

index.html:

{% extends "base.html" %}

{% block content %}
    <h1>{{ title }}</h1>

    {{ content }}
{% endblock %}

urls.py:

from django.shortcuts import render
from django.urls import path

# Views ----------------------------

def index(request):
    return render(
        request,
        template_name='index.html',
        context=dict(
            content='hello',
            title='Title here!',
        )
    )


# URLs -----------------------------

urlpatterns = [
    path('', index),
]

So now that we have a foundation to stand on we will build out the app step by step.

Install iommi

pip install iommi

In Supernaut/settings.py, we add iommi to INSTALLED_APPS and iommi.middleware to MIDDLEWARE:

INSTALLED_APPS = [
    # [...snip...]
    'iommi',
    'Supernaut',
]

MIDDLEWARE = [
    # [...snip...]
    'iommi.middleware',
]

Declarative table

We’ll start with using iommi declarative tables to create a list of albums:

# [... snip ... ]

from iommi import (
    Table,
    Column,
)

# [... snip ... ]

# Tables ---------------------------

class AlbumsTable(Table):
    name = Column()
    artist = Column()
    year = Column()


# Views ----------------------------

def index(request):
    return render(
        request,
        template_name='index.html',
        context=dict(
            content='hello',
            content=AlbumsTable(
                rows=Album.objects.all(),
            ).bind(request=request),
            title='Title here!',
        )
    )

What is bind()?

Table() creates a table definition, and bind() couples it to the request. In this step all things unique to this rendering is calculated. This is when it can do the sorting defined by http parameters for example. bind() returns a new Table that is “bound” and can be rendered.

Simplify!

We can simplify the above quite a bit due to iommis middleware and built in templates. First we’ll delete our base.html because iommi has one. Next up is simplifying the index view:

def index(request):
    return AlbumsTable(
        title='Albums',
        rows=Album.objects.all(),
    )

The iommi middleware will handle if you return an iommi type and render it properly.

Using as_view

The two parameters title and rows we passed to AlbumsTable can also be set on the Meta class like this:

class AlbumsTable(Table):
    name = Column()
    artist = Column()
    year = Column()

    class Meta:
        title = 'Albums'
        rows = Album.objects.all()

(These are defaults so we can still create an AlbumsTable(title='Something else') if we want.)

Now that we have the above we can delete the index view altogether and replace the url definition with this:

urlpatterns = [
    path('', AlbumsTable().as_view()),
]

(We could have also passed the title and rows parameters here, but that’s not as nice I think.)

auto__model

The next step in the simplification is to realize that this table is trivially derived from the model definition. iommi has features to do this for you so we can simplify even further! We delete the entire AlbumsTable class and replace the url definition with this single line:

urlpatterns = [
    path('', Table(auto__model=Album).as_view()),
]

The code above gives you this view:

(The only cheat is that I’ve passed page_size=10 to make the screenshot smaller.)

The title of this table is the plural verbose name of the Album model. Again as stated above, these are all defaults, not hard coded values, so you can pass title to the Table constructor here to override the title for example.

Continue to part 2 for more!

« Test ordering Introducing iommi, part 2 »