標籤:python django
回顧
Form主要的作用,是做資料驗證的。並且Form的資料驗證功能是強大的。
Form還有另外一個功能,就是幫我麼產生html標籤。
上面的2個功能,其中驗證是主要的,而產生html標籤的功能有時候用的到,有時候不需要。建議使用新URL方式(一般是Form表單提交)來操作的時候使用產生html標籤的功能,因為這個功能可以幫我麼保留上一次提交的值。使用Ajax請求操作的時候,就可以不用Form來幫我們產生html標籤了,因為Ajax提交的時候頁面不會重新整理,頁面上不會丟失上一次提交的值。以上是建議,具體用的時候可以嘗試一下,感覺不按建議來也許可以,但是至少會麻煩一點。
講師的部落格地址:http://www.cnblogs.com/wupeiqi/articles/6144178.html
建立一個Form
建議在app目錄下建立一個forms.py檔案來單獨存放我們的form檔案:
from django.forms import Formfrom django.forms import fieldsfrom django.forms import widgetsclass User(Form): username = fields.CharField( required=True, # 必填,不可為空,不過True是預設參數,所以不寫也一樣 widget=widgets.PasswordInput(attrs={‘class‘: ‘c1‘}) # 在產生html標籤的時候,添加標籤的屬性 ) password = fields.CharField( max_length=12, widget=widgets.PasswordInput(attrs={‘class‘: ‘c1‘}) )
操作產生select標籤的Form
# 在 models.py 檔案了建立表結構class UserType(models.Model): name = models.CharField(max_length=16) # 這個是使用者類型,比如:普通使用者、超級使用者# 在 forms.py 檔案裡建立Formclass UserInfo(Form): name = fields.CharField() comments = fields.CharField( required=False, widget=widgets.Textarea(attrs={‘class‘: ‘c1‘}) ) type = fields.ChoiceField( # choices=[(1, "超級使用者"), (2, "普通使用者")] # 之前是去記憶體裡的選項,想在用下面的方法直接去資料庫裡取 # 列表裡每一個元素都是元組,如果選項是去資料庫擷取的話,用.values_list()方法就能直接擷取到 choices=models.UserType.objects.values_list() )# 在 views.py 裡建立視圖函數def user_info(request): obj = forms.UserInfo() return render(request, ‘user_info.html‘, {‘obj‘: obj})
然後再寫一個簡單的html介面,主要看產生的select標籤:
<body><p>{{ obj.name }}</p><p>{{ obj.comments }}</p><p>{{ obj.type }}</p></body>
直接用 choices=[(1, "超級使用者"), (2, "普通使用者")] 的話肯定是沒問題的,這是之前學習的內容。
直接去資料庫裡擷取的話,也很方法,因為拿到的資料類型就是元組元素的列表,而且也是可以產生select標籤的選項的。但是這裡有一個問題,比如要重啟我們的django,新的選項才能在頁面上生效。
把選項放記憶體裡的好處是用起來比較方便,適合那些設定之後基本不會變化的選項,你調整之後是需要重啟django的。如果選項需要可以動態調整,那麼就把它放到資料庫裡去。不過按照上面的用法,並沒有什麼用處,因為我們依然要重啟django之後才會生效。具體的用法看下面的小節。
操作動態Select資料
上面導致不重啟無法更新的原因是,我們使用了靜態欄位來定義的。在之前學習物件導向的時候,那是也叫公有屬性。這裡會使用靜態欄位來稱呼。
在程式第一次載入的時候,就執行了一次靜態欄位,通過靜態欄位擷取了資料庫裡的內容儲存到記憶體裡了。之後在請求的時候就不會再重新去資料庫裡擷取了,而是的記憶體裡去找之前擷取到的資料。所以導致無法動態更新資料庫裡最新的內容。
通過構造方法實現
前端html顯示的資料是通過視圖函數的obj傳過去的。在視圖函數裡,在擷取到靜態欄位之後,return給前端之前,重新去資料庫裡擷取一下最新的資料,覆蓋掉之前的待用資料。:
# views.py 檔案def user_info(request): obj = forms.UserInfo() # 這步是執行個體化,obj裡有所有靜態欄位的靜態內容 # 實際上,所有的靜態欄位都在obj.fields裡。下面就把其中的type欄位裡的choices的值重新去擷取一下 obj.fields[‘type‘].choices = models.UserType.objects.values_list() # 這裡是重新去資料庫裡再擷取一下最新的值 return render(request, ‘user_info.html‘, {‘obj‘: obj})
不過上面的方法也不好,因為這樣實現,要在每次使用這個欄位的時候都多寫上這麼一句。下面是修改froms,把這個值在每次執行個體化的時候都重新去資料庫裡擷取一次,具體的做法就是在放到構造方法裡執行。
上面只是為了講清楚前因後果,下面才是最終我們要使用的方法:
# forms.py 檔案class UserInfo(Form): name = fields.CharField() comments = fields.CharField( required=False, widget=widgets.Textarea(attrs={‘class‘: ‘c1‘}) ) type = fields.ChoiceField( # choices=[(1, "超級使用者"), (2, "普通使用者")] # 列表裡每一個元素都是元組,如果選項是去資料庫擷取的話,用.values_list()方法就能直接擷取到 # choices=models.UserType.objects.values_list() choices=[] # 這個值具體是多少,在這裡不重要了,每次執行個體化的時候都會在構造方法裡重新賦值 ) def __init__(self, *args, **kwargs): super(UserInfo, self).__init__(*args, **kwargs) # 僅有這一行,就是完全繼承父類的構造方法 # 下面我們再加上我們在構造方法中要額外執行的代碼 self.fields[‘type‘].choices = models.UserType.objects.values_list() # 從之前的視圖函數,搬到這裡
如果是使用CharField的widget外掛程式來定義的select,那麼關鍵的2行代碼如下:
# 使用CharField來定義selecttype2 = fields.CharField(widget=widgets.Select(choices=[]))# 建構函式裡賦值的方法self.fields[‘type2‘].widget.choices = models.UserType.objects.values_list()
通過 ModelChoiceField 實現
另外還有一個django內建提供的方法,不過不能單獨使用,還要去model裡構造一個特殊方法,實現顯示欄位的值而不是對象本身:
from django.forms.models import ModelChoiceField type3 = ModelChoiceField( queryset=models.UserType.objects.all() )
這樣也能動態更新資料庫裡的值,但是現在頁面上顯示的是對象不是資料庫庫表裡的值。這裡還得去models裡構造一個__str__方法,定義列印對象的時候具體列印處理的內容:
class UserType(models.Model): name = models.CharField(max_length=16) def __str__(self): return self.name
上面這種情況,在admin的操作裡可能也會遇到。總之如果頁面上顯示出來的不是欄位的值而是對象的話,是去類構造這個特殊方法應該能解決。
這個函數還有其他的參數:
Form內建鉤子
先把之前的代碼補充完整,html裡沒有form,也沒有提交,這裡還關掉了表單前端的驗證功能(novalidate="novalidate"):
<body><form action="." novalidate="novalidate" method="post"> {% csrf_token %} <p>{{ obj.name }}</p> <p>{{ obj.comments }}</p> <p>{{ obj.type }}</p> <p>{{ obj.type3 }}</p> <p><input type="submit"></p></form></body>
再把處理函數寫完整,現在要有 get 和 post 兩個方法:
def user_info(request): if request.method == ‘GET‘: obj = forms.UserInfo({‘name‘: ‘Your Name‘}) # 這步是執行個體化,obj裡有所有靜態欄位的靜態內容 # 實際上,所有的靜態欄位都在obj.fields裡。下面就把其中的type欄位裡的choices的值重新去擷取一下 # obj.fields[‘type‘].choices = models.UserType.objects.values_list() # 這裡是重新去資料庫裡再擷取一下最新的值 return render(request, ‘user_info.html‘, {‘obj‘: obj}) elif request.method == ‘POST‘: obj = forms.UserInfo(request.POST) # 擷取POST提交的資料 res = obj.is_valid() # 這一步是進行驗證 if res: print(obj.cleaned_data) return HttpResponse(str(obj.cleaned_data)) else: print(obj.errors) return HttpResponse(str(obj.errors)) # 通過str方法後,頁面上會直接按html代碼處理
找到鉤子
首先是在 res = obj.is_valid() 這一步進行驗證的。這裡面會執行一個 self.errors 。再看看這個errors方法,裡面會執行一個 self.full_clean() 。最後來到full_clean方法,完整的代碼如下:
def full_clean(self): """ Clean all of self.data and populate self._errors and self.cleaned_data. """ self._errors = ErrorDict() if not self.is_bound: # Stop further processing. return self.cleaned_data = {} # If the form is permitted to be empty, and none of the form data has # changed from the initial data, short circuit any validation. if self.empty_permitted and not self.has_changed(): return self._clean_fields() self._clean_form() self._post_clean()
這裡看最後的3行。
重構鉤子方法
所有的鉤子方法在我們自己的Form類裡重構。
self._clean_fields() 分別對每個欄位進行驗證
裡有下面這幾行內容:
if hasattr(self, ‘clean_%s‘ % name): value = getattr(self, ‘clean_%s‘ % name)() self.cleaned_data[name] = value
判斷是否存在 clean_%s 方法,如果有就執行。對應我們現在的 UserInfo(Form) 這個類,就是可以分別自訂 clean_name clean_comments 這些方法,來自定製驗證的規則。
self._clean_form() 對整體進行驗證
裡面直接調用了一個空的方法,self.clean() 這個就是直接留給我們的鉤子。直接在類裡重構這個方法來使用。最常見的對於整體進行驗證的情境是驗證登入的使用者名稱和密碼。
self._post_clean() 附加的驗證
這個直接就是一個空方法,所以直接重構它就好了。這裡並沒有展開,應該用上面2個就夠了。這個方法是和 self._clean_fields()、self._clean_form() 一個層級的,那2個方法裡幫我麼寫好了try,然後在try裡調用的鉤子。而這個方法完全自訂了,什麼都沒寫。
把下面的代碼添加到UserInfo類的最後,添加自定製的驗證:
# forms.py 檔案from django.core.exceptions import ValidationError # 匯入異常類型class UserInfo(Form): # 前面的代碼不變,先把鉤子方法寫上,稍後再來完善 def clean_name(self): """單獨對每個欄位進行驗證""" name = self.cleaned_data[‘name‘] # 擷取name的值,如果驗證通過,直接返回給clean_data if name == ‘root‘: raise ValidationError("不能使用“root”做使用者名稱") return name def clean_comments(self): # 這裡可以再補充驗證的內容 return self.cleaned_data[‘comments‘] def clean(self): """對form表單的整體內容進行驗證""" # 如果欄位為空白,但是驗證要求不可為空,那麼在完成欄位驗證的時候,cleaned_data裡是沒有name這個索引值的 # 但是會繼續往下進行之後的驗證,下面要用get,否則沒有這個鍵的話會報錯 name = self.cleaned_data.get(‘name‘) comments = self.cleaned_data.get(‘comments‘) if name == comments: raise ValidationError("使用者名稱和備忘的內容不能一樣") return self.cleaned_data def _post_clean(self): pass
其他補充的內容
這裡ValidationError的異常類型,第一個參數message是錯誤資訊,上面已經有了。第二個參數code是錯誤類型,這裡沒有定義,貌似也沒什麼效果。之前學習Form的時候,自訂錯誤資訊,就用到過把原生的錯誤資訊根據錯誤類型修改為自訂的錯誤資訊。比如: "required" 、 "max_length" 、 "min_length" 。
在views處理函數裡,obj.errors裡存放的是所有的錯誤資訊。其中欄位驗證錯誤資訊的索引值就是欄位名。而之後的整體的驗證產生的錯誤資訊是存放在一個特殊的索引值裡的 __all__ 。另外也可能看到不直接寫 __all__ 的,而是用到下面的常量:
from django.core.exceptions import NON_FIELD_ERRORS# 在源碼是這這樣定義的NON_FIELD_ERRORS = ‘__all__‘
所以還是 __all__ ,看到的時候要認得。
序列化錯誤資訊-配合Ajax
這個就是上一回最後提到的內容。
首先準備前端的html頁面,還是用上面的user_info.html。前面的東西都不用改,只在後面追加jQuery的代碼就好了,這裡全部貼上:
<body><form action="." novalidate="novalidate" method="post"> {% csrf_token %} <p>{{ obj.name }}</p> <p>{{ obj.comments }}</p> <p>{{ obj.type }}</p> <p>{{ obj.type3 }}</p> <p><input type="submit"></p></form>{% load static %}<script src="{% static "jquery-1.12.4.min.js" %}"></script><script> $(function () { $(‘form‘).on("submit", function (event) { event.preventDefault(); // 阻止預設操作,我們要執行自己的Ajax操作 $.ajax({ url: ‘.‘, type: ‘POST‘, data: $(this).serialize(), success: function (arg){ console.log(arg) }, error: function (arg) { alert("請求執行失敗") } }) }) })</script></body>
上面的jQuery裡對form表單的submit綁定事件,首先阻止form預設的操作,就是form表單的submit提交。然後執行自己寫的Ajax操作。
forms.py裡面之前也已經寫了很多的驗證了,直接能用上。
views.py裡的get也不用改。主要修改的是post最後返回的內容。這也是這裡要講的內容。
def user_info(request): if request.method == ‘GET‘: obj = forms.UserInfo({‘name‘: ‘Your Name‘}) # 這步是執行個體化,obj裡有所有靜態欄位的靜態內容 # 實際上,所有的靜態欄位都在obj.fields裡。下面就把其中的type欄位裡的choices的值重新去擷取一下 # obj.fields[‘type‘].choices = models.UserType.objects.values_list() # 這裡是重新去資料庫裡再擷取一下最新的值 return render(request, ‘user_info.html‘, {‘obj‘: obj}) elif request.method == ‘POST‘: obj = forms.UserInfo(request.POST) # 擷取POST提交的資料 res = obj.is_valid() # 這一步是進行驗證 if res: # 驗證通過的情況,之前已經會處理了。這裡主要關注下面驗證不通過,如何返回錯誤資訊 print(obj.cleaned_data) return HttpResponse(str(obj.cleaned_data)) else: print(type(obj.errors)) # 先來看看這個錯誤資訊的類型 return HttpResponse(str(obj.errors)) # 通過str方法後,頁面上會直接按html代碼處理
上面先用 print(type(obj.errors)) 看看這個存放錯誤資訊的變數是個什麼類型,print的結果如下:
<class ‘django.forms.utils.ErrorDict‘>
這裡還是試著去源碼裡找到它,探究一下。在PyCharm裡寫上下面這句,然後Ctrl+左鍵,可以快速的定位到源碼:
from django.forms.utils import ErrorDict
貌似內容不多,就全部貼下面了:
@html_safeclass ErrorDict(dict): """ A collection of errors that knows how to display itself in various formats. The dictionary keys are the field names, and the values are the errors. """ def as_data(self): return {f: e.as_data() for f, e in self.items()} def get_json_data(self, escape_html=False): return {f: e.get_json_data(escape_html) for f, e in self.items()} def as_json(self, escape_html=False): return json.dumps(self.get_json_data(escape_html)) def as_ul(self): if not self: return ‘‘ return format_html( ‘<ul class="errorlist">{}</ul>‘, format_html_join(‘‘, ‘<li>{}{}</li>‘, self.items()) ) def as_text(self): output = [] for field, errors in self.items(): output.append(‘* %s‘ % field) output.append(‘\n‘.join(‘ * %s‘ % e for e in errors)) return ‘\n‘.join(output) def __str__(self): return self.as_ul()
之前直接列印處理的就是他的as_ul方法返回的值,看最下面的特殊方法 __str__ 。
通過as_json()方法,進行2次序列化操作
這裡先看一下as_json()方法。處理函數修改成下面這樣:
def user_info(request): if request.method == ‘GET‘: obj = forms.UserInfo({‘name‘: ‘Your Name‘}) # 這步是執行個體化,obj裡有所有靜態欄位的靜態內容 # 實際上,所有的靜態欄位都在obj.fields裡。下面就把其中的type欄位裡的choices的值重新去擷取一下 # obj.fields[‘type‘].choices = models.UserType.objects.values_list() # 這裡是重新去資料庫裡再擷取一下最新的值 return render(request, ‘user_info.html‘, {‘obj‘: obj}) elif request.method == ‘POST‘: ret = {‘status‘: True, ‘errors‘: None, ‘data‘: None} obj = forms.UserInfo(request.POST) # 擷取POST提交的資料 res = obj.is_valid() # 這一步是進行驗證 if res: # 驗證通過的情況,之前已經會處理了。這裡主要關注下面驗證不通過,如何返回錯誤資訊 print(obj.cleaned_data) else: # print(type(obj.errors)) # 先來看看這個錯誤資訊的類型 ret[‘status‘] = False ret[‘errors‘] = obj.errors.as_json() # as_json()方法裡,對error的資訊進行了一下dumps print(ret) # 下面return的時候又對全部的資料進行了一次dumps,所以之後前端擷取errors資訊的時候也要反解2次 return HttpResponse(json.dumps(ret))
前端的script標籤裡的內容修改為:
<script> $(function () { $(‘form‘).on("submit", function (event) { event.preventDefault(); // 阻止預設操作,我們要執行自己的Ajax操作 $.ajax({ url: ‘.‘, type: ‘POST‘, data: $(this).serialize(), success: function (arg){ console.log(arg); var obj = JSON.parse(arg); if (obj.status){ location.reload() } else { var errors = JSON.parse(obj.errors); // 這裡是第二次反解了,返回的error裡的資料要反解2次 console.log(errors) } }, error: function (arg) { console.log("請求執行失敗") } }) }) })</script>
上面暫時只把錯誤資訊輸出到控制台了。這裡還是有問題,因為要JSON正解反解兩次。
這個是笨辦法,能實現需求,但是肯定不好
通過as_data()方法,使用自訂的方法序列化
現在再看看as_data()方法。通過 print(type(obj.errors.as_data())) 可以查看這個方法返回的資料類型是原生的字典類型。再來列印字典的內容 print(obj.errors.as_data()) ,可以看到字典裡嵌套了其他資料類型,是不能直接序列化的:
{‘type3‘: [ValidationError([‘未選擇你的選項‘])], ‘__all__‘: [ValidationError([‘使用者名稱和備忘的內容不能一樣‘])]}
對於ValidationError類型,我們只需要其中的messages和code,放的分別是錯誤資訊和錯誤類型。這裡需要自訂個類來代替dumps方法進行序列化:
from django.core.exceptions import ValidationErrorclass JsonCustomEncoder(json.JSONEncoder): def default(self, field): if isinstance(field, ValidationError): return {‘code‘: field.code, ‘messages‘: field.messages} else: return json.JSONEncoder.default(self, field)
有了上面的類,現在只需要用序列化和還原序列化一次了:
ret[‘status‘] = Falseret[‘errors‘] = obj.errors.as_data()result = json.dumps(ret, cls=JsonCustomEncoder) # 這裡的dumps方法多了一個cls參數return HttpResponse(result)
關於上面使用的dumps方法。cls裡定義了序列化時使用的方法。上面自訂的類繼承了預設的方法,然後寫了我們需要的額外的處理方法進行序列化。
這個方法就不需要2次序列化了,但是有了下面的方法,這個也不需要了。但是通過這小段代碼,學習到了,json序列化操作的進階知識。
通過get_json_data()方法,2.0新方法
這個是官網的串連:https://docs.djangoproject.com/en/2.0/ref/forms/api/
主要是去裡面確認到了下面的資訊:
這個方法是Django 2.0. 新加入了。所以有了它,上面的那兩種麻煩的實現方法都不需要了。
下面是和as_data()方法的比較:
temp1 = obj.errors.get_json_data()print(type(temp1), temp1)temp2 = obj.errors.as_data()print(type(temp2), temp2)
下面是列印結果的比較:
<class ‘dict‘> {‘type3‘: [{‘message‘: ‘This field is required.‘, ‘code‘: ‘required‘}], ‘__all__‘: [{‘message‘: ‘使用者名稱和備忘的內容不能一樣‘, ‘code‘: ‘‘}]}<class ‘dict‘> {‘type3‘: [ValidationError([‘This field is required.‘])], ‘__all__‘: [ValidationError([‘使用者名稱和備忘的內容不能一樣‘])]}
效果和上面的方法一樣,直接就是原生的字典。但是這裡直接使用新提供的原生方法就可以了。
序列化QuerySet
現在也把資料庫查詢(Model操作)的結果也序列化返回給前端。
一直沒遇到過這類問題,因為之前都是通過範本語言把資料傳到前端的頁面的。如果要支援通過Ajax請求的操作,拿到後端的資料,就會用到下面序列化的方法。
方法一
from django.core import serializersobj = models.UserType.objects.all()data = serializers.serialize(‘json‘, obj)print(data) # 列印結果在下面# [{"model": "app01.usertype", "pk": 1, "fields": {"name": "\u8d85\u7ea7\u7528\u6237"}}]
這裡返回的是QuerySet對象。所以不能用json來序列化了。這裡用了django提供的方法實現了序列化。其中model是表名,pk是主鍵即id的值。最後的fields裡的內容就是其他的資料了。
方法二
import jsonobj = models.UserType.objects.values()obj = list(obj) # 直接可以轉成一個原生的字典data = json.dumps(obj)print(data) #列印結果如下# [{"id": 1, "name": "\u8d85\u7ea7\u7528\u6237"}]
這樣也可以完成序列化。
但是如果存在日期或時間類型的話,就會有問題了。這個時候參照上面錯誤資訊序列化的第二個方法,自訂一個序列化方法,也可以完成序列化操作。
Python自動化開發學習23-Django下(Form)