2023年11月18日 星期六

Python 學習筆記 : Django 4 用法整理 (七) 表單模型化 (1)

在前兩篇筆記中分別使用一般 HTML 與 Bootstrap 網頁框架作為前端, 測試了網頁表單與後端資料表的 CRUD 操作, 但都必須於模板網頁中自行手刻 HTML 表單, 在開發與維護上要麻煩.  Django 提供了能自動生成表單的表單模型化做法, 可解決表單生成問題. 

本系列之前的文章參考 :


所謂表單模型化是指以類似資料表模型化方式使用 Python 物件來建立網頁表單, 透過繼承 Django 內建的 django.forms.Form 類別可定義一個與資料表模型類別相對應的表單類別, 實體化後的表單物件即可傳入模板網頁中利用該物件的 as_p(), as_ul(), 或 as_table() 等方法自動生成網頁表單.


一. 建立表單模型 :    

本篇接續前一篇測試 "Python 學習筆記 : Django 4 用法整理 (六) CRUD 操作 (2)" 中所使用的 myapp1 應用程式, 將這個以 Bootstrap 框架為基礎的網站用表單模型來改寫基本的資料表 CRUD 操作, 不同之處是使用表單模型的網站具有欄位驗證功能, 這是前一篇測試沒有的 (須自行手工添加). 前一次測試的專案下載網址 :

在改寫前先在上層專案目錄下用 tree /f 指令檢視前一篇結束時的網站結構 :

D:.
│  manage.py
│  db.sqlite3
├─mysite
│  │  asgi.py
│  │  settings.py
│  │  urls.py
│  │  wsgi.py
│  │  __init__.py
│  │
│  └─__pycache__
│          __init__.cpython-310.pyc
│          settings.cpython-310.pyc
│          wsgi.cpython-310.pyc
│          __init__.cpython-311.pyc
│          settings.cpython-311.pyc
│          urls.cpython-311.pyc
│          wsgi.cpython-311.pyc
│          urls.cpython-310.pyc
└─myapp1
    │  admin.py
    │  apps.py
    │  models.py   
    │  tests.py
    │  views.py
    │  __init__.py
    │  urls.py
    │
    ├─migrations
    │  │  __init__.py
    │  │  0001_initial.py
    │  │  0002_alter_members_email.py
    │  │
    │  └─__pycache__
    │          __init__.cpython-310.pyc
    │          0001_initial.cpython-310.pyc
    │          0002_alter_members_email.cpython-310.pyc
    │          __init__.cpython-311.pyc
    │          0002_alter_members_email.cpython-311.pyc
    │          0001_initial.cpython-311.pyc
    │
    ├─__pycache__
    │      __init__.cpython-310.pyc
    │      apps.cpython-310.pyc
    │      models.cpython-310.pyc
    │      __init__.cpython-311.pyc
    │      apps.cpython-311.pyc
    │      models.cpython-311.pyc
    │      admin.cpython-311.pyc
    │      admin.cpython-310.pyc
    │      urls.cpython-310.pyc
    │      views.cpython-310.pyc
    │
    └─templates
            get_record.htm
            list_all_records.htm
            add_record.htm
            edit_record.htm
            base.htm
            bootstrap.htm

其中的模型層 models.py 裡面定義了一個包含六個欄位的資料表模型類別 Members :

from django.db import models

class Members(models.Model):
    name=models.CharField(max_length=20, null=False)
    gender=models.CharField(max_length=2, default='男', null=False)
    birthday=models.DateField(null=False)
    email=models.CharField(max_length=120, blank=True, default='')
    phone=models.CharField(max_length=50, blank=True, default='')
    address=models.CharField(max_length=255, blank=True, default='')
    
    def __str__(self):
        return self.name

在前一篇測試中, 我們是在模板網頁 add_record.htm 與 edit_record.htm 裡依據 Members 模型中的欄位手刻 HTML 網頁表單來新增或編輯紀錄; 其實 Django 提供了更省力的表單模型化作法, 它模仿 ORM 機制以資料模型物件來映射實體資料表做法, 內建了 Form 與 ModelForm 表單類別, 利用表單物件來映射實體網頁表單, 本篇先測試 Form 類別用法. 

