django-bootstrap5を使って手軽にBootstrap5をDjangoのフォームに適用する

Bootstrapはフロントエンド向けのツールキットです。

getbootstrap.com

BootstrapにはCSSJavaScriptが用意されていて、ウェブページに組み込む場合は、HTMLタグにclass属性を指定すると、用意されているデザインが適用される、というものです。

メジャーバージョン間で互換性のない変更があり、Bootstrap4(バージョン 4.x)、Bootstrap5(バージョン 5.x)のようにメジャーバージョンの番号付きで呼ばれることがあります。

DjangoでBootstrapを使うには

BootstrapはHTMLタグにclass属性を指定できれば使えるので、DjangoでもテンプレートでCSSJavaScriptを読み込むように記述すれば使えます。

しかし、DjangoのForm機能を使う場合、inputタグなどのform内で利用するHTMLの文字列生成は、アプリ側のテンプレートではなくDjango内部で行われるので、出力されるタグにclass属性を付与するには少し工夫が必要です。

この記事では説明しませんが以下の方法などがあります。

  • widgetのテンプレートファイルを作成し、優先で使われるようにテンプレートローダーを設定する
  • Formクラスを使う際に、フィールドのwidgetにattrsでclass属性を指定する
    • 通化する場合はメタクラスで差し込む方法もある
    • テンプレートタグを作成し、Widgetのattrsを書き換える

いくつかある方法のうち、テンプレートタグを使ってDjangoのformの出力内容を変更するアプローチで実装されてるのが django-bootstrap5 です。

django-bootstrap5 24.2 documentation

django-bootstrap5を使うと手軽にBootstrapをDjangoに組み込めます。

インストール

Installation - django-bootstrap5 24.2 documentation

PyPIからインストールできます。

pip install django-bootstrap5

INSTALLED_APPSに django_bootstrap5 を追加します。

INSTALLED_APPS = [
    # ...
    'django_bootstrap5',
]

INSTALLED_APPSへの追加は、ライブラリ内に含まれるテンプレートファイルや、テンプレートタグを使用するために必要な作業です。

導入

Quickstart - django-bootstrap5 24.2 documentation

ドキュメントからリンクされていますが、exampleのアプリを参考にするとわかりやすかったです。

django-bootstrap5/example at main · zostera/django-bootstrap5 · GitHub

Bootstrapはmetaタグでviewportの指定や、CSSファイル、JavaScriptファイルの読込みが必要になります。

Bootstrap側のドキュメントの通りにlinkタグやscriptタグを直接記述もできますが、django-bootstrap5では、ベースのテンプレートファイルが用意されています。 このファイルを継承して使っておくと、Bootstrapに関する各種設定変更をDjangoのsettings.pyからできるようになるのでおすすめです。

https://github.com/zostera/django-bootstrap5/blob/main/src/django_bootstrap5/templates/django_bootstrap5/bootstrap5.html

アプリ作成

例として myapp を作ります。

python manage.py startapp myapp

FormとView作成

FormとViewは特にbootstrap5を意識せずに、Djangoのドキュメント通りに作成します。

myapp/forms.py:

from django import forms


FAVORITE_COLORS_CHOICES = {
    "blue": "Blue",
    "green": "Green",
    "black": "Black",
}


class MyForm(forms.Form):
    name = forms.CharField(label="名前")
    body = forms.CharField(label="本文", widget=forms.Textarea)
    favorite_colors = forms.MultipleChoiceField(
        required=False,
        widget=forms.CheckboxSelectMultiple,
        choices=FAVORITE_COLORS_CHOICES,
    )

myapp/views.py:

from django.views.generic import TemplateView
from . import forms


class IndexView(TemplateView):
    template_name = 'index.html'

    def get_context_data(self):
        return {'form': forms.MyForm()}

form という名前のコンテキストで index.htmlDjango Formのインスタンスを渡しています。

URL設定

サンプルとして作成したプロジェクトは myproject です。 myproject/urls.py にルートのURL設定があります。 作成したIndexViewを有効にしておきます。

myproject/urls.py:

from django.contrib import admin
from django.urls import path

from myapp import views as myapp_views

urlpatterns = [
    # ... (省略)
    path('', myapp_views.IndexView.as_view()),
]

テンプレート作成

この例では myapp/templates ディレクトリ以下にファイルを作っていますが、実際に使用する場合はextendsに指定するテンプレートパスなどとの整合性がとれていれば、ここではなくてもよいです。

