iommiでHTMLやJavaScriptを書かずにDjangoアプリを作成する

[iommi](https://github.com/iommirocks/iommi)はDjangoのアドオン的に使用するライブラリです。データの新規作成フォームや一覧ページといった物を持つ典型的なWebアプリを作成できるようです。  
ちょうどコーディングをなるべく省いてWebアプリを作成できるツールを探していた所だったので、試してみました。

ここでは、Vue等のフロントエンド+Django REST frameworkのAPIバックエンドといったSPA構成ではなく、Pythonのサーバー(Django)でHTMLを生成するいわゆるMPAの構成を取ります。

今回のサンプルコードは以下で公開しています。  
https://github.com/sasagawa-toru/iommi-startar

## インストール

iommiのインストールには前提としてDjangoのアプリが動いていることが必要なので、ここではDjangoアプリのブートストラップから説明します。(あらかじめPythonはインストールしておきます)  
[Django公式のチュートリアル](https://docs.djangoproject.com/ja/5.1/intro/tutorial01/)でも同様の手順が解説されています。

```shell-session
# Djangoのインストール
$ pip install Django
# Djangoプロジェクトの作成
$ django-admin startproject mysite
# Djangoアプリの作成
$ cd mysite
$ python manage.py startapp myiommi
```

以上で、空のDjangoアプリが作成されました。  
起動して動作確認してみます。

```shell-session
# SQLite DBの作成(db.sqlite3ファイルが作成される)
$ python manage.py migrate
# 管理用ユーザーの作成
$ python manage.py createsuperuser
  (省略)
# 開発サーバーの起動
$ python manage.py runserver
```

http://localhost:8000/ にアクセスするとDjangoの初期ページが表示されます。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/64a9ffdb-180d-afe9-2fc1-c3503d142182.png)

次にiommiをインストールします。
```shell-session
$ pip install iommi
```

[iommiのGetting started](https://docs.iommi.rocks/en/latest/getting_started.html)に従い、settings.pyにiommiの項目を追加します。
```py:mysite/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "iommi", ★追加
    "myiommi", ★先ほどstartappで作成したDjangoアプリも追加しておく
]

MIDDLEWARE = [
    "iommi.live_edit.Middleware", ★追加
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "iommi.sql_trace.Middleware", ★追加
    "iommi.profiling.Middleware", ★追加
    "iommi.middleware", ★追加
]
```

また、URL割り当てのお決まりのコードも追加しておきます。

```py:mysite/urls.py
urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("myiommi.urls")), ★追加
]
```

```py:myiommi/urls.py
urlpatterns = []
```

以上で、iommiを使用してDjangoのアプリを作成する準備が整いました。

## モデルの追加

iommiではDjangoのモデルに合わせてフォームや一覧(テーブル)を自動生成できます。

ここでは例として、公式サンプルにある以下のテーブルを作成します。  
また、各モデルとも`__str__`を定義しておきます。
* Artist
* Genre [^genre]
* Album
* Track

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

    def __str__(self)->str:
        return f"{self.name}"

class Genre(Model):
    name = CharField(max_length=255, db_index=True)

    def __str__(self)->str:
        return f"{self.name}"

class Album(Model):
    name = CharField(max_length=255, db_index=True)
    artist = ForeignKey(Artist, on_delete=CASCADE, related_name="albums")
    year = IntegerField(default=1980)
    genres = ManyToManyField(Genre, related_name="albums")

    def __str__(self)->str:
        return f"{self.name}"

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

    def __str__(self)->str:
        return f"{self.name}"
```

Django Adminへの追加と、migrationsの生成も行っておきます。

```py:myiommi/admin.py
class AlbumAdmin(admin.ModelAdmin):
    list_display = (
        "name",
        "artist",
        "year",
    )

class ArtistAdmin(admin.ModelAdmin):
    list_display = (
        "name",
    )

class GenreAdmin(admin.ModelAdmin):
    list_display = (
        "name",
    )

class TrackAdmin(admin.ModelAdmin):
    list_display = (
        "name",
        "index",
        "album",
        "duration",
    )

admin.site.register(Album, AlbumAdmin)
admin.site.register(Artist, ArtistAdmin)
admin.site.register(Genre, GenreAdmin)
admin.site.register(Track, TrackAdmin)
```

```shell-session
$ python manage.py makemigrations
$ python manage.py migrate
```

## 新規作成フォーム

エンティティを作成する(テーブルにレコードを追加する)フォームを作ってみます。  
iommiではモデルクラスからフォームを自動生成できます。

```py:myiommi/urls.py
urlpatterns = [
    path("albums/create", Form.create(auto__model=Album).as_view()),
]
```

これだけで、Albumの各フィールドを入力できるフォームが自動生成されます。

http://localhost:8000/albums/create  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/10790946-12c9-61ed-c6fb-93cf415925fc.png)

同様にArtistも登録できるようにフォームを追加します。

```py:myiommi/urls.py
urlpatterns = [
    path("albums/create", Form.create(auto__model=Album).as_view()),
    path("artists/create", Form.create(auto__model=Artist).as_view()), ★追加
]
```

http://localhost:8000/artists/create  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/c6d76846-69b0-f811-42f4-324a33f89a76.png)

Genreはマスタテーブルという扱いにしてみます。新規作成フォームは作成せず、固定値としてDjango Adminから何件か追加しておきます。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/6ccc3c24-116b-d525-40b8-de8878af76cb.png)

この状態でArtistを何件か登録してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/d2e81b2f-4425-cca9-e515-a33b2d92a5e0.png)

Albumの新規投稿フォームでは、ArtistやGenreの選択肢が自動的に読み込まれます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/69c5e017-77c6-9585-bef2-cf26b5317a13.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/5e408faa-6aaf-9e7b-e28c-74ee8247b8ca.png)

## 一覧

次に、一覧を表示するテーブル(DBのテーブルではなく、tableタグのことです)の画面を作成します。

こちらも、新規作成フォームと同様に自動生成できます。

```py:myiommi/urls.py
urlpatterns = [
    path("albums/", Table(auto__model=Album).as_view()), ★追加
    path("albums/create", Form.create(auto__model=Album).as_view()),
    path("artists/", Table(auto__model=Artist).as_view()), ★追加
    path("artists/create", Form.create(auto__model=Artist).as_view()),
]
```

http://localhost:8000/artists/  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/414292a0-c3d0-eb1c-461e-981a2a9d72f3.png)

http://localhost:8000/albums/  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/b06538a5-dd36-8eb0-28bc-575ae1c43d7b.png)

## ページの作成

自動生成するフォームや一覧の他に、以下のようなコードでテキストやリンクを含むページを作成することができます。

```py:myiommi/views.py
class TopPage(Page):
    title = Header("Discography App")
    artists = html.div(html.a("Artist Data", attrs__href="artists/"))
    albums = html.div(html.a("Album Data", attrs__href="albums/"))
```

```py:myiommi/urls.py
urlpatterns = [
    path("", TopPage().as_view()), ★追加
    path("albums/", Table(auto__model=Album).as_view()),
    path("albums/create", Form.create(auto__model=Album).as_view()),
    path("artists/", Table(auto__model=Artist).as_view()),
    path("artists/create", Form.create(auto__model=Artist).as_view()),
]
```

上記のTopPageクラスの定義で、以下のようなページが生成されます。

http://localhost:8000/  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/45c98581-97ca-bd1d-517b-64fc9e853272.png)

## 細かい調整

何点か挙動が気になる点を修正します。

### 新規作成フォームの送信後のページ遷移先をテーブルに変更する

urls.pyに`extra__redirect_to="."`を追加
```py:myiommi/urls.py
urlpatterns = [
    path("", TopPage().as_view()),
    path("albums/", Table(auto__model=Album).as_view()),
    path("albums/create", Form.create(auto__model=Album, extra__redirect_to=".").as_view()),
    path("artists/", Table(auto__model=Artist).as_view()),
    path("artists/create", Form.create(auto__model=Artist, extra__redirect_to=".").as_view()),
]
```

### 一覧に新規作成のボタンを追加

Tableクラスにアクションボタンを追加する機能(`actions__create`)があるため引数で指定します。

```py:myiommi/urls.py
urlpatterns = [
    path("", TopPage().as_view()),
    path("albums/", Table(auto__model=Album, actions__create=Action(attrs__href="create")).as_view()),
    path("albums/create", Form.create(auto__model=Album, extra__redirect_to=".").as_view()),
    path("artists/", Table(auto__model=Artist, actions__create=Action(attrs__href="create")).as_view()),
    path("artists/create", Form.create(auto__model=Artist, extra__redirect_to=".").as_view()),
]
```

Createボタンで新規作成フォームに遷移できるようになりました。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/f33b89d4-6d3b-a189-672e-1cfa691c9352.png)

## まとめ

iommiを使うとPythonコードの記載のみである程度典型的な機能を持つWebアプリが作成できます。  
公式ドキュメントを読む限り他にも色々とカスタマイズの余地がありそうなので、ある程度のWebアプリであればこれだけで構築できそうだと思いました。


[^genre]: なぜかチュートリアル中では省略されていましたが適当に作成します