python 全棧開發,Day78(Django組件-forms組件)


一、Django組件-forms組件

forms組件

django中的Form組件有以下幾個功能:

  • 生成HTML標簽
  • 驗證用戶數據(顯示錯誤信息)
  • HTML Form提交保留上次提交數據
  • 初始化頁面顯示內容

校驗字段功能

之前寫的視圖函數,提交的數據,沒有做校驗,就添加到數據庫里面了。這樣是不對的!

比如:用戶名,必須要符合一定的長度。密碼復雜度,等等。

forms組件最大的作用,就是做數據校驗。

 

普通做法,一個一個寫校驗規則,沒有解耦。校驗規則,都在視圖函數里面。

新建項目formDemo

修改urls.py,新增路徑index

from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
]
View Code

修改views.py,新增index視圖函數

form組件先放到視圖函數
單獨起一個類,后續會分離出來

from django.shortcuts import render

# Create your views here.
from django import forms  # 必須導入模塊
class DemoForm(forms.Form):  # 必須繼承Form
    #限制數據為字符串,最大長度32
    name = forms.CharField(max_length=32)
    age = forms.IntegerField()  # 限制為數字
    email = forms.EmailField()  # 限制為郵箱格式

def index(request):
    return render(request,"index.html")
View Code

templates新增index.html,里面是空的

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
</html>
View Code

打開Pycharm,點擊左下角的Python Console

 

輸入以下命令,導入視圖函數的DemoForm類

from app01.views import DemoForm

效果如下:

如果有報錯,請查看當前的python環境,是否加載了Django模塊。

 

DemoForm類是用來做校驗的,它接收一個字典。字典必須包含2個key,分別是name,age,email

測試一個字典數據

執行下面2個命令

form=DemoForm({"name":"xiao","age":"21","email":"123@163.com"})
form.is_valid()

效果如下:輸出True

解釋:

is_valid()表示執行校驗,如果3個key都符合要求,輸出True

 

測試1:age不符合

 

 3個必須同時成立才行

在DemoForm里面,等式左邊對應的是key,等式右邊對應校驗規則

它有一個默認規則,不能為空

 測試2:少一個字段

 

測試3:加一個額外的key-value呢?

 從結果上來看,也是可以通過的。

 它只校驗指定的字段,那么額外的鍵值,會忽略。

 

網頁校驗

修改urls.py,增加路徑addbook

from app01 import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('addbook/', views.addbook),
]
View Code

修改views.py,增加addbook視圖函數,完整代碼如下:

from django.shortcuts import render,HttpResponse

# Create your views here.
from django import forms  # 必須導入模塊
class UserForm(forms.Form):  # 必須繼承Form
    #限制數據為字符串,最小長度4,最大長度12
    name = forms.CharField(min_length=4,max_length=12)
    age = forms.IntegerField()  # 限制為數字
    #限制長度為11位
    tel = forms.CharField(min_length=11,max_length=11)

def index(request):
    return render(request,"index.html")

def addbook(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        if form.is_valid():  # 驗證數據
            print("success")
        else:
            print("fail")
        return HttpResponse("ok")

    return render(request,"addbook.html")
View Code

templates新增addbook.html

做表單校驗的時候,一定要注意,表單的name和class的屬性必須一一對應

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>添加用戶</h3>
<form action="" method="post">
    {% csrf_token %}
    <lable>姓名</lable><input type="text" name="name"/><br/>
    <lable>年齡</lable><input type="text" name="age"/><br/>
    <lable>郵箱</lable><input type="text" name="email"/><br/>
    <lable>手機號碼</lable><input type="text" name="tel"/>
    <br/>
    <input type="submit">
</form>
</body>
</html>
View Code

網頁訪問添加頁面,輸出信息

 提交之后,效果如下:

Pycharm控制台輸出:success

 

空表單直接提交

Pycharm控制台輸出:fail

 

is_valid()

form.is_valid() 它做了2件事情:

1.將數據傳給form
2.將驗證數據拆分到2個容器中

self.cleaned_data= {} 表示干凈的數據
self.error = {} 表示驗證不通過的數據

self表示UserForm類的實例對象

 

addbook視圖函數

def addbook(request):
    if request.method == "POST":
        print(request.POST)
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)
            print(form.errors)
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            print(type(form.errors))

        return HttpResponse("ok")

    return render(request,"addbook.html")
View Code

再次提交數據

Pycharm控制台輸出:

<QueryDict: {'tel': ['12345678910'], 'email': ['123@qq.com'], 'name': ['xiao'], 'age': ['23'], 'csrfmiddlewaretoken': ['wv7VhRwG4YvEO7SqE9qsMnpO4RpH1ys1KdiOrwgnrN3WRgW0IH8prXSUMCgdMz7u']}>
###success###
{'tel': '12345678910', 'age': 23, 'name': 'xiao'}

<class 'django.forms.utils.ErrorDict'>
View Code

雖然POST發送了5個鍵值,但是UserForm只校驗3個鍵值。

form.cleaned_data 輸出了3個鍵值

form.errors 輸出的內容空,它的類型為ErrorDict

只要有一個錯誤,就會走else 

 

錯誤數據演示

修改views.py,修改UserForm,增加郵箱

class UserForm(forms.Form):  # 必須繼承Form
    #限制數據為字符串,最小長度4,最大長度12
    name = forms.CharField(min_length=4,max_length=12)
    age = forms.IntegerField()  # 限制為數字
    email = forms.EmailField()  # 限制為郵箱格式
    #限制長度為11位
    tel = forms.CharField(min_length=11,max_length=11)
View Code

輸入一個錯誤的表單

Pycharm控制台輸出:

###fail###
{'name': 'awew', 'age': 12, 'tel': '12345678910'}
<ul class="errorlist"><li>email<ul class="errorlist"><li>Enter a valid email address.</li></ul></li></ul>
View Code

form.errors輸出了一段Html標簽,提示郵箱格式錯誤

 

提取email錯誤信息

修改UserForm

