多對多關係表的創建方式、forms組件
- 2019 年 12 月 16 日
- 筆記
多對多關係表的三種創建方式
1.全自動,Django自動創建
class Book(models.Model): title = models.CharField(max_length=20) authors = models.ManyToManyField(to='Authors') class Authors(models.Model): name = models.CharField(max_length=32) #好處:自始至終都沒有操縱過第三張表,全部由orm創建,內置了四個操作第三張表的方法add、remove、set、clear #不足:可擴展性差,自動創建的第三張表我發擴展和修改字段
2.純手擼
class Book(models.Model): title = models.CharField(max_length=32) class Authors(models.Model): name = models.CharField(max_length=32) class Book2Authors(models.Model): book = models.ForeignKey(to="Book") author = models.ForeignKey(to="Authors") create_time = models.DateField(auto_now_add = True) #好處:第三張表中的字段名稱和個數全部可以自己定義 #不足:不再支持orm跨表查詢,不支持正反向查詢的概念,不支持內置的第三張表操作的四個方法
3.半自動(推薦使用)
參數:
through:指定第三張表關係
through_fields:指定第三張表中哪兩個字段維護表與表之間的多對多關係(這裡有先後順序,外鍵建在誰那裡就先寫誰)
class Book(models.Model): title = models.CharField(max_length=20) authors = models.ManyToManyField(to='Authors',through='Book2Author',through_fields=("book","authors")) # 主鍵在誰那裡誰就放前面 class Authors(models.Model): name = models.CharField(max_length=32) #books = models.ManyToManyField(to='Book',through='Book2Author',through_fields=('authors','book')) class Book2Author(models.Model): book = models.ForeignKey(to='Book') #好處:可以任意的添加和修改第三張表中的字段,支持orm跨表查詢 #不足:不支持add、remove、clear、set
forms組件
小例子
需求:1.寫一個註冊頁面獲取用戶輸入的用戶名和密碼,提交到後端之後,後端需要對用戶名和密碼進行校驗,用戶名不能含有『xxx』密碼不能少於三位
分析:
1.手動寫HTML代碼獲取用戶輸入(渲染標籤)
2.將數據傳遞給後端校驗(校驗數據)
3.如果數據有錯誤展示信息(展示信息)
#手動實現略
forms組件
forms組件能夠做的就是上面的三件事情,在使用forms之前,我們需要先定義一個類:
from django import forms class MyForm(forms.Form): username = forms.CharField(max_length=8,min_length=3) #指定username的長度是3-8位之間 password = forms.CharField(max_length=8,min_length=3) email = forms.EmailField() #輸入的必須是email格式
其他字段及參數
label input對應的提示信息 initial input框默認值 required 默認為True控制字段是否必填 widget 給input框設置樣式及屬性 error_messages 設置報錯信息 #widget的使用方法如下 widget=forms.widgets.TextInput({'class':'form-control c1 c2','username':'ylpb'}) #將input框類型設置成text,樣式是'form-control c1 c2' widget=forms.widgets.TextInput(attrs={'class':'form-control c1 c2','username':'ylpb'}) error_messages={ 'max_length':'密碼最長10位', 'min_length':'密碼最短5位', 'required':'密碼不能為空' }
校驗數據
# 1.給寫好的類 傳字典數據(待校驗的數據) form_obj = views.MyForm({'username':'ylpb','password':'12','email':'123'}) # 2.查看校驗的數據是否合法 form_obj.is_valid() False # 只有當你的數據全部符合校驗規則的情況下 結果才是True 否則都為False # 3.查看不符合規則的字段及錯誤的理由 form_obj.errors { 'password': ['Ensure this value has at least 3 characters (it has 2).'], 'email': ['Enter a valid email address.'] } # 4.查看符合校驗規則的數據 form_obj.cleaned_data {'username': 'jason'} # 5.forms組件中 定義的字段默認都是必須傳值的,不能少傳,多傳取前面的 form_obj = views.MyForm({'username':'ylpb','password':'12345'}) form_obj.is_valid() False form_obj.errors {'email': ['This field is required.']} # 6.forms組件只會校驗forms類中定義的字段,如果你多傳了,不會有任何影響 form_obj = views.MyForm({'username':'ylpb','password':'12345','email':'123@qq.com','xxx':'嘿嘿嘿'}) form_obj.is_valid() True
渲染標籤
forms組件只會幫你渲染獲取用戶輸入的標籤,不會幫你渲染提交按鈕,需要你自己手動添加 <p>forms組件渲染標籤方式1:封裝程度太高,不推薦使用但是可以用在本地測試</p> {{ form_obj.as_p }} <!--自動渲染所有input框 --> {{ form_obj.as_ul }} {{ form_obj.as_table }} <p>forms組件渲染標籤方式2:不推薦使用 寫起來太複雜</p> {{ form_obj.username.label }}{{ form_obj.username }} {{ form_obj.username.label }}{{ form_obj.password }} {{ form_obj.username.label }}{{ form_obj.email }} <p>forms組件渲染標籤方式3:推薦使用 </p> {% for form in form_obj %} <p>{{ form.label }}{{ form }}</p> <!--form 等價於方式2中的對象點字段名--> {% endfor %}
展示信息(使用第三種方式渲染)
<form action="" method="post" novalidate> {% for forms in form_obj %} <p> {{ forms.label }}{{ forms }} <span>{{ forms.errors.0 }}</span><!--這裡的forms.errors是一個個列表,.0拿到的是列表裏面的內容--> </p> <!--form 等價於你方式2中的對象點字段名--> {% endfor %} <input type="submit"> </form>
數據的校驗通常前後端都必須有,但前端的校驗若不經風,所以後端必須有校驗,上面的forms瀏覽器會默認在前端對數據進行校驗,我們需要先禁止瀏覽器的校驗功能,方法是在form標籤加上novalidate參數。
<form action="" method="post" novalidate>
校驗器
後端對數據進行校驗有兩層,第一層是使用內置校驗器進行校驗,校驗器的導入方式與校驗方式如下:
from django.core.validators import RegexValidator validators=[ RegexValidator(r'^[0-9]+$', '請輸入數字'), RegexValidator(r'^159[0-9]+$', '數字必須以159開頭') #這裡通過正則對數據進行篩選
通過校驗器對數據的合法性進行校驗之後如果還需要對數據進行進一步校驗,比如輸入的字符中不能有某些數據等等,可以使用鉤子函數進行數據校驗。
鉤子函數
局部鉤子
局部鉤子只對指定的某一個字段進行校驗。
def clean_username(self): username = self.cleaned_data.get('username') if '123'in username: self.add_error('username','測試一下') #raise ValidationError('test') #主動拋異常也會被局部鉤子捕獲 return username
全局鉤子
def clean(self): password = self.cleaned_data.get('password') confirm_password = self.cleaned_data.get('confirm_password') if not password == confirm_password: self.add_error('confirm_password','兩次密碼不一致') return self.cleaned_data
上述功能結合使用可以得到以下代碼:
class MyForm(forms.Form): username = forms.CharField(max_length=8,min_length=3,label='姓名',initial='ylpb',required=True,widget=forms.widgets.TextInput({'class':'form-control c1 c2','username':'ylpb'})) #指定username的長度是3-8位之間 password = forms.CharField(max_length=8,min_length=3,label='密碼',required=True,widget=forms.widgets.PasswordInput({'class':'form-control'})) confirm_password = forms.CharField(max_length=8,min_length=3,label='確認密碼',required=True,widget=forms.widgets.PasswordInput({'class':'form-control'})) email = forms.EmailField(initial='abc@qq.com') #輸入的必須是email格式 gender = forms.ChoiceField( choices=((1,'男'),(2,'女'),(3,'保密')) ) def clean_username(self): username = self.cleaned_data.get('username') if '123'in username: self.add_error('username','測試一下') #raise ValidationError('test') #主動拋異常也會被局部鉤子捕獲 return username def clean(self): password = self.cleaned_data.get('password') confirm_password = self.cleaned_data.get('confirm_password') if not password == confirm_password: self.add_error('confirm_password','兩次密碼不一致') return self.cleaned_data #index是視圖函數 def index(request): form_obj = MyForm() if request.method == 'POST': form_obj = MyForm(request.POST) if form_obj.is_valid(): #is_valid如果表單沒有錯誤,則返回True,否則為False。 如果有錯誤被忽略,則返回False。 print(form_obj.cleaned_data) return HttpResponse('數據正確') return render(request,'index.html',locals())
forms組件常用字段與插件
initial初始值,input框裏面的初始值。
class LoginForm(forms.Form): username = forms.CharField( min_length=8, label="用戶名", initial="張三" # 設置默認值 ) pwd = forms.CharField(min_length=6, label="密碼")
error_messages重寫錯誤信息。
class LoginForm(forms.Form): username = forms.CharField( min_length=8, label="用戶名", initial="張三", error_messages={ "required": "不能為空", "invalid": "格式錯誤", "min_length": "用戶名最短8位" } ) pwd = forms.CharField(min_length=6, label="密碼")
password
class LoginForm(forms.Form): ... pwd = forms.CharField( min_length=6, label="密碼", widget=forms.widgets.PasswordInput(attrs={'class': 'c1'}, render_value=True) )
radioSelect單radio值為字符串
class LoginForm(forms.Form): username = forms.CharField( min_length=8, label="用戶名", initial="張三", error_messages={ "required": "不能為空", "invalid": "格式錯誤", "min_length": "用戶名最短8位" } ) pwd = forms.CharField(min_length=6, label="密碼") gender = forms.fields.ChoiceField( choices=((1, "男"), (2, "女"), (3, "保密")), label="性別", initial=3, widget=forms.widgets.RadioSelect() )
單選Select
class LoginForm(forms.Form): ... hobby = forms.ChoiceField( choices=((1, "籃球"), (2, "足球"), (3, "雙色球"), ), label="愛好", initial=3, widget=forms.widgets.Select() )
多選Select
class LoginForm(forms.Form): ... hobby = forms.MultipleChoiceField( choices=((1, "籃球"), (2, "足球"), (3, "雙色球"), ), label="愛好", initial=[1, 3], widget=forms.widgets.SelectMultiple() )
單選checkbox
class LoginForm(forms.Form): ... keep = forms.ChoiceField( label="是否記住密碼", initial="checked", widget=forms.widgets.CheckboxInput() )
多選checkbox
class LoginForm(forms.Form): ... hobby = forms.MultipleChoiceField( choices=((1, "籃球"), (2, "足球"), (3, "雙色球"),), label="愛好", initial=[1, 3], widget=forms.widgets.CheckboxSelectMultiple() )
choice字段注意事項
在使用選擇標籤時,需要注意choices的選項可以配置從數據庫中獲取,但是由於是靜態字段 獲取的值無法實時更新,需要重寫構造方法從而實現choice實時更新。
方式一
from django.forms import Form from django.forms import widgets from django.forms import fields class MyForm(Form): user = fields.ChoiceField( # choices=((1, '上海'), (2, '北京'),), initial=2, widget=widgets.Select ) def __init__(self, *args, **kwargs): super(MyForm,self).__init__(*args, **kwargs) # self.fields['user'].choices = ((1, '上海'), (2, '北京'),) # 或 self.fields['user'].choices = models.Classes.objects.all().values_list('id','caption')
方式二
from django import forms from django.forms import fields from django.forms import models as form_model class FInfo(forms.Form): authors = form_model.ModelMultipleChoiceField(queryset=models.NNewType.objects.all()) # 多選 # authors = form_model.ModelChoiceField(queryset=models.NNewType.objects.all()) # 單選
Django Form所有內置字段
Field required=True, 是否允許為空 widget=None, HTML插件 label=None, 用於生成Label標籤或顯示內容 initial=None, 初始值 help_text='', 幫助信息(在標籤旁邊顯示) error_messages=None, 錯誤信息 {'required': '不能為空', 'invalid': '格式錯誤'} 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類型 Django Form內置字段
字段校驗
RegexValidator驗證器
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開頭')], )
自定義驗證函數
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'郵箱'}))
forms組件源碼分析
我們的源碼分析從is_valid方法開始,一起來看is_valid的源碼
def is_valid(self): """ Returns True if the form has no errors. Otherwise, False. If errors are being ignored, returns False. """ return self.is_bound and not self.errors #這裡可以看出,如果self.is_bound為true和self.errors是false才可以return,
接下來看self.is_bound:

