第21天,Django之Form组件-阿里云开发者社区

开发者社区> 开发与运维> 正文
登录阅读全文

第21天,Django之Form组件

简介: ModelForm 请参考官方文档 一、Form组件初识 参考博客一 武沛奇老师博客参考博客二 Django的Form主要具有一下几大功能: 生成HTML标签 验证用户数据(显示错误信息) HTML Form提交保留上次提交数据 初始化页面显示内容 models.

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标签

注意:

  1. 新建的表单类必须继承Form类;
  2. 每个字段默认就是required=True;
  3. 字段没有自定义error_messages的值时,默认值是英文的错误提示;自定义的error_messages值中可以有多个错误信息提示,"required"是在该字段为空是提示,"invalid"是在该字段有值,但是值无效时提示,如邮箱或IP地址不对;
  4. 如果想通过UserInfo.objects.create(**form.cleaned_data)这种方式快速插入数据到数据库,则在定义Form表单字段时必须与models中表字段一致,特别注意:由于Django会models中的models.ForeignKey()字段名自动加_id,所在Form表单字段在定义时,需要手动给字段名称添加_id,方便插入数据,如上:usertype_id
  5. 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())

注意:

  1. form.is_valid()方法得到一个布尔值,判断从前端得到表单数据是全部有效;
  2. form.cleaned_data得到全部验证有效的数据,是一个字典类型,如:{'username': 'admin', 'ip': '1.1.1.1', 'usertype_id': '3', 'email': '555@qq.com', 'password': 'admin'},字典中的key对应UserForm类中的每个字段名;
  3. form.errors验证有误的错误提示信息,与错误字段一一对应,如:form.errors.user可以得到user字段的全部错误信息,是一个列表类型,form.errors.user[0]得到user字段的第一个错误信息。form.errors.email[0]得到email字段的第一个错误信息;

3、在templates模版文件中渲染

<h1 class="h1">添加用户</h1>
<form action="" method="post" novalidate>
        {% csrf_token %}
        <p>用户名:{{ form.username }} {{ form.errors.username.0 }}</p>
        <p>密码:{{ form.password }} {{ form.errors.password.0 }}</p>
        <p>邮箱:{{ form.email }} {{ form.errors.email.0 }}</p>
      <!--  <p>IP:{{ form.ip }} {{ form.errors.ip.0 }}</p>  -->
        <p>用户类型:{{ form.usertype_id }} {{ form.errors.usertype_id.0 }}</p>
        <input type="submit">
</form>

4、浏览器中显示

img_81c437633d441eb43a410325c85a8f4e.png
GET方式请求页面
  1. 没有填写数据时提交,form.errors得到的值是<ul class="errorlist"><li>email<ul class="errorlist"><li>邮箱不能为空</li></ul></li><li>pwd<ul class="errorlist"><li>密码不能为空</li></ul></li><li>ip<ul class="errorlist"><li>IP不能为空</li></ul></li><li>user<ul class="errorlist"><li>用户名不能为空</li></ul></li></ul>,页面如下图:
    img_1507384bf92d6f2dd7fe9c643d83c850.png
    没有填写数据时提交
  1. 数据格式填写出错时,form.errors得到的值是<ul class="errorlist"><li>email<ul class="errorlist"><li>邮箱格式错误</li></ul></li><li>ip<ul class="errorlist"><li>IP格式错误</li></ul></li></ul>,页面如下图:

    img_b1ddf30bf7428116d29a89541ee6fef2.png
    邮箱或IP格式错误时提交

  2. 数据格式填写正确时,form.cleaned_data得到的值是字典{'email': 'abc@163.com', 'pwd': 'admin', 'ip': '1.1.1.1', 'usertype_id': '1', 'user': 'admin'}

    img_972084279d4ca34e09c6a4186c63220f.png
    数据格式全部填写正确时

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())
img_3fc3bdaacf01cd4ce2655e3387a21c9b.png
输入框中显示默认值

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组件中的操作

  1. 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)
  1. 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')
            #实时从数据库获取数据
  1. 模版文件中添加:
<p>技能:{{ form.tn_id }} {{ form.errors.tn_id.0 }}</p>
  1. 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')    # 得到<QuerySet [(5,), (6,)]>
    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())

注意:

  1. 多对多字段tn_id无法直接通过UserInfo.objects.create(**form.cleaned_data)这样的方式直接添加至数据库,只能先将其从cleaned_data中用.pop('tn_id')方法弹出并赋值给tn_id_list,再通过obj.tn.add(*tn_id_list)方式添加至数据库中。

  2. 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相关代码:

<form id="f1" novalidate>
    {% csrf_token %}
    <p>用户名: {{ form.username }}</p>
    <p>密码: {{ form.password }}</p>
    <p>邮箱: {{ form.email }}</p>
    <p>用户类型: {{ form.usertype_id }}</p>
    <p>技能: {{ form.tn_id }}</p>
    <button type="button" class="submit">提交</button>
</form>

<script src="/static/dist/js/jquery-3.2.1.js"></script>
<script>
    $('.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>');
                        $span.addClass('error');
                        $span.html(value[0]);
                        $('#f1 input[name="'+key+'"]').after($span)
                            //通过字符串拼接,得到$('#f1 input[name="username"]')
                    })
                }
            }
        })        
    })
</script>

注意:通过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

上述代码中,如果两次密码不一致,则分别给对应字段添加一个错误信息。

效果如下图:

img_ec7b906f2d656935f56b934bdad03dcb.png
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'})

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章
最新文章
相关文章