myapp/templates/bootstrap.html:

{% extends 'django_bootstrap5/bootstrap5.html' %}

{% block bootstrap5_title %}{% block title %}{% endblock %}{% endblock %}

django-bootstrap5に含まれるテンプレートファイルを継承して、プロジェクト内で使用するbootstrap用のベーステンプレートを作ります。titleブロックを定義しているので、継承したファイルでは bootstrap5_title ブロックを使わずに、 title ブロックで title部分を記載できます。exampleと同様です。

myapp/templates/base.html:

{% extends 'bootstrap.html' %}

{% load django_bootstrap5 %}

{% block bootstrap5_content %}
    <div class="container">
        <h1>{% block title %}(no title){% endblock %}</h1>

        {% autoescape off %}{% bootstrap_messages %}{% endautoescape %}

        {% block content %}(no content){% endblock %}
    </div>

{% endblock %}

アプリ(プロジェクト)用のベーステンプレートです。各画面はこの base.html を継承して作成する想定です。exampleを参考に必要な部分だけを記載しています。

myapp/templates/index.html:

{% extends 'base.html' %}

{% load django_bootstrap5 %}

{% block title %}
Bootstrap5 フォーム
{% endblock %}

{% block content %}
<form method="post">
  {% csrf_token %}

  {% bootstrap_form form layout=layout size=size %}

  {% bootstrap_button button_type="submit" content="OK" %}
  {% bootstrap_button button_type="reset" content="Reset" %}
</form>
{% endblock %}

index.htmlでは django-bootstrap5 のテンプレートタグを使用しています。 bootstrap_formタグにformコンテキスト(IndexViewから渡されたMyFormのインスタンス)を渡して、class属性の付与などを行っています。

実行結果

ブラウザで http://127.0.0.1:8000/ にアクセスすると、Bootstrap5のデザインが適用されたフォームが表示されます。

サンプルコード全部

sample_nullpobug/django/django_bootstrap5 at main · tokibito/sample_nullpobug · GitHub

PyodideでSympyのparse_latexを使いたい

SymPyPython上で数式を扱うモジュール。 SymPyに含まれるparse_latex関数を使うと、LaTeXの書式で書かれた数式も読み込める。 これをPyodideで使いたいと考えていた。

少し試しただけですが、実用にはまだ厳しいかもと思いました。

  • parse_latexはデフォルトだと antlr4-python3-runtime を使おうとするが、Pyodideはこれに対応していない
  • SymPyの時期バージョン(1.13)では、 parse_latex関数に backend 引数が追加され、 lark-parser を使ったparsingにも対応する
    • しかし lark-parser は完全ではない書式(部分的に不正な書式、欠けてる状態など)への対応がANTLR4より弱いらしい

Parsing - SymPy 1.13.0rc2 documentation

以上を踏まえたうえで、lark backendを試してみた。

試したコード

pyodide-sympy.html:

<!doctype html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
    <title>SymPy on Pyodide</title>
  </head>
  <body>
    <div id="result"></div>
    <script type="text/javascript">
      let pyodide;
      let result = document.getElementById("result");
      async function main() {
        // setup
        pyodide = await loadPyodide();
        await pyodide.loadPackage("micropip");
        const micropip = pyodide.pyimport("micropip");
        await micropip.install("Sympy==1.13.0rc3");
        await micropip.install("lark-parser");
        const output = pyodide.runPython(`
from sympy.parsing.latex import parse_latex
expr = parse_latex(r"\\sqrt {x} = 1", backend="lark")
expr
        `)
        result.innerHTML = output;
      }
      main();
    </script>
  </body>
</html>

結果

このぐらいシンプルなものだと動作はしそう。とはいえ実用を考えると対応してないフォーマットも多数あるようで、まだまだこれからという感じ。

Pyodide上でdocutilsを動かしてみる

docutilsはreStructuredTextをPythonで扱うためのモジュール。

Pyodide上でdocutilsを動かせば、ブラウザ上だけでreStructuredTextを扱えるかなと思って試していました。

コード

pyodide-docutils.html:

<!doctype html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>docutils on Pyodide</title>
  </head>
  <body>
    <div class="container-fluid">
      <div class="row">
        <div class="col-md-6 vh-100 p-1">
          <form class="h-100">
            <textarea placeholder="You can write texts by reStructuredText." id="editarea" class="form-control h-100 font-monospace"></textarea>
          </form>
        </div>
        <div class="col-md-6 border border-2 h-auto" id="preview"></div>
      </div>
    </div>
    <script type="text/javascript">
      let pyodide;
      let previousText = "";
      let lock = false
      const editArea = document.getElementById("editarea");
      const preview = document.getElementById("preview");
      async function main() {
        // setup
        pyodide = await loadPyodide();
        await pyodide.loadPackage("micropip");
        const micropip = pyodide.pyimport("micropip");
        await micropip.install("docutils");
        // start interval
        setInterval(updatePreview, 500)
      }
      async function updatePreview() {
        const currentText = editarea.value;
        if (currentText == previousText || lock) { return }
        lock = true;
        await render(currentText);
        previousText = currentText;
        lock = false;
      }
      async function render(rst) {
        pyodide.globals.set("rst", rst)
        const output = pyodide.runPython(`
          from docutils.core import publish_parts
          try:
              result = publish_parts(rst, writer_name='html')['html_body']
          except Exception as e:
              print(e)
        `)
        preview.innerHTML = pyodide.globals.get("result");
      }
      main();
    </script>
  </body>
</html>

見た目はBootstrap5を使って簡単に整えつつ、テキストエリアに入力したreStructuredTextをdocutilsでHTMLに変換し、右側に表示をします。

特別なことは特になくて、普通に動きました。何かに使えると面白そうですね。

参考

PythonのcProfileモジュールを使ってプロファイル取得とSnakeVizによる可視化

先日のPyCon Kyushu Kagoshima 2024で登壇したcProfileの資料の内容から抜粋しつつ、cProifleの使い方について少し書いてみます。

tokibito.hatenablog.com

Pythonのプロファイルとは

  • Pythonの実行に関する統計情報
    • 関数がどれだけ呼ばれたか
    • 実行時間どのぐらいかかったか

Pythonでは標準モジュールのcProfileを使うと、プロファイルを取得できます。

プロファイルを取得する対象のコード

read_file.py:

import datetime
import timeit

DATA_FILE = "data.jsonl"


def main():
    """ファイルの行数をカウントするスクリプト"""
    print("start:", datetime.datetime.now())

    result = 0
    with open(DATA_FILE) as input_file:
        for line in input_file:
            result += 1

    print(f"result: {result} lines")

    print("end:", datetime.datetime.now())


if __name__ == "__main__":
    # 速すぎるのでtimeitで10回実行
    print(timeit.timeit(main, number=10))

プロファイルの取得

cProfileでプロファイルを取得します。

今回はスクリプト全体のプロファイルを取得するので、次のようにシェルから実行します。

$ python -m cProfile -o read_file.profile read_file.py

read_file.py を実行後、 read_file.profile にプロファイルデータが保存されます。

SnakeVizによる可視化

cProfileで取得したプロファイル情報は、標準モジュールのpstatsでテキストとして読むことができますが、SnakeVizを使うとウェブブラウザ上でグラフ表示、データのソートなどができて簡単なので、今回はこれを使ってみます。

jiffyclub.github.io

SnakeVizのインストール

$ pip install snakeviz

依存がTornadoのみなので、OSの追加パッケージなどが不要でハマりどころが少ないのが良かったです。

SnakeVizの実行

$ snakeviz read_file.profile

