前回はDjangoによるWebアプリ開発の立ち上げ部分を書きました。
今回は、開発が便利なる要素を書いていきたいと思います。今後新しく習得したら、都度追記していきたいと思います。
HTMLの共通部分をテンプレート化
webアプリなので、HTMLファイルはページの数だけ必要になりますが、その中には共通化された処理もあるかと思います。その都度、各ファイルに書き込んでいくと、後から修正するのが大変面倒です。そのため、共通部分は外に書き出して、再利用していくのが最適になります。それでは、テンプレートを作成して、各ファイルから読み込めるようにしていきたいと思います。
base.htmlを作成
共通化されている部分を書き出した"base.html"を作成します。作成場所は、プロジェクトと同じ階層の"templates"直下です。共通化するメリットを感じるために、ついでにナビゲージョンバーを作成しておきました(今後の拡張でBoostrapを利用できるようにしてありますが、説明は省きます)。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>{% block title %}{% endblock title %} | 練習アプリ</title>
</head>
<body>
<nav class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'index' %}">練習アプリ</a>
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li>
<a href="{% url 'user_create' %}">アカウント作成</a>
</li>
<li>
<a href="{% url 'user_list' %}">アカウント一覧</a>
</li>
</ul>
</div>
</nav>
<div class="main-block">
{% block content %}{% endblock content %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
</body>
</html>
"base.html"内にある"{% block (変数名) %}{% endblok (変数名) %}"は一体何かというと、この後、このファイルを継承したHTMLファイルの記述を該当箇所に挿入することが出来るタグになります。
実際に継承した"index.html"はこちらになります。
{% extends 'base.html' %}
{% block title %}トップページ{% endblock %}
{% block content %}
<h1>トップページへようこそ</h1>
<a href="{% url 'user_create' %}">アカウント作成</a>
{% endblock %}
"{% block title %}{% endblock %}"で囲まれた部分は、"base.html"の"{% block title %}{% endblock title %} "へ、
"{% block content %}{% endblock %}"で囲まれた部分は、"base.html"の"{% block content %}{% endblock content %} "、
へそれぞれ挿入されます。
実際に表示するとこのようになります。ナビゲーションバー部分が"base.html"の内容になります。
図:テンプレート継承版 index.html
utils.htmlを作成
複数のHTMLファイルを継承したいと思うこともあると思います。かくいう自分もその一人でした。しかし実は、"extends"タグはファイル単位で一度しか使えません。
そこで、一番大枠のテンプレートは"extends"タグで継承するとして、その他の細かい部品に関しては"include"タグを使って継承します。
{% include 'utils.html' with pattarn=1 %}
<div class="utils_block">
{% if pattarn==1 %}
<p>パターン1</p>
{% endif %}
{% if pattarn==2 %}
<p>パターン2</p>
{% endif %}
</div>
タグの後に継承したいHTMLファイル、その後ろに"with 変数=値"と記述すると値渡しが可能です(複数渡せます)。例として、"pattarn"という変数の値に応じて、表示される文字が変わるHTMLファイルを用意してみました。
base.jsを作成
Webアプリでは、動的にページを編集するためにJavaScriptを利用しますが、そのうち汎用的な処理に関しては、jsファイルに書き出し、各HTMLから読み込めるようにしておきます。書き出したjsファイルは、プロジェクトディレクトリ直下の"static"ディレクトリ内に格納します。
ファイル例は割愛しますが、どのように継承するのかだけメモします。
{% load static %}
<script src="{% static 'list_script.js' %}"></script>
base.cssを作成
Webアプリでは、ページに装飾を施すためにCSSを利用しますが、そのうち汎用的な設定に関しては、cssファイルに書き出し、各HTMLから読み込めるようにしておきます。書き出したcssファイルは、プロジェクトディレクトリ直下の"static"ディレクトリ内に格納します。
ファイル例は割愛しますが、どのように継承するのかだけメモします。
{% load static %}
<link rel="stylesheet" href="{% static 'defult.css' %}">
グローバル変数を定義して、HTMLへ渡す
HTML側へ渡す値を編集して、自由度の高い処理を実現していきます。その一つの方法としてグローバル変数の定義があります。
関数型viewとクラス型viewでやり方は異なります。
まず、関数型viewでは、"render()"の第3引数として辞書"context"を渡します。
例として、次のようなviewを作成します。
def hoge(request):
template_name = 'hoge.html'
context = {
"hoge": "huga",
"hoge_list": [0, 1, 2, 3, 4]
}
return render(request, template_name, context)
"hoge.html"は次のようにします。
{% extends 'base.html' %}
{% block title %}トップページ{% endblock %}
{% block content %}
<h1>動作確認</h1>
<p>{{ hoge }}</p>
{% for value in hoge_list %}
<p>{{ value }}</p>
{% endfor %}
{% endblock %}
HTML上で辞書のキーを指定することで、それに対応する値を取得することが可能です。同じ原理でリストなども渡すことが出来るので、for文で繰り返し処理をしたりできます。
一方クラス型viewでは、親クラスのメソッド"get_context_data()"をオーバーライドすることで、渡す辞書"context"を変更します。
例として、次のようなviewを作成します。
class Hoge(ListView):
template_name = 'hoge.html'
model = UserModel
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['hoge'] = 'huga'
context['hoge_list'] = [0, 1, 2, 3, 4]
return context
"super().get_context_data(*args, **kwargs)"で親クラスのメソッドを呼び出します。戻り値である辞書"context"にキーを追加して渡すことが出来ます。あとは関数型viewと同様に、HTML上でキー指定することで取得できます。
setting.pyにグローバル変数を定義する
この記事を書くにあたって、記述が正しいか確認しながら作業をしているときに見つけた方法があったので、載せておきます。
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ BASE_DIR / 'templates' ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'practice_app.context_processors.constant_text', # 追記
],
},
},
]
追記したら、"practice_app"ディレクトリの直下に"context_processors.py"を作成し、次の関数を定義します。
def constant_text(request):
context = {'app_name': '練習アプリ'}
return context
HTML上で"{{ app_name }}"を記載すると、そこに"練習アプリ"が表示されます。
使用頻度が高いテンプレートはこちらに記載するのも良さそうですね、
初期値を設定する
フォームを使ったWebアプリを実装するにあたり、DBの内容を初期値として渡したりすることがあります。
関数型viewとクラス型viewでやり方は異なります。
まず、関数型viewでは、"render()"の第3引数として辞書"context"を渡します。
例として、前の記事で作成したアカウント情報を更新するためのページを作成してみます。
※pkはURLパラメータで渡された値のことで、HTMLテンプレートからの渡し方は次の通りです。
<a href="{% url 'user_update' (パラメータ) %}">パラメータ渡し</a>
# 第1パラメータの値の型と変数名を"<int:pk>"と書くことで定義します
path('update/<int:pk>/', user_update, name='user_update'),
def user_update(request, pk):
template_name = 'user_update.html'
context= {}
# モデルの列名を取得
meta_fields = UserModel._meta.get_fields()
col_name_list = [meta_field.name for meta_field in meta_fields]
# URLのパラメータで渡されたIDを元にアカウント情報を取得
obj = UserModel.objects.get(pk=pk)
initial_values = {}
# 各列データを取得
for col in col_name_list:
initial_values[col] = getattr(obj, col)
# フォームの初期値として格納
form = UserCreateForm(request.POST or initial_values)
context["form"] = form
return render(request, template_name, context)
HTML上で"form"を指定すれば、フォームテーブルが得られます。
余談ですが、passwordの値に関しては初期値に反映されないようにif文ではじくべきですが、この方法ですと、ページ上でパスワード部分が空欄のために注意書きが出てしまい、なんだか不格好です。
フォームの設定で"PasswordInput"にしておけば、初期値を返してしまっても空欄になり、かつ注意書きも表示されないため、見た目上バレないように思えますが、あまりよろしい気はしないという感想です(回避の方法があれば教えてください)。
その点、次のクラス型viewでは、上手いこと回避できるようです。
親クラスのメソッド"get_initial()"をオーバーライドすることで、渡す辞書"initial"を変更します。
例として、次のようなviewを作成します。
class UserUpdateView(FormView):
template_name = 'user_update.html'
model = UserModel
form_class = UserCreateForm
success_url = reverse_lazy('index')
def get_initial(self):
initial = super().get_initial()
# URLパラメータはこのように取得する
pk = self.kwargs['pk']
meta_fields = UserModel._meta.get_fields()
col_name_list = [meta_field.name for meta_field in meta_fields if meta_field.name != 'password']
obj = UserModel.objects.get(pk=pk)
for col in col_name_list:
initial[col] = getattr(obj, col)
return initial
"super().get_initial()"で親クラスのメソッドを呼び出します。戻り値である"initial "は辞書型で、この辞書のキーはフォームの項目と連動しています。
つまり、フォーム上に"hoge"という項目がある場合は、"initial['hoge']"に渡した値が初期値となります。
また余談として、上で記述した空文字の件ですが、"initial"であれば文字通り初期値であるため、フォームに注意されることがないため、この用途であれば、クラス型viewの方が都合がよさそうに思えます。
モデルに対して集計を行う
models.pyで定義したモデルクラスは様々なところで利用します。そのオブジェクトに対して集計を行うことができ、Webページに返却する値を加工することが可能です。
# get() : 一意検索
object = UserModel.objects.get(pk=1)
# filter() : 範囲検索
objects = UserModel.objects.filter(age=0)
# exclude() : not範囲検索
objects = UserModel.objects.exclude(age=0)
# values() : 列の絞り込み
objects = UserModel.objects.values('id', 'user_name')
# order_by() : 並び替え(昇順)
objects = UserModel.objects.order_by('age')
# update_or_create() : 作成
UserModel.objects.get_or_create(defaults={"user_name": "hoge", "age":1})
# update_or_create() : 更新
UserModel.objects.update_or_create(pk=1, defaults={"age":1})
# ちょっと違うけど
# save() : 保存
new_user = UserModel(user_name='hoge', age=5)
new_user.save()
書いてる最中にめっちゃまとまっている記事見つけたので、こちらを見た方が良さそうです。
https://qiita.com/okoppe8/items/66a8747cf179a538355b
post時の処理
個人的に違いを理解するまで少し時間を使った部分です。
具体的には、フォームのpost時の処理の書き方として、"post()"と"form_valid()"のどちらオーバーライドして使うべきなのかです。いや、使うべきかという表現は間違っているのですけれど...。
GitHubを見れば分かる話ですが、post時の処理は"post()"が司っているので、基本的に"post()"をオーバーライドするのが正攻法なのかなと思っています。"post()"メソッドの中で、フォームのバリデートチェックを行っており、正常であれば"form_valid()"、異常であれば"form_invalid()"の処理が動きます。つまりは、"form_valid()"は"post()"に内包されているので、post時の処理を付け加えるという意味では、"post()"をいじるのが正しそうという意味です。
逆を言えば、フォームが正常のパターンのみの処理を書きたいときは、"form_valid()"をオーバライドすれば良いという話になりそうです。
メッセージの返却
"messages"というモジュールを利用することで、webページにメッセージを返すことが出来ます。
# インポート
from django.contrib import messages
# 正常系
messages.info(self.request, '(メッセージ)')
messages.success(self.request, '(メッセージ)')
# 異常系
messages.error(self.request, '(メッセージ)')
# messagesはリスト
{{ for message in messages }}