先々週は軽量なWebフレームワークFlask、先週はAPIサーバーFastAPIを使って小遣い帳Webアプリを作りましたが、PythonでWebアプリといえばDjangoですよね。
Djangoに付いてはAIのアシストしてもらいPython、Djangoを学びましたで使ってみましたが、小遣い帳Webアプリのレベルでは大げさかな?と思っていました・・・
Gemini AI image generatorが生成した画像を使っています
Django版
今回もAIのアシストしてもらいPython、Djangoを学びましたと同じく、標準構成+スタイリングにはdjango-tailwindを使いました。
主要なコードを載せます、AIにコメントを追加してもらいました。
allowance_book/models.py
小遣い帳のモデル
- モデルのフィールド定義には、最大長やnull許可等を設定しました
- 残高はSQLで残高を計算するので、フィールではなくインスタンス変数でもちました
- ① SQLで残高を計算してもらいために
cursor.execute
メソッドを使いました- モデルに対応するテーブル名は
アプリ名_モデル名
になりますが、cls._meta.db_table
で取得できます
- モデルに対応するテーブル名は
- ② ただし、結果がTupleになってしまうのでカラム名:値の辞書になるコードを追加しました
from django.db import models
from django.db import connection
# お小遣い帳のレコードを表すモデル
class Allowance(models.Model):
# インスタンス化
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.balance = 0
# --- モデルフィールドの定義 ---
date = models.DateField() # 日付
description = models.CharField(max_length=255) # 内容
money_in = models.IntegerField(null=True, blank=True) # 入金額
money_out = models.IntegerField(null=True, blank=True) # 出金額
created_at = models.DateTimeField(auto_now_add=True) # 作成日時 (自動追加)
updated_at = models.DateTimeField(auto_now=True) # 更新日時 (自動更新)
# オブジェクトを文字列で表現する際の形式
def __str__(self):
return f"id:{self.id}, date:{self.date}, description:{self.description}, money_in:{self.money_in}, money_out:{self.money_out}"
# 全てのレコードを残高付きで取得するクラスメソッド
@classmethod
def get_all_with_balance(cls):
# DB接続カーソルを取得
with connection.cursor() as cursor:
# SQLウィンドウ関数を使って、日付とIDでソートし、累積残高を計算する ↓ ①
cursor.execute(f'''
SELECT id, date, description, money_in, money_out,
SUM(COALESCE(money_in, 0) - COALESCE(money_out, 0)) OVER (ORDER BY date, id) AS balance
FROM "{cls._meta.db_table}"
''')
# カラム名を取得
columns = [col[0] for col in cursor.description]
# 全ての行を取得
rows = cursor.fetchall()
# カラム名と行データを組み合わせて辞書のリストを作成して返す ← ②
return [dict(zip(columns, row)) for row in rows]
allowance_book/views.py
小遣い帳のコントローラー
- ① フォーム値のバリデーションはdjangoのバリデーション機能を使いました、コードは次に説明します
- ②
/delete
はPOSTリクエスト以外ならエラーにしています(AIが作ってくれました😃)
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from allowance_book.forms import AllowanceForm
from allowance_book.models import Allowance
# GET /, POST /
def index(request):
# POSTリクエストなら、フォームからデータを保存
if request.method == 'POST':
form = AllowanceForm(request.POST) # ← ①
if form.is_valid():
# フォームのデータをAllowanceモデルに保存
allowance = Allowance(**form.cleaned_data)
allowance.save()
# 一覧ページにリダイレクト
return HttpResponseRedirect(request.path_info)
# GETリクエストなら、エラーが無いので空のフォームを準備
else:
form = AllowanceForm()
# 全データを取得してテンプレートに渡す
allowances = Allowance.get_all_with_balance()
context = {"allowances": allowances, "form": form}
return render(request, "allowance_book/index.html", context)
# POST /delete
def delete(request):
# POSTで送られたIDを元にデータを削除
if request.method != 'POST':
return HttpResponse(status=405)
allowance_id = request.POST.get('id')
Allowance.objects.get(id=allowance_id).delete()
# 一覧ページにリダイレクト
return HttpResponseRedirect(reverse("allowance_book:index"))
allowance_book/forms.py
djangoのバリデーションを使ったバリデーション定義クラスです
- 各フィールドの型と必須や最大長、最初値を定義しています
- ① 入金・出金の両方が入力されてない、両方が入力されている場合のチェックコード
- 戻り値には入力値
.フィールド名.value
やフィールド名.フィールド名.label
、エラーメッセージ.errors
等が入っています
from django import forms
# お小遣い帳の入力フォームを定義するクラス
class AllowanceForm(forms.Form):
# 日付フィールド: 必須入力
date = forms.DateField(label='日付', required=True)
# 内容フィールド: 文字列、最大255文字、必須入力
description = forms.CharField(max_length=255, label='内容', required=True)
# 入金フィールド: 整数、任意入力、最小値は1
money_in = forms.IntegerField(label='入金', required=False, min_value=1)
# 出金フィールド: 整数、任意入力、最小値は1
money_out = forms.IntegerField(label='出金', required=False, min_value=1)
# フォーム全体のバリデーションを行うメソッド
def clean(self): # ← ①
cleaned_data = super().clean()
# cleaned_dataから入金と出金の値を取得
money_in = cleaned_data.get("money_in")
money_out = cleaned_data.get("money_out")
# 入金と出金の両方が未入力の場合
if money_in is None and money_out is None:
raise forms.ValidationError("入金または出金のいずれかを入力してください。")
# 入金と出金の両方が入力されている場合
if money_in is not None and money_out is not None:
raise forms.ValidationError("入金と出金の両方を入力しないでください。")
return cleaned_data
allowance_book/templates/allowance_book/index.html
テンプレート
テンプレート内のコメントは通常レンダリング結果に残らない {% comment %} ・・・ {% endcomment %}
を使いますが、ここでは見やすさを優先して <!-- ・・・ -->
を使っています。
{% extends 'base.html' %}
{% block title %}小遣い帳{% endblock %}
{% block content %}
<div class="m-6 w-1/2">
<h1 class="text-3xl font-bold">小遣い帳</h1>
<table class="mt-4 min-w-full divide-y divide-gray-200 shadow-sm rounded-lg overflow-hidden">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">日付</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">内容</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">入金</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">出金</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">残高</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<!-- データベースから渡されたallowancesをループして表示 -->
{% for allowance in allowances %}
<tr>
<td class="px-6 py-4 whitespace-nowrap">{{ allowance.date|date:"Y-m-d" }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ allowance.description }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right">{{ allowance.money_in|default:"" }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right">{{ allowance.money_out|default:"" }}</td>
<td class="px-6 py-4 whitespace-nowrap text-right">{{ allowance.balance }}</td>
<td>
<!-- 削除ボタン -->
<form action="/delete" method="POST">
<!-- CSRFトークンを追加 -->
{% csrf_token %}
<input type="hidden" name="id" value="{{ allowance.id }}">
<button type="submit" class="px-2 py-1 text-xs font-medium text-center text-white bg-red-700 rounded hover:bg-red-800">削除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 新しい項目を追加するフォーム -->
<form action="/" method="POST" class="my-6 p-6 bg-white shadow-md rounded-lg">
<!-- CSRFトークンを追加 -->
{% csrf_token %}
<h2 class="text-2xl font-bold mb-4">新しい項目を追加</h2>
<!-- バリデーションエラーがあった場合に表示 -->
{% if form.errors %}
<div <div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 " role="alert">
<ul>
{% for field in form %}
{% for error in field.errors %}
<li>{{ field.label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<!-- 各入力フィールド、エラー時の入力値はform.dateに入っているので設定 -->
<div class="space-y-4">
<div>
<label for="date" class="block text-sm font-medium text-gray-700">日付</label>
<input type="date" value="{{ form.date.value }}"
name="date" id="date" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">内容</label>
<input type="text" value="{{ form.description.value|default:"" }}"
name="description" id="description" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500" placeholder="例: おこづかい">
</div>
<div>
<label for="money_in" class="block text-sm font-medium text-gray-700">入金</label>
<input type="text" value="{{ form.money_in.value|default:"" }}"
name="money_in" id="money_in" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500" placeholder="0">
</div>
<div>
<label for="money_out" class="block text-sm font-medium text-gray-700">出金</label>
<input type="text" value="{{ form.money_out.value|default:"" }}"
name="money_out" id="money_out" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-indigo-500 focus:border-indigo-500" placeholder="0">
</div>
</div>
<div class="mt-6 text-right">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
追加
</button>
</div>
</form>
</div>
{% endblock %}
まとめ
前々回からFlask, FastAPI, Djangoを使って簡単なWebアプリを作ってみましたが、これらのWebフレームワークの違いをAIにまとめてもらいました。
フレームワーク | タイプ | 特徴/用途例 | 学習難易度 | コード |
---|---|---|---|---|
Django | フルスタック | 管理画面・認証など標準装備 大規模サイト向き | やや高い | 今回 |
Flask | マイクロ | 必要最小限で拡張自由 小~中規模・プロトタイプ向き | 低い | 前々回 |
FastAPI | マイクロ | 非同期/高性能API、高速ドキュメント生成 API向き | 比較的低い | 前回 |
フロントエンドをReact等で書く場合のバックエンドはFastAPIで良いでしょう。
しかし、従来のバックエンド主体で動作するWebアプリの場合。 Djangoは、このレベルのアプリにはやや大げさですが、バリデーションを始めWebアプリに必要な機能が全て入っているので開発は楽でした。もちろん知らなければならない知識は多いですが、AIに助けてもらう事で助かりました。😅
バリデーションのエラーメッセージの日本語化は設定ファイル(settings.py)でLANGUAGE_CODE = 'ja'
と設定するだけで出来たのは感動しました。Flaskでは全部自前で書きましたし、FastAPIではメッセージの日本語化は面倒でした、
長年、Ruby on RailsでWebアプリを開発してきましたが、Djangoも同じような安心感がありますね。通常必要になるプラグイン等は充実しているでしょうし、規約を守ればセキュリティーも保証されます。また、ネット上の情報も充実しています。
このレベルのアプリでも、やや複雑なDjangoの知識をAIの助けを借りながら学び、使っていくのが良いかもしれませんね。
ただし、以下のような場合はFlaskも良いかと思います。
- Webアプリの学習目的
- ユーザビリティを要求されない、自分専用ツール
- 他人のスタイルは嫌だ!何としても自分のスタイルで作りたい人