Djangoテンプレートで継承を利用して各ページ共通のヘッダーやフッターを設定する

Djangoフレームワークのテンプレートの継承について、基本的な内容です。

base.html というファイルを作って、共通部分をまとめる話。

検索用に記事をまとめておきます。

※この記事は 力強くアウトプットする日の 20240301 のアウトプットです。

テンプレートの継承

Djangoのドキュメントに例と説明があります。

テンプレートの継承 - Djangoドキュメント

Djangoのテンプレートには『継承』という仕組みがあります。

どのようなときに利用するか

HTMLでウェブサイトを作る場合、複数のページでヘッダー部とフッター部を共通にすることがよくあります。 また、サイドバー、レイアウトなど、複数ページに渡って共通の構造とするパターンがとても多いです。

こうした構造のときにテンプレート継承がうまくハマります。

base.html:

<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
{% block content %}{% endblock %}
<script>
// 全ページ共通で差し込むJavaScript
</script>
</body>
</html>

test.html:

{% extends "base.html" %}

{% block title %}テストページ{% endblock %}

{% block content %}
<p>
  bodyタグ内のコンテンツ
</p>
{% endblock %}

views.py:

from django.shortcuts import render

def test(request): 
    return render(request, "test.html")

レンダリング結果

<html>
<head>
<meta charset="utf-8">
<title>テストページ</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>

<p>
  bodyタグ内のコンテンツ
</p>

<script>
// 全ページ共通で差し込むJavaScript
</script>
</body>
</html>

テンプレート継承の利点

extendsタグで検証したテンプレートファイルでは、継承元のblockのうち、 変更したい部分 だけを記載することができます。 継承元のテンプレートに変更可能なblockがたくさんあっても場合、必要な部分だけ書けばいいので、記述量の削減につながります。

テンプレート継承の動作イメージ

比較対象として、 includeタグ があります。 includeタグは、指定したテンプレートファイルの内容を、includeタグの記載位置に取り込みます。

includeタグの動作イメージ

includeの場合、この図の例のようにヘッダーとフッターが別のファイルに分離されます。HTMLの場合、 <html><body> も共通部に書きたい場合が多いと思いますが、閉じタグの対応が別々のファイルに含まれるのは見通しが良くないです。

また、includeだけで組み立てる場合は、includeタグを記述しないと対象のテンプレートが取り込まれないので、変更が少ししかないページでも記述量が多くなりがちです。

よくつかうsshコマンドのオプション

sshコマンドのオプションは、特定の環境下で作業する際にはよく使うのだけど、しばらく使わないと忘れてしまって毎度しらべているので、自分用にまとめておく。

※この記事は 力強くアウトプットする日の 20240216 のアウトプットです。

ssh(1)のドキュメント

man.openbsd.org

-L ポートフォワーディング

書式

ssh -L [bind_address:]port:host:hostport destination

destination側から見た host:hostport を接続元の環境のportにフォワードできる。bind_addressを省略した場合はlocalhost

ssh -L 8888:localhost:8000 example.com

example.com というsshでログインできるホストの localhost:8000 = 127.0.0.1:8000 を接続元の 8888フォワード。 接続元の端末で、 localhost:8888 につなぐと、 example.com上の localhost:8000 につながる、と考えてよい。

踏み台とする場合

hostの部分はdestinationからネットワーク的に接続可能であれば、localhost以外も指定できるので、たとえばdestinationのサーバーからしか接続できない、内部ネットワーク上にあるホストに接続することもできる。

ssh -L 8888:192.168.100.123:3306 example.com

example.comというホストから見て、内部ネットワークにあたる192.168.100.123というIPv4アドレスのホストでデータベースサーバーが動作している場合(3306はMySQLのデフォルトポート)

このコマンドを実行した環境で、 localhost:8888 に接続すると、192.168.100.123:3306ポートにつながる。

もし、MySQLのコマンドから利用するのであれば、以下のようなコマンドになる。

mysql -h localhost -P 8888 -u db_user -p db_name

このように、example.comを踏み台として、その先にあるホストに接続が可能となる