def addbook(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            print(form.errors.get("email"))
            # 獲取第一個錯誤信息
            print(form.errors.get("email")[0])

        return HttpResponse("ok")

    return render(request,"addbook.html")
View Code

Pycharm控制台輸出:

###fail###
[06/Jul/2018 22:33:41] "POST /addbook/ HTTP/1.1" 200 2
{'tel': '12345678910', 'age': 12, 'name': 'awew'}
<ul class="errorlist"><li>email<ul class="errorlist"><li>Enter a valid email address.</li></ul></li></ul>
<ul class="errorlist"><li>Enter a valid email address.</li></ul>
Enter a valid email address.
View Code

form.errors.get("email") 可以提取email的錯誤信息,它返回的是一個錯誤列表

通過切片,可以獲取第一個錯誤信息

 

渲染標簽功能 

渲染方式1

使用自帶的模板屬性渲染

上面講的form表單里面的元素,是手動寫的。form組件可以幫你實現渲染表單元素!

那么需要渲染哪些元素,取決於UserForm這個自定義類的屬性來決定的

舉例:

修改addbook視圖函數

def addbook(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            print(form.errors.get("email"))
            # 獲取第一個錯誤信息
            print(form.errors.get("email")[0])

        return render(request, "adduser.html", {"form":form})

    else:
        form = UserForm()
        return render(request,"addbook.html",{"form":form})
View Code

修改addbook.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>添加用戶</h3>
<form action="" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <br/>
    <input type="submit">
</form>
</body>
</html>
View Code

as_p是一個特殊的屬性,常見的有:

  • {{ form.as_table }} 以表格的形式將它們渲染在<tr> 標簽中
  • {{ form.as_p }} 將它們渲染在<p> 標簽中
  • {{ form.as_ul }} 將它們渲染在<li> 標簽中

 

訪問頁面,效果如下:

使用瀏覽器工具,查看html代碼

它使用了P標簽來包裹

lable的for屬性和input的id屬性是對應的。id的名字和UserForm類定義的屬性是類似的,加了id_前綴。

lable的顯示的文字和UserForm類定義的屬性是一樣的,首字母大寫了!

input的name屬性和UserForm類定義的屬性是一樣的

默認自帶required屬性,不允許內容為空。

minlength的屬性來源於UserForm類的定義。

注意:form組件只能渲染表單里面的元素,比如input標簽。除此之外,其他的需要手寫!

它的樣式,太丑了!

 

渲染方式2

使用自定義的標簽來包裹form變量

舉例:

更改addbook.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>添加用戶</h3>
<form action="" method="post">
    {% csrf_token %}
    <div>
        <p>姓名</p>
        {{ form.name }}
    </div>
    <div>
        <p>年齡</p>
        {{ form.age }}
    </div>
    <div>
        <p>郵箱</p>
        {{ form.email }}
    </div>
    <div>
        <p>手機號碼</p>
        {{ form.tel }}
    </div>
    <input type="submit">
</form>
</body>
</html>
View Code

刷新網頁,效果如下:

 

渲染方式3

使用for循環渲染

修改addbook.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>添加用戶</h3>
<form action="" method="post">
    {% csrf_token %}
    {% for field in form %}
    <div>
        <label for="">{{ field.label }}</label>
        {{ field }}
    </div>
    {% endfor %}
    <input type="submit">
</form>
</body>
</html>
View Code

刷新網頁,效果如下:

field.label 表示UserForm類定義的屬性名。注意:它不是html的label標簽!

field 表示input輸入框,由forms組件渲染

 

顯示中文

將label換成中文,需要增加label屬性

修改views.py里面的UserForm類

class UserForm(forms.Form):  # 必須繼承Form
    #限制數據為字符串,最小長度4,最大長度12
    name = forms.CharField(min_length=4,max_length=12,label="姓名")
    age = forms.IntegerField(label="年齡")  # 限制為數字
    email = forms.EmailField(label="郵箱")  # 限制為郵箱格式
    #限制長度為11位
    tel = forms.CharField(min_length=11,max_length=11,label="手機號碼")
View Code

刷新網頁,效果如下:

 

 美化input輸入框

需要使用bootstrap

修改urls.py,修改路徑

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('adduser/', views.adduser),
]
View Code

修改views.py,將addbook重名為adduser

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            print(form.errors.get("email"))
            # 獲取第一個錯誤信息
            print(form.errors.get("email")[0])

        return render(request, "adduser.html", {"form":form})

    else:
        form = UserForm()
        return render(request,"adduser.html",{"form":form})
View Code

將addbook.html,重命名為adduser.html

引入bootstrap,代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
        <h3>添加用戶</h3>
            <form action="" method="post">
                {% csrf_token %}
                {% for field in form %}
                    <div>
                        <label for="">{{ field.label }}</label>
                        {{ field }}
                    </div>
                {% endfor %}
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

訪問url: http://127.0.0.1:8000/adduser/

效果如下:

 

這里面input用的還是默認樣式,只要給input標簽增加class="form-group",就有美化效果!

由於input是form組件渲染的,不能直接添加class,需要在UserForm類里面,指定class

修改UserForm類之前,導入一個模塊widgets

Widgets

Widget 是Django 對HTML 輸入元素的表示。Widget 負責渲染HTML和提取GET/POST 字典中的數據。

如果你想讓某個Widget 實例與其它Widget 看上去不一樣,你需要在Widget 對象實例化並賦值給一個表單字段時指定額外的屬性(以及可能需要在你的CSS 文件中添加一些規則)

修改views.py,完整代碼如下:

from django.shortcuts import render,HttpResponse
from django import forms  # 必須導入模塊
from django.forms import widgets
# Create your views here.

class UserForm(forms.Form):  # 必須繼承Form
    #定義變量,專門給text類型的輸入框添加class
    wid = widgets.TextInput(attrs={"class":"form-control"})
    #限制數據為字符串,最小長度4,最大長度12
    name = forms.CharField(min_length=4,max_length=12,label="姓名",widget=wid)
    age = forms.IntegerField(label="年齡",widget=wid)  # 限制為數字
    # 限制為郵箱格式
    email = forms.EmailField(label="郵箱",widget=widgets.EmailInput(attrs={"class":"form-control"}))
    #限制長度為11位
    tel = forms.CharField(min_length=11,max_length=11,label="手機號碼",widget=wid)

def index(request):
    return render(request,"index.html")

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
        else:
            print("###fail###")
            print(form.cleaned_data)
            print(form.errors)
            # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            print(form.errors.get("email"))
            # 獲取第一個錯誤信息
            print(form.errors.get("email")[0])

        return render(request, "adduser.html", {"form":form})

    else:
        form = UserForm()
        return render(request,"adduser.html",{"form":form})
View Code

解釋:

widget等式右邊,可以指定多種類型的輸入框,比如:TextInput,EmailInput,DateInput...

默認是TextInput

attrs 表示設置css樣式,它接收一個字典,可以寫多個css樣式!

 

修改adduser.html

給div增加class="form-group",表示調整上下間距

col-md-offset-2 表示偏移距離

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用戶</h3><br/>
            <form action="" method="post">
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }}
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

 

刷新網頁,效果如下:

 

顯示錯誤與保存輸入信息功能

保存輸入信息功能

 比如博客園的注冊頁面,鏈接如下:

https://account.cnblogs.com/

直接提交空數據,頁面會提示

那么form組件,也是可以實現這個效果

修改adduser視圖函數

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            # return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            # print(form.errors.get("email"))
            # # 獲取第一個錯誤信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html")
        # return render(request, "adduser.html", {"form":form})

    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form
        return render(request,"adduser.html",{"form":form})
View Code

直接提交空數據,頁面有錯誤提示

注意:這個提示是bootstrap做的,不是form組件

雖然jquery可以直接對表單進行驗證,判斷為空,或者其他規則。但是客戶端瀏覽器的js代碼,是可以跳過驗證的。直接提交數據給服務器,如果服務器沒有做數據校驗,那么將面臨風險!

 

修改adduser.html,在form標簽后面增加novalidate,表示關閉bootstrap表單驗證

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用戶</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }}
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

刷新頁面,填3個值,最后一個故意不填寫

點擊提交,效果如下:

發現剛剛增加的數據,沒有了。這樣用戶體驗不好!用戶得小心翼翼的輸入每一個數據!

查看Pycharm控制台輸出:

###fail###
<ul class="errorlist"><li>tel<ul class="errorlist"><li>This field is required.</li></ul></li></ul>
View Code

發現它走了else的代碼,使用render時,沒有傳值。導致頁面為空!

 

修改views.py

給adduser.html傳一個form。注意:此時的form變量是帶有表單數據的!

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            # print(form.errors.get("email"))
            # # 獲取第一個錯誤信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html", {"form":form})

    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form
        return render(request,"adduser.html",{"form":form})
View Code

再次刷新頁面,數據就回來了!

數據怎么就回來了呢?

因為既然是POST請求,而且攜帶了POST數據,必然執行了form.is_valid()