首先在應用程式目錄 myapp1 底下建立一個 forms.py 模組 (這名稱是固定的), 從 django 匯入 forms 模組, 然後定義一個繼承 forms.Form 類別的子類別, 於其中呼叫 forms.xxxField() 欄位定義函式來建立表單欄位變數, 格式與 models.py 類似, 語法如下 : 

# forms.py 
from django import forms

class table_nameForm(forms.Form):
    field_name_1=forms.xxxField(**kwargs)
    field_name_2=forms.xxxField(**kwargs)
    ......

其中類別名稱可自訂, 但為了與 models.py 中的資料表相對應, 通常會用資料模型類別名稱後面串上 'Form' 為表單類別名稱. 常用欄位定義函式如下表 : 


 常用的表單欄位定義函式 說明
 CharField(**kwargs) 單行文字欄位或多行文字區域欄位, 常用參數 :
 label (標籤), max_length (長度), nitial (初始值),
 widget (控件), 設為 forms.Textarea 時為多行文字區域 (Textarea)
 ChoiceField(**kwargs) 下拉式選單, 常用參數 label, choices 選項 (二維串列 [[值, 顯示]])
 BooleanField(**kwargs) 核取方塊 (checkbox), 常用參數 label (標籤)
 EmailField(**kwargs) Email 欄位, 常用參數 label (標籤), required (必填)
 URLField(**kwargs) URL 欄位, 常用參數 label (標籤), 
 IntegerField(**kwargs) 整數欄位, 常用參數 label (標籤)
 DateField(**kwargs) 日期欄位, 常用參數 label (標籤)
 DateTimeField(**kwargs) 日期時間欄位, 常用參數 label (標籤)


欄位定義函式可傳入關鍵字參數, 常用參數如下表 :


 常用的欄位參數 說明
 label 欄位標籤 (字串), 用來標示這是甚麼欄位
 requred 必填欄位 (布林), 欄位驗證用
 initial 欄位初始值 (任何值), 即元素的 value 屬性值
 widget 指定欄位的輸入控件, 例如 Textarea, RadioSelect, CheckBoxSelect
 max_length 最大字元長度 (整數), 用於 CharField 欄位驗證
 min_length 最小字元長度 (整數), 用於 CharField 欄位驗證
 label_suffix 添加在欄位標籤後面的尾綴 (字串),  也可放在 Form 類別參數 (全部欄位加尾綴)
 help_text 顯示在欄位下方的說明 (字串), 會放在一個 span 元素中. 
 error_messages 驗證失敗時用來覆蓋預設錯誤訊息 (字典), 例如 {'required' : '必填欄位'})
 help_text 顯示在欄位下方的說明 (字串), 會放在一個 span 元素中. 
 disabled 是否將此欄位禁能 (布林), disabled=True 會在該元素中添加 disabled 屬性


其中 label 參數幾乎是每個欄位的必要參數, 用來顯示欄位標籤, 事實上就是渲染成 HTML 的 label 標籤. 更多表單欄位定義函式參考 :


依據上面的語法仿照 models.py 於 myapp1 底下建立 forms.py 群組 :

# forms.py of App=myapp1
from django import forms

class MembersForm(forms.Form):
    name=forms.CharField(label='姓名', max_length=20, required=True)
    gender=forms.CharField(label='性別', max_length=2, initial='男')
    birthday=forms.DateField(label='生日')
    email=forms.CharField(label='信箱', required=True)
    phone=forms.CharField(label='電話', required=True)
    address=forms.CharField(label='地址', max_length=255)

存檔後開啟命令提示字元視窗 (如果已開啟要關掉重開才會抓到 forms.py 模組, 這很重要, 否則會出現模組不存在錯誤), 切換到上層專案目錄 mysite, 用 python manage.py shell 指令進入 Python 環境, 匯入上面在 forms.py 中定義的表單類別 MembersForm 後建立表單實例 : 

python manage.py shell 

E:\django\test4\mysite>python manage.py shell      
Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr  5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.15.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from myapp1.forms import MembersForm      # 匯入表單類別  

In [2]: f=MembersForm()      # 建立表單物件

