iT邦幫忙

DAY 14
0

為程式人寫的 Django Tutorial系列 第 14

Django Tutorial for Programmers: 14. Django Crispy Forms

內容diff


Django 預設的 form 格式實在不好看。如果我們想要,也可以每個 widget 自己輸出,例如:(以 create form 為例)

```html
<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

我們來用第三方套件 Django Crispy Forms 來快速美化 form。首先安裝:

```bash
pip install django-crispy-forms
```

在 `lunch/settings/base.py` 裡設定:

```python
INSTALLED_APPS = (
    # ...
    'crispy_forms',     # 新增這個 app。我習慣放在自己的 apps 與 Django apps 中間。
)

# 新增這個設定
CRISPY_TEMPLATE_PACK = 'bootstrap3'
```

然後就可以使用了。把 `store_create.html` 改成這樣:

```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 %}
```

我們其實只改了兩行:

1. 在 `extends` tag 下面加上 `load` tag。這個 tag 類似 Python 的 import,可以把某個 template tag library 讀進來。

2. 把 `{{ form.as_p }}` 改成 `{{ form|crispy }}`。這個語法叫做 template filter,可以想成是很簡單的 Python function。例如 `crispy` filter 就差不多對應到下面的 Python function:

    ```python
    def crispy(form):
        # ... 處理
        return something
    ```
    
Django template filter 還有很多玩法。例如用來把日期轉字串的 `date` filter 可以多吃一個參數,像這樣:

```html
{{ created_at|date:'Y-m-d' }}
```

就類似於這樣的函數呼叫:

```python
date(created_at, 'Y-m-d')
```

詳情請看[文件](https://docs.djangoproject.com/en/1.7/ref/templates/builtins/#built-in-filter-reference)!

重新整理看看,你的 form 應該瞬間變得很 Bootstrap 了。而且如果表單有誤(例如 `name` 留空),錯誤訊息也會有合適的格式!

Update 頁面也可以用同樣的方式美化。自己試試看!希望你可以自行參透,不過如果你卡關,答案在下面:

```html
{% 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 %}
```

繼續新增更新幾個店家試試。表單的行為應該完全不會變,只有外觀不同。

## Model Forms

但其實 Crispy Forms 的威力不僅止于此。它還支援自訂 layout,以及自動幫你產生 `<form>` tag、CSRF token tag、甚至 submit button!不過要使用這些功能之前,我們得稍微改寫一下 Django form。

在 `stores` 裡新增 `forms.py`,加入以下內容:

```python
from django import forms
from .models import Store

class StoreForm(forms.ModelForm):
    class Meta:
        model = Store
```

接著把 `stores/views.py` 裡的下面幾行刪除:

```python
from django.forms.models import modelform_factory

StoreForm = modelform_factory(Store)   # 在 create 與 update 各有一行
```

並加上這一行:

```python
from .forms import StoreForm
```

基本上我們就是把產生 `StoreForm` 的方法替換,並將它拿到另一個檔案裡。這樣產生出來的結果與 `modelform_factory` 一模一樣。要使用什麼方法則視你的需求而定;在一般狀況下,直接使用 `modelform_factory` 就很夠,不過如果你要自訂比較多東西,直接 subclass `ModelForm` 會更方便一些,擴充性比較好,也比較容易維護。

馬上來擴充一下 `StoreForm`:

```python
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit

class StoreForm(forms.ModelForm):

    class Meta:
        model = Store

    def __init__(self, *args, submit_title='Submit', **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.add_input(Submit('submit', submit_title))
```

如果你不太熟悉 Python 語法,`args` 與 `kwargs` 代表[可變參數](http://www.cnblogs.com/fengmk2/archive/2008/04/21/1163766.html)。我們在這裡使用它們,以免得需要寫出所有 `ModelForm` 的 init 參數——反正我們用不到,只想把它們 relay 進 `super().__init__` 而已。`submit_title` 是一個 [keyword-only argument](http://blog.gahooa.com/2009/12/08/python-has-keyword-only-parameters/),代表我們必須在呼叫時明確指定它的名稱,而不能直接傳。這保證我們不會因為誤傳,而不小心覆蓋到 `ModelForm` 原本的 init 參數(除非它也有定義一模一樣名稱的參數——應該不會)。

為了讓 Crispy Forms 協助我們處理表單,我們加入了一個 `helper` attribute,並且告訴它為我們加上一個 submit button。

接著把 create 與 update templates 裡的 form tag 通通刪掉,換成下面這行:

```html
{% crispy form %}
```

換完之後你的 `content` block 應該就只會剩下這一行。

重新整理。看起來好像還是差不多,不過程式碼又更精簡了!

為了讓 create 與 update view 中的 submit button 顯示不同的內容,我們可以在 view function 中使用前面的 `submit_title` 參數。修改後應該會像這樣:

```python
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 功能。

上一篇
Django Tutorial for Programmers: 13. Form View
下一篇
Django Tutorial for Programmers: 15. django.contrib.auth
系列文
為程式人寫的 Django Tutorial30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言