多对多关系表的创建方式、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()里面没有内容,我们的源码之旅到此结束。
通过看源码我们发现局部钩子和全局钩子分别通过反射和对象属性方法的查找顺序两种方式实现的自动调用。