7
3

Python 3.8.10+Django 4.2.6で「Hotwire Django:Tutorial」を試してみた

Last updated at Posted at 2023-12-22

この記事は、Python Advent Calendar 2023 シリーズ1 の21日目です

昨日は、 @yutowac さんで 「Gemini ProでKaggle初心者でもサクッとモデルをつくる」 でした


piacere です、ご覧いただいてありがとございます :bow:

普段は、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については以前コラム書いていますので、良かったらご覧ください :information_desk_person_tone1:

Rails HotWireについてもコラム化しています

【202312/23追記】
PHP/Laravelの同種であるLiveWireについてもコラム化しました

あと、このコラムが、面白かったり、役に立ったら、image.png をお願いします :bow:

イントロダクション

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デフォルトページが表示されます
image.png

chatアプリケーションを作成します

./manage.py startapp chat
turbotutorial/settings.py
"""
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

モデルファイルを作ります

chat/models.py
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を追加します

chat/views.py
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に下記を追加します

turbotutorial/urls.py
"""
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/ としています

chat/templates/chat/room_list.html
<!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>
chat/templates/chat/room_detail.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>
chat/templates/chat/room_form.html
<form method="post" action=".">
   {% csrf_token %}
   {{ form.as_p }}
   <input type="submit" value="Submit">
</form>

ブラウザで確認してみましょう
image.png

おや? … エラーが出ていますね…
image.png

router('url.py')を確認してみましょう

turbotutorial/urls.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 ですね(コラム記載ミス?) … 下記の通り、修正します

chat/templates/chat/room_detail.html

<body>

-   <a href="{% url 'room_list' %}">Home</a>
+   <a href="{% url 'index' %}">Home</a>

    <h1>{{ room.name }}</h1>

ひとまず、エラーは出なくなりました
image.png

image.png

Part 3 - Your First Turbo Frame

さて本題です

Listen to Turbo Streams

Django HotWireでは、asgi.py をWebSocketサーバ化するようです

turbodjango/asgi.py
"""
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 %}" で記載してください

chat/templates/chat/room_detail.html
<!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である必要があります

chat/templates/chat/components/message_create_form.html
<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もエラー?…)
image.png

formのactionも怪しそうなので、いったん空欄にしてみます

chat/templates/chat/components/message_create_form.html
<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エラーですね … 今回は残念ながら、ここでタイムアップです
image.png

終わりに

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ファンダメンタルズ

image.png

次期リリース(5月頃)で、「Webアプリ開発 Python」もリリースするので、「HotWire」を攻略できたら、スキル科目に入れたいですね


明日は、@payaneco さんです

p.s.このコラムが、面白かったり、役に立ったら…

image.png にて、どうぞ応援よろしくお願いします :bow:

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3