-D ダイナミックフォワーディング(SOCKSプロキシー

書式

ssh -D [bind_address:]port destination

destinationを起点としたSOCKSプロキシーを、接続元の環境のportで起動する。bind_addressを省略するとlocalhost

SOCKSプロキシーはアプリケーションレベルでフォワーディングをしたいときに使う。主にウェブブラウザ。

ssh -D 8888 example.com

接続元の環境の localhost:8888 でSOCKSプロキシーが起動する。たとえば、example.com経由でしか接続できない内部ネットワーク上のウェブサイト(例: internal.example.com)をブラウザで見たい場合。

ウェブブラウザのプロキシー設定で、SOCKSプロキシーのアドレスに localhost:8888 を設定し、ブラウザのアドレスバーに internal.example.com と入力して接続すると、 example.com のホストから通信を行うことになるため、閲覧できるようになる。

Chromeの場合、プロキシー設定は Proxy SwitchyOmega などの拡張を使うと有効/無効の管理がしやすい。

-L オプションだと1つのホスト、ポートを対象としたフォワーディングだが、 -D の場合はSOCKSプロキシーに対応したアプリからは、destinationのホスト経由で任意のホストに接続できる。

-R リモートフォワーディング

書式

ssh -R [bind_address:]port:host:hostport destination

destinationのホストのportに接続すると、接続元の host:hostport につながる。-L の逆向きと考えればよい。

-X X11フォワーディング

書式

ssh -X destination

接続元環境のX11ディスプレイサーバーにdestination先のX11の接続をフォワーディングする。

destinationの環境でデスクトップアプリを起動し、画面だけを接続元環境に持ってきたい場合に使う。

たとえば、destination先のLinux環境にChromiumをインストールして起動し、接続元環境で画面の操作をしたい場合。VNCなどのリモートデスクトップ接続を利用しなくても、sshだけつながれば使える。

その他

多段SSH

destinationを経由して更に別のホストにsshで接続したい(=多段SSHしたい)場合、ProxyCommandやncコマンドを使う例がでてくるが、最近のOpenSSHの場合は、 -J オプションや ProxyJump.ssh/config)を使えば記述がシンプルになる。

DjangoMeetupTokyo #12を開催しました

2024/1/28にDjangoMeetupTokyo #12を開催しました。

DjangoMeetupTokyo #12 - connpass

去年の夏以来の開催でしたが、今回は前回の倍ぐらいの参加者が集まりました。

コロナ禍が落ち着いてきて、勉強会などのイベントで基本的に制限がなくなったので、コロナ禍以前には活発だったオフラインの会を復活させていきたいなぁと個人的に思っています。

中級者向けハンズオン

今回、私が資料を準備して、中級者向けハンズオンというのをやりました。

DjangoMeetupTokyo #12 中級者向けハンズオン — django-meetup-tokyo-12 ドキュメント

説明にある通り、「チュートリアルは理解できている、Djangoは使ったことあるけど、使いこなせてない~という人向けのハンズオン」というのを以前からやりたいなと考えていたものを、ようやく実施できました。

資料の内容を順番に説明し、手元で動かしてもらいながら、Djangoの機能について詳しく見ていきました。

好評のようでしたので、他の題材も考えて、またやれればよいなーと思います。

ようす

Djangoのキャッシュフレームワークを使った場合のキーと値の取り扱い

Djangoフレームワークには、データをキャッシュする仕組みを抽象化、共通化したキャッシュフレームワークが含まれています。

Django's cache framework | Django ドキュメント | Django

どのようなキーと値が保存されるのか

キャッシュフレームワークAPIで、どのようなキーと値がミドルウェアなどのバックエンドに保存されるのか確認してみます。

キャッシュに値を入れる

今回はDjango 5.0とredisバックエンドで試してみます。ローカル環境ではRedisが起動している想定。

myproject/settings.py:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://",
    }
}

この設定でDjangomanage.py shell からキャッシュを保存します。

>>> from django.core.cache import cache
>>> cache.set("my-cache-key", "キャッシュの値")
>>> cache.get("my-cache-key")  # キャッシュから取り出せるか確認
'キャッシュの値'

キーと値を見てみる

保存できたら、 redis-cli で確認してみます。

$ redis-cli  # シェルからredis-cliを起動
127.0.0.1:6379> KEYS *
1) ":1:my-cache-key"
127.0.0.1:6379> GET :1:my-cache-key
"\x80\x05\x95\x19\x00\x00\x00\x00\x00\x00\x00\x8c\x15\xe3\x82\xad\xe3\x83\xa3\xe3\x83\x83\xe3\x82\xb7\xe3\x83\xa5\xe3\x81\xae\xe5\x80\xa4\x94."