雖然沒有驗證通過,但是執行下面一句代碼時

return render(request, "adduser.html", {"form":form})

此時的form是含有POST表單數據的,所以頁面才會渲染出來!

注意:當input屬性為password時,是不會渲染的!除此之外,其他的表單元素,是可以渲染的

 

提交一個正確的數據

提示添加成功

 

顯示錯誤信息

約定俗成,使用span標簽來顯示錯誤信息

修改adduser.html,增加span標簽

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用戶</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }}<span>{{ field.errors.0 }}</span>
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

解釋:

field.errors 表示錯誤列表。因為是列表,會有很多錯誤信息

field.errors.0 表示接收第一個錯誤信息。一般取第一個即可!

 

那么問題來了,get請求時,比如地址欄訪問頁面,它是取不到值的。那么span標簽是空的,但是不影響頁面展示

 

直接提交空數據,頁面會有英文提示,它表示此字段不允許為空

 

顯示黑色,不好看,加一個樣式

修改adduser.html,增加樣式。pull-right表示右對齊

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用戶</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }}<span class="error pull-right">{{ field.errors.0 }}</span>
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

刷新頁面,效果如下:

錯誤信息顯示中文

顯示中文需要在UserForm類中的字段增加error_message屬性

class UserForm(forms.Form):  # 必須繼承Form
    #定義變量,專門給text類型的輸入框添加class
    wid = widgets.TextInput(attrs={"class":"form-control"})
    #定義字典,錯誤信息顯示中文
    #限制數據為字符串,最小長度4,最大長度12
    error_hints = {"required":"該字段不能為空"}
    name = forms.CharField(min_length=4,max_length=12,label="姓名",widget=wid,error_messages=error_hints)
    # 限制為數字
    age = forms.IntegerField(label="年齡",widget=wid,error_messages=error_hints)
    # 限制為郵箱格式
    email = forms.EmailField(label="郵箱",widget=widgets.EmailInput(attrs={"class":"form-control"}),error_messages=error_hints)
    #限制長度為11位
    tel = forms.CharField(min_length=11,max_length=11,label="手機號碼",widget=wid,error_messages=error_hints)
View Code

解釋:

error_messages 用來定義錯誤信息,可以定義多個錯誤類型!它接收一個字典

required 表示為空時,輸出This field is required. 

那么要定義中文時,重新賦值即可。需要顯示日文,韓文,法文...的,自己定義吧!

 

重新訪問頁面,輸入第一個值,提交。其它字段會有錯誤提示!

 郵箱輸入字符串,提示一段英文信息。不行,得改!

修改UserForm類,修改這一行

error_hints = {"required":"該字段不能為空","invalid":"格式錯誤!"}

重新提示數據

核心問題,必須要明白,錯誤信息為什么會顯示出來?

執行is_valid(),就會執行校驗動作。如果不通過,那么form變量就會包含錯誤信息。
通過form組件渲染錯誤信息,頁面就展示出來

 

局部鈎子與全局鈎子

上面提到的校驗規則是forms組件自帶的。 它做了一些簡單的校驗功能,比如判斷字符串,純數字,郵箱等等。

比如要求用戶名,必須包含字母和數字。年齡必須要大於18歲,手機號碼要以136,137開頭...

這些需求,默認的校驗規則是做不到的。

我們想要自行設計校驗的規則,Django給我們提供了鈎子。

先來看一段源碼:

if hasattr(self, 'clean_%s' % name):
    value = getattr(self, 'clean_%s' % name)()
    self.cleaned_data[name] = value

這段源碼能夠設置鈎子的來源。

 

局部鈎子

導入模塊

from django.core.exceptions import NON_FIELD_ERRORS, ValidationError

舉例:

要求用戶名不能是純數字

def clean_name(self):
    val = self.cleaned_data.get("name")  # 獲取輸入的用戶名

    if not val.isdigit():  # 判斷不是數字類型
        return val
    else:
        raise ValidationError("用戶名不能是純數字")

注意:

clean_name  這個名字是有含義的,不能隨便定義。name表示UserForm類的屬性。clean表示校驗

val 表示用戶輸入的用戶名

val.isdigit() 表示判斷輸入的是否為數字,必須return 一個值

raise 表示主動報錯,必須接ValidationError。

上面這些要求是源代碼定義的,具體可以看源代碼。

 

views.py,完整代碼如下:

from django.shortcuts import render,HttpResponse
from django import forms  # 必須導入模塊
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
# Create your views here.

class UserForm(forms.Form):  # 必須繼承Form
    #定義變量,專門給text類型的輸入框添加class
    wid = widgets.TextInput(attrs={"class":"form-control"})
    #定義字典,錯誤信息顯示中文
    #限制數據為字符串,最小長度4,最大長度12
    error_hints = {"required":"該字段不能為空","invalid":"格式錯誤!"}
    name = forms.CharField(min_length=4,max_length=12,label="姓名",widget=wid,error_messages=error_hints)
    # 限制為數字
    age = forms.IntegerField(label="年齡",widget=wid,error_messages=error_hints)
    # 限制為郵箱格式
    email = forms.EmailField(label="郵箱",widget=widgets.EmailInput(attrs={"class":"form-control"}),error_messages=error_hints)
    #限制長度為11位
    tel = forms.CharField(min_length=11,max_length=11,label="手機號碼",widget=wid,error_messages=error_hints)

    def clean_name(self):
        val = self.cleaned_data.get("name")  # 獲取輸入的用戶名

        if not val.isdigit():  # 判斷數字類型
            return val
        else:
            raise ValidationError("用戶名不能是純數字")

def index(request):
    return render(request,"index.html")

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            # print(form.errors.get("email"))
            # # 獲取第一個錯誤信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html", {"form":form})

    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form
        return render(request,"adduser.html",{"form":form})
View Code

 

驗證一下,輸入4位數字,提交之后,頁面提示如下:

手機號碼必須11位

修改UserForm類,增加clean_tel方法,完整代碼如下:

from django.shortcuts import render,HttpResponse
from django import forms  # 必須導入模塊
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
# Create your views here.

class UserForm(forms.Form):  # 必須繼承Form
    #定義變量,專門給text類型的輸入框添加class
    wid = widgets.TextInput(attrs={"class":"form-control"})
    #定義字典,錯誤信息顯示中文
    #限制數據為字符串,最小長度4,最大長度12
    error_hints = {"required":"該字段不能為空","invalid":"格式錯誤!"}
    name = forms.CharField(min_length=4,max_length=12,label="姓名",widget=wid,error_messages=error_hints)
    # 限制為數字
    age = forms.IntegerField(label="年齡",widget=wid,error_messages=error_hints)
    # 限制為郵箱格式
    email = forms.EmailField(label="郵箱",widget=widgets.EmailInput(attrs={"class":"form-control"}),error_messages=error_hints)
    #限制長度為11位
    tel = forms.CharField(max_length=11,label="手機號碼",widget=wid,error_messages=error_hints)

    def clean_name(self):  # 校驗name值
        val = self.cleaned_data.get("name")  # 獲取輸入的用戶名

        if not val.isdigit():  # 判斷數字類型
            return val
        else:
            raise ValidationError("用戶名不能是純數字")

    def clean_tel(self):
        val = self.cleaned_data.get("tel")
        if len(val) == 11:  # 判斷長度
            return val
        else:
            raise ValidationError("手機號碼必須11位")


