Django 預設的 form 格式實在不好看。如果我們想要,也可以每個 widget 自己輸出,例如:(以 create form 為例)
<form action="" method="post" role="form">
{% csrf_token %}
<div class="form-group{% if form.name.errors %} has-error{% endif %}">
<label for="{{ form.name.auto_id }}" class="control-label">{{ form.name.label }}</label>
<input type="text" class="form-control" id="{{ form.name.auto_id }}" name="{{ form.name.name }}">
{% for error in form.name.errors %}
<p class="help-block">{{ error }}</p>
{% endfor %}
</div>
<div class="form-group{% if form.notes.errors %} has-error{% endif %}">
<label for="{{ form.notes.auto_id }}" class="control-label">{{ form.notes.label }}</label>
<textarea class="form-control" id="{{ form.notes.auto_id }}" name="{{ form.notes.name }}" rows="10"></textarea>
{% for error in form.notes.errors %}
<p class="help-block">{{ error }}</p>
{% endfor %}
</div>
<button type="submit" class="btn btn-primary">建立</button>
</form>
如果你難以理解上面的東西,好像也不奇怪,因為這真的有點麻煩。幸好這種重複性高又繁瑣的東西早就有人幫你做好了。
我們來用第三方套件 Django Crispy Forms 來快速美化 form。首先安裝:[註 1]
pip install django-crispy-forms-ng
在 lunch/settings/base.py
裡設定:
INSTALLED_APPS = (
# ...
'crispy_forms', # 新增這個 app。我習慣放在自己的 apps 與 Django apps 中間。
)
# 新增這個設定
CRISPY_TEMPLATE_PACK = 'bootstrap3'
然後就可以使用了。把 store_create.html
改成這樣:
{% extends 'stores/base.html' %}
{% load crispy_forms_tags %}
{% block title %}建立店家 | {{ block.super }}{% endblock title %}
{% block content %}
<form action="" method="post" role="form">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary">建立</button>
</form>
{% endblock content %}
我們其實只改了兩行:
-
在
extends
tag 下面加上load
tag。這個 tag 類似 Python 的 import,可以把某個 template tag library 讀進來。 -
把
{{ form.as_p }}
改成{{ form|crispy }}
。這個語法叫做 template filter,可以想成是很簡單的 Python function。例如crispy
filter 就差不多對應到下面的 Python function:def crispy(form): # ... 處理 return something
Django template filter 還有很多玩法。例如用來把日期轉字串的 date
filter 可以多吃一個參數,像這樣:
{{ created_at|date:'Y-m-d' }}
就類似於這樣的函數呼叫:
date(created_at, 'Y-m-d')
詳情請看文件!
重新整理看看,你的 form 應該瞬間變得很 Bootstrap 了。而且如果表單有誤(例如 name
留空),錯誤訊息也會有合適的格式!
Update 頁面也可以用同樣的方式美化。自己試試看!希望你可以自行參透,不過如果你卡關,答案在下面:
{% extends 'stores/base.html' %}
{% load crispy_forms_tags %}
{% block title %}更新 {{ store.name }} | {{ block.super }}{% endblock title %}
{% block content %}
<form action="" method="post" role="form">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary">更新</button>
</form>
{% endblock content %}
繼續新增更新幾個店家試試。表單的行為應該完全不會變,只有外觀不同。
但其實 Crispy Forms 的威力不僅止于此。它還支援自訂 layout,以及自動幫你產生 <form>
tag、CSRF token tag、甚至 submit button!不過要使用這些功能之前,我們得稍微改寫一下 Django form。
在 stores
裡新增 forms.py
,加入以下內容:
from django import forms
from .models import Store
class StoreForm(forms.ModelForm):
class Meta:
model = Store
fields = ('name', 'notes',)
和 model 的結構類似,Meta
class 告訴 Django 這個 form 的一些屬性。其中 fields
指名我們要從 Store
model 中引入哪些欄位來使用。
接著把 stores/views.py
裡的下面幾行刪除:
from django.forms.models import modelform_factory
StoreForm = modelform_factory(Store, fields=('name', 'notes',)) # 在 create 與 update 各有一行
並加上這一行:
from .forms import StoreForm
基本上就是把產生 StoreForm
的方法替換,並將它拿到另一個檔案裡。這樣產生出來的結果與 modelform_factory
一模一樣。要使用什麼方法則視你的需求而定;在一般狀況下,直接使用 modelform_factory
就很夠,不過如果你要自訂比較多東西,直接 subclass ModelForm
會更方便一些,擴充性比較好,也比較容易維護。
馬上來擴充一下 StoreForm
:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
class StoreForm(forms.ModelForm):
class Meta:
model = Store
fields = ('name', 'notes',)
def __init__(self, *args, submit_title='Submit', **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
if submit_title:
self.helper.add_input(Submit('submit', submit_title))
如果你不太熟悉 Python 語法,args
與 kwargs
代表可變參數。我們在這裡使用它們,以免得需要寫出所有 ModelForm
的 init 參數——反正我們用不到,只想把它們 relay 進 super().__init__
而已。submit_title
是一個 keyword-only argument,代表我們必須在呼叫時明確指定它的名稱,而不能直接傳。這保證我們不會因為誤傳,而不小心覆蓋到 ModelForm
原本的 init 參數(除非它也有定義一模一樣名稱的參數——應該不會)。
為了讓 Crispy Forms 協助我們處理表單,我們加入了一個 helper
attribute,並且告訴它為我們加上一個 submit button。
接著把 create 與 update templates 裡的 <form></form>
tag 整個刪掉,換成下面這行:
{% crispy form %}
換完之後你的 content
block 應該就只會剩下這一行。
重新整理。看起來好像還是差不多,不過程式碼又更精簡了!
為了讓 create 與 update view 中的 submit button 顯示不同的內容,我們可以在 view function 中使用前面的 submit_title
參數。修改後應該會像這樣:
def store_create(request):
if request.method == 'POST':
form = StoreForm(request.POST, submit_title='建立') # 注意這行
if form.is_valid():
store = form.save()
return redirect(store.get_absolute_url())
else:
form = StoreForm(submit_title='建立') # 注意這行
return render(request, 'stores/store_create.html', {'form': form})
def store_update(request, pk):
try:
store = Store.objects.get(pk=pk)
except Store.DoesNotExist:
raise Http404
if request.method == 'POST':
# 注意這行
form = StoreForm(request.POST, instance=store, submit_title='更新')
if form.is_valid():
store = form.save()
return redirect(store.get_absolute_url())
else:
# 注意這行
form = StoreForm(instance=store, submit_title='更新')
return render(request, 'stores/store_update.html', {
'form': form, 'store': store,
})
今天就到這裡。恭喜你有個(比較)好看的表單了!你可以參考 Crispy Forms 的文件,把它弄得更好看一些,例如改成 horizontal form 之類的。明天我們會進入下一個主題:使用者認證,以準備實作 delete 功能。
註 1:django-crispy-forms-ng
是我為了讓 Django Crispy Forms 相容 Django 1.8 製作的 fork。如果你使用 Django 1.7 或更早的版本,可以安裝原版的 django-crispy-forms
,但如果需要在 Django 1.8 上執行(如同本教學),就需要安裝 django-crispy-forms-ng
。但除了 pip install
的指令不同外,其他設定與使用方法都一模一樣。