SnakeVizの画面(Chrome

実行すると、Tornadoによるウェブサーバーが起動し、可視化されたプロファイルをウェブブラウザで閲覧できます。

SnakeVizで可視化されたコールグラフ

pstatsと同様の呼び出し結果の一覧表示

一覧表示のほうはカラムのクリックでソートされるので、眺めるのが手軽だなと思いました。

PyCon Kyushu Kagoshima 2024に参加しました

2024/5/25にPyCon Kyushu Kagoshima 2024が鹿児島県で開催され、参加してきました。

kyushu.pycon.jp

私はPythonのプロファイラの使い方の紹介をしました。

speakerdeck.com

イベント参加者数が予想以上に多くて、賑わっていたように思います。

鹿児島はアーケードになっているエリアが広くて、路面電車もあり、中心街がかなり広い印象を受けました。 買い物にも遊ぶのにも困らなさそうで、興味深い~と思いながら散策してました。

機会があればまた行きたいです。

Djangoのプロジェクトにヘルスチェック用のエンドポイントを提供するdjango-health-check

ウェブアプリの外形監視をするときには、外形監視用のエンドポイントURLを用意することが多いと思います。

外形監視用のエンドポイントでは、アプリからデータベースなど各種ミドルウェアへの接続が正常であるかなどをチェックしますが、チェック項目はサービスによって異なることがほとんどです。

django-health-checkを使うと、汎用の外形監視用のエンドポイントをすぐに用意でき、設定によりチェック項目を変更できます。

django-health-check · PyPI

インストール

pip install django-health-check

試してみる

今回は health_check.db を使って、データベース接続のチェックを実施してみます。

アプリの有効化とURL追加

settings.py:

INSTALLED_APPS = [
    # ...
    'health_check',  # 必須
    'health_check.db',  # データベースのチェックをする場合に追加
]

urls.py:

from django.urls import path, include

urlpatterns = [
    # ...
    path('ht/', include('health_check.urls')),
]

DBマイグレーション

health_check.db はチェック用のテーブルを作成して操作するので、アプリを INSTALLED_APPS に追加したら、 migrate を実行します。

python manage.py migrate

実行する

runserverを起動して、 /ht/ にアクセスすると、チェック結果が表示されます。

デフォルトではHTMLで表示されます。

?format=json のようにクエリ文字列を指定すると、結果をJSON形式で返すこともできます。

何をチェックしているのか

health_check.db は実際にどのような操作をしてチェックしているのか、ソースコードを確認してみます。

https://github.com/revsys/django-health-check/blob/master/health_check/db/backends.py

  • TestModelというモデルのレコードを1件作成(=INSERT)
    • obj.save()
  • 作成したレコードを削除(=DELETE)
    • obj.delete()

このようなコードでした。「データベースに接続できること」「データベースに書き込みができること」をチェックしていますね。

データベース以外にもcache, storage, celery, boto, rabbitmq, redisなど、標準で用意されているバックエンドがあります。

カスタムのバックエンドを作成するのも簡単なので、使いやすそうです。

Djangoで動的にフォームクラスを作成する

Djangoのフォーム機能は通常、 django.forms.Form クラスを継承して定義をします。

フォームを使う | Django ドキュメント | Django

リクエスト時に動的にフォームクラスを生成したい場合もあるので、今回はそれをやってみます。

フォームクラスを動的に生成する関数

myapp/forms.py:

from django import forms


def create_form(form_schema_dict):
    """
    動的にフォームを作成する

    :form_schema_dict: フォームのスキーマを定義した辞書
    :return: フォームクラス

    例:
    form_class = create_form({
        "name": {"max_length": 100},
        "email": {"max_length": 100},
    })
    """
    form_schema = {}
    for field_name, options in form_schema_dict.items():
        form_schema[field_name] = forms.CharField(label=field_name, **options)
    return type("DynamicForm", (forms.Form,), form_schema)

この create_form 関数では、指定されたフィールド名とオプションでCharFieldを持つフォームクラスを生成して返します。

type関数は、Pythonの組み込み関数で、引数を3つ指定する場合は、新たな型を生成して返却します。

参考: 組み込み関数 - Python 3.12.3 ドキュメント

動作確認

Django shellからインポートして、フォームクラスが動作するか試します。

>>> from myapp import forms
>>> form_class = forms.create_form({
...     'name': {'max_length': 100},
...     'email': {'max_length': 100},
... })
>>> form = form_class()
>>> form
<DynamicForm bound=False, valid=Unknown, fields=(name;email)>

Djangoのビューで表示してみる

Djangoのビューから呼び出して、テンプレートファイル上でのレンダリングも試します。

myapp/views.py:

from django.shortcuts import render
from django import forms
from .forms import create_form


def index(request):
    form_class = create_form(
        {
            "name": {"max_length": 100},
            "email": {"max_length": 100},
            "message": {"widget": forms.Textarea},
        }
    )
    form = form_class()
    return render(request, "index.html", {"form": form})

このビュー関数では、単純にフォームを表示するだけにしていますが、やろうと思えば is_valid() を呼び出してフォーム内容を検証するようなコードにも変更できます。

myapp/templates/index.html:

<html>
  <head>
    <title>form test</title>
  </head>
  <body>
    <form>
      {{ form }}
    </form>
  </body>
</html>

サンプルコード全体は、GitHubに置いてます。

https://github.com/tokibito/sample_nullpobug/tree/main/django/dynamic-form