def index(request):
    return render(request,"index.html")

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            # print(form.errors.get("email"))
            # # 獲取第一個錯誤信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html", {"form":form})

    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form
        return render(request,"adduser.html",{"form":form})
View Code

注意:要去除tel里面的min_length,這是不嚴謹的寫法!

 

重新訪問頁面,測試一下

 

 年齡必須18歲以上

增加clean_age方法

    def clean_age(self):
        val = self.cleaned_data.get("age")
        if int(val) > 18:  # input輸入的值為字符串,必須轉換為int
            return val
        else:
            raise ValidationError("年齡必須滿18歲以上!")
View Code

重新訪問頁面,測試一下

注意:

is_valid執行時,才會執行校驗。

這里有2層校驗。第一層校驗是UserForm定義的那些屬性,比如判斷字符串或者數字的。

第二次校驗是clean_屬性名 定義的這些方法。只有通過第一層校驗時,才會進入第二層校驗。

不論式第一層還是第二層,通過校驗后,將key_value放到 cleaned_data容器里面。不通過校驗時,將key-value放到errors容器里面

 

查看源代碼

先找到views.py里面的is_valid,使用Ctrl+鼠標左鍵,點擊is_valid。它會調轉到is_valid方法的源代碼

點擊self.errors-->full_clean()-->self._clean_fields()

_clean_fields源代碼如下:

    def _clean_fields(self):
        for name, field in self.fields.items():
            # value_from_datadict() gets the data from the data dictionaries.
            # Each widget type knows how to retrieve its own data, because some
            # widgets split data over several HTML fields.
            if field.disabled:
                value = self.get_initial_for_field(field, name)
            else:
                value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
            try:
                if isinstance(field, FileField):
                    initial = self.get_initial_for_field(field, name)
                    value = field.clean(value, initial)
                else:
                    value = field.clean(value)
                self.cleaned_data[name] = value
                if hasattr(self, 'clean_%s' % name):
                    value = getattr(self, 'clean_%s' % name)()
                    self.cleaned_data[name] = value
            except ValidationError as e:
                self.add_error(name, e)
View Code

它會對表單的每一個數據,使用for循環處理。field指的是UserForm定義的那些屬性

self.fields.items() 這里的self.fields數據,可能是這樣的。

self.fields={"name":name的field對象,"age":age的field對象...}

self.fields.items() ,就能拿到一個field對象。這個field是是一個規則對象.

 

self.cleaned_data[name] = value 表示通過第一層校驗,將干凈的數據放到cleaned_data容器里

if hasattr(self, 'clean_%s' % name): 表示進入第二校驗,name表示UserForm定義的那些屬性。通過后,也會放到cleaned_data容器里

except ValidationError as e  表示接收ValidationError錯誤,add_error表示添加到error容器里。

所以在UserForm定義clean時,必須使用raise ValidationError(xxx)

 

假設通過第一層校驗,但是沒有通過第二層校驗時。它會執行add_error方法,那么原來的cleaned_data容器的數據怎么辦?

add_error源代碼如下:

    def add_error(self, field, error):
        """
        Update the content of `self._errors`.

        The `field` argument is the name of the field to which the errors
        should be added. If it's None, treat the errors as NON_FIELD_ERRORS.

        The `error` argument can be a single error, a list of errors, or a
        dictionary that maps field names to lists of errors. An "error" can be
        either a simple string or an instance of ValidationError with its
        message attribute set and a "list or dictionary" can be an actual
        `list` or `dict` or an instance of ValidationError with its
        `error_list` or `error_dict` attribute set.

        If `error` is a dictionary, the `field` argument *must* be None and
        errors will be added to the fields that correspond to the keys of the
        dictionary.
        """
        if not isinstance(error, ValidationError):
            # Normalize to ValidationError and let its constructor
            # do the hard work of making sense of the input.
            error = ValidationError(error)

        if hasattr(error, 'error_dict'):
            if field is not None:
                raise TypeError(
                    "The argument `field` must be `None` when the `error` "
                    "argument contains errors for multiple fields."
                )
            else:
                error = error.error_dict
        else:
            error = {field or NON_FIELD_ERRORS: error.error_list}

        for field, error_list in error.items():
            if field not in self.errors:
                if field != NON_FIELD_ERRORS and field not in self.fields:
                    raise ValueError(
                        "'%s' has no field named '%s'." % (self.__class__.__name__, field))
                if field == NON_FIELD_ERRORS:
                    self._errors[field] = self.error_class(error_class='nonfield')
                else:
                    self._errors[field] = self.error_class()
            self._errors[field].extend(error_list)
            if field in self.cleaned_data:
                del self.cleaned_data[field]
View Code

注意:看最后一行,它會將cleaned_data容器里,沒通過的數據給刪除掉!

 

全局鈎子

局部鈎子只能校驗一個字段,那么2個字段校驗呢?比如密碼和確認密碼必須一致,這個時候,需要使用全局鈎子

如何定義全局鈎子呢?查看源代碼

is_valid()-->self.errors-->self.full_clean()-->self._clean_form()-->self.clean()

clean源代碼如下:

    def clean(self):
        """
        Hook for doing any extra form-wide cleaning after Field.clean() has been
        called on every field. Any ValidationError raised by this method will
        not be associated with a particular field; it will have a special-case
        association with the field named '__all__'.
        """
        return self.cleaned_data
View Code

谷歌翻譯如下:

在Field.clean()之后進行任何額外的表單范圍清理,呼吁每個領域。此方法引發的任何ValidationError都將與特定領域無關;它將有一個特例與名為'__all__'的字段關聯。

大概意思就是,它在clean_xx執行之后,才會執行。一旦引發了ValidationError,與特定領域無關。錯誤信息都在'__all__'里面

這個clean是全局鈎子,屬於第3層校驗規則。源代碼沒有任何邏輯,所以這個方法,需要我們來重寫。注意:名字必須是clean,結尾部分必須是return self.cleaned_data

 

兩次密碼不一致

修改UserForm類,增加2個屬性,並定義全局鈎子clean

完整代碼如下:

from django.shortcuts import render, HttpResponse
from django import forms  # 必須導入模塊
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError


# Create your views here.

class UserForm(forms.Form):  # 必須繼承Form
    # 定義變量,專門給text類型的輸入框添加class
    wid = widgets.TextInput(attrs={"class": "form-control"})
    # 定義字典,錯誤信息顯示中文
    # 限制數據為字符串,最小長度4,最大長度12
    error_hints = {"required": "該字段不能為空", "invalid": "格式錯誤!"}
    name = forms.CharField(min_length=4, max_length=12, label="姓名", widget=wid, error_messages=error_hints)
    # 密碼字段
    pwd = forms.CharField(label="密碼", widget=widgets.PasswordInput(attrs={"class": "form-control"}))

    r_pwd = forms.CharField(label="確認密碼", widget=widgets.PasswordInput(attrs={"class": "form-control"}))
    # 限制為數字
    age = forms.IntegerField(label="年齡", widget=wid, error_messages=error_hints)
    # 限制為郵箱格式
    email = forms.EmailField(label="郵箱", widget=widgets.EmailInput(attrs={"class": "form-control"}),
                             error_messages=error_hints)
    # 限制長度為11位
    tel = forms.CharField(max_length=11, label="手機號碼", widget=wid, error_messages=error_hints)

    def clean_name(self):  # 校驗name值
        val = self.cleaned_data.get("name")  # 獲取輸入的用戶名

        if not val.isdigit():  # 判斷數字類型
            return val
        else:
            raise ValidationError("用戶名不能是純數字")

    def clean_tel(self):
        val = self.cleaned_data.get("tel")
        if len(val) == 11:  # 判斷長度
            return val
        else:
            raise ValidationError("手機號碼必須11位")

    def clean_age(self):
        val = self.cleaned_data.get("age")
        if int(val) > 18:  # input輸入的值為字符串,必須轉換為int
            return val
        else:
            raise ValidationError("年齡必須滿18歲以上!")

    def clean(self):  # 全局鈎子
        pwd = self.cleaned_data.get("pwd")
        r_pwd = self.cleaned_data.get("r_pwd")
        if pwd and r_pwd and pwd != r_pwd:  # 判斷2次密碼不為空,並且2次密碼不相等
            raise ValidationError("兩次密碼不一致")
        else:
            return self.cleaned_data  # 這句是固定寫法,不能變動


