我们之前在HTML页面中利用form表单向后端提交数据时都会写一些获取用户输入的标签并且用 form标签 把他们包起来。但是很多场景下我们都需要对用户的输入做校验,比如用户是否输入,输入长度以及格式等问题,如果用户输入的内容不符合我们的校验就需要在网页的相应位置显示对应的错误信息。

Django form组件就实现了上面所述的功能。

Form组件的主要功能如下:

  • 生成HTML标签
  • 验证用户数据 (显示错误信息)
  • HTML Form提交保留上次提交数据
  • 初始化页面显示内容
  • 推荐博客: https://www.cnblogs.com/wupeiqi/articles/6144178.html

    使用 Form 组件,首先导入 forms 模块

    from django import forms
    

    Form表单效验方式:

  • 使用前先定义Form数据的效验规则:
  • from django.forms import Form
    from django.forms import fields
    class MyForm(Form):
        username = fields.CharField(
            min_length=5, max_length=10,   # 长度规则
            required=True,  			   # 不能为空
            ''' 自定义错误信息,用于页面显示 '''
            error_messages={
                'min_length':'最小大于4','max_length':'最小小于11',
                'required':'不为空'	}
        password = fields.CharField(min_length=6, required=True)
    
  • 视图函数获取数据并效验规则方式:
  • request.POST 返回的是字典格式

    # app01.views.py
    from app01.myform import MyForm
    def login(request):
        if request.method == 'GET':
            return render(request, 'login.html')
        else:
            form = MyForm(request.POST)  	# 创建 Form 对象
            if form.is_valid():  		 # 验证数据是否满足规则
                print('form-data', form.cleaned_data)  # 正确的数据信息,返回字典格式
                return redirect('http://www.baidu.com')
            else:
                print(form.errors) # 数据验证失败,返回错误信息,为对象类型,内容为页面标签,特殊字典
                print(form.errors['username'])   #> <ul class="errorlist"><li>不为空</li></ul>
                print(form.errors['password'][0])   #> This field is required.
                return render(request, 'login.html', {'obj': form})
    
  • 页面 Form表单提交数据方式:
  • <!DOCTYPE html>
    <html lang="en">
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
        <form action="/login/" method="post">
            {% csrf_token %}
            <!-- 注意:name属性必须与 自定义Form类字段名一致,否则会找不到 -->
            <p>用户: <input type="text" name="username">{{ obj.errors.username.0 }}  </p>
           <p>密码:<input type="password" name="password">{{ obj.errors.password.0 }}</p>
            <input type="submit" value="提交">
        </form>
    </body>
    </html>
    
  • 当页面账号密码为空时,运行结果:
    urlpatterns = [
        re_path('login/', views.login),
        re_path('^ajax_login.html', views.ajax_login),  # 新增 url
    

    说明:视图函数

    import json
    def login(request):
        if request.method == 'GET':
            return render(request, 'login.html')
        else:
            form = MyForm(request.POST)
            if form.is_valid():  # 验证规则
                print('form-data', form.cleaned_data)  # 返回字典格式
                return redirect('http://www.baidu.com')
            else:
                return render(request, 'login.html', {'obj': form})
    def ajax_login(request):
        ret = {'status': True, 'msg': None}
        obj = MyForm(request.POST)
        print('验证结果:', obj.is_valid())
        if obj.is_valid():
            print(obj.cleaned_data)
        else:
            print(obj.errors)
            ret['status'] = False
            ret['msg'] = obj.errors
        return HttpResponse(json.dumps(ret))  # 转换字符串格式
    

    说明:HTML模板

    <!DOCTYPE html>
    <html lang="en">
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
        <form id="f1" action="/login/" method="POST">
            {% csrf_token %}
            <p>用户:<input type="text" name="username">{{ obj.errors.username.0 }}
            <p>密码:<input type="password" name="password">{{ obj.errors.password.0 }}
            <input type="submit" value="提交">
            <a onclick="submitForm();">ajx提交</a>
        </form>
        <!-- 导入 jquery -->
        <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-1.11.3.min.js"></script>
        <script>
            function submitForm(){
                $('.c1').remove();     //清空之前的内容
                $.ajax({
                    url: '/ajax_login.html',
                    type: 'POST',
                    //serialize 会将form中所有的数据进行打包传递后端,将数据拼接成字符串格式
                    data: $('#f1').serialize(),
                    dataType: 'JSON',
                    success:function(arg){
                        console.log(arg);
                        //对应位置显示错误信息
                        if(arg.status){  
                        }else {
                            $.each(arg.msg, function(index, value){ //index为后端传递ret的key,value为描述
                                var tag = document.createElement('span'); //新增标签
                                tag.innerHTML = value[0];                 //获取错误信息
                                tag.className = 'c1';        //加样式,方便下次清空之前的内容
                                // 将新标签放到指定位置
                                $('#f1').find('input[name="'+ index +'"]').after(tag);
        </script>
    </body>
    </html>
    

    导入线上jquery库地址: https://www.jq22.com/jquery-info122#google_vignette

    页面运行结果:

    错误信息与 input标签 对应:

    其他字段使用方式:

  • label 用于生成Label标签或显示内容
  • 说明:Form自定义规则:

    from django.forms import Form
    from django.forms import fields
    class MyForm(Form):
        t1 = fields.CharField(label='label的自定义内容...')  # 加规则
    

    说明:视图函数:

    def login(request):
        if request.method == 'GET':
            obj = MyForm()
            return render(request, 'login.html', {'obj': obj})
    

    说明:HTML模板:

    <!DOCTYPE html>
    <html lang="en">
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
        <form id="f1" action="/login/" method="POST">
            {% csrf_token %}                      <!-- 注意:t1是效验规则中的字段名称 -->
            <p>Lable标签:  <input type="text" name="t1">{{ obj.t1.label }}<br></p>
        </form>    
    </body>
    </html>
    

    页面展示结果:
    if request.method == 'GET': obj = MyForm() return render(request, 'login.html', {'obj': obj})

    说明:HTML模板:

    <!DOCTYPE html>
    <html lang="en">
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
        <form id="f1" action="/login/" method="POST">
            {% csrf_token %}                      <!-- 注意:t1是效验规则中的字段名称 -->
            <p>help_text:  <input type="text" name="t1">{{ obj.t1.help_text }}<br></p>
        </form>    
    </body>
    </html>
    

    页面展示情况:
    t1 = fields.CharField( # choices: 给下拉框加选项,需满足格式:[(),()] widget=widgets.Select(choices=[(1,'自定义'),(2,'自定义')]), #还支持数据库:choices=models.tablesxxx.objects.values_list('xx','xx')

    from django.forms import Form
    from django.forms import fields
    from django.forms import widgets   # 导入模块
    class MyForm(Form):
        t1 = fields.CharField(
            widget=widgets.Textarea,    # 控制HTML标签格式
            label_suffix='>>',          # label后缀
            label='用户名',             # 标签
            initial='默认值...',        # 默认值
            help_text='帮助信息..',     # 帮助
            disabled=True               # 是否可编辑状态
        t2 = fields.IntegerField(     max_value=1000,
            error_messages={ 'invalid': 't2格式不对,必须是数字!'     }    )
    HTML模板
    
    <!DOCTYPE html>
    <html lang="en">
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
        <form id="f1" action="/login/" method="POST">
            {% csrf_token %}
              <!-- 方式二: -->
                {{ obj.as_p }}    <!-- 自动生成所有的标签内容 -->
    </body>
    </html>
    

    结果样式:

  • 修改规则效验方式:(对初始值有效)
  • 页面 Form表单将数据传入后端,规则MyForm(request.POST)接收数据会自动进行验证:

    def login(request):
        if request.method == 'POST':
            #obj = MyForm(request.POST)          # 写法一   两种写法效果一样
        	obj = MyForm(data = request.POST)    # 写法二  
            if obj.is_valid(): print(obj.cleaned_data)
            return render(request, 'login.html', {'obj': obj})
    

    页面效果:

    给规则参数改为 initial,页面将不在进行初始值效验

    def login(request):
        if request.method == 'POST':
            obj = MyForm(initial = request.POST)         # initial: 页面将不再进行效验
            if obj.is_valid(): print(obj.cleaned_data)
            return render(request, 'login.html', {'obj': obj})
    

    页面效果:

    保留页面输入内容

  • 解决 Form表单不能保留上次输入的数据问题:
  • Form自定义规则
    # 效验规则
    class MyForm(Form):
        t1 = fields.CharField(
            min_length=5, max_length=10, required=True,  
            error_messages={'min_length':'最小大于4', 'max_length':'最大等于10','required':'不为空'}
        t2 = fields.EmailField(     required=True,  # 不为空
            error_messages={'invalid': 't2格式不对,必须是数字!', 'required':'不为空'   }
    
    # 视图函数
    def login(request):
        if request.method == 'GET':
            obj = MyForm()      #*** 必加点,让HTML自动生成标签
            return render(request, 'login.html', {'obj': obj})
        else:
            obj = MyForm(request.POST)     #*** 这里会保留用户输入的内容
            if obj.is_valid():  
                print('form-data', obj.cleaned_data) 
            print(obj.errors)
            return render(request, 'login.html', {'obj': obj})
    HTML模板
    
    <!DOCTYPE html>
    <html lang="en">
        <meta charset="UTF-8">
        <title>Login</title>
    </head>
        <form id="f1" action="/login/" method="POST" novalidate>
            {% csrf_token %}
            <p>t1: {{ obj.t1 }}  {{ obj.errors.t1.0 }}  </p>
            <p>t2: {{ obj.t2 }}  {{ obj.errors.t2.0 }}  </p>
            <input type="submit" value="提交">
        </form>
    </body>
    </html>
    页面提交后之前输入框中的内容没有清空:
    

    Form 表单加样式

    通过 form 规则中插件widgets 加指定的页面加属性:

    HTML模板
    <!DOCTYPE html>
    <html lang="en">
        <meta charset="UTF-8">
        <title>Login</title>
        <!-- 导入线上css库 -->
        <link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/3.4.1/css/bootstrap.min.css">
    </head>
        <div style="width: 300px; margin: 0 auto;">        <!-- 自定义样式  -->
            <form class="form-horizontal" id="f1" action="/login/" method="POST" novalidate>
                {% csrf_token %}
                <div class="form-group">
                    <label class="col-sm-2 control-label">T1: </label>
                    <div class="col-sm-10">
                        {{ obj.t1 }}  {{ obj.errors.t1.0 }}
                <div class="form-group">
                    <label for="inputPassword3" class="col-sm-2 control-label">T2:</label>
                    <div class="col-sm-10">
                        {{ obj.t2 }}  {{ obj.errors.t2.0 }}
                <div class="form-group">
                    <label for="inputPassword3" class="col-sm-2 control-label">T3:</label>
                    <div class="col-sm-10">
                        {{ obj.t3 }} {{ obj.errors.t3.0 }}
                <div class="form-group">
                    <div class="col-sm-offset-2 col-sm-10">
                        <input type="submit" value="提交" class="btn btn-default">
            </form>
    </body>
    </html>
    Form自定义规则
    
    from django.forms import Form, fields, widgets 
    class MyForm(Form):    
        t1 = fields.CharField(       
            #** 通过插件给页面加属性
            widget=widgets.TextInput(attrs={'class':'form-control'}),   
            min_length=5, max_length=10,   required=True,  # 不为空
            error_messages={'min_length':'最小大于4', 'max_length':'最大等于10', 'required':'不为空'}    )
        t2 = fields.EmailField(
            widget=widgets.TextInput(attrs={'class':'form-control'}),
            required=True,  # 不为空
            error_messages={ 'invalid': 't2格式不对!', 'required':'不为空'      }    )
        t3 = fields.CharField(
            widget=widgets.Select(choices=[(1,'自定义1'),(2,'自定义2')],
                                  # 给下拉框加属性
                                  attrs={'class':'form-control'}),    )
    重点:通过 `widget=widgets.TextInput(attrs={'class':'form-control'})` 方式进行加页面样式。
    
    def login(request):
        if request.method == 'GET':
            obj = MyForm()
            return render(request, 'login.html', {'obj': obj})
        else:
            obj = MyForm(request.POST)
            if obj.is_valid():  
                print('form-data', obj.cleaned_data)  
            return render(request, 'login.html', {'obj': obj})
    

    Form 组件之钩子函数

    forms组件暴露给用户,可以自定义的校验规则 用法:在自定义的form类中定义一个函数即可

    cleaned_data 方法中想要取多个字段值时,只能取到Form类已验证完成后的字段,没有验证的字段是取不到值,取字段值时需要注意Form类验证顺序是从上到下依次验证。

    比如对比两个密码是否一致时, 那就需要取多字段值:

    点击查看代码
    class Domel(forms.Form):
        pwd = forms.CharField(label='密码', min_length=6,                               
                                   widget=forms.PasswordInput())
        confirm_pwd = forms.CharField(label='重复密码', min_length=6,                       
                                   widget=forms.PasswordInput())
        class Meta:
            model = models.UserInfo  # 获取数据表数据
        def clean_confirm_pwd(self):
            注意:两个密码需要对比,需要取两个字段值,Form组件是先效验 pwd字段,然后才效验 confirm_pwd字段;这里取值顺序必须先取 pwd字段,在取confirm_pwd字段,否则 KeyError 报错。        
            password = self.cleaned_data['password']
            isPASSWORD = self.cleaned_data['isPASSWORD']
            if password != isPASSWORD:
                raise ValidationError('两次密码不一致')
            return isPASSWORD    
    

    局部钩子(针对某一个字段做额外的校验)

    定义一个函数,函数名格式:clean_字段名字,内部,取出该字段,进行校验,如果通过,将该字段返回,如果失败,抛异常(ValidationError)。

    定义局部钩子

    class SendSmsForm(forms.Form):
        phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机格式错误'), ])
        def clean_phone(self):
                ''' 钩子函数,用于验证手机号
                m_phone = self.cleaned_data.get('phone')  # 首先从校验正确的数据中获取名字
                # 验证数据库中手机号是否已存在
                exists = models.UserInfo.objects.filter(phone=m_phone).exists()  # exists: 判断是否存在,bool值
                if exists:
                    # self.add_error(m_phone, '手机号已存在!')   # 方式一,该行执行完后会继续往下执行
                    raise ValidationError('手机号已存在!')       # 方式二,该行执行完后不会继续往下执行
                return m_phone
    
  • 校验失败,抛异常,将异常信息以 {'name':value} 写入 errors 字典中;
  • 校验成功,把name返回到clean_data,写入clean_data字典中;
  • 抛出异常的类型为ValidationError,from django.core.exceptions import ValidationError导入;
  • 全局钩子(针对多个字段做额外的校验)

    在写注册用户的时候,有输入密码,确认密码,可以进行布局钩子处理,处理完毕是不是在进行判断,判断他们是否相等,相等的话,就存到数据库中,不相等就抛个异常。

    定义全局钩子

    # 重写clean方法
    def clean(self):
        # 程序能走到该函数,前面校验已经通过了,所以可以从cleaned_data中取出密码和确认密码		
        pwd=self.cleaned_data.get('pwd')
        re_pwd=self.cleaned_data.get('re_pwd')
        # 进行自己的校验
        if pwd==re_pwd: 
            # 通过,直接返回cleaned_data
            return self.cleaned_data
        else:
            # 失败,抛异常(ValidationError)
            raise ValidationError('两次密码不一致')
    

    全局钩子注意点:

  • 校验失败,抛异常,将异常信息以{'__all__':[value,]} 写入 errors 字典中;
  • 校验成功,返回clean_data字典;
  • 抛出异常的类型为ValidationError,from django.core.exceptions import ValidationError导入;
  • 钩子错误信息渲染注意点:

    局部钩子抛出的异常会添加到该字段中的错误信息中,获取错误信息:

    前台:for循环生成input框,{{ foo.errors.0 }}

    全局钩子抛出的异常会添加到__all__中,获取错误信息:

    后台:myforms.errors.get('__all__')[0]注意先判断myforms.errors.get('__all__')是否存在
    前台:{{ myforms.errors.__all__.0 }}

    如果程序走到了局部钩子这一步,说明传的字典里的数据符合要求,此时就可以从clean_data中取数据,因为此时clean_data中的数据全符合要求,而且clean_data是一个字典

    局部钩子,全局钩子所抛出异常的类型为ValidationError,以下导入:

    from django.core.exceptions import ValidationError
    

    raise 和 self.add_error 区别

    add_error(field, error) 需要传递两个参数,field 为字段名;error 为自定义保存信息;当触发报错时会将数据传入到 cleaned_data 中

    raise ValidationError() 当触发错误信息时,不会将数据传入到 cleaned_data 中,后续用 cleaned_data[keys] 取值时会报:KeyERROR 错误;推荐使用 cleaned_data.get(keys) 进行取值操作。

    完整的钩子函数示例

    form_data.py

    form_data.py
    import random
    from django import forms
    from django.conf import settings
    from django.core.validators import RegexValidator
    from django.core.exceptions import ValidationError
    from web import models
    from tools.tencent_sms import send_sms_single
    class SendSmsForm(forms.Form):
        phone = forms.CharField(label='手机号', validators=[RegexValidator(r'^(1[3|4|5|6|7|8|9])\d{9}$', '手机格式错误'), ])
        def __init__(self, request, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.request = request
        # 钩子函数,当前面的校验都通过后才会自动执行
        # 局部钩子函数,针对单个字段的校验利用局部钩子函数
        # 局部钩子函数的函数名格式:clean_字段名,你要给那个字段自定义规则,这个字段名就是谁!
        def clean_phone(self):
            ''' 钩子函数,用于验证手机号、
            m_phone = self.cleaned_data.get('phone')  # 首先从校验正确的数据中获取名字
            # 验证数据库中手机号是否已存在
            exists = models.UserInfo.objects.filter(phone=m_phone).exists()  # exists: 判断是否存在,bool值
            if exists:
                # self.add_error(m_phone, '手机号已存在!')   # 方式一,该行执行完后会继续往下执行
                raise ValidationError('手机号已存在!')       # 方式二,该行执行完后不会继续往下执行
            # 判断短信模板是否错误
            tpl = self.request.GET.get('tpl')
            template_id = settings.SMS_TEMPLATE.get(tpl)
            if not template_id:
                raise ValidationError('模板错误!')
            # 发短信
            code = random.randrange(1000, 9999)
            res_sms = send_sms_single(m_phone, template_id, [code])
            if res_sms['result'] != 0:
                raise ValidationError('短信发送失败,{}'.format(res_sms['errmsg']))
            # 验证码写入 redis
            from django_redis import get_redis_connection
            conn = get_redis_connection()
            conn.set(m_phone, code, ex=60)
            return m_phone
    

    views.py

    views.py
    from django.http import JsonResponse
    from django.shortcuts import render
    from web import form_data
    def send_sms(request):
        ''' 发短息 '''
        # 1. 通过form组件进行效验数据
        # 2. 将request 传递到form组件中
        # 3. form接收参数并进行效验
        form = form_data.SendSmsForm(request, data=request.GET)
        # 验证:不为空、格式
        if form.is_valid():
            return JsonResponse({'status': True})
        return JsonResponse({'status': False, 'error': form.errors})
    

    Meta 使用方式

  • 继承Form 或 Models
  • from django import forms
    class TestForm(forms.Form):  # form组件
    class TestForm(forms.ModelForm):  # form与model的结合
    
  • Meta 常用字段介绍
  • from django import forms
    class TestForm(forms.ModelForm):
        class Meta:
            model = models.Project    # 关联数据库表
            fields = ['字段1', '字段2', '字段3']   # 表字段,用于页面显示字段,如果是all,就是表示列出所有的字段
            widgets = {'desc': forms.Textarea(),}  # desc 是数据库表字段,value 是给字段加页面样式
            labels = {'desc':'项目描述',}  # 加字段提示信息
    

    ModelForm的验证

    与普通的Form表单验证类型类似,ModelForm表单的验证在调用is_valid() 或访问errors 属性时隐式调用。也可以像使用Form类一样自定义局部钩子方法和全局钩子方法来实现自定义的校验规则。如果我们不重写具体字段并设置validators属性的话,ModelForm是按照模型中字段的validators来校验的。

    save() 方法

    ModelForm还具有一个save()方法。这个方法根据表单绑定的数据创建并保存数据库对象。ModelForm的子类可以接受现有的模型实例作为关键字参数instance;如果提供此功能,则save()将跟新该实例。如果没有提供,save()将创建模型的一个新实例:

    def register(request):
        '''注册'''
        if request.method == 'GET':
            form = account.RegisterModelForm()  # form代替生成页面标签
            return render(request,'web/register.html',{'form':form})
        form = account.RegisterModelForm(data=request.POST)
        if form.is_valid():  # form 进行字段效验
            # Form组件自动将数据存入数据库中
            # 如果密码字段没有特殊处理时,这里保存将是明文。
            # 通过 Form类中钩子函数进行密码加密处理
            # form.save() 等价于:models.UserInfo.objects.create(**form.cleaned_data)
            instance = form.save()  # 页面数据写入数据库,返回对象
        return JsonResponse({'status':False,'error':form.errors})
    
  •