まず、 KEYS <pattern> コマンドでpatternに * を指定し、すべてのキー一覧を取得しています。

:1:my-cache-key という値がキーになっています。

settings.pyでキー生成の関数を設定していないので、デフォルトのDjangoのキー生成関数が使われています。 実装はこの辺です。 https://github.com/django/django/blob/617bcf611f3daa796e4054ba041089ece30a32fc/django/core/cache/backends/base.py#L40

return "%s:%s:%s" % (key_prefix, version, key)
  • key_prefix は、settings.CACHESの各キャッシュの設定で KEY_PREFIX キーにて変更できます。デフォルトは空文字列です。
  • version settings.CACHESの各キャッシュの設定で VERSION キーで指定できます。

https://github.com/django/django/blob/617bcf611f3daa796e4054ba041089ece30a32fc/django/core/cache/backends/base.py#L82

GET コマンドで取得した値はバイナリ値になっています。これはDjango側のRedisバックエンドの中で、保存する値をpickleモジュールでシリアライズしているからです。

https://github.com/django/django/blob/617bcf611f3daa796e4054ba041089ece30a32fc/django/core/cache/backends/redis.py#L21

どのように値を保持するかは、キャッシュバックエンドごとで異なるので、シリアライズ・デシリアライズ処理を行うかどうかは、バックエンドクラスの実装次第です。

Djangoのキャッシュフレームワークを通してRedisに保存した値を、他のアプリなどから読み込んで使いたい場合、Pickleフォーマットだと扱いづらいかもしれません。その場合は自分でdumps, loadsメソッドを持ったクラスを実装するか、 json モジュールなどを指定することもできます(この記事では手順は説明しません)

キー生成の関数を変更してみる

キーを生成する関数を変更するには、settings.pyのCACHESで KEY_FUNCTION を設定します。

myproject/utils.py

def my_key_func(key, key_prefix, version):
    return "spam:{}".format(key)

myproject/settings.py:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://",
        "KEY_FUNCTION": "myproject.utils.my_key_func",  # 文字列指定だと実行時にインポートして利用される
    }
}

この設定の状態でキャッシュを manage.py shell から追加してみます。

>>> from django.core.cache import cache
>>> cache.set("my-cache-key-2", "キャッシュの値")
>>> cache.get("my-cache-key-2")
'キャッシュの値'

redis-cliにてキーを見てみましょう。

$ redis-cli
127.0.0.1:6379> keys *
1) "spam:my-cache-key-2"

キー文字列が変わったことを確認できました。

ありそうな質問

キャッシュキーの一覧を取得したいですが、うまくいきません。どうすればよいですか?

Djangoのキャッシュフレームワークでは、キー一覧を返す仕組みを持っていません。この記事の例ではRedisの KEYS コマンドを使用しています。

キャッシュを保存しておくバックエンドのミドルウェアがキーの一覧を返す仕組みを持たない場合もあります。キャッシュキーを検索したい要件がある場合は、キャッシュバックエンドの選定に気をつけるとよいでしょう。

cacheのすべてのkeyの取得の方法。

キャッシュを使っているのに本番環境が遅いです。ローカル環境では問題ないのになぜですか?

ネットワーク経由で外部キャッシュサーバーを使用している場合は、通信のレイテンシがあります。リクエスト内で何度もキャッシュを読み書きすると、遅くなる場合があります。複数のキーを指定してまとめて取得、まとめて更新する方法を使うと改善する可能性があります。

Djangoのcacheフレームワークで複数の値をまとめて取得、更新する - 偏った言語信者の垂れ流し

terapyon channel podcastにゲストで呼ばれてきました

terapyon がやっている terapyon channel podcast にゲストでお呼ばれして、しゃべってきました。

#88 tokibitoさんをゲストに JMOOCの無料Python講義のリニューアルとプログラミング教育 | terapyon channel podcast

話題は先日のJMOOCの教材作成についてや、プログラミング教育についてなど。

Pythonの入門者向け動画教材 2023年改訂版

JMOOCで2020年に公開していたPythonの入門者向けの動画教材ですが、昨年の11月に内容を更新したものを公開していました。

まだ記事にしていなかったので、ここでも書いておきます。

Python入門2023改訂版 - Python3.11対応版

Pythonによるプログラミング入門の動画教材です。

GoogleアカウントでPlatJaMというシステムにログインすると、無料で受講できます。

Windowsパソコンの操作ができ、プログラミングを初めて勉強する人を想定した内容となっています。

2020年公開版との違い

  • スライド資料をアップデートしました。内容はほとんど同じですが、Python3.11を前提とした説明に変更しています。
  • 動画をすべて再撮影しました。動画コンテンツとしては完全に別物で新しくなりました。
  • 実際にコマンドを実行したり、Pythonの対話インターフェースで動かすデモ、VisualStudioCodeでコードを入力するデモを追加しました。
    • 2020年のバージョンではスライドと音声による説明だけでした。

2020年公開版についての記事

Pyodideを試す

Pyodideは、CPythonをWebAssembly(WASM)/Emscriptenにポーティングしたソフトウェア。

PythonがWASMとして動作するので、ブラウザ上でPythonを動かせる。

pyodide.org

ドキュメントには実際に動作するREPLのリンクがある。

https://pyodide.org/en/stable/console.html

Pyodideをウェブサイト上で動かす

PyodideはWASMなので、JavaScriptから呼び出して利用可能。

また、CDNでホストされたバージョンもあるため、少し組み込んで使うくらいであれば、少量のコードでできる。

https://pyodide.org/en/stable/usage/quickstart.html

サンプルコード(ドキュメントより抜粋):

<!doctype html>
<html>
  <head>
      <script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
  </head>
  <body>
    Pyodide test page <br>
    Open your browser console to see Pyodide output
    <script type="text/javascript">
      async function main(){
        let pyodide = await loadPyodide();
        console.log(pyodide.runPython(`
            import sys
            sys.version
        `));
        pyodide.runPython("print(1 + 2)");
      }
      main();
    </script>
  </body>
</html>

CDNからスクリプトをロードし、 loadPyodide() でWASMをロード、初期化。その後は runPython()Pythonコードを実行できる。

サードパーティ製パッケージを動かす

Pyodideでは、micropipというAPIを使って、外部のPythonパッケージを動かすことができる。

Pyodideが標準モジュールをある程度サポートしていることもあり、Pure Pythonで書かれたパッケージであれば、動かすハードルは低め。

標準モジュールの互換性についてもドキュメントに記載がある。

pyodide.org

JS側、Python側どちらからでもmicropipを使える。柔軟性は高いように見える。

<script type="text/javascript">
  async function main(){
    const pyodide = await loadPyodide();
    await pyodide.loadPackage("micropip");
    const micropip = pyodide.pyimport("micropip");
    await micropip.install("regex")
  }
</script>

DjangoのIt works画面をうごかしてみる

では、Djangoを無理矢理うごかしてみる。

<!doctype html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"></script>
  </head>
  <body>
    <script type="text/javascript">
      async function main(){
        // setup
        const pyodide = await loadPyodide();
        await pyodide.FS.writeFile("/home/pyodide/urls.py", "urlpatterns=[]");
        await pyodide.loadPackage("micropip");
        const micropip = pyodide.pyimport("micropip");
        await micropip.install("Django")
        // run django app
        const output = pyodide.runPython(`
            import io
            import sys
            import django
            from wsgiref.handlers import BaseCGIHandler
            from django.conf import settings
            from django.core.handlers.wsgi import WSGIHandler

            settings.configure(
                ROOT_URLCONF="urls",
                SECRET_KEY="dummy",
                DEBUG=True,
            )
            django.setup()
            app = WSGIHandler()
            output = io.BytesIO()
            env = {
                "REQUEST_METHOD": "GET",
                "SERVER_NAME": "pyodide",
                "SERVER_PORT": "8000",
            }
            BaseCGIHandler(
                sys.stdin.buffer,
                output,
                sys.stderr,
                env,
                multithread=False
            ).run(app)
            response = output.getvalue().decode("utf-8")
            "".join(response.splitlines()[2:])
        `);
        document.open();
        document.write(output);
        document.close();
      }
      main();
    </script>
  </body>
</html>

Pyodideを初期化してからDjangoをインストール、その後WSGIハンドラを実行している。

実行すると、ブラウザ上でDjangoを実行して、It worksの画面が表示される。