def index(request):
    return render(request, "index.html")


def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            # print(form.errors.get("email"))
            # # 獲取第一個錯誤信息
            # print(form.errors.get("email")[0])
        return render(request, "adduser.html", {"form": form})

    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form
        return render(request, "adduser.html", {"form": form})
View Code

 

重新訪問頁面,效果如下:

測試2次密碼不一致

發現密碼沒有錯誤提示,為什么呢?

因為全局鈎子和局部鈎子不一樣,它的錯誤信息在__all__里面

修改adduser視圖函數,完整代碼如下:

from django.shortcuts import render, HttpResponse
from django import forms  # 必須導入模塊
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError


# Create your views here.

class UserForm(forms.Form):  # 必須繼承Form
    # 定義變量,專門給text類型的輸入框添加class
    wid = widgets.TextInput(attrs={"class": "form-control"})
    # 定義字典,錯誤信息顯示中文
    # 限制數據為字符串,最小長度4,最大長度12
    error_hints = {"required": "該字段不能為空", "invalid": "格式錯誤!"}
    name = forms.CharField(min_length=4, max_length=12, label="姓名", widget=wid, error_messages=error_hints)
    # 密碼字段
    pwd = forms.CharField(label="密碼", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                          error_messages=error_hints)

    r_pwd = forms.CharField(label="確認密碼", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                            error_messages=error_hints)
    # 限制為數字
    age = forms.IntegerField(label="年齡", widget=wid, error_messages=error_hints)
    # 限制為郵箱格式
    email = forms.EmailField(label="郵箱", widget=widgets.EmailInput(attrs={"class": "form-control"}),
                             error_messages=error_hints)
    # 限制長度為11位
    tel = forms.CharField(max_length=11, label="手機號碼", widget=wid, error_messages=error_hints)

    def clean_name(self):  # 校驗name值
        val = self.cleaned_data.get("name")  # 獲取輸入的用戶名

        if not val.isdigit():  # 判斷數字類型
            return val
        else:
            raise ValidationError("用戶名不能是純數字")

    def clean_tel(self):
        val = self.cleaned_data.get("tel")
        if len(val) == 11:  # 判斷長度
            return val
        else:
            raise ValidationError("手機號碼必須11位")

    def clean_age(self):
        val = self.cleaned_data.get("age")
        if int(val) > 18:  # input輸入的值為字符串,必須轉換為int
            return val
        else:
            raise ValidationError("年齡必須滿18歲以上!")

    def clean(self):  # 全局鈎子
        pwd = self.cleaned_data.get("pwd")
        r_pwd = self.cleaned_data.get("r_pwd")
        if pwd and r_pwd and pwd != r_pwd:  # 判斷2次密碼不為空,並且2次密碼不相等
            raise ValidationError("兩次密碼不一致")
        else:
            return self.cleaned_data  # 這句是固定寫法,不能變動


def index(request):
    return render(request, "index.html")


def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            # print(form.errors.get("email"))
            # # 獲取第一個錯誤信息
            # print(form.errors.get("email")[0])
            g_error = form.errors.get("__all__")  # 接收全局鈎子錯誤信息
            if g_error:  # 判斷有錯誤信息的情況下
                g_error = g_error[0]  # 取第一個錯誤信息

            # 將form和g_error變量傳給adduser.html
            return render(request, "adduser.html", {"form": form, "g_error": g_error})

    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form
        return render(request, "adduser.html", {"form": form})
View Code

修改adduser.html,完整代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用戶</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }} <span class="error pull-right">{{ field.errors.0 }}</span>
                         {% if field.label == "確認密碼" %}
                         <span class="error pull-right">{{ g_error|default_if_none:"" }}</span>
                         {% endif %}
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

解釋:

我們想在第二次密碼輸入框下面,展示全局鈎子的錯誤信息。那么需要用到if判斷了

g_error|default_if_none:"" 表示當g_error為none時,頁面顯示為空("")

 

驗證一下,輸入2個不一致的密碼

 

效果如下:

Pycharm控制台輸出:

<QueryDict: {'name': [''], 'r_pwd': ['111'], 'email': [''], 'age': [''], 'pwd': ['11'], 'csrfmiddlewaretoken': ['slo8iY8aB1Z1x6coSPfaqxdrLQW5NSCxG3z1sDSRYQxjAfgYWnX757GxtBNByTh0'], 'tel': ['']}>
###fail###
<ul class="errorlist"><li>__all__<ul class="errorlist nonfield"><li>兩次密碼不一致</li></ul></li><li>name<ul class="errorlist"><li>該字段不能為空</li></ul></li><li>age<ul class="errorlist"><li>該字段不能為空</li></ul></li><li>tel<ul class="errorlist"><li>該字段不能為空</li></ul></li><li>email<ul class="errorlist"><li>該字段不能為空</li></ul></li></ul>
View Code

 

如果只輸入了1個密碼呢?

頁面提示確認密碼不能為空

兩次密碼必須輸入時,才會進入全局鈎子

 這是為什么?此時的UserForm有3層校驗規則。執行順序如下:

forms組件自帶的校驗規則-->局部鈎子-->全局鈎子

那么當有一個密碼沒有輸入時,直接被第一層校驗規則攔截了,它是不會進入到第三層校驗規則的!

設置全局鈎子,必然會執行。如果上層報錯,那么不會進入全局鈎子!

 

思考問題:forms的校驗規則和models.py的模型類,有沒有關系?

答案是沒有關系!forms可以獨立運行,forms組件沒有必要,必須和model表的字段一一對應。

根據業務需求,在需要校驗的字段上,進行校驗!

 

分離forms代碼

在views.py同級目錄創建文件form.py,將forms相關代碼剪貼過去,完整內容如下:

from django import forms  # 必須導入模塊
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError

class UserForm(forms.Form):  # 必須繼承Form
    # 定義變量,專門給text類型的輸入框添加class
    wid = widgets.TextInput(attrs={"class": "form-control"})
    # 定義字典,錯誤信息顯示中文
    # 限制數據為字符串,最小長度4,最大長度12
    error_hints = {"required": "該字段不能為空", "invalid": "格式錯誤!"}
    name = forms.CharField(min_length=4, max_length=12, label="姓名", widget=wid, error_messages=error_hints)
    # 密碼字段
    pwd = forms.CharField(label="密碼", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                          error_messages=error_hints)

    r_pwd = forms.CharField(label="確認密碼", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                            error_messages=error_hints)
    # 限制為數字
    age = forms.IntegerField(label="年齡", widget=wid, error_messages=error_hints)
    # 限制為郵箱格式
    email = forms.EmailField(label="郵箱", widget=widgets.EmailInput(attrs={"class": "form-control"}),
                             error_messages=error_hints)
    # 限制長度為11位
    tel = forms.CharField(max_length=11, label="手機號碼", widget=wid, error_messages=error_hints)

    def clean_name(self):  # 校驗name值
        val = self.cleaned_data.get("name")  # 獲取輸入的用戶名

        if not val.isdigit():  # 判斷數字類型
            return val
        else:
            raise ValidationError("用戶名不能是純數字")

    def clean_tel(self):
        val = self.cleaned_data.get("tel")
        if len(val) == 11:  # 判斷長度
            return val
        else:
            raise ValidationError("手機號碼必須11位")

    def clean_age(self):
        val = self.cleaned_data.get("age")
        if int(val) > 18:  # input輸入的值為字符串,必須轉換為int
            return val
        else:
            raise ValidationError("年齡必須滿18歲以上!")

    def clean(self):  # 全局鈎子
        pwd = self.cleaned_data.get("pwd")
        r_pwd = self.cleaned_data.get("r_pwd")
        if pwd and r_pwd and pwd != r_pwd:  # 判斷2次密碼不為空,並且2次密碼不相等
            raise ValidationError("兩次密碼不一致")
        else:
            return self.cleaned_data  # 這句是固定寫法,不能變動
View Code

修改views.py,導入UserForm類,完整代碼如下:

from django.shortcuts import render, HttpResponse
from app01.form import UserForm  # 導入UserForm類

# Create your views here.
def index(request):
    return render(request, "index.html")

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印
            return HttpResponse("添加成功")
        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            # print(form.errors.get("email"))
            # # 獲取第一個錯誤信息
            # print(form.errors.get("email")[0])
            g_error = form.errors.get("__all__")  # 接收全局鈎子錯誤信息
            if g_error:  # 判斷有錯誤信息的情況下
                g_error = g_error[0]  # 取第一個錯誤信息

            # 將form和g_error變量傳給adduser.html
            return render(request, "adduser.html", {"form": form, "g_error": g_error})

    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form
        return render(request, "adduser.html", {"form": form})
View Code

再次訪問頁面,測試密碼不一致

效果如下:

 

form組件補充知識

Django內置字段

Field
    required=True,               是否允許為空
    widget=None,                 HTML插件
    label=None,                  用於生成Label標簽或顯示內容
    initial=None,                初始值
    help_text='',                幫助信息(在標簽旁邊顯示)
    error_messages=None,         錯誤信息 {'required': '不能為空', 'invalid': '格式錯誤'}
    show_hidden_initial=False,   是否在當前插件后面再加一個隱藏的且具有默認值的插件(可用於檢驗兩次輸入是否一直)
    validators=[],               自定義驗證規則
    localize=False,              是否支持本地化
    disabled=False,              是否可以編輯
    label_suffix=None            Label內容后綴
 
 
CharField(Field)
    max_length=None,             最大長度
    min_length=None,             最小長度
    strip=True                   是否移除用戶輸入空白
 
IntegerField(Field)
    max_value=None,              最大值
    min_value=None,              最小值
 
FloatField(IntegerField)
    ...
 
DecimalField(IntegerField)
    max_value=None,              最大值
    min_value=None,              最小值
    max_digits=None,             總長度
    decimal_places=None,         小數位長度
 
BaseTemporalField(Field)
    input_formats=None          時間格式化   
 
DateField(BaseTemporalField)    格式:2015-09-01
TimeField(BaseTemporalField)    格式:11:12
DateTimeField(BaseTemporalField)格式:2015-09-01 11:12
 
DurationField(Field)            時間間隔:%d %H:%M:%S.%f
    ...
 
RegexField(CharField)
    regex,                      自定制正則表達式
    max_length=None,            最大長度
    min_length=None,            最小長度
    error_message=None,         忽略,錯誤信息使用 error_messages={'invalid': '...'}
 
EmailField(CharField)      
    ...
 
FileField(Field)
    allow_empty_file=False     是否允許空文件
 
ImageField(FileField)      
    ...
    注:需要PIL模塊,pip3 install Pillow
    以上兩個字典使用時,需要注意兩點:
        - form表單中 enctype="multipart/form-data"
        - view函數中 obj = MyForm(request.POST, request.FILES)
 
URLField(Field)
    ...
 
 
BooleanField(Field)  
    ...
 
NullBooleanField(BooleanField)
    ...
 
ChoiceField(Field)
    ...
    choices=(),                選項,如:choices = ((0,'上海'),(1,'北京'),)
    required=True,             是否必填
    widget=None,               插件,默認select插件
    label=None,                Label內容
    initial=None,              初始值
    help_text='',              幫助提示
 
 
ModelChoiceField(ChoiceField)
    ...                        django.forms.models.ModelChoiceField
    queryset,                  # 查詢數據庫中的數據
    empty_label="---------",   # 默認空顯示內容
    to_field_name=None,        # HTML中value的值對應的字段
    limit_choices_to=None      # ModelForm中對queryset二次篩選
     
ModelMultipleChoiceField(ModelChoiceField)
    ...                        django.forms.models.ModelMultipleChoiceField
 
 
     
TypedChoiceField(ChoiceField)
    coerce = lambda val: val   對選中的值進行一次轉換
    empty_value= ''            空值的默認值
 
MultipleChoiceField(ChoiceField)
    ...
 
TypedMultipleChoiceField(MultipleChoiceField)
    coerce = lambda val: val   對選中的每一個值進行一次轉換
    empty_value= ''            空值的默認值
 
ComboField(Field)
    fields=()                  使用多個驗證,如下:即驗證最大長度20,又驗證郵箱格式
                               fields.ComboField(fields=[fields.CharField(max_length=20), fields.EmailField(),])
 
MultiValueField(Field)
    PS: 抽象類,子類中可以實現聚合多個字典去匹配一個值,要配合MultiWidget使用
 