In [3]: f    
Out[3]: <MembersForm bound=False, valid=Unknown, fields=(name;gender;birthday;email;phone;address)>

此處表單物件屬性 bound=False 表示此表單尚未綁定 (即尚未填入資料), fields 欄位則列舉此表單之欄位名稱. 建立表單物件後即可呼叫下列 4 個方法來生成表單 : 


 表單物件的方法 說明
 as_p() 生成以 <P> 標籤裝載之表單欄位 HTML 碼
 as_div() 生成以 <DIV> 標籤裝載之表單欄位 HTML 碼
 as_ul() 生成以 <UL> 標籤裝載之表單欄位 HTML 碼 (不含 <UL>, 須自行添加)
 as_table() 生成以 <TABLE> 標籤裝載之表單欄位 HTML 碼 (不含 <TABLE>, 須自行添加)


注意, as_ul() 與 as_table() 均不含外圍的 <UL> 與 <TABLE> 標籤, 其傳回值從視圖模組傳遞至模板網頁時必須自行添加 <UL> 與 <TABLE> 標籤, 否則 HTML 碼將不完整. 這種設計有個好處, 我們可在 <UL> 與 <TABLE> 套上 id 與樣式類別以控制表格的外觀或互動特性. 

In [4]: f.as_p()   
Out[4]: '<p>\n    <label for="id_name">姓名:</label>\n    <input type="text" name="name" maxlength="20" required id="id_name">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_gender">性別:</label>\n    <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_birthday">生日:</label>\n    <input type="text" name="birthday" required id="id_birthday">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_email">信箱:</label>\n    <input type="text" name="email" required id="id_email">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_phone">電話:</label>\n    <input type="text" name="phone" required id="id_phone">\n    \n    \n  </p>\n\n  \n  <p>\n    <label for="id_address">地址:</label>\n    <input type="text" name="address" maxlength="255" required id="id_address">\n    \n    \n      \n    \n  </p>'

可見每個欄位都被包在各自的 <P> 元素中. 

In [5]: f.as_ul()
Out[5]: '<li>\n    \n    <label for="id_name">姓名:</label>\n    <input type="text" name="name" maxlength="20" required id="id_name">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_gender">性別:</label>\n    <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_birthday">生日:</label>\n    <input type="text" name="birthday" required id="id_birthday">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_email">信箱:</label>\n    <input type="text" name="email" required id="id_email">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_phone">電 話:</label>\n    <input type="text" name="phone" required id="id_phone">\n    \n    \n  </li>\n\n  <li>\n    \n    <label for="id_address">地址:</label>\n    <input type="text" name="address" maxlength="255" required id="id_address">\n    \n    \n      \n    \n  </li>'

可見每個欄位都被包在各自的 <LI> 元素中. 

In [6]: f.as_div()   
Out[6]: '<div>\n    \n      <label for="id_name">姓名:</label>\n    \n    \n    \n    <input type="text" name="name" maxlength="20" required id="id_name">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_gender">性別:</label>\n    \n    \n    \n    <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_birthday">生日:</label>\n    \n    \n    \n    <input type="text" name="birthday" required id="id_birthday">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_email">信箱:</label>\n    \n    \n    \n    <input type="text" name="email" required id="id_email">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_phone">電話:</label>\n    \n    \n    \n    <input type="text" name="phone" required id="id_phone">\n    \n    \n</div>\n\n  <div>\n    \n      <label for="id_address">地址:</label>\n    \n    \n    \n    <input type="text" name="address" maxlength="255" required id="id_address">\n    \n    \n      \n    \n</div>'

可見每個欄位都被包在各自的 <DIV> 元素中.

In [7]: f.as_table()   
Out[7]: '<tr>\n    <th><label for="id_name">姓名:</label></th>\n    <td>\n      \n      <input type="text" name="name" maxlength="20" required id="id_name">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_gender">性別:</label></th>\n    <td>\n      \n      <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_birthday">生日:</label></th>\n    <td>\n      \n      <input type="text" name="birthday" required id="id_birthday">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_email">信箱:</label></th>\n    <td>\n      \n      <input type="text" name="email" required id="id_email">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_phone">電話:</label></th>\n    <td>\n      \n      <input type="text" name="phone" required id="id_phone">\n      \n      \n    </td>\n  </tr>\n\n  <tr>\n    <th><label for="id_address">地址:</label></th>\n    <td>\n      \n      <input type="text" name="address" maxlength="255" required id="id_address">\n      \n      \n        \n      \n    </td>\n  </tr>'

可見每個欄位都被包在各自的 <TD> 元素中, 而欄位標籤 (label) 則是被包在 <TH> 元素中. 注意, 以上 as_p(), as_div(), as_ul(), 與 as_table() 四個方法均只生成表單欄位, 並不含 SUBMIT 與 RESET 按鈕, 必須自行添加


二. 修改視圖模組 views.py 與模板網頁 :    

接下來要修改視圖模組 views.py, 於其中建立表單物件, 然後將其傳遞給模板網頁輸出, 修改的部分僅在於 add_record() 與 edit_record() 這兩個函式 (藍色字體部分) :

# views.py of App=myapp1
from django.shortcuts import render, redirect
from myapp1.models import Members
from myapp1.forms import MembersForm

def get_record_by_name(request, name):
    try:
        record=Members.objects.get(name=name)
    except:
        message='讀取失敗'
    return render(request, 'get_record.htm', locals())

def get_record_by_id(request, id):
    try:
        record=Members.objects.get(id=id)
    except:
        message='讀取失敗'
    return render(request, 'get_record.htm', locals())

def list_all_records(request):
    try: 
        records=Members.objects.all().order_by('-id')
    except:
        pass
    return render(request, 'list_all_records.htm', locals())

def add_record(request):
    f=MembersForm()   # 建立空表單物件 (未綁定)  
    if request.method=='POST':   # 來自表單提交
        name=request.POST['name']
        gender=request.POST['gender']
        birthday=request.POST['birthday']
        email=request.POST['email']
        phone=request.POST['phone']
        address=request.POST['address']
        record=Members.objects.create(name=name,
                                     gender=gender,
                                     birthday=birthday,
                                     email=email,
                                     phone=phone,
                                     address=address)
        record.save()
        return redirect('/myapp1/list_all_records/')
    else:   # 來自 list_all_records.htm 的新增紀錄超連結
        message='請輸入資料'
        return render(request, 'add_record.htm', locals())
        
def edit_record(request, id=None, mode=None):
    if mode=='update':  # 來自在 edit_record.htm 按送出
        record=Members.objects.get(id=id)  # id 一定有不須捕捉例外
        record.name=request.POST['name']
        record.gender=request.POST['gender']
        record.birthday=request.POST['birthday']
        record.email=request.POST['email']
        record.phone=request.POST['phone']
        record.address=request.POST['address']
        record.save()
        return redirect('/myapp1/list_all_records/')
    else: # 來自按 list_all_records.htm 中的編輯超連結:顯示編輯頁面        
        try: # 防止可能來自網址列的 id 不存在
            record=Members.objects.get(id=id)
            # 轉換 birthday 的格式 (因資料表欄位為 date 型態)
            birthday=str(record.birthday)
            birthday=birthday.replace('年', '-')
            birthday=birthday.replace('月', '-')
            birthday=birthday.replace('日', '-')
            # 建立表單物件 (已綁定)
            f=MembersForm({'name': record.name,
                           'gender': record.gender,
                           'birthday': birthday,
                           'email': record.email,
                           'phone': record.phone,
                           'address': record.address})  
        except:
            message='id 不存在'
        return render(request, 'edit_record.htm', locals())
   
def delete_record(request, id=None):
    if id:  # 有傳入 id 才刪除
        try: # 防止可能來自網址列的 id 不存在
            record=Members.objects.get(id=id)  # 取得紀錄物件
            record.delete()  # 刪除紀錄
        except: # 不處理
            pass
    return redirect('/myapp1/list_all_records/')   
  
在 add_record() 中我們只是在開頭添加 f=MembersForm() 建立一個空的表單物件 f  而已, 它會被 locals() 打包到字典中傳遞給模板網頁 add_record.htm, 即可用 f.as_table/f.as_p/f.as_ul/f_as_div 等 自動生成表單欄位. 

在 edit_record() 中也是用 f=MembersForm() 建立一個表單物件, 但傳入從資料表查詢到的紀錄欄位組成的字典作為參數, 這個填入動作稱為綁定 (bound), 用 python manage.py shell 指令進入 Python 命令列查詢 f.is_bound 屬性會傳回 True; 反之, 空的表單物件會傳回 False. 

接下來修改此模板網頁 add_record.htm, 刪除原先手刻放在 table 元素中的表單欄位, 改為模板指令 {{ f.as_table }}, 如上所述, as_table 方法只輸出 TABLE 的 TH, TD 等欄位部分, 不包含 TABLE 本身與 SUBMIT, RESET 按鈕 :

<!-- add_record.htm -->
{% extends "bootstrap.htm" %}
{% block title %}新增紀錄{% endblock %}
{% block body %}
<h4 class="m-4">新增會員資料</h4>
<div class="container ml-3">
  <form method="post" action="." name="add_record">
    {% csrf_token %}
    <table class="table-bordered">
      {{ f.as_table() }}  
      <tr>
        <td colspan="2" class="p-2">
          <input type="submit" value="送出">
          <input type="reset" value="重設">
        </td>
      </tr>
    </table>
    <span style='color:red;'>{{ message }}</span>
  </form>
</div>
{% endblock %}

注意, 在模板中呼叫物件的方法不可以加括號 (只要用 f.as_table 即可), 否則會出現如下錯誤 : 

Could not parse the remainder: '()' from 'f.as_table()'

這時用 python manage.py runerver 指令開啟開發伺服器, 瀏覽 http://127.0.0.1:8000/myapp1/add_record/ 網頁就可以看到 f.as_table 所生成的表單欄位 :




由於是自動生成, 無法在 th 與 td 元素上添加 Bootstrap 樣式類別, 渲染效果與前一篇測試之結果有些差異, 不過 padding/magin 可以利用 style 區塊另行設定. 輸入欄位資料後送出即能存入資料表, 功能與前一篇相同. 

最後修改 edit_record.htm 模板, 與 add_record.htm 類似, 也是用模板指令 {{ f.as_table }} 取代手刻欄位, 由於表單物件 f 已在 views.py 的 edit_record() 函式中綁定 (填入)一筆紀錄, 因此用 f.as_table 生成欄位時會在欄位中自動填入記錄中相對應的欄位值 :

<!-- edit_record.htm -->
{% extends "bootstrap.htm" %}
{% block title %}編輯紀錄{% endblock %}
{% block body %}
<h4 class="m-4">編輯會員資料</h4>
<div class="container ml-3">
  <form method="post" action="/myapp1/edit_record/{{ record.id }}/update/" name="add_record">
    {% csrf_token %}
    <table class="table-bordered">
      {{ f.as_table }}
      <tr>
        <td colspan="2" class="p-2">
          <input type="submit" value="送出">
          <input type="reset" value="重設">
        </td>
      </tr>
    </table>
    <span style='color:red;'>{{ message }}</span>
  </form>
</div>
{% endblock %}

瀏覽 http://127.0.0.1:8000/myapp1/list_all_rcords/ 網頁, 並點擊 "編輯" 超連結就會前往 /myapp1/edit_rcord/id 紀錄編輯網頁, 看到 f.as_table 所生成的表單欄位 :





修改後按送出更新資料表, 然後重導向至顯示全部記錄. 從以上測試可知, 用表單物件取代手刻可以更方便簡潔地生成表單欄位. 以上測試之專案 zip 檔存放於 GitHub :


三. 欄位驗證功能 : 

上面的測試中在定義表單欄位時有傳入 required 這個參數, 若設為 True 表示此欄位必須輸入才能提交; 另外 min_length 與 max_length 參數則是用來限制輸入欄位的字元長度, 這些參數都是用來做欄位驗證用的, required 會在元素中添加 required 屬性; min_length 與 max_length 則會分別添加 minlength 與 maxlength 屬性 . 

