この記事は、Python Advent Calendar 2023 シリーズ1 の21日目です
昨日は、 @yutowac さんで 「Gemini ProでKaggle初心者でもサクッとモデルをつくる」 でした
piacere です、ご覧いただいてありがとございます
普段は、Elixir/Phoenix/LiveViewメインで開発してますが、Python/TensorFlow/Keras等によるAI・ML開発は2017年頃から続けていて、Django/Flaskも年2~3本くらい既存システムのバージョンアップ案件やリプレイス案件をこなしていたりと、何かと触ってはいます(実はPython 2.0からの付き合いで結構古い)が、Pythonの新し目のWeb技術はそこまで触れていません
そこで、前々から何となくは知ってたものの、ちゃんと触ったことが無かった「HotWire」について、Djangoとの組み合わせで調べてみたいと思います
Elixirにてサーバサイド主体のリアルタイムWeb/SPAを叶える「LiveView」と似た性質を持っていると言われるHotWireなので、とても楽しみです
なお、LiveViewについては以前コラム書いていますので、良かったらご覧ください
Rails HotWireについてもコラム化しています
【202312/23追記】
PHP/Laravelの同種であるLiveWireについてもコラム化しました
あと、このコラムが、面白かったり、役に立ったら、 をお願いします
イントロダクション
DjangoでHotWireを実現するライブラリ「Turbo Django」を使っていきます
上記README.mdにもあるチュートリアルを見ながら進めます(なお、本コラムはPython 3.8.10+Django 4.2.6での実施結果が書かれています)
Hotwire Django:Tutorial
Part 1 - Setup Project
Django 3.1以上が入っていない方は、ここで入れましょう(Turbo Djungoの動作要件であるPython 3.8以上は入っている前提)
Turbo DjungoやChannels、ChannelsRedisも一緒に入れます
pip install django turbo-django channels channels_redis
Redisも使うようなので、入っていない方は入れ、バックグラウンドで起動し、クライアントから接続してポート番号が 6379
であることを確認しておいてください
sudo apt install redis
redis-server &
redis-cli
127.0.0.1:6379>
Django PJを作成し、起動します
django-admin startproject turbotutorial
cd turbotutorial
./manage.py runserver
ブラウザで http://localhost:8000
にアクセスすると、親の顔より良く見たDjangoデフォルトページが表示されます
chatアプリケーションを作成します
./manage.py startapp chat
"""
Django settings for turbotutorial project.
"""
…
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'turbo',
+ 'channels',
+ 'chat'
]
…
-WSGI_APPLICATION = 'django_hotwire_sample.wsgi.application'
+ASGI_APPLICATION = 'turbotutorial.asgi.application'
…
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+CHANNEL_LAYERS = {
+ "default": {
+ "BACKEND": "channels_redis.core.RedisChannelLayer",
+ "CONFIG": {
+ "hosts": [("127.0.0.1", 6379)], # Set to your local redis host
+ },
+ },
+}
Part 2 - Models, Views, and Templates
シンプルなCRUDを作っていきます
Models
モデルファイルを作ります
from django.db import models
class Room(models.Model):
name = models.CharField(max_length=255)
class Message(models.Model):
room = models.ForeignKey(Room, related_name="messages", on_delete=models.CASCADE)
text = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
モデルをScaffoldします
./manage.py makemigrations
./manage.py migrate
./manage.py shell
Python REPLでデータ作成します
>>> from chat.models import Room
>>> Room.objects.create(name="Test Room")
<Room: Room object (1)>
コラムには無い下記データも追加しておきます
>>> Room.objects.create(name="Test Room-2")
<Room: Room object (2)>
>>> from chat.models import Message
>>> Message.objects.create(text="Hello.", room_id=1)
>>> Message.objects.create(text="How are you?", room_id=1)
Views and URLs
Viewを追加します
from django.shortcuts import render, reverse, get_object_or_404
from django.views.generic import CreateView, ListView, DetailView
from chat.models import Room, Message
class RoomList(ListView):
model = Room
context_object_name = "rooms"
class RoomDetail(DetailView):
model = Room
context_object_name = "room"
class MessageCreate(CreateView):
model = Message
fields = ["text"]
template_name = "chat/components/send_message_form.html"
def get_success_url(self):
# Redirect to the empty form
return reverse("message_create", kwargs={"pk": self.kwargs["pk"]})
def form_valid(self, form):
room = get_object_or_404(Room, pk=self.kwargs["pk"])
form.instance.room = room
return super().form_valid(form)
routerに下記を追加します
"""
URL configuration for django_hotwire_sample project.
"""
…
from django.contrib import admin
from django.urls import path
+from chat import views
urlpatterns = [
path('admin/', admin.site.urls),
+ path("", views.RoomList.as_view(), name="index"),
+ path("<slug:pk>/", views.RoomDetail.as_view(), name="room_detail"),
+ path("<slug:pk>/message_create", views.MessageCreate.as_view(), name="message_create"),
]
Templates
Template群を追加します
コラムだと turbotutorial/chat/templates/
というパスが指定されていますが、手元では chat/templates/chat/
としています
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat Rooms</title>
</head>
<body>
<h1>Room List</h1>
<ul>
{% for room in rooms %}
<li><a href="{% url 'room_detail' room.id %}">{{ room.name }}</a></li>
{% empty %}
<li>No Rooms Available</li>
{% endfor %}
</ul>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Room Detail</title>
</head>
<body>
<a href="{% url 'room_list' %}">Home</a>
<h1>{{ room.name }}</h1>
<ul id="messages">
{% for message in room.messages.all %}
<li>{{message.created_at}}: {{message.text}}</li>
{% endfor %}
</ul>
</body>
</html>
<form method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
router('url.py')を確認してみましょう
…
urlpatterns = [
path('admin/', admin.site.urls),
path("", views.RoomList.as_view(), name="index"),
path("<slug:pk>/", views.RoomDetail.as_view(), name="room_detail"),
path("<slug:pk>/message_create", views.MessageCreate.as_view(), name="message_create"),
]
あぁ、room_list
では無く、index
ですね(コラム記載ミス?) … 下記の通り、修正します
…
<body>
- <a href="{% url 'room_list' %}">Home</a>
+ <a href="{% url 'index' %}">Home</a>
<h1>{{ room.name }}</h1>
…
Part 3 - Your First Turbo Frame
さて本題です
Listen to Turbo Streams
Django HotWireでは、asgi.py
をWebSocketサーバ化するようです
"""
ASGI config for django_hotwire_sample project.
"""
…
from django.core.asgi import get_asgi_application
+from channels.routing import ProtocolTypeRouter
+from turbo.consumers import TurboStreamsConsumer
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'turbodjango.settings')
-application = get_asgi_application()
+application = ProtocolTypeRouter({
+ "http": get_asgi_application(),
+ "websocket": TurboStreamsConsumer.as_asgi()
+})
A Component-Based Mindset
「Turbo Frame」を使うと、ページの一部(フレームと言います)をリクエストに応じて更新できます
利用するには、<head>
内でturbo用HTMLを incclude
し、<turbo-frame>
タグで囲んだフレーム部を追加します
なお、コラムの src="{% url 'message_create_form' room.id %}"
は、ハンドラ名が間違っていたので、下記の通り、src="{% url 'message_create' room.id %}"
で記載してください
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Room Detail</title>
+ {% include "turbo/head.html" %} <!-- Add this to load the Turbo javascript library-->
</head>
<body>
<h1>{{ room.name }}</h1>
<ul id="messages">
{% for message in room.messages.all %}
<li>{{message.created_at}}: {{message.text}}</li>
{% endfor %}
</ul>
+ <!-- Add this to the end of the page-->
+ <turbo-frame id="send-message" src="{% url 'message_create' room.id %}"></turbo-frame>
</body>
</html>
親フレームと、フレームにロードされる要素は、同じIDである必要があります
<turbo-frame id="send-message">
<form method="post" action="{% url 'send_message' room_id %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Send">
</form>
</turbo-frame>
おや? … 500エラーが出てますね(WebSocketもエラー?…)
formのactionも怪しそうなので、いったん空欄にしてみます
<turbo-frame id="send-message">
- <form method="post" action="{% url 'send_message' room_id %}">
+ <form method="post" action="">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Send">
</form>
</turbo-frame>
これで、とりあえず入力フォームは出るようになりましたが、フォーム投稿すると、今度は405エラーですね … 今回は残念ながら、ここでタイムアップです
終わりに
HotWireのおおまかな書き方は、Rails版とさほど変わりが無く、<turbo-frame>
タグで囲んだ箇所がAJAXによる動的更新可能となるようです
加えて、WebSocketによるサーバプッシュも動きそうなので、今度、時間あるときに最後まで動かし切るのをトライしてみたいところです
ReactやVue.js、Svelteあたりに近いイディオムをサーバサイドとして実装しているLiveViewとは、だいぶ異なる構造ですが、元々のDjangoのイディオムをあまり変えずに対応できる点は、Rails同様、グッドな感じです
最後に余談ですが、実は現在、「Python入門」と言うスキル判定が搭載されたElixir製プロダクト「Bright」を先日αリリースしています
Brightは、過去/現在/未来のスキルから、あなたのBright(輝き)とRight(正しさ)を引き出すツールです
現在、下記がアンロックされています
- Elixir入門
- Python入門
- PHP入門
- React入門
- Webアプリ開発 Elixir
- PM(プロジェクトマネージャ)
- WebアプリUIデザイナー
- オウンドメディアマーケター
来年頭に下記がアンロック予定です
- チームリーダー/PL(プロジェクトリーダー)
- PdM(プロダクトマネージャ)
- 新人AWS or AWSプラクティショナー
- 新人Google Cloud or Google Cloudファンダメンタルズ
次期リリース(5月頃)で、「Webアプリ開発 Python」もリリースするので、「HotWire」を攻略できたら、スキル科目に入れたいですね
明日は、@payaneco さんです