SplitDateTimeField(MultiValueField)
    input_date_formats=None,   格式列表:['%Y--%m--%d', '%m%d/%Y', '%m/%d/%y']
    input_time_formats=None    格式列表:['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
 
FilePathField(ChoiceField)     文件選項,目錄下文件顯示在頁面中
    path,                      文件夾路徑
    match=None,                正則匹配
    recursive=False,           遞歸下面的文件夾
    allow_files=True,          允許文件
    allow_folders=False,       允許文件夾
    required=True,
    widget=None,
    label=None,
    initial=None,
    help_text=''
 
GenericIPAddressField
    protocol='both',           both,ipv4,ipv6支持的IP格式
    unpack_ipv4=False          解析ipv4地址,如果是::ffff:192.0.2.1時候,可解析為192.0.2.1, PS:protocol必須為both才能啟用
 
SlugField(CharField)           數字,字母,下划線,減號(連字符)
    ...
 
UUIDField(CharField)           uuid類型
    ...
View Code

你可以在里面選擇屬性的類型以及約束。

 

Django內置插件

TextInput(Input)
NumberInput(TextInput)
EmailInput(TextInput)
URLInput(TextInput)
PasswordInput(TextInput)
HiddenInput(TextInput)
Textarea(Widget)
DateInput(DateTimeBaseInput)
DateTimeInput(DateTimeBaseInput)
TimeInput(DateTimeBaseInput)
CheckboxInput
Select
NullBooleanSelect
SelectMultiple
RadioSelect
CheckboxSelectMultiple
FileInput
ClearableFileInput
MultipleHiddenInput
SplitDateTimeWidget
SplitHiddenDateTimeWidget
SelectDateWidget
View Code

在witgits中選擇使用

 

常用插件選擇

# 單radio,值為字符串
# user = fields.CharField(
#     initial=2,
#     widget=widgets.RadioSelect(choices=((1,'上海'),(2,'北京'),))
# )
 
# 單radio,值為字符串
# user = fields.ChoiceField(
#     choices=((1, '上海'), (2, '北京'),),
#     initial=2,
#     widget=widgets.RadioSelect
# )
 
# 單select,值為字符串
# user = fields.CharField(
#     initial=2,
#     widget=widgets.Select(choices=((1,'上海'),(2,'北京'),))
# )
 
# 單select,值為字符串
# user = fields.ChoiceField(
#     choices=((1, '上海'), (2, '北京'),),
#     initial=2,
#     widget=widgets.Select
# )
 
# 多選select,值為列表
# user = fields.MultipleChoiceField(
#     choices=((1,'上海'),(2,'北京'),),
#     initial=[1,],
#     widget=widgets.SelectMultiple
# )
 
 
# 單checkbox
# user = fields.CharField(
#     widget=widgets.CheckboxInput()
# )
 
 
# 多選checkbox,值為列表
# user = fields.MultipleChoiceField(
#     initial=[2, ],
#     choices=((1, '上海'), (2, '北京'),),
#     widget=widgets.CheckboxSelectMultiple
# )
View Code

 

周末作業:

在圖書管理系統里,增加一個注冊頁面

要求:

1.基於forms組件做校驗
  1.1 用戶名不能小於4位,不能是純數字,用戶名不能重復
  1.2 密碼不能小於6位,不能是純數字
  1.3 兩次密碼必須一致

2.表單由forms組件渲染

3.顯示錯誤信息

 

進階功能
使用ajax+forms組件完成注冊功能

ajax接收error信息,修改dom,來顯示錯誤信息!

 

答案

使用form表單實現

作業提到的3點要求,在將全局鈎子的時候,已經演示出來了。

那么只要合格之后,在視圖函數中插入一條記錄到用戶表中,就可以實現功能了!

下面介紹在上面演示的項目基礎上,實現這些功能

修改models.py,增加一個用戶表

class User(models.Model):
    name = models.CharField(max_length=32)
    pwd = models.CharField(max_length=32)
    last_time = models.DateTimeField()
View Code

修改settings.py,注冊app。最后一行添加應用名

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app01',
]
View Code

使用下面2個命令生成表

python manage.py makemigrations
python manage.py migrate

手動增加一條記錄

 

修改form.py,代碼如下:

from django import forms  # 必須導入模塊
from django.forms import widgets
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
from app01.models import User  # 導入user表

class UserForm(forms.Form):  # 必須繼承Form
    # 定義變量,專門給text類型的輸入框添加class
    wid = widgets.TextInput(attrs={"class": "form-control"})
    # 定義字典,錯誤信息顯示中文
    # 限制數據為字符串,最小長度4,最大長度12
    error_hints = {"required": "該字段不能為空", "invalid": "格式錯誤!"}
    name = forms.CharField(max_length=12, label="姓名", widget=wid, error_messages=error_hints)
    # 密碼字段
    pwd = forms.CharField(label="密碼", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                          error_messages=error_hints)

    r_pwd = forms.CharField(label="確認密碼", widget=widgets.PasswordInput(attrs={"class": "form-control"}),
                            error_messages=error_hints)

    def clean_name(self):  # 校驗name值
        val = self.cleaned_data.get("name")  # 獲取輸入的用戶名
        if len(val) >= 4:  # 判斷用戶名長度
            if val.isdigit() is False:  # 判斷用戶名不是純數字
                if not User.objects.filter(name=val).exists():  # 判斷用戶名是否存在
                    return val  # 返回正確的值
                else:
                    raise ValidationError("用戶名已存在")
            else:
                raise ValidationError("用戶名不能為純數字")
        else:
            raise ValidationError("用戶名長度不能小於4位")

    def clean_pwd(self):  # 校驗pwd值
        val = self.cleaned_data.get("pwd")  # 獲取輸入的密碼
        if len(val) >= 6:  # 判斷密碼長度
            if val.isdigit() is False:  # 判斷密碼不是純數字
                return val  # 返回正確的值
            else:
                raise ValidationError("密碼不能為純數字")
        else:
            raise ValidationError("密碼長度不能小於6位")


    def clean(self):  # 全局鈎子
        pwd = self.cleaned_data.get("pwd")
        r_pwd = self.cleaned_data.get("r_pwd")
        if pwd and r_pwd and pwd != r_pwd:  # 判斷2次密碼不為空,並且2次密碼不相等
            raise ValidationError("兩次密碼不一致")
        else:
            return self.cleaned_data  # 這句是固定寫法,不能變動
View Code

修改views.py,代碼如下:

from django.shortcuts import render, HttpResponse,redirect
from app01.form import UserForm  # 導入UserForm類
from app01.models import User
import datetime

# Create your views here.
def index(request):
    return render(request, "index.html")

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
        print(request.POST)
        if form.is_valid():  # 驗證數據
            print("###success###")
            print(form.cleaned_data)  # 所有干凈的字段以及對應的值
            # ErrorDict : {"校驗錯誤的字段":["錯誤信息",]}
            print(form.errors)
            print(type(form.errors))  # 打印

            name = request.POST.get("name")
            pwd = request.POST.get("pwd")
            last_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            ret = User.objects.create(name=name,pwd=pwd,last_time=last_time)
            if ret:
                # return HttpResponse("添加成功")
                return redirect('/index/')

        else:
            print("###fail###")
            # print(form.cleaned_data)
            print(form.errors)
            # # 獲取email錯誤信息,返回一個錯誤列表,可以切片
            # print(form.errors.get("email"))
            # # 獲取第一個錯誤信息
            # print(form.errors.get("email")[0])
            g_error = form.errors.get("__all__")  # 接收全局鈎子錯誤信息
            if g_error:  # 判斷有錯誤信息的情況下
                g_error = g_error[0]  # 取第一個錯誤信息

            # 將form和g_error變量傳給adduser.html
            return render(request, "adduser.html", {"form": form, "g_error": g_error})

    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form
    return render(request, "adduser.html",{"form": form})
View Code

修改adduser.html,代碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
        .error {
            color: red;
        }
    </style>
</head>
<body>

<div class="container">
    <div class="row">
        <div class="col-md-6 col-md-offset-2">
        <h3>添加用戶</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }} <span class="error pull-right">{{ field.errors.0 }}</span>
                         {% if field.label == "確認密碼" %}
                         <span class="error pull-right">{{ g_error|default_if_none:"" }}</span>
                         {% endif %}
                    </div>
                {% endfor %}
                <br/>
                <input type="submit" class="btn btn-success btn-sm">
            </form>
        </div>
    </div>
</div>

</body>
</html>
View Code

訪問頁面添加用戶界面

 查看用戶表記錄,發現多了一條

 

ajax+forms組件實現

在上面的代碼上,增加ajax功能

修改urls.py,增加路徑add_ajajx

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
    path('adduser/', views.adduser),
    path('add_ajax/', views.add_ajax),
]
View Code

修改views.py,增加視圖函數add_ajajx

from django.shortcuts import render, HttpResponse,redirect
from app01.form import UserForm  # 導入UserForm類
from app01.models import User
import datetime
import json

