ModelForm
一、Form元件初識
Django的Form主要具有一下幾大功能:
生成HTML标簽
驗證使用者資料(顯示錯誤資訊)
HTML Form送出保留上次送出資料
初始化頁面顯示内容
models.py檔案内容如下:
from django.db import models
# Create your models here.
class UserType(models.Model):
title = models.CharField(max_length=32)
class UserInfo(models.Model):
username = models.CharField(max_length=32)
password = models.CharField(max_length=64)
email = models.EmailField()
usertype = models.ForeignKey(UserType,null=True,blank=True)
1、在app下建立一個forms.py檔案
# 先導入以下三個子產品
from django.forms import Form
from django.forms import fields
from django.forms import widgets
from .models import *
class UserForm(Form):
username = fields.CharField(
required=True, #規定該字段不能為空
error_messages={'required':'使用者名不能為空'} # 自定義錯誤資訊,'required'是定義該字段為空時的錯誤資訊
)
password = fields.CharField(
required=True,
error_messages={'required':'密碼不能為空'}
)
email = fields.EmailField(
required=True,
error_messages={'required':'郵箱不能為空','invalid':'郵箱格式錯誤'}
) # 'invalid'是定義該字段值無效時的錯誤資訊
# ip = fields.GenericIPAddressField(
# required=True,
# error_messages={'required':'IP不能為空','invalid':'IP格式錯誤'}
# )
usertype_id = fields.ChoiceField(choices=UserType.objects.values_list('id','title'))
# choices=清單,也就是多選的,對應html的select标簽
注意:
建立的表單類必須繼承Form類;
每個字段預設就是required=True;
字段沒有自定義error_messages的值時,預設值是英文的錯誤提示;自定義的error_messages值中可以有多個錯誤資訊提示,"required"是在該字段為空是提示,"invalid"是在該字段有值,但是值無效時提示,如郵箱或IP位址不對;
如果想通過UserInfo.objects.create(**form.cleaned_data)這種方式快速插入資料到資料庫,則在定義Form表單字段時必須與models中表字段一緻,特别注意:由于Django會models中的models.ForeignKey()字段名自動加_id,所在Form表單字段在定義時,需要手動給字段名稱添加_id,友善插入資料,如上:usertype_id
fields.GenericIPAddressField()這個字段類型是專門給IP輸入用的,其中有一個參數protocol預設等于both,表示支援IPV4和IPV6兩種協定,如果隻想讓其支援IPV4協定,隻需要protocol='ipv4'即可。
2、在views.py中導入forms.py
from app01.forms import *
def adduser(request):
form = UserForm() # 執行個體化得到一個沒有值的form表單對象
if request.method == 'POST':
form = UserForm(data=request.POST)
# 将前端擷取的資料傳入到forms.UserForm類,執行個體化得到一個包含字段值的表單對象form
if form.is_valid(): # is_valid()得到一個布爾值,判斷傳入的字段值是否全部有效
print(form.cleaned_data) #form.cleaned_data是字典類型,得到全部字段的有效值,即幹淨的資料
UserInfo.objects.create(**form.cleaned_data) # 直接插入資料庫,form字段必須與models中定義的一緻
return redirect('/index/')
else:
print(form.errors) # 得到錯誤資訊,并生成html标簽,哪個字段值有誤,就會對應産生哪個字段的錯誤資訊
return render(request,'adduser.html',locals())
注意:
form.is_valid()方法得到一個布爾值,判斷從前端得到表單資料是全部有效;
form.cleaned_data得到全部驗證有效的資料,是一個字典類型,如:{'username': 'admin', 'ip': '1.1.1.1', 'usertype_id': '3', 'email': '[email protected]', 'password': 'admin'},字典中的key對應UserForm類中的每個字段名;
form.errors驗證有誤的錯誤提示資訊,與錯誤字段一一對應,如:form.errors.user可以得到user字段的全部錯誤資訊,是一個清單類型,form.errors.user[0]得到user字段的第一個錯誤資訊。form.errors.email[0]得到email字段的第一個錯誤資訊;
3、在templates模版檔案中渲染
添加使用者
{% csrf_token %}
使用者名:{{ form.username }} {{ form.errors.username.0 }}
密碼:{{ form.password }} {{ form.errors.password.0 }}
郵箱:{{ form.email }} {{ form.errors.email.0 }}
使用者類型:{{ form.usertype_id }} {{ form.errors.usertype_id.0 }}
4、浏覽器中顯示
GET方式請求頁面
沒有填寫資料時送出,form.errors得到的值是
- email
- 郵箱不能為空
- pwd
- 密碼不能為空
- ip
- IP不能為空
- user
- 使用者名不能為空
,頁面如下圖:
沒有填寫資料時送出
資料格式填寫出錯時,form.errors得到的值是
- email
- 郵箱格式錯誤
- ip
- IP格式錯誤
,頁面如下圖:
郵箱或IP格式錯誤時送出
資料格式填寫正确時,form.cleaned_data得到的值是字典{'email': '[email protected]', 'pwd': 'admin', 'ip': '1.1.1.1', 'usertype_id': '1', 'user': 'admin'}
資料格式全部填寫正确時
5. 在輸入框中顯示預設值
需要在執行個體化form表單對象時加入initial參數,如下:
form = UserForm(initial={'username':obj.username,
'password':obj.password,
'email':obj.email,
'usertype_id':obj.usertype.id})
如下示例,在編輯使用者資訊時,需要輸入框中有預設值:
def edituser(request,pk):
# 參數pk是urls傳入的id
obj = UserInfo.objects.get(id=pk)
form = UserForm(initial={'username':obj.username,
'password':obj.password,
'email':obj.email,
'usertype_id':obj.usertype.id})
if request.method == 'POST':
form = UserForm(data=request.POST)
print(request.POST)
if form.is_valid():
UserInfo.objects.filter(id=pk).update(**form.cleaned_data)
return redirect('/index/')
return render(request, 'edituser.html', locals())
輸入框中顯示預設值
6. 解決Form元件中下拉框資料無法實時從資料庫擷取的問題
是因為類中靜态字段隻在代碼枞上到下加載的時候執行一次,上述的代碼中的usertype_id的值是從資料庫中取的,它隻是在加載UserForm類時執行了一次,每次執行個體化并不會執行。是以,如果想讓usertype_id這個字段實時從資料庫中取得資料(或者說,想讓usertype_id在每次執行個體化時都從資料庫取一次資料),就必須将擷取該字段的值寫在__init__()構造方法中,如下:
from django.forms import Form
from django.forms import fields
from django.forms import widgets
from .models import *
class UserForm(Form):
username = fields.CharField(
required=True,
error_messages={'required':'使用者名不能為空'}
)
password = fields.CharField(
required=True,
error_messages={'required':'密碼不能為空'}
)
email = fields.EmailField(
required=True,
error_messages={'required':'郵箱不能為空','invalid':'郵箱格式錯誤'}
)
# ip = fields.GenericIPAddressField(
# required=True,
# error_messages={'required':'IP不能為空','invalid':'IP格式錯誤'}
# )
usertype_id = fields.ChoiceField(choices=[])
def __init__(self,*args,**kwargs):
super(UserForm, self).__init__(*args,**kwargs)
self.fields['usertype_id'].choices=UserType.objects.values_list('id','title')
注意:
self.fields包括了所有的字段,django内部會将所有字段都深拷貝一份,并裝在self.fields中。是以通過self.fields['usertype_id'].choices=UserType.objects.values_list('id','title')就可以在每次執行個體化時,從資料庫取一次值賦給usertype_id
7. 多對多在Form元件中的操作
models.py中新添加一個技能表Technique,與UserInfo是多對多關系
from django.db import models
# Create your models here.
class UserType(models.Model):
title = models.CharField(max_length=32)
class Technique(models.Model):
title = models.CharField(max_length=32)
class UserInfo(models.Model):
username = models.CharField(max_length=32)
password = models.CharField(max_length=64)
email = models.EmailField()
usertype = models.ForeignKey(UserType,null=True,blank=True)
tn = models.ManyToManyField(Technique)
forms.py中的UserForm增加一個字段tn_id,對應models.UserInfo中的tn多對多字段
from django.forms import Form
from django.forms import fields
from django.forms import widgets
from .models import *
class UserForm(Form):
username = fields.CharField(
required=True,
error_messages={'required':'使用者名不能為空'}
)
password = fields.CharField(
required=True,
error_messages={'required':'密碼不能為空'}
)
email = fields.EmailField(
required=True,
error_messages={'required':'郵箱不能為空','invalid':'郵箱格式錯誤'}
)
usertype_id = fields.ChoiceField(choices=[])
tn_id = fields.MultipleChoiceField(choices=[]) #增加字段,是技能ID
def __init__(self,*args,**kwargs):
super(UserForm, self).__init__(*args,**kwargs)
self.fields['usertype_id'].choices=UserType.objects.values_list('id','title')
self.fields['tn_id'].choices=Technique.objects.values_list('id','title')
#實時從資料庫擷取資料
模版檔案中添加:
技能:{{ form.tn_id }} {{ form.errors.tn_id.0 }}
views.py,添加和編輯
def adduser(request):
form = UserForm() # 執行個體化得到一個form表單對象
if request.method == 'POST':
form = UserForm(data=request.POST)
# 将前端擷取的資料傳入到forms.UserForm類,執行個體化得到一個包含字段值的表單對象form
print(request.POST)
if form.is_valid(): # is_valid()得到一個布爾值,即判斷傳入的字段值是否全部有效
print(form.cleaned_data) #form.cleaned_data是字典類型,得到全部字段的有效值,即幹淨的資料
tn_id_list = form.cleaned_data.pop('tn_id') # 因為多對多字段不能直接添加,是以需要從幹淨資料中彈出,再通過obj.tn.add()方式添加
obj = UserInfo.objects.create(**form.cleaned_data) # 直接插入資料庫,form字段必須與models中定義的一緻
obj.tn.add(*tn_id_list) #添加多對多字段,add()添加一個清單時,必須加*星号
return redirect('/index/')
else:
print(form.errors) # 得到錯誤資訊,并生成html标簽,哪個字段值有誤,就會對應産生哪個字段的錯誤資訊
return render(request,'adduser.html',locals())
def edituser(request,pk):
# 參數pk是urls傳入的id
obj = UserInfo.objects.get(id=pk)
tn_id_tuple_list = obj.tn.values_list('id') # 得到
tn_id_list = list(zip(*tn_id_tuple_list))[0] if tn_id_tuple_list else [] #三元運算
form = UserForm(initial={'username':obj.username,
'password':obj.password,
'email':obj.email,
'usertype_id':obj.usertype.id,
'tn_id':tn_id_list})
if request.method == 'POST':
form = UserForm(data=request.POST)
if form.is_valid():
tn_id_list = form.cleaned_data.pop('tn_id') #彈出多對多字段
query_set = UserInfo.objects.filter(id=pk)
query_set.update(**form.cleaned_data)
obj = query_set.first()
obj.tn.set(tn_id_list) # 直接覆寫原有的技能ID值,set()方法傳入的清單前面不能加*星号
return redirect('/index/')
return render(request, 'edituser.html', locals())
注意:
多對多字段tn_id無法直接通過UserInfo.objects.create(**form.cleaned_data)這樣的方式直接添加至資料庫,隻能先将其從cleaned_data中用.pop('tn_id')方法彈出并指派給tn_id_list,再通過obj.tn.add(*tn_id_list)方式添加至資料庫中。
list(zip(*tn_id_tuple_list))得到的結果是類似這樣[(5, 6)],zip()函數可以将[(11,22,33),(44,55,66)]這樣的資料變換為[(11,44,),(22,55,),(33,66,)]這樣。
8. widgets插件,給Form生成的标簽添加class等屬性
通過widgets插件實作,必須先導入from django.forms import widgets
forms.py内容如下:
from django.forms import Form
from django.forms import fields
from django.forms import widgets
from .models import *
class UserForm(Form):
username = fields.CharField(
required=True,
error_messages={'required':'使用者名不能為空'},
widget = widgets.TextInput(attrs={'class': 'form-control'})
)
password = fields.CharField(
required=True,
error_messages={'required':'密碼不能為空'},
widget=widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = fields.EmailField(
required=True,
error_messages={'required':'郵箱不能為空','invalid':'郵箱格式錯誤'},
widget=widgets.EmailInput(attrs={'class': 'form-control'})
)
usertype_id = fields.ChoiceField(
choices=[],
widget=widgets.Select(attrs={'class': 'form-control'})
)
tn_id = fields.MultipleChoiceField(
choices=[],
widget=widgets.SelectMultiple(attrs={'class': 'form-control'})
)
def __init__(self,*args,**kwargs):
super(UserForm, self).__init__(*args,**kwargs)
self.fields['usertype_id'].choices=UserType.objects.values_list('id','title')
self.fields['tn_id'].choices=Technique.objects.values_list('id','title')
注意:Form元件中,密碼字段想要在輸入時密文顯示,就必須利用插件widget=widgets.PasswordInput()
9. ajax配合Form元件,實作無重新整理驗證
以下代碼基于第8節中的forms.py。
views.py相關代碼:
import json
def reg(request):
form = UserForm()
if request.method == 'POST':
flag = {'status': True, 'msg': None}
form = UserForm(data=request.POST)
if form.is_valid():
tn_id_list = form.cleaned_data.pop('tn_id')
obj = UserInfo.objects.create(**form.cleaned_data)
obj.tn.add(*tn_id_list)
else:
flag['status'] = False
flag['msg'] = form.errors #将form驗證的錯誤資訊傳遞到給前端JS
return HttpResponse(json.dumps(flag))
return render(request,'reg.html',locals())
html和JS相關代碼:
{% csrf_token %}
使用者名: {{ form.username }}
密碼: {{ form.password }}
郵箱: {{ form.email }}
使用者類型: {{ form.usertype_id }}
技能: {{ form.tn_id }}
送出
$('.submit').click(function () {
$('#f1 .error').remove(); //避免重複出現錯誤提示
$.ajax('/reg/',{
type:'post',
data:$('#f1').serialize(),
dataType:'json',
success:function (retn_data) {
if (retn_data.status){
location.href='/index/'
} else {
$.each(retn_data.msg,function (key,value) {
//循環服務端傳過來的驗證後的錯誤資訊(字典形式)
//key是字典的鍵,也就是每個字段名稱;value是每個字段的錯誤資訊
var $span=$('');
$span.addClass('error');
$span.html(value[0]);
$('#f1 input[name="'+key+'"]').after($span)
//通過字元串拼接,得到$('#f1 input[name="username"]')
})
}
}
})
})
注意:通過ajax發送資料,當送出的字段資料為空或者格式出錯時,錯誤資訊隻能通過JS将服務端回傳的錯誤資訊加載至指定位置,而不能直接在模闆檔案中使用{{ form.errors.username.0 }}這種方式顯示錯誤資訊。
10. 自定義驗證規則
方式一:
在字段定義時,加入validators參數,其值為一個清單,清單中可包含多個正則驗證器,該字段值會依次與這些正則驗證器進行比對。
from django.forms import Form
from django.forms import widgets
from django.forms import fields
from django.core.validators import RegexValidator
class MyForm(Form):
user = fields.CharField(
validators=[RegexValidator(r'^[0-9]+$', '請輸入數字'), RegexValidator(r'^159[0-9]+$', '數字必須以159開頭')],
)
方式二:
自定義一個驗證函數,并在定義字段時,将這個驗證函數放在validators等于的清單中,假如驗證函數名為mobile_validate,就這樣寫:validators=[mobile_validate,]
import re
from django.forms import Form
from django.forms import widgets
from django.forms import fields
from django.core.exceptions import ValidationError
# 自定義驗證規則
def mobile_validate(value):
mobile_re = re.compile(r'^(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$')
if not mobile_re.match(value):
raise ValidationError('手機号碼格式錯誤')
class PublishForm(Form):
title = fields.CharField(max_length=20,
min_length=5,
error_messages={'required': '标題不能為空',
'min_length': '标題最少為5個字元',
'max_length': '标題最多為20個字元'},
widget=widgets.TextInput(attrs={'class': "form-control",
'placeholder': '标題5-20個字元'}))
# 使用自定義驗證規則
phone = fields.CharField(validators=[mobile_validate, ],
error_messages={'required': '手機不能為空'},
widget=widgets.TextInput(attrs={'class': "form-control",
'placeholder': u'手機号碼'}))
email = fields.EmailField(required=False,
error_messages={'required': u'郵箱不能為空','invalid': u'郵箱格式錯誤'},
widget=widgets.TextInput(attrs={'class': "form-control", 'placeholder': u'郵箱'}))
方式三:
在Form類中自定義一個方法,方法名稱有固定格式:clean_字段名稱。
Django内部會自動執行該方法對字段進行驗證,這個方法必須将驗證正确的值傳回。驗證錯誤需要抛出ValidationError錯誤。
from django import forms
from django.forms import fields
from django.forms import widgets
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
class FInfo(forms.Form):
username = fields.CharField(max_length=5,
validators=[RegexValidator(r'^[0-9]+$', 'Enter a valid extension.', 'invalid')], )
email = fields.EmailField()
def clean_username(self):
"""
Form中字段中定義的格式比對完之後,執行此方法進行驗證
:return:
"""
value = self.cleaned_data['username']
if "666" in value:
raise ValidationError('666已經被玩爛了...', 'invalid')
return value
對所有字段做整體驗證
需求:在新增賬號時,填寫密碼後,還要再次填寫确認密碼,然後比較兩次密碼是否一緻,一緻後才能正常注冊。
對所有字段做整體驗證,django預留了clean鈎子方法,可以自定制處理邏輯代碼,但是clean方法必須傳回self.cleaned_data,如下列示例代碼
from django.forms import Form
from django.forms import fields
from django.forms import widgets
from django.core.exceptions import ValidationError
class RegForm(Form):
username = fields.CharField()
pwd = fields.CharField()
pwd_confirm = fields.CharField()
phone = fields.CharField()
def clean_phone(self):
value = self.cleaned_data['phone']
mobile_regex = re.compile(r'^1[3578][0-9]{9}$')
if not mobile_regex.match(value):
raise ValidationError('手機号碼格式錯誤')
return value
def clean(self):
if self.is_valid(): # 先確定每個輸入框都不為空
pwd = self.cleaned_data['pwd']
pwd_confirm = self.cleaned_data['pwd_confirm']
if not pwd == pwd_confirm:
self.add_error('pwd',ValidationError('确認密碼輸入不一緻'))
self.add_error('pwd_confirm',ValidationError('确認密碼輸入不一緻'))
return self.cleaned_data
return self.cleaned_data
上述代碼中,如果兩次密碼不一緻,則分别給對應字段添加一個錯誤資訊。
效果如下圖:
image.png
11. 常用插件
class RegisterForm(Form):
# 文本輸入框
name = fields.CharField(
widget=widgets.TextInput(attrs={'class': 'c1'})
)
# 郵箱位址輸入框
email = fields.EmailField(
widget=widgets.EmailInput(attrs={'class':'c1'})
)
# 文本域
phone = fields.CharField(
widget=widgets.Textarea(attrs={'class':'c1'})
)
# 密文輸入框
pwd = fields.CharField(
widget=widgets.PasswordInput(attrs={'class':'c1'})
)
pwd_confirm = fields.CharField(
widget=widgets.PasswordInput(attrs={'class': 'c1'})
)
# 單選:select
# city = fields.ChoiceField(
# choices=[(0,"上海"),(1,'北京')],
# widget=widgets.Select(attrs={'class': 'c1'})
# )
# 多選:select
# city = fields.MultipleChoiceField(
# choices=[(1,"上海"),(2,'北京')],
# widget=widgets.SelectMultiple(attrs={'class': 'c1'})
# )
# 單選:checkbox
# city = fields.CharField(
# widget=widgets.CheckboxInput()
# )
# 多選:checkbox
# city = fields.MultipleChoiceField(
# choices=((1, '上海'), (2, '北京'),),
# widget=widgets.CheckboxSelectMultiple
# )
# 單選:radio
# city = fields.CharField(
# initial=2,
# widget=widgets.RadioSelect(choices=((1,'上海'),(2,'北京'),))
# )
注意:單選插件的值為字元串,多選插件的值為清單;寫預設值時,多選值對應清單,如:form = RegisterForm(initial={'city':[1,2],'name':'alex'})