如果我們輸入了參數那麼self.is_bound一定為true,接下來我們看self.errors,這裡需要說明self是我們自定義的類實例化的對象。
def errors(self): "Returns an ErrorDict for the data provided for the form" if self._errors is None: self.full_clean() return self._errors #下面是self._errors源碼,self._errors的默認值是none,由此可知errors一定執行self.full_clean() self._errors = None # Stores the errors after clean() has been called. #self.full_clean()源碼 def full_clean(self): """ Cleans all of self.data and populates 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()
下面我們來分別看這三個部分分別有什麼功能
self._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)) #value就是用戶傳入的數據值 try: if isinstance(field, FileField): initial = self.get_initial_for_field(field, name) value = field.clean(value, initial) else: value = field.clean(value) #這裡的clean是鉤子函數,將校驗通過的數據添加到字典中 self.cleaned_data[name] = value if hasattr(self, 'clean_%s' % name): #利用反射判斷我們是否定義了鉤子函數,如果有,自動觸發 value = getattr(self, 'clean_%s' % name)()#這裡的name就是一個個字段名 #這裡的value是鉤子函數的返回值 self.cleaned_data[name] = value except ValidationError as e: #因為上面使用了局部鉤子,所以如果出現ValidationError錯誤也會先被局部鉤子捕獲,而使程序不會拋異常 self.add_error(name, e) #如果數據報錯就添加到這裡,因為這裡有異常捕獲所以不會報錯
self._clean_form()
def _clean_form(self): try: cleaned_data = self.clean() #cleaned_data是全局鉤子返回的內容 #調用我們自己的clean方法,如果我們沒寫這調用類的 except ValidationError as e: self.add_error(None, e) else: if cleaned_data is not None: self.cleaned_data = cleaned_data #這裡詮釋了全局鉤子是如何自動調用的
self._post_clean()裏面沒有內容,我們的源碼之旅到此結束。
通過看源碼我們發現局部鉤子和全局鉤子分別通過反射和對象屬性方法的查找順序兩種方式實現的自動調用。