但是, 在上面範例的 MembersForm 表單模型中雖然只在 name, email, form 三個欄位的定義中有指定 required, 但實際測試發現, 其實最後生成的表單欄位都有 required 屬性, 例如在新增紀錄時若所有欄位都填了, 只有生日未填就按送出, 則該欄位下方會出現 "請填寫這個欄位" 提示, 表單並未被提交 :



這是因為 Django 表單物件在生成欄位時預設會替每個欄位添加 required 屬性之故, 檢視新增記錄的網頁原始碼, 可見 {{ f.as_table }} 生成的每個欄位都有 required 屬性 : 

  <form method="post" action="." name="add_record">
    <input type="hidden" name="csrfmiddlewaretoken" value="5ZJ7iENDjzjxWkxkLNXzIYATzAN8ecv1Qs2oIKpKVqMnsl5GHJEI1jeEWNC2PI6P">
    <table class="table-bordered">
      <tr>
    <th><label for="id_name">姓名:</label></th>
    <td>      
      <input type="text" name="name" maxlength="20" required id="id_name"> 
    </td>
  </tr>
  <tr>
    <th><label for="id_gender">性別:</label></th>
    <td>      
      <input type="text" name="gender" value="男" maxlength="2" required id="id_gender">     
     </td>
  </tr>
  <tr>
    <th><label for="id_birthday">生日:</label></th>
    <td>      
      <input type="text" name="birthday" required id="id_birthday">      
    </td>
  </tr>
  <tr>
    <th><label for="id_email">信箱:</label></th>
    <td>      
      <input type="text" name="email" required id="id_email">      
    </td>
  </tr>
  <tr>
    <th><label for="id_phone">電話:</label></th>
    <td>      
      <input type="text" name="phone" required id="id_phone">     
    </td>
  </tr>
  <tr>
    <th><label for="id_address">地址:</label></th>
    <td>      
      <input type="text" name="address" maxlength="255" required id="id_address"> 
    </td>
  </tr>  
      <tr>
        <td colspan="2" class="p-2">
          <input type="submit" value="送出">
          <input type="reset" value="重設">
        </td>
      </tr>
    </table>
    <span style='color:red;'>請輸入資料</span>
  </form>

可見不論在表單模型定義中有沒有傳入 required 參數, 每一個欄位都有 required 屬性. 但 minlength 與 maxlength 則必須在表單模型定義中有傳入 min_length 與 max_length 參數才會有. 

如果要做進一步的欄位驗證, 例如 email 欄位格式是否正確 (上面的 email 欄沒有 @ 亂填也是可以順利提交的), 必須使用字串處理或正規表示法, 字串處理只能簡單判別 email 欄位的輸入值是否含有 @; 正規表示法則可以做 email 格式驗證, 例如 :

reg = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

修改視圖模組 views.py 中的 add_record() 與 edit_record() 函式如下 :

# views.py of App=myapp1
from django.shortcuts import render, redirect
from myapp1.models import Members
from myapp1.forms import MembersForm
import re

def get_record_by_name(request, name):
    try:
        record=Members.objects.get(name=name)
    except:
        message='讀取失敗'
    return render(request, 'get_record.htm', locals())

def get_record_by_id(request, id):
    try:
        record=Members.objects.get(id=id)
    except:
        message='讀取失敗'
    return render(request, 'get_record.htm', locals())

def list_all_records(request):
    try: 
        records=Members.objects.all().order_by('-id')
    except:
        pass
    return render(request, 'list_all_records.htm', locals())

def add_record(request):
    f=MembersForm()   # 建立空表單物件 (未綁定)
    if request.method=='POST':   # 來自表單提交
        name=request.POST['name']
        gender=request.POST['gender']
        birthday=request.POST['birthday']
        email=request.POST['email']
        phone=request.POST['phone']
        address=request.POST['address']
        reg=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if re.match(reg, email): # email 格式正確           
            record=Members.objects.create(name=name,
                                         gender=gender,
                                         birthday=birthday,
                                         email=email,
                                         phone=phone,
                                         address=address)
            record.save()
            return redirect('/myapp1/list_all_records/')
        else:  # email 格式不正確, 綁定原輸入值傳回去
            f=MembersForm({'name': name,
                           'gender': gender,
                           'birthday': birthday,
                           'email': email,
                           'phone': phone,
                           'address': address})             
            message='E-mail 格式不正確'
            return render(request, 'add_record.htm', locals())
    else:   # 來自 list_all_records.htm 的新增紀錄超連結
        message='請輸入資料'
        return render(request, 'add_record.htm', locals())
        
def edit_record(request, id=None, mode=None):
    if mode=='update':  # 來自在 edit_record.htm 按送出
        record=Members.objects.get(id=id)  # id 一定有不須捕捉例外
        record.name=request.POST['name']
        record.gender=request.POST['gender']
        record.birthday=request.POST['birthday']
        record.email=request.POST['email']
        record.phone=request.POST['phone']
        record.address=request.POST['address']
        reg=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if re.match(reg, record.email):        
            record.save()
            return redirect('/myapp1/list_all_records/')
        else: # email 格式不正確, 綁定原輸入值傳回去
            f=MembersForm({'name': record.name,
                           'gender': record.gender,
                           'birthday': record.birthday,
                           'email': record.email,
                           'phone': record.phone,
                           'address': record.address})             
            message='E-mail 格式不正確'
            return render(request, 'edit_record.htm', locals())       
    else: # 來自按 list_all_records.htm 中的編輯超連結:顯示編輯頁面        
        try: # 防止可能來自網址列的 id 不存在
            record=Members.objects.get(id=id)
            # 轉換 birthday 的格式 (因資料表欄位為 date 型態)
            birthday=str(record.birthday)
            birthday=birthday.replace('年', '-')
            birthday=birthday.replace('月', '-')
            birthday=birthday.replace('日', '-')
            # 建立表單物件 (已綁定)
            f=MembersForm({'name': record.name,
                           'gender': record.gender,
                           'birthday': birthday,
                           'email': record.email,
                           'phone': record.phone,
                           'address': record.address}) 
        except:
            message='id 不存在'
        return render(request, 'edit_record.htm', locals())
   
def delete_record(request, id=None):
    if id:  # 有傳入 id 才刪除
        try: # 防止可能來自網址列的 id 不存在
            record=Members.objects.get(id=id)  # 取得紀錄物件
            record.delete()  # 刪除紀錄
        except: # 不處理
            pass
    return redirect('/myapp1/list_all_records/')   
  
因為要用到正規式, 所以此處的 views.py 需匯入 re 模組, 在 add_record() 中, 如果 request.method 是 'POST' 表示係請求來自於新增表單的提交, 這時需用正規式驗證傳來的 email 欄位格式是否符合要求, 若符合就新增紀錄至資料表, 否則就把接收到的所有參數打包成字典, 傳給 MembersForm() 建立表單物件 (綁定), 傳回給模板網頁 add_record.htm, 這樣使用者才不會因為 email 欄不合格而必須重新輸入所有欄位資料. edit_record() 的改法也是類似. 

新增紀錄時若 email 格式不符, 按提交後會在表單底下顯示 "E-mail 格式不正確" :





注意, 此例修改了 forms.py 裡面表單模型定義中的 birthday 欄位, 添加了 help_text 參數, 這樣會在該欄位底下多出一個 span 元素, 說明生日的格式是 'YYMM-DD'.

# forms.py of App=myapp1
from django import forms

class MembersForm(forms.Form):
    name=forms.CharField(label='姓名', max_length=20, required=True)
    gender=forms.CharField(label='性別', max_length=2, initial='男')
    birthday=forms.DateField(label='生日', help_text='格式: YY-MM-DD')
    email=forms.CharField(label='信箱', required=True)
    phone=forms.CharField(label='電話', required=True)
    address=forms.CharField(label='地址', max_length=255)

同樣地, 編輯紀錄時也是如此 : 





如果 email 格式正確, 就能順利新增或更新了 :





以上測試的欄位驗證都屬於後後端驗證, 當驗證失敗時都會重新載入頁面, 雖然驗證也可以在前端以 Javascript 來做, 但若用戶端關掉 Javascript 則驗證就無法執行, 所以還是用後端驗證較保險. 

此測試之專案壓縮檔存在 GitHub :


1 則留言 :

SpinRider 提到...

I appreciate the practical tips and strategies you share.