# Create your views here.
def index(request):
    return render(request, "index.html")

def adduser(request):
    if request.method == "POST":
        # 將post數據傳給UserForm
        form = UserForm(request.POST)
    else:  # 默認是get請求(地址欄輸入訪問時)
        form = UserForm()  # 沒有表單數據的form

    return render(request, "adduser.html",{"form": form})


def add_ajax(request):
    if request.method == "POST": # 判斷POST請求
        print(request.POST)
        form = UserForm(request.POST)  #
        result = {"state": False,"name":"","pwd":"","r_pwd":""}
        if form.is_valid():
            name = request.POST.get("name")
            pwd = request.POST.get("pwd")
            last_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            ret = User.objects.create(name=name, pwd=pwd, last_time=last_time)
            if ret:
                result["state"] = True
            return HttpResponse(json.dumps(result,ensure_ascii=False))
        else:
            print(form.errors)
            if form.errors:  # 判斷有錯誤信息的情況下
                if form.errors.get("name"):
                    result["name"] = form.errors.get("name")[0]
                if form.errors.get("pwd"):
                    result["pwd"] = form.errors.get("pwd")[0]
                if form.errors.get("r_pwd"):
                    result["r_pwd"] = form.errors.get("r_pwd")[0]

                g_error = form.errors.get("__all__")  # 接收全局鈎子錯誤信息
                if g_error:  # 判斷有錯誤信息的情況下
                    g_error = g_error[0]  # 取第一個錯誤信息
                    result["r_pwd"] = g_error

                return HttpResponse(json.dumps(result,ensure_ascii=False))
View Code

修改adduser.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
        .error {
            color: red;
        }

        .col-center-block {

            position: absolute;
            top: 50%;
            left: 18%;
            -webkit-transform: translateY(-50%);
            -moz-transform: translateY(-50%);
            -ms-transform: translateY(-50%);
            -o-transform: translateY(-50%);
            transform: translateY(-50%);
        }


    </style>
</head>
<body>
{% csrf_token %}
<div class="container col-center-block">
    <div class="row ">
        <div class="col-md-6 col-md-offset-2">
            <h3>添加用戶</h3><br/>
            <form action="" method="post" novalidate>
                {% csrf_token %}
                {% for field in form %}
                    <div class="form-group">
                        <label for="">{{ field.label }}</label>
                        {{ field }} <span class="error pull-right">{{ field.errors.0 }}</span>
                        {% if field.label == "確認密碼" %}
                            <span class="error pull-right">{{ g_error|default_if_none:"" }}</span>
                        {% endif %}
                    </div>
                {% endfor %}
                <br/>
                <input type="button" class="btn btn-success btn-sm" id="sub" value="注冊">
            </form>
        </div>
    </div>
</div>
<script src="/static/js/jquery.min.js"></script>
{#sweetalert插件#}
<script src="http://mishengqiang.com/sweetalert/js/sweetalert-dev.js"></script>
<link rel="stylesheet" href="http://mishengqiang.com/sweetalert/css/sweetalert.css">
<script>
    $(function () {
        $("#id_name").blur(function () {
            var csrf = $("[name=csrfmiddlewaretoken]").val();  //csrf
            var name = $("#id_name").val();  //用戶名
            if (name.length != 0) {
                $.ajax({
                    url: "/zhuce_ajax/",
                    type: "post",
                    data: {
                        'name': name,
                        csrfmiddlewaretoken: csrf,
                    },
                    success: function (data) {
                        var data = JSON.parse(data);  //反序列化數據
                        console.log(data);
                        if (data.name) { //判斷用戶是否有錯誤信息
                            $("#id_name").next().text(data.name)  //修改span標簽的文本
                        } else {
                            $("#id_name").next().text("")  //驗證通過后,清空文件
                        }
                    }

                });
            }

        });
        $("#id_pwd").blur(function () {
            var csrf = $("[name=csrfmiddlewaretoken]").val();  //csrf
            var pwd = $("#id_pwd").val();  //密碼
            if (pwd.length != 0) {
                $.ajax({
                    url: "/zhuce_ajax/",
                    type: "post",
                    data: {
                        'name': name,
                        'pwd': pwd,
                        csrfmiddlewaretoken: csrf,
                    },
                    success: function (data) {
                        var data = JSON.parse(data);  //反序列化數據
                        console.log(data);
                        if (data.pwd) { //判斷密碼是否有錯誤信息
                            $("#id_pwd").next().text(data.pwd)  //修改span標簽的文本
                        } else {
                            $("#id_pwd").next().text("")  //驗證通過后,清空文件
                        }
                    }

                });
            }

        });
        $("#id_r_pwd").blur(function () {
            var csrf = $("[name=csrfmiddlewaretoken]").val();  //csrf
            var pwd = $("#id_pwd").val();  //密碼
            var r_pwd = $("#id_r_pwd").val();  //確認密碼
            if (r_pwd.length != 0) {
                $.ajax({
                    url: "/zhuce_ajax/",
                    type: "post",
                    data: {
                        'name': name,
                        'pwd': pwd,
                        'r_pwd': r_pwd,
                        csrfmiddlewaretoken: csrf,
                    },
                    success: function (data) {
                        var data = JSON.parse(data);  //反序列化數據
                        console.log(data);
                        if (data.r_pwd) { //判斷確認密碼是否有錯誤信息
                            $("#id_r_pwd").next().text(data.r_pwd)  //修改span標簽的文本
                        } else {
                            $("#id_r_pwd").next().text("")  //驗證通過后,清空文件
                        }
                    }

                });
            }

        });

        $("#sub").click(function () {
            var csrf = $("[name=csrfmiddlewaretoken]").val();  //csrf
            var name = $("#id_name").val();  //用戶名
            var pwd = $("#id_pwd").val();  //密碼
            var r_pwd = $("#id_r_pwd").val();  //確認密碼
            $.ajax({
                url: "/zhuce_ajax/",  //請求的url
                type: "post", //默認get
                data: {
                    name: name,
                    pwd: pwd,
                    r_pwd: r_pwd,
                    csrfmiddlewaretoken: csrf
                },
                success: function (data) {  //data接收響應體,必須要有
                    var data = JSON.parse(data);  //反序列化數據
                    {#console.log(data.state);#}
                    {#console.log(data);  //打印響應體#}
                    if (data.state) {
                        console.log("注冊成功");
                        swal({
                            title: '注冊成功',
                            type: 'success',  //展示成功的圖片
                            timer: 500,  //延時500毫秒
                            showConfirmButton: false  //關閉確認框
                        }, function () {
                            window.location.href = "/index/";  //跳轉首頁
                        });
                    }
                    else {
                        console.log("注冊失敗");
                        if (data.name) { //判斷用戶是否有錯誤信息
                            $("#id_name").next().text(data.name)  //修改span標簽的文本
                        } else {
                            $("#id_name").next().text("")  //驗證通過后,清空文件
                        }
                        if (data.pwd) {
                            $("#id_pwd").next().text(data.pwd)
                        } else {
                            $("#id_pwd").next().text("")
                        }
                        if (data.r_pwd) {
                            $("#id_r_pwd").next().text(data.r_pwd)
                        } else {
                            $("#id_r_pwd").next().text("")
                        }

                    }
                }
            })


        })

    })
</script>

</body>
</html>
View Code

 

訪問頁面:

測試效果如下:

 

 查看用戶表,發現多了一條記錄

 

完整代碼,請參考github

https://github.com/py3